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:
co-authored by
Claude Opus 5
parent
71dc3f2da8
commit
2ee907626e
@@ -57,6 +57,11 @@ YTDLP_PATH=yt-dlp
|
|||||||
# на хосте, используйте socks5h://host.docker.internal:1080 и добавьте в
|
# на хосте, используйте socks5h://host.docker.internal:1080 и добавьте в
|
||||||
# compose.yml к extra_hosts строку "host.docker.internal:host-gateway".
|
# compose.yml к extra_hosts строку "host.docker.internal:host-gateway".
|
||||||
|
|
||||||
|
# JS-рантайм для yt-dlp: YouTube требует исполнять свой JS, иначе извлечение
|
||||||
|
# деградирует и начинаются бот-проверки. В образе уже есть Node, поэтому по
|
||||||
|
# умолчанию используется он. Пустое значение отключает флаг.
|
||||||
|
YTDLP_JS_RUNTIME=node
|
||||||
|
|
||||||
# Необязательно: дополнительные --extractor-args, через ";".
|
# Необязательно: дополнительные --extractor-args, через ";".
|
||||||
# Помогает, когда YouTube не отдаёт форматы серверному IP:
|
# Помогает, когда YouTube не отдаёт форматы серверному IP:
|
||||||
# YTDLP_EXTRACTOR_ARGS=youtube:player_client=default,web_safari
|
# YTDLP_EXTRACTOR_ARGS=youtube:player_client=default,web_safari
|
||||||
|
|||||||
@@ -276,6 +276,25 @@ YTDLP_PROXY=socks5h://127.0.0.1:1080
|
|||||||
Прямые ссылки и интернет-радио ffmpeg скачивает сам, и там прокси применяется только для схем
|
Прямые ссылки и интернет-радио ffmpeg скачивает сам, и там прокси применяется только для схем
|
||||||
`http://` и `https://`.
|
`http://` и `https://`.
|
||||||
|
|
||||||
|
## «Sign in to confirm you're not a bot»
|
||||||
|
|
||||||
|
Три причины, по которым YouTube так отвечает, в порядке частоты:
|
||||||
|
|
||||||
|
1. **Нет JS-рантайма.** Современный yt-dlp обязан исполнять JavaScript плеера YouTube; без этого
|
||||||
|
извлечение деградирует и запросы выглядят как ботовые. В образе уже есть Node, и бот включает
|
||||||
|
его флагом `--js-runtimes node` (`YTDLP_JS_RUNTIME`). При старте в логе видно, что рантайм
|
||||||
|
подхвачен: `"jsRuntime":"node"`. Если там `null` — рантайм не определился, будет отдельное
|
||||||
|
предупреждение.
|
||||||
|
2. **Нет или протухли cookies** — см. раздел ниже. Проверить файл прямо в контейнере:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec mbot yt-dlp --cookies /data/cookies.txt --simulate https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Подозрительный IP.** Выходные узлы Psiphon и прочих публичных прокси YouTube знает и
|
||||||
|
проверяет чаще. Здесь помогают именно cookies; экспортировать их лучше из браузера,
|
||||||
|
работающего через тот же прокси, чтобы сессия не выглядела прыгающей между странами.
|
||||||
|
|
||||||
## Учётка YouTube (cookies)
|
## Учётка YouTube (cookies)
|
||||||
|
|
||||||
Логин и пароль для YouTube yt-dlp не поддерживает — единственный рабочий способ авторизоваться
|
Логин и пароль для YouTube yt-dlp не поддерживает — единственный рабочий способ авторизоваться
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ const schema = z.object({
|
|||||||
),
|
),
|
||||||
/** Extra `--extractor-args` values, separated by ";" — e.g. youtube:player_client=default,web_safari */
|
/** Extra `--extractor-args` values, separated by ";" — e.g. youtube:player_client=default,web_safari */
|
||||||
YTDLP_EXTRACTOR_ARGS: z.string().optional(),
|
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(),
|
LOCAL_MEDIA_DIR: z.string().optional(),
|
||||||
|
|
||||||
DEFAULT_VOLUME: z.coerce.number().min(0).max(200).default(60),
|
DEFAULT_VOLUME: z.coerce.number().min(0).max(200).default(60),
|
||||||
|
|||||||
+8
-3
@@ -6,9 +6,14 @@ import { logger } from "./logger.js";
|
|||||||
import { checkCookies, checkProxy, checkYtDlp, describeProxy } from "./sources/index.js";
|
import { checkCookies, checkProxy, checkYtDlp, describeProxy } from "./sources/index.js";
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const ytdlpVersion = await checkYtDlp();
|
const ytdlp = await checkYtDlp();
|
||||||
if (ytdlpVersion) {
|
if (ytdlp) {
|
||||||
logger.info({ version: ytdlpVersion }, "yt-dlp detected");
|
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 {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ path: config.YTDLP_PATH },
|
{ path: config.YTDLP_PATH },
|
||||||
|
|||||||
+30
-3
@@ -31,8 +31,28 @@ interface YtDlpEntry {
|
|||||||
entries?: YtDlpEntry[] | null;
|
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[] {
|
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_COOKIES) args.push("--cookies", config.YTDLP_COOKIES);
|
||||||
if (config.YTDLP_PROXY) args.push("--proxy", config.YTDLP_PROXY);
|
if (config.YTDLP_PROXY) args.push("--proxy", config.YTDLP_PROXY);
|
||||||
for (const value of config.YTDLP_EXTRACTOR_ARGS?.split(";") ?? []) {
|
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 {
|
try {
|
||||||
const { stdout } = await runYtDlp(["--version"], 15_000);
|
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 {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user