fix(voice): copy cookies to temp before yt-dlp + fall back to Invidious on cookie/permission errors

yt-dlp 2026.07.04 rewrites the --cookies file on close. Handing it the
root-owned /etc/.../ytcookies.txt (not writable by the gmw service user)
caused PermissionError -> exit 1 on every screen-share download attempt.

- buildCookieArgs on-disk branch now copies the system cookie file into a
  per-run temp file (like the env branch) so write-back lands somewhere we
  own; unreadable -> anonymous.
- resolveInputWithRetry Invidious fallback regex now also matches
  permission|EACCES|cookie, so a cookie failure triggers the link-alternative
  (no-auth Invidious mirror) path instead of failing all retries.
- adds regression test asserting the original cookie path is never passed to yt-dlp
This commit is contained in:
asepharyana
2026-08-13 17:16:05 +07:00
parent f156fc0c9e
commit c285a4c813
3 changed files with 76 additions and 5 deletions
@@ -4,6 +4,7 @@ import {
existsSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
@@ -244,7 +245,7 @@ function buildCookieArgs(): string[] {
// which the Nix deploy writes from BWS once at start.
if (_cachedCookiePath) return ["--cookies", _cachedCookiePath];
const envCookies = process.env.GMW_YT_DOWNLOADER_COOKIES?.trim();
if (envCookies && envCookies.includes("LOGIN_INFO")) {
if (envCookies?.includes("LOGIN_INFO")) {
const fdPath = join(tmpdir(), `gmw-ytcookies.${process.pid}.txt`);
writeFileSync(fdPath, envCookies);
try {
@@ -263,12 +264,37 @@ function buildCookieArgs(): string[] {
process.env.GMW_YT_COOKIES_PATH ?? "/etc/gmw-discord-gateway/ytcookies.txt";
try {
if (cookiePath && existsSync(cookiePath)) {
_cachedCookiePath = cookiePath;
// Never hand the ORIGINAL system file to yt-dlp: recent yt-dlp rewrites
// the cookie file on close (`--cookies` implies write-back). The system
// file is owned by another user (root/deploy) and the service user
// cannot write it → PermissionError → yt-dlp exits 1 → screen share
// fails for every attempt. Copy to a per-run temp file (like the env
// branch above) so write-back lands somewhere we own; if the original
// is not readable we fall back to anonymous (YouTube may 403 → the
// screen-share controller retries via Invidious mirrors without auth).
let cookieContents: string;
try {
cookieContents = readFileSync(cookiePath, "utf8");
} catch {
logger.warn(
{ cookiePath },
"Cookie file not readable; continuing without cookies (anon)",
);
return [];
}
const fdPath = join(tmpdir(), `gmw-ytcookies.${process.pid}.txt`);
writeFileSync(fdPath, cookieContents);
try {
chmodSync(fdPath, 0o600);
} catch {
/* best-effort */
}
_cachedCookiePath = fdPath;
logger.info(
{ cookiePath, source: "on-disk file" },
{ cookiePath: fdPath, source: "on-disk file (copied)" },
"Using YouTube cookies for yt-dlp",
);
return ["--cookies", cookiePath];
return ["--cookies", fdPath];
}
} catch {
/* ignore — fallback to anon */
@@ -87,7 +87,9 @@ export class ScreenShareController {
if (
isYt &&
lastError &&
/403|bot|Sign in|not a bot|access denied/i.test(lastError.message) &&
/403|bot|Sign in|not a bot|access denied|permission|EACCES|cookie/i.test(
lastError.message,
) &&
invidiousIdx < INVIDIOUS_INSTANCES.length
) {
const inst = INVIDIOUS_INSTANCES[invidiousIdx];