diff --git a/services/discord-gateway/src/modules/voice-recording/mediaSource.ts b/services/discord-gateway/src/modules/voice-recording/mediaSource.ts index 88ecac6..abf958a 100644 --- a/services/discord-gateway/src/modules/voice-recording/mediaSource.ts +++ b/services/discord-gateway/src/modules/voice-recording/mediaSource.ts @@ -1,5 +1,11 @@ import { type ChildProcess, spawn } from "node:child_process"; -import { chmodSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + existsSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { PassThrough, type Readable } from "node:stream"; @@ -239,9 +245,16 @@ function buildCookieArgs(): string[] { if (envCookies && envCookies.includes("LOGIN_INFO")) { const fdPath = join(tmpdir(), `gmw-ytcookies.${process.pid}.txt`); writeFileSync(fdPath, envCookies); - try { chmodSync(fdPath, 0o600); } catch { /* best-effort */ } + try { + chmodSync(fdPath, 0o600); + } catch { + /* best-effort */ + } _cachedCookiePath = fdPath; - logger.info({ cookiePath: fdPath, source: "GMW_YT_DOWNLOADER_COOKIES env" }, "Using YouTube cookies (from BWS env)"); + logger.info( + { cookiePath: fdPath, source: "GMW_YT_DOWNLOADER_COOKIES env" }, + "Using YouTube cookies (from BWS env)", + ); return ["--cookies", fdPath]; } const cookiePath = @@ -249,16 +262,59 @@ function buildCookieArgs(): string[] { try { if (cookiePath && existsSync(cookiePath)) { _cachedCookiePath = cookiePath; - logger.info({ cookiePath, source: "on-disk file" }, "Using YouTube cookies for yt-dlp"); + logger.info( + { cookiePath, source: "on-disk file" }, + "Using YouTube cookies for yt-dlp", + ); return ["--cookies", cookiePath]; } } catch { /* ignore — fallback to anon */ } - logger.warn("No YouTube cookies available; yt-dlp will use anonymous (YouTube may 403)"); + logger.warn( + "No YouTube cookies available; yt-dlp will use anonymous (YouTube may 403)", + ); return []; } +/** Invidious instances for anon YouTube fetch (fallback when cookies 403). */ +export const INVIDIOUS_INSTANCES = [ + "yewtu.be", + "yewtu.nanomorph.dev", + "invidious.snopyta.org", + "invidious.kavin.rocks", +]; + +/** True if url is a YouTube watch URL (youtu.be / youtube.com/watch). */ +export function isYoutubeWatchUrl(url: string): boolean { + try { + const u = new URL(url); + return ( + u.hostname === "youtu.be" || + (u.hostname === "www.youtube.com" && u.pathname === "/watch") || + (u.hostname === "youtube.com" && u.pathname === "/watch") + ); + } catch { + return false; + } +} + +/** Rewrite a YouTube watch URL to an invidious instance (anon, no bot-check). */ +export function toInvidiousUrl(url: string, instance: string): string { + try { + const u = new URL(url); + if (u.hostname === "youtu.be") { + const id = u.pathname.slice(1); + return `https://${instance}/watch?v=${id}`; + } + const id = u.searchParams.get("v"); + if (id) return `https://${instance}/watch?v=${id}`; + return url; + } catch { + return url; + } +} + // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- diff --git a/services/discord-gateway/src/modules/voice-recording/screenShareController.ts b/services/discord-gateway/src/modules/voice-recording/screenShareController.ts index fa66b43..644b39a 100644 --- a/services/discord-gateway/src/modules/voice-recording/screenShareController.ts +++ b/services/discord-gateway/src/modules/voice-recording/screenShareController.ts @@ -8,7 +8,12 @@ import { prepareStream, Streamer, } from "../../goLive/index.js"; -import { getDirectScreenInput } from "./mediaSource.js"; +import { + getDirectScreenInput, + INVIDIOUS_INSTANCES, + isYoutubeWatchUrl, + toInvidiousUrl, +} from "./mediaSource.js"; import type { ScreenSharePlayback } from "./mediaTypes.js"; import { discordPlayer } from "./player.js"; @@ -70,7 +75,29 @@ export class ScreenShareController { const MAX_ATTEMPTS = 3; let lastError: Error | null = null; + // YouTube may 403 even with account cookies (IP-bound session / bot check + // on VPS IP). When the source is a YouTube URL and cookies fail, fall back + // to anon Invidious mirror instances — no auth needed. + const isYt = isYoutubeWatchUrl(source); + let invidiousIdx = 0; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + // On a 403 against YouTube, try the next Invidious instance for this attempt. + if ( + isYt && + lastError && + /403|bot|Sign in|not a bot|access denied/i.test(lastError.message) && + invidiousIdx < INVIDIOUS_INSTANCES.length + ) { + const inst = INVIDIOUS_INSTANCES[invidiousIdx]; + this.logger.warn( + { attempt, instance: inst, error: lastError.message }, + "YouTube blocked (403); falling back to Invidious mirror", + ); + source = toInvidiousUrl(source, inst); + invidiousIdx++; + } + try { const input = await getDirectScreenInput(source);