2026-05-15 19:43:06 +07:00
|
|
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
|
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
|
|
|
|
|
|
|
|
export interface YtDlpMetadata {
|
|
|
|
|
title: string;
|
|
|
|
|
webpageUrl: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface YtDlpClient {
|
|
|
|
|
getMetadata(url: string): Promise<YtDlpMetadata>;
|
|
|
|
|
getDirectAudioUrl(url: string): Promise<string>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface YtDlpDependencies {
|
|
|
|
|
spawn?: typeof nodeSpawn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createYtDlp(dependencies: YtDlpDependencies = {}): YtDlpClient {
|
|
|
|
|
const spawn = dependencies.spawn ?? nodeSpawn;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
async getMetadata(url: string): Promise<YtDlpMetadata> {
|
|
|
|
|
const data = await runYtDlp(spawn, [
|
|
|
|
|
url,
|
|
|
|
|
"--dump-single-json",
|
|
|
|
|
"--no-playlist",
|
|
|
|
|
"--no-warnings",
|
|
|
|
|
"--quiet",
|
|
|
|
|
]);
|
2026-05-15 19:49:01 +07:00
|
|
|
const parsed = JSON.parse(data) as {
|
|
|
|
|
title?: string;
|
|
|
|
|
webpage_url?: string;
|
|
|
|
|
};
|
2026-05-15 19:43:06 +07:00
|
|
|
return {
|
|
|
|
|
title: parsed.title || url,
|
|
|
|
|
webpageUrl: parsed.webpage_url || url,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async getDirectAudioUrl(url: string): Promise<string> {
|
|
|
|
|
const value = await runYtDlp(spawn, [
|
|
|
|
|
url,
|
|
|
|
|
"--get-url",
|
|
|
|
|
"--format",
|
|
|
|
|
"bestaudio[protocol^=http]/bestaudio/best",
|
|
|
|
|
"--no-playlist",
|
|
|
|
|
"--no-warnings",
|
|
|
|
|
"--quiet",
|
|
|
|
|
]);
|
|
|
|
|
return value.trim().split("\n")[0] || url;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runYtDlp(
|
|
|
|
|
spawn: typeof nodeSpawn,
|
|
|
|
|
args: string[],
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const proc = spawn("yt-dlp", args, {
|
|
|
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
|
|
|
}) as unknown as ChildProcessWithoutNullStreams;
|
|
|
|
|
let stdout = "";
|
|
|
|
|
let stderr = "";
|
|
|
|
|
|
|
|
|
|
proc.stdout.on("data", (chunk) => {
|
|
|
|
|
stdout += chunk.toString();
|
|
|
|
|
});
|
|
|
|
|
proc.stderr.on("data", (chunk) => {
|
|
|
|
|
stderr += chunk.toString();
|
|
|
|
|
});
|
|
|
|
|
proc.on("error", reject);
|
|
|
|
|
proc.on("close", (code) => {
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
resolve(stdout);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
reject(new Error(`yt-dlp failed with code ${code}: ${stderr.trim()}`));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|