upd / src /main.ts
cursorpro's picture
Upload 57 files
7a4c980 verified
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<void>((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);
});
}