import { Hono } from "hono"; import { companionRoutes, miscRoutes } from "./routes/index.ts"; import { Innertube, Platform } from "youtubei.js"; import { poTokenGenerate, type TokenMinter } from "./lib/jobs/potoken.ts"; import { USER_AGENT } from "bgutils"; import { retry } from "@std/async"; import type { HonoVariables } from "./lib/types/HonoVariables.ts"; import { parseArgs } from "@std/cli/parse-args"; import { existsSync } from "@std/fs/exists"; import { parseConfig } from "./lib/helpers/config.ts"; const config = await parseConfig(); import { Metrics } from "./lib/helpers/metrics.ts"; import { PLAYER_ID } from "./constants.ts"; import { jsInterpreter } from "./lib/helpers/jsInterpreter.ts"; import { initProxyManager, markProxyFailed, isProxyManagerReady, } from "./lib/helpers/proxyManager.ts"; // Initialize auto proxy manager if enabled if (config.networking.auto_proxy) { console.log("[INFO] Auto proxy is enabled, initializing proxy manager..."); try { await initProxyManager(config.networking.vpn_source); } catch (err) { console.error("[ERROR] Failed to initialize proxy manager:", err); console.log("[WARN] Continuing without auto proxy..."); } } const args = parseArgs(Deno.args); if (args._version_date && args._version_commit) { console.log( `[INFO] Using Invidious companion version ${args._version_date}-${args._version_commit}`, ); } let getFetchClientLocation = "getFetchClient"; if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) { if (Deno.env.has("DENO_COMPILED")) { getFetchClientLocation = Deno.mainModule.replace("src/main.ts", "") + Deno.env.get("GET_FETCH_CLIENT_LOCATION"); } else { getFetchClientLocation = Deno.env.get( "GET_FETCH_CLIENT_LOCATION", ) as string; } } const { getFetchClient } = await import(getFetchClientLocation); declare module "hono" { interface ContextVariableMap extends HonoVariables { } } const app = new Hono({ getPath: (req) => new URL(req.url).pathname, }); const companionApp = new Hono({ getPath: (req) => new URL(req.url).pathname, }).basePath(config.server.base_path); const metrics = config.server.enable_metrics ? new Metrics() : undefined; let tokenMinter: TokenMinter | undefined; let innertubeClient: Innertube; let innertubeClientFetchPlayer = true; const innertubeClientOauthEnabled = config.youtube_session.oauth_enabled; const innertubeClientJobPoTokenEnabled = config.jobs.youtube_session.po_token_enabled; const innertubeClientCookies = config.youtube_session.cookies; // Promise that resolves when tokenMinter initialization is complete (for tests) let tokenMinterReadyResolve: (() => void) | undefined; export const tokenMinterReady = new Promise((resolve) => { tokenMinterReadyResolve = resolve; }); if (!innertubeClientOauthEnabled) { if (innertubeClientJobPoTokenEnabled) { console.log("[INFO] job po_token is active."); // Don't fetch fetch player yet for po_token innertubeClientFetchPlayer = false; } else if (!innertubeClientJobPoTokenEnabled) { console.log("[INFO] job po_token is NOT active."); } } Platform.shim.eval = jsInterpreter; innertubeClient = await Innertube.create({ enable_session_cache: false, retrieve_player: innertubeClientFetchPlayer, fetch: getFetchClient(config), cookie: innertubeClientCookies || undefined, user_agent: USER_AGENT, player_id: PLAYER_ID, }); if (!innertubeClientOauthEnabled) { if (innertubeClientJobPoTokenEnabled) { // Initialize tokenMinter in background to not block server startup console.log("[INFO] Starting PO token generation in background..."); // Wrapper function that rotates proxy on failure when auto_proxy is enabled const poTokenGenerateWithProxyRotation = async () => { try { return await poTokenGenerate(config, metrics); } catch (err) { // If auto_proxy is enabled and PO token generation failed, rotate to a new proxy if (config.networking.auto_proxy) { console.log( "[INFO] PO token generation failed, rotating to new proxy...", ); await markProxyFailed(); } throw err; // Re-throw to trigger retry } }; retry( poTokenGenerateWithProxyRotation, { minTimeout: 1_000, maxTimeout: 60_000, multiplier: 5, jitter: 0 }, ).then((result) => { innertubeClient = result.innertubeClient; tokenMinter = result.tokenMinter; tokenMinterReadyResolve?.(); }).catch((err) => { console.error("[ERROR] Failed to initialize PO token:", err); metrics?.potokenGenerationFailure.inc(); tokenMinterReadyResolve?.(); }); } else { // If PO token is not enabled, resolve immediately tokenMinterReadyResolve?.(); } // Resolve promise for tests tokenMinterReadyResolve?.(); } const regenerateSession = async () => { if (innertubeClientJobPoTokenEnabled) { try { ({ innertubeClient, tokenMinter } = await poTokenGenerate( config, metrics, )); } catch (err) { metrics?.potokenGenerationFailure.inc(); // If auto_proxy is enabled and PO token generation failed, rotate to a new proxy if (config.networking.auto_proxy) { console.log( "[INFO] Session regeneration failed, rotating to new proxy...", ); await markProxyFailed(); } // Don't rethrow for cron/manual trigger to avoid crashing the server loop console.error("[ERROR] Failed to regenerate session:", err); } } else { innertubeClient = await Innertube.create({ enable_session_cache: false, fetch: getFetchClient(config), retrieve_player: innertubeClientFetchPlayer, user_agent: USER_AGENT, cookie: innertubeClientCookies || undefined, player_id: PLAYER_ID, }); } }; if (!innertubeClientOauthEnabled) { Deno.cron( "regenerate youtube session", config.jobs.youtube_session.frequency, { backoffSchedule: [5_000, 15_000, 60_000, 180_000] }, regenerateSession, ); } companionApp.use("*", async (c, next) => { c.set("innertubeClient", innertubeClient); c.set("tokenMinter", tokenMinter); c.set("config", config); c.set("metrics", metrics); await next(); }); companionRoutes(companionApp, config); app.use("*", async (c, next) => { c.set("metrics", metrics); await next(); }); miscRoutes(app, config, regenerateSession); app.route("/", companionApp); // This cannot be changed since companion restricts the // files it can access using deno `--allow-write` argument const udsPath = config.server.unix_socket_path; export function run(signal: AbortSignal, port: number, hostname: string) { if (config.server.use_unix_socket) { try { if (existsSync(udsPath)) { // Delete the unix domain socket manually before starting the server Deno.removeSync(udsPath); } } catch (err) { console.log( `[ERROR] Failed to delete unix domain socket '${udsPath}' before starting the server:`, err, ); } const srv = Deno.serve( { onListen() { Deno.chmodSync(udsPath, 0o777); console.log( `[INFO] Server successfully started at ${udsPath} with permissions set to 777.`, ); }, signal: signal, path: udsPath, }, app.fetch, ); return srv; } else { return Deno.serve( { onListen() { console.log( `[INFO] Server successfully started at http://${config.server.host}:${config.server.port}${config.server.base_path}`, ); }, signal: signal, port: port, hostname: hostname, }, app.fetch, ); } } if (import.meta.main) { const controller = new AbortController(); const { signal } = controller; run(signal, config.server.port, config.server.host); Deno.addSignalListener("SIGTERM", () => { console.log("Caught SIGINT, shutting down..."); controller.abort(); Deno.exit(0); }); Deno.addSignalListener("SIGINT", () => { console.log("Caught SIGINT, shutting down..."); controller.abort(); Deno.exit(0); }); }