diff --git a/api/src/config.js b/api/src/config.js index bff1eda3..54b1214e 100644 --- a/api/src/config.js +++ b/api/src/config.js @@ -53,7 +53,10 @@ const env = { keyReloadInterval: 900, enabledServices, + customInnertubeClient: process.env.CUSTOM_INNERTUBE_CLIENT, + ytSessionServer: process.env.YOUTUBE_SESSION_SERVER, + ytSessionReloadInterval: 300, } const genericUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"; diff --git a/api/src/core/api.js b/api/src/core/api.js index e4d3dfcf..3f9128d8 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -18,8 +18,10 @@ import { verifyTurnstileToken } from "../security/turnstile.js"; import { friendlyServiceName } from "../processing/service-alias.js"; import { verifyStream, getInternalStream } from "../stream/manage.js"; import { createResponse, normalizeRequest, getIP } from "../processing/request.js"; + import * as APIKeys from "../security/api-keys.js"; import * as Cookies from "../processing/cookie/manager.js"; +import * as YouTubeSession from "../processing/helpers/youtube-session.js"; const git = { branch: await getBranch(), @@ -376,6 +378,10 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { if (env.cookiePath) { Cookies.setup(env.cookiePath); } + + if (env.ytSessionServer) { + YouTubeSession.setup(); + } }); if (isCluster) { diff --git a/api/src/processing/helpers/youtube-session.js b/api/src/processing/helpers/youtube-session.js new file mode 100644 index 00000000..5235c42a --- /dev/null +++ b/api/src/processing/helpers/youtube-session.js @@ -0,0 +1,74 @@ +import * as cluster from "../../misc/cluster.js"; + +import { env } from "../../config.js"; +import { Green, Yellow } from "../../misc/console-text.js"; + +let session; + +const validateSession = (sessionResponse) => { + if (!sessionResponse.potoken) { + throw "no poToken in session response"; + } + + if (!sessionResponse.visitor_data) { + throw "no visitor_data in session response"; + } + + if (!sessionResponse.updated) { + throw "no last update timestamp in session response"; + } + + // https://github.com/iv-org/youtube-trusted-session-generator/blob/c2dfe3f/potoken_generator/main.py#L25 + if (sessionResponse.potoken.length < 160) { + console.error(`${Yellow('[!]')} poToken is too short and might not work (${new Date().toISOString()})`); + } +} + +const updateSession = (newSession) => { + session = newSession; +} + +const loadSession = async () => { + const sessionServerUrl = new URL(env.ytSessionServer); + sessionServerUrl.pathname = "/token"; + + const newSession = await fetch(sessionServerUrl).then(a => a.json()); + validateSession(newSession); + + if (!session || session.updated < newSession?.updated) { + cluster.broadcast({ youtube_session: newSession }); + updateSession(newSession); + } +} + +const wrapLoad = (initial = false) => { + loadSession() + .then(() => { + if (initial) { + console.log(`${Green('[✓]')} poToken & visitor_data loaded successfully!`); + } + }) + .catch((e) => { + console.error(`${Yellow('[!]')} Failed loading poToken & visitor_data at ${new Date().toISOString()}.`); + console.error('Error:', e); + }) +} + +export const getYouTubeSession = () => { + return session; +} + +export const setup = () => { + if (cluster.isPrimary) { + wrapLoad(true); + if (env.ytSessionReloadInterval > 0) { + setInterval(wrapLoad, env.ytSessionReloadInterval * 1000); + } + } else if (cluster.isWorker) { + process.on('message', (message) => { + if ('youtube_session' in message) { + updateSession(message.youtube_session); + } + }); + } +} diff --git a/api/src/processing/services/youtube.js b/api/src/processing/services/youtube.js index fc712e24..a10a8b08 100644 --- a/api/src/processing/services/youtube.js +++ b/api/src/processing/services/youtube.js @@ -4,6 +4,7 @@ import { fetch } from "undici"; import { Innertube, Session } from "youtubei.js"; import { env } from "../../config.js"; +import { getYouTubeSession } from "../helpers/youtube-session.js"; import { getCookie, updateCookieValues } from "../cookie/manager.js"; const PLAYER_REFRESH_PERIOD = 1000 * 60 * 15; // ms @@ -70,16 +71,22 @@ const cloneInnertube = async (customFetch) => { const shouldRefreshPlayer = lastRefreshedAt + PLAYER_REFRESH_PERIOD < new Date(); const rawCookie = getCookie('youtube'); - const rawCookieValues = rawCookie?.values(); const cookie = rawCookie?.toString(); + const sessionTokens = getYouTubeSession(); + const retrieve_player = Boolean(sessionTokens || cookie); + + if (env.ytSessionServer && !sessionTokens?.potoken) { + throw "no_session_tokens"; + } + if (!innertube || shouldRefreshPlayer) { innertube = await Innertube.create({ fetch: customFetch, - retrieve_player: !!cookie, + retrieve_player, cookie, - po_token: rawCookieValues?.po_token, - visitor_data: rawCookieValues?.visitor_data, + po_token: sessionTokens?.potoken, + visitor_data: sessionTokens?.visitor_data, }); lastRefreshedAt = +new Date(); } @@ -135,7 +142,9 @@ export default async function (o) { }) ); } catch (e) { - if (e.message?.endsWith("decipher algorithm")) { + if (e === "no_session_tokens") { + return { error: "youtube.no_session_tokens" }; + } else if (e.message?.endsWith("decipher algorithm")) { return { error: "youtube.decipher" } } else if (e.message?.includes("refresh access token")) { return { error: "youtube.token_expired" }