feat(gateway): Invidious fallback for YouTube 403 in screen share
YouTube blocks anon + cookies terbind ke IP browser (403 download). Auto-rewrite youtube.com -> yewtu.be/invidious mirror saat cookies gagal. - mediaSource: export isYoutubeWatchUrl/toInvidiousUrl/INVIDIOUS_INSTANCES - screenShareController: resolveInputWithRetry tries Invidious instances on 403
This commit is contained in:
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user