Update bundled yt-dlp and report source failures in chat
The image shipped a year-old yt-dlp, which YouTube now rejects with "The page needs to be reloaded". Bumped to 2026.08.19 and documented rebuilding as the standard fix, including how to pass a newer tag without waiting for a repository update. A downloader dying mid-stream also looked exactly like a very short track: ffmpeg saw EOF, the player advanced, and the channel only got "queue finished". The failure reason now reaches the chat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
823a9f1565
commit
315760e076
+4
-1
@@ -28,7 +28,10 @@ ENV HOME=/tmp
|
||||
WORKDIR /app
|
||||
|
||||
# yt-dlp_linux is a self-contained binary, so no Python runtime is needed.
|
||||
ARG YTDLP_VERSION=2025.08.20
|
||||
# YouTube breaks extractors regularly, so keep this current: rebuilding with
|
||||
# --build-arg YTDLP_VERSION=<tag> (or bumping this default) is the usual fix for
|
||||
# "The page needs to be reloaded" and similar extraction errors.
|
||||
ARG YTDLP_VERSION=2026.08.19
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl \
|
||||
&& curl -fsSL "https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_VERSION}/yt-dlp_linux" -o /usr/local/bin/yt-dlp \
|
||||
|
||||
@@ -188,6 +188,24 @@ cd /opt/stoat-mbot && docker compose up -d --build && docker compose logs -f
|
||||
- `YTDLP_COOKIES` — путь к `cookies.txt`, см. раздел ниже.
|
||||
- `YTDLP_EXTRACTOR_ARGS` — дополнительные `--extractor-args` через `;`.
|
||||
|
||||
## Обновление yt-dlp
|
||||
|
||||
YouTube регулярно ломает экстракторы, и симптом всегда один: трек находится, но не играет, а в
|
||||
логах — `ERROR: [youtube] ...: The page needs to be reloaded` или подобное. Лечится обновлением
|
||||
yt-dlp: версия зашита в [Dockerfile](Dockerfile) как `ARG YTDLP_VERSION`.
|
||||
|
||||
```bash
|
||||
cd /opt/stoat-mbot && git pull && docker compose up -d --build
|
||||
```
|
||||
|
||||
Если свежая версия вышла, а обновления репозитория ещё нет — можно указать её сразу:
|
||||
|
||||
```bash
|
||||
docker compose build --build-arg YTDLP_VERSION=$(date +%Y.%m.%d) && docker compose up -d
|
||||
```
|
||||
|
||||
Актуальный тег — на [странице релизов yt-dlp](https://github.com/yt-dlp/yt-dlp/releases).
|
||||
|
||||
## Учётка YouTube (cookies)
|
||||
|
||||
Логин и пароль для YouTube yt-dlp не поддерживает — единственный рабочий способ авторизоваться
|
||||
|
||||
@@ -276,6 +276,14 @@ export class GuildPlayer extends EventEmitter<GuildPlayerEvents> {
|
||||
// stop() rebuilds the volume transformer, so volume is applied per track.
|
||||
media.setVolume(this.volume / 100);
|
||||
this.startTicker();
|
||||
|
||||
// A downloader that dies mid-stream just looks like a very short track, so
|
||||
// say why instead of silently moving on.
|
||||
void input.failure?.then((reason) => {
|
||||
if (!reason || this.current?.id !== track.id) return;
|
||||
this.log.warn({ reason, track: track.title }, "source failed while streaming");
|
||||
this.notify(`⚠️ **${track.title}** — источник отдал ошибку: ${reason}`);
|
||||
});
|
||||
} catch (err) {
|
||||
this.log.warn({ err, track: track.title }, "playback failed");
|
||||
const message = err instanceof UserFacingError ? err.message : "неизвестная ошибка";
|
||||
|
||||
@@ -110,6 +110,8 @@ export interface PlaybackInput {
|
||||
input: string | Readable;
|
||||
inputOptions: string[];
|
||||
cleanup(): void;
|
||||
/** Resolves with a reason if the downloader died on its own, for reporting. */
|
||||
failure?: Promise<string | null>;
|
||||
}
|
||||
|
||||
const HTTP_RESILIENCE = [
|
||||
@@ -139,5 +141,10 @@ export async function openPlayback(track: Track, seekSeconds = 0): Promise<Playb
|
||||
}
|
||||
|
||||
const proc = ytdlp.openAudioStream(track.url);
|
||||
return { input: proc.stream, inputOptions: [], cleanup: () => proc.kill() };
|
||||
return {
|
||||
input: proc.stream,
|
||||
inputOptions: [],
|
||||
cleanup: () => proc.kill(),
|
||||
failure: proc.failure,
|
||||
};
|
||||
}
|
||||
|
||||
+14
-2
@@ -209,6 +209,8 @@ export async function resolveStreamUrl(pageUrl: string): Promise<string> {
|
||||
export interface AudioProcess {
|
||||
stream: Readable;
|
||||
kill(): void;
|
||||
/** Resolves with a reason when the download fails, or null when it was fine. */
|
||||
failure: Promise<string | null>;
|
||||
}
|
||||
|
||||
/** Spawns yt-dlp writing the best audio to stdout, for piping straight into ffmpeg. */
|
||||
@@ -220,19 +222,29 @@ export function openAudioStream(pageUrl: string): AudioProcess {
|
||||
);
|
||||
|
||||
let stderr = "";
|
||||
let killed = false;
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stderr.on("data", (chunk: string) => {
|
||||
stderr = (stderr + chunk).slice(-2000);
|
||||
});
|
||||
|
||||
const failure = new Promise<string | null>((resolve) => {
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0 && code !== null && stderr.trim()) {
|
||||
log.warn({ code, stderr: stderr.slice(0, 500) }, "yt-dlp stream exited with error");
|
||||
if (killed || code === 0 || code === null) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
log.warn({ code, stderr: stderr.slice(0, 500) }, "yt-dlp stream exited with error");
|
||||
resolve(firstUsefulError(stderr));
|
||||
});
|
||||
child.on("error", (err: Error) => resolve(err.message));
|
||||
});
|
||||
|
||||
return {
|
||||
stream: child.stdout,
|
||||
failure,
|
||||
kill: () => {
|
||||
killed = true;
|
||||
if (child.exitCode === null) child.kill("SIGKILL");
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user