Document YouTube cookie auth and warn when the file is unusable
Cookies were already wired up but only mentioned in passing, and the non-obvious parts were undocumented: yt-dlp has no username/password support for YouTube, it rewrites the cookie file to persist rotated cookies (so a read-only file expires early), and the export has to happen in a private window that is logged out before closing. Startup now reports whether the cookie file is usable, missing, or read-only, and YTDLP_EXTRACTOR_ARGS is passed through for the cases where YouTube blocks a server IP outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1ac99b4a10
commit
2b999bf2a0
+8
-1
@@ -27,9 +27,16 @@ SESSION_TTL_HOURS=168
|
||||
# Путь к yt-dlp. В docker-образе он уже установлен.
|
||||
YTDLP_PATH=yt-dlp
|
||||
|
||||
# Необязательно: файл cookies.txt для приватных/возрастных видео.
|
||||
# Необязательно: cookies.txt аккаунта YouTube — снимает возрастные ограничения,
|
||||
# «Sign in to confirm you're not a bot» и открывает приватные/платные видео.
|
||||
# Файл должен быть доступен на ЗАПИСЬ: yt-dlp обновляет в нём ротируемые куки.
|
||||
# Подробности — в README, раздел «Учётка YouTube (cookies)».
|
||||
# YTDLP_COOKIES=/data/cookies.txt
|
||||
|
||||
# Необязательно: дополнительные --extractor-args, через ";".
|
||||
# Помогает, когда YouTube не отдаёт форматы серверному IP:
|
||||
# YTDLP_EXTRACTOR_ARGS=youtube:player_client=default,web_safari
|
||||
|
||||
# Необязательно: каталог с локальной медиатекой (смонтируйте том).
|
||||
# LOCAL_MEDIA_DIR=/media/music
|
||||
|
||||
|
||||
@@ -164,7 +164,35 @@ cd /opt/stoat-mbot && docker compose up -d --build && docker compose logs -f
|
||||
- `IDLE_TIMEOUT_SECONDS` — через сколько секунд простоя (или пустого канала) бот выходит из войса.
|
||||
- `LOCAL_MEDIA_DIR` — примонтируйте том с музыкой и укажите путь внутри контейнера,
|
||||
тогда заработают `local:` и поиск по медиатеке.
|
||||
- `YTDLP_COOKIES` — путь к `cookies.txt`, если YouTube просит подтверждения возраста или логина.
|
||||
- `YTDLP_COOKIES` — путь к `cookies.txt`, см. раздел ниже.
|
||||
- `YTDLP_EXTRACTOR_ARGS` — дополнительные `--extractor-args` через `;`.
|
||||
|
||||
## Учётка YouTube (cookies)
|
||||
|
||||
Логин и пароль для YouTube yt-dlp не поддерживает — единственный рабочий способ авторизоваться
|
||||
это `cookies.txt`. С ним открываются видео с возрастным ограничением, приватные и «только для
|
||||
участников», а также снимается `Sign in to confirm you're not a bot`, которое YouTube любит
|
||||
показывать серверным IP.
|
||||
|
||||
**Заводить лучше отдельный (одноразовый) аккаунт** — за автоматизацию YouTube может его
|
||||
заблокировать, терять основной незачем.
|
||||
|
||||
1. Откройте **приватное окно** браузера и войдите в YouTube этим аккаунтом.
|
||||
2. Экспортируйте куки для `youtube.com` расширением в формате Netscape (`Get cookies.txt LOCALLY`
|
||||
и аналоги) — либо, если yt-dlp стоит локально: `yt-dlp --cookies-from-browser chrome --cookies cookies.txt`.
|
||||
3. **Не закрывая приватное окно, выйдите из аккаунта в нём** (Log out) и только потом закройте
|
||||
окно. Так YouTube не отзовёт сессию, к которой привязаны выгруженные куки.
|
||||
4. Положите файл в `/opt/stoat-mbot/data/cookies.txt` и убедитесь, что он писабельный для uid 1000:
|
||||
yt-dlp перезаписывает файл после каждого запуска, сохраняя обновлённые куки. Без права на запись
|
||||
сессия быстро протухнет.
|
||||
5. В `.env`: `YTDLP_COOKIES=/data/cookies.txt`, затем `docker compose up -d`.
|
||||
|
||||
В логах при старте появится `using YouTube cookies`; если файла нет или он только на чтение —
|
||||
будет предупреждение с указанием причины.
|
||||
|
||||
Куки живут не вечно (обычно недели): когда в логах снова полезут ошибки авторизации, повторите
|
||||
экспорт. Если YouTube упирается именно в бот-детект, попробуйте дополнительно
|
||||
`YTDLP_EXTRACTOR_ARGS=youtube:player_client=default,web_safari`.
|
||||
|
||||
## Разработка
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ const schema = z.object({
|
||||
|
||||
YTDLP_PATH: z.string().default("yt-dlp"),
|
||||
YTDLP_COOKIES: z.string().optional(),
|
||||
/** Extra `--extractor-args` values, separated by ";" — e.g. youtube:player_client=default,web_safari */
|
||||
YTDLP_EXTRACTOR_ARGS: z.string().optional(),
|
||||
LOCAL_MEDIA_DIR: z.string().optional(),
|
||||
|
||||
DEFAULT_VOLUME: z.coerce.number().min(0).max(200).default(60),
|
||||
|
||||
+13
-1
@@ -3,7 +3,7 @@ import { startBot } from "./bot/index.js";
|
||||
import { config } from "./config.js";
|
||||
import { MusicManager } from "./core/manager.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { checkYtDlp } from "./sources/index.js";
|
||||
import { checkCookies, checkYtDlp } from "./sources/index.js";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const ytdlpVersion = await checkYtDlp();
|
||||
@@ -16,6 +16,18 @@ async function main(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
const cookies = await checkCookies();
|
||||
if (cookies === "ok") {
|
||||
logger.info({ path: config.YTDLP_COOKIES }, "using YouTube cookies");
|
||||
} else if (cookies === "read-only") {
|
||||
logger.warn(
|
||||
{ path: config.YTDLP_COOKIES },
|
||||
"cookie file is not writable — yt-dlp cannot persist rotated cookies and the session will expire early",
|
||||
);
|
||||
} else if (cookies === "missing") {
|
||||
logger.warn({ path: config.YTDLP_COOKIES }, "cookie file from YTDLP_COOKIES does not exist");
|
||||
}
|
||||
|
||||
const manager = new MusicManager();
|
||||
const bot = await startBot(manager);
|
||||
const app = await startApiServer({ manager, context: bot.context });
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as direct from "./direct.js";
|
||||
import * as local from "./local.js";
|
||||
import * as ytdlp from "./ytdlp.js";
|
||||
|
||||
export { checkAvailable as checkYtDlp } from "./ytdlp.js";
|
||||
export { checkAvailable as checkYtDlp, checkCookies } from "./ytdlp.js";
|
||||
export { isEnabled as isLocalLibraryEnabled, listFiles as listLocalFiles } from "./local.js";
|
||||
|
||||
const YOUTUBE_HOSTS = ["youtube.com", "youtu.be", "music.youtube.com", "m.youtube.com"];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { access } from "node:fs/promises";
|
||||
import type { Readable } from "node:stream";
|
||||
import { config } from "../config.js";
|
||||
import { logger } from "../logger.js";
|
||||
@@ -31,9 +33,34 @@ interface YtDlpEntry {
|
||||
function baseArgs(): string[] {
|
||||
const args = ["--no-warnings", "--no-playlist-reverse", "--ignore-config", "--no-color"];
|
||||
if (config.YTDLP_COOKIES) args.push("--cookies", config.YTDLP_COOKIES);
|
||||
for (const value of config.YTDLP_EXTRACTOR_ARGS?.split(";") ?? []) {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed) args.push("--extractor-args", trimmed);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
export type CookieStatus = "ok" | "read-only" | "missing";
|
||||
|
||||
/**
|
||||
* yt-dlp rewrites the cookie file after every run to persist rotated cookies,
|
||||
* so a read-only file quietly degrades back to anonymous access.
|
||||
*/
|
||||
export async function checkCookies(): Promise<CookieStatus | null> {
|
||||
if (!config.YTDLP_COOKIES) return null;
|
||||
try {
|
||||
await access(config.YTDLP_COOKIES, constants.R_OK | constants.W_OK);
|
||||
return "ok";
|
||||
} catch {
|
||||
try {
|
||||
await access(config.YTDLP_COOKIES, constants.R_OK);
|
||||
return "read-only";
|
||||
} catch {
|
||||
return "missing";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runYtDlp(args: string[], timeoutMs = 45_000): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(config.YTDLP_PATH, args, { windowsHide: true });
|
||||
|
||||
Reference in New Issue
Block a user