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:
@@ -4,6 +4,7 @@ import {
|
|||||||
existsSync,
|
existsSync,
|
||||||
mkdtempSync,
|
mkdtempSync,
|
||||||
readdirSync,
|
readdirSync,
|
||||||
|
readFileSync,
|
||||||
rmSync,
|
rmSync,
|
||||||
statSync,
|
statSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
@@ -244,7 +245,7 @@ function buildCookieArgs(): string[] {
|
|||||||
// which the Nix deploy writes from BWS once at start.
|
// which the Nix deploy writes from BWS once at start.
|
||||||
if (_cachedCookiePath) return ["--cookies", _cachedCookiePath];
|
if (_cachedCookiePath) return ["--cookies", _cachedCookiePath];
|
||||||
const envCookies = process.env.GMW_YT_DOWNLOADER_COOKIES?.trim();
|
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`);
|
const fdPath = join(tmpdir(), `gmw-ytcookies.${process.pid}.txt`);
|
||||||
writeFileSync(fdPath, envCookies);
|
writeFileSync(fdPath, envCookies);
|
||||||
try {
|
try {
|
||||||
@@ -263,12 +264,37 @@ function buildCookieArgs(): string[] {
|
|||||||
process.env.GMW_YT_COOKIES_PATH ?? "/etc/gmw-discord-gateway/ytcookies.txt";
|
process.env.GMW_YT_COOKIES_PATH ?? "/etc/gmw-discord-gateway/ytcookies.txt";
|
||||||
try {
|
try {
|
||||||
if (cookiePath && existsSync(cookiePath)) {
|
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(
|
logger.info(
|
||||||
{ cookiePath, source: "on-disk file" },
|
{ cookiePath: fdPath, source: "on-disk file (copied)" },
|
||||||
"Using YouTube cookies for yt-dlp",
|
"Using YouTube cookies for yt-dlp",
|
||||||
);
|
);
|
||||||
return ["--cookies", cookiePath];
|
return ["--cookies", fdPath];
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore — fallback to anon */
|
/* ignore — fallback to anon */
|
||||||
|
|||||||
@@ -87,7 +87,9 @@ export class ScreenShareController {
|
|||||||
if (
|
if (
|
||||||
isYt &&
|
isYt &&
|
||||||
lastError &&
|
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
|
invidiousIdx < INVIDIOUS_INSTANCES.length
|
||||||
) {
|
) {
|
||||||
const inst = INVIDIOUS_INSTANCES[invidiousIdx];
|
const inst = INVIDIOUS_INSTANCES[invidiousIdx];
|
||||||
|
|||||||
@@ -113,4 +113,47 @@ describe("downloadScreenInput", () => {
|
|||||||
process.env.PATH = realPath2;
|
process.env.PATH = realPath2;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("copies the on-disk cookies to a temp file instead of handing yt-dlp the original path", async () => {
|
||||||
|
// Regression: recent yt-dlp rewrites `--cookies` file on close. If we hand
|
||||||
|
// it the original system file (root-owned, not writable by the service
|
||||||
|
// user), save-back throws PermissionError → exit 1 → screen share fails.
|
||||||
|
// The copy lives in tmpdir where the service user owns it.
|
||||||
|
const cookieDir = mkdtempSync(join(tmpdir(), "gmw-fake-cookies-"));
|
||||||
|
const cookiePath = join(cookieDir, "ytcookies.txt");
|
||||||
|
writeFileSync(
|
||||||
|
cookiePath,
|
||||||
|
"# Netscape HTTP Cookie File\n.youtube.com\tTRUE\t/\tTRUE\t0\tLOGIN_INFO\tabc123\n",
|
||||||
|
);
|
||||||
|
const argsDump = join(
|
||||||
|
tmpdir(),
|
||||||
|
`gmw-ytargs-${process.pid}-${Date.now()}.txt`,
|
||||||
|
);
|
||||||
|
process.env.GMW_FAKE_YTDLP_DUMP_ARGS = argsDump;
|
||||||
|
process.env.GMW_YT_COOKIES_PATH = cookiePath;
|
||||||
|
const realPath2 = process.env.PATH;
|
||||||
|
const dir = fakeBinDir as unknown as string;
|
||||||
|
const existing = join(dir, "yt-dlp");
|
||||||
|
writeFileSync(existing, ytShimDump);
|
||||||
|
chmodSync(existing, 0o755);
|
||||||
|
try {
|
||||||
|
await downloadScreenInput("https://youtu.be/abc");
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
const args = readFileSync(argsDump, "utf8").trim();
|
||||||
|
expect(args).toContain("--cookies");
|
||||||
|
const cookieArg = args
|
||||||
|
.split(/\s+/)
|
||||||
|
.at(args.split(/\s+/).indexOf("--cookies") + 1);
|
||||||
|
expect(cookieArg).toBeDefined();
|
||||||
|
expect(cookieArg).not.toBe(cookiePath); // never the original system file
|
||||||
|
expect(cookieArg).toMatch(/gmw-ytcookies\.\d+\.txt/); // per-process temp copy
|
||||||
|
expect(cookieArg).not.toMatch(/^\/etc\//);
|
||||||
|
} finally {
|
||||||
|
delete process.env.GMW_FAKE_YTDLP_DUMP_ARGS;
|
||||||
|
delete process.env.GMW_YT_COOKIES_PATH;
|
||||||
|
rmSync(argsDump, { force: true });
|
||||||
|
rmSync(cookieDir, { recursive: true, force: true });
|
||||||
|
process.env.PATH = realPath2;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user