Give yt-dlp a JavaScript runtime to avoid YouTube's bot checks

Downloads failed with "Sign in to confirm you're not a bot" even with
cookies and a working proxy. yt-dlp was also warning that no JavaScript
runtime was available: YouTube's player challenges now require one, and
without it extraction degrades into exactly those bot checks.

The image already ships Node, so yt-dlp is pointed at it via
--js-runtimes, but only after confirming the installed build understands
the flag — an older binary would otherwise fail on an unknown option.
Startup logs which runtime was picked and warns when there is none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-09 00:34:42 +03:00
co-authored by Claude Opus 5
parent 71dc3f2da8
commit 2ee907626e
5 changed files with 64 additions and 6 deletions
+2
View File
@@ -53,6 +53,8 @@ const schema = z.object({
),
/** Extra `--extractor-args` values, separated by ";" — e.g. youtube:player_client=default,web_safari */
YTDLP_EXTRACTOR_ARGS: z.string().optional(),
/** JS runtime yt-dlp uses for YouTube's player challenges; "" disables the flag. */
YTDLP_JS_RUNTIME: z.string().default("node"),
LOCAL_MEDIA_DIR: z.string().optional(),
DEFAULT_VOLUME: z.coerce.number().min(0).max(200).default(60),
+8 -3
View File
@@ -6,9 +6,14 @@ import { logger } from "./logger.js";
import { checkCookies, checkProxy, checkYtDlp, describeProxy } from "./sources/index.js";
async function main(): Promise<void> {
const ytdlpVersion = await checkYtDlp();
if (ytdlpVersion) {
logger.info({ version: ytdlpVersion }, "yt-dlp detected");
const ytdlp = await checkYtDlp();
if (ytdlp) {
logger.info({ version: ytdlp.version, jsRuntime: ytdlp.jsRuntime }, "yt-dlp detected");
if (!ytdlp.jsRuntime) {
logger.warn(
"no JavaScript runtime for yt-dlp — YouTube may answer with bot checks; set YTDLP_JS_RUNTIME",
);
}
} else {
logger.warn(
{ path: config.YTDLP_PATH },
+30 -3
View File
@@ -31,8 +31,28 @@ interface YtDlpEntry {
entries?: YtDlpEntry[] | null;
}
/**
* Modern yt-dlp needs a JavaScript runtime for YouTube's player challenges;
* without one, extraction is degraded and YouTube starts answering with "Sign in
* to confirm you're not a bot". Our image already ships Node, so we point yt-dlp
* at it — but only once we have confirmed this build understands the flag.
*/
let jsRuntimeArgs: string[] = [];
async function detectJsRuntime(): Promise<string | null> {
if (!config.YTDLP_JS_RUNTIME) return null;
try {
const { stdout } = await runYtDlp(["--help"], 15_000);
if (!stdout.includes("--js-runtimes")) return null;
jsRuntimeArgs = ["--js-runtimes", config.YTDLP_JS_RUNTIME];
return config.YTDLP_JS_RUNTIME;
} catch {
return null;
}
}
function baseArgs(): string[] {
const args = ["--no-warnings", "--no-playlist-reverse", "--ignore-config", "--no-color"];
const args = ["--no-warnings", "--no-playlist-reverse", "--ignore-config", "--no-color", ...jsRuntimeArgs];
if (config.YTDLP_COOKIES) args.push("--cookies", config.YTDLP_COOKIES);
if (config.YTDLP_PROXY) args.push("--proxy", config.YTDLP_PROXY);
for (const value of config.YTDLP_EXTRACTOR_ARGS?.split(";") ?? []) {
@@ -351,10 +371,17 @@ export function openAudioStream(pageUrl: string): AudioProcess {
};
}
export async function checkAvailable(): Promise<string | null> {
export interface YtDlpStatus {
version: string;
jsRuntime: string | null;
}
export async function checkAvailable(): Promise<YtDlpStatus | null> {
try {
const { stdout } = await runYtDlp(["--version"], 15_000);
return stdout.trim() || null;
const version = stdout.trim();
if (!version) return null;
return { version, jsRuntime: await detectJsRuntime() };
} catch {
return null;
}