streamion / src /lib /jobs /potoken.ts
cursorpro's picture
Upload 57 files
5ec2e9b verified
import { Innertube } from "youtubei.js";
import {
youtubePlayerParsing,
youtubeVideoInfo,
} from "../helpers/youtubePlayerHandling.ts";
import type { Config } from "../helpers/config.ts";
import { Metrics } from "../helpers/metrics.ts";
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);
import { InputMessage, OutputMessageSchema } from "./worker.ts";
import { PLAYER_ID } from "../../constants.ts";
interface TokenGeneratorWorker extends Omit<Worker, "postMessage"> {
postMessage(message: InputMessage): void;
}
const workers: TokenGeneratorWorker[] = [];
function createMinter(worker: TokenGeneratorWorker) {
return (videoId: string): Promise<string> => {
const { promise, resolve } = Promise.withResolvers<string>();
// generate a UUID to identify the request as many minter calls
// may be made within a timespan, and this function will be
// informed about all of them until it's got its own
const requestId = crypto.randomUUID();
const listener = (message: MessageEvent) => {
const parsedMessage = OutputMessageSchema.parse(message.data);
if (
parsedMessage.type === "content-token" &&
parsedMessage.requestId === requestId
) {
worker.removeEventListener("message", listener);
resolve(parsedMessage.contentToken);
}
};
worker.addEventListener("message", listener);
worker.postMessage({
type: "content-token-request",
videoId,
requestId,
});
return promise;
};
}
export type TokenMinter = ReturnType<typeof createMinter>;
// Adapted from https://github.com/LuanRT/BgUtils/blob/main/examples/node/index.ts
export const poTokenGenerate = (
config: Config,
metrics: Metrics | undefined,
): Promise<{ innertubeClient: Innertube; tokenMinter: TokenMinter }> => {
const { promise, resolve, reject } = Promise.withResolvers<
Awaited<ReturnType<typeof poTokenGenerate>>
>();
const worker: TokenGeneratorWorker = new Worker(
new URL("./worker.ts", import.meta.url).href,
{
type: "module",
name: "PO Token Generator",
},
);
// take note of the worker so we can kill it once a new one takes its place
workers.push(worker);
worker.addEventListener("message", async (event) => {
const parsedMessage = OutputMessageSchema.parse(event.data);
// worker is listening for messages
if (parsedMessage.type === "ready") {
const untypedPostMessage = worker.postMessage.bind(worker);
worker.postMessage = (message: InputMessage) =>
untypedPostMessage(message);
worker.postMessage({ type: "initialise", config });
}
if (parsedMessage.type === "error") {
console.log({ errorFromWorker: parsedMessage.error });
worker.terminate();
reject(parsedMessage.error);
}
// worker is initialised and has passed back a session token and visitor data
if (parsedMessage.type === "initialised") {
try {
const instantiatedInnertubeClient = await Innertube.create({
enable_session_cache: false,
po_token: parsedMessage.sessionPoToken,
visitor_data: parsedMessage.visitorData,
fetch: getFetchClient(config),
generate_session_locally: true,
cookie: config.youtube_session.cookies || undefined,
player_id: PLAYER_ID,
});
const minter = createMinter(worker);
// check token from minter
await checkToken({
instantiatedInnertubeClient,
config,
integrityTokenBasedMinter: minter,
metrics,
});
console.log("[INFO] Successfully generated PO token");
const numberToKill = workers.length - 1;
for (let i = 0; i < numberToKill; i++) {
const workerToKill = workers.shift();
workerToKill?.terminate();
}
return resolve({
innertubeClient: instantiatedInnertubeClient,
tokenMinter: minter,
});
} catch (err) {
console.log("[WARN] Failed to get valid PO token, will retry", {
err,
});
worker.terminate();
reject(err);
}
}
});
return promise;
};
async function checkToken({
instantiatedInnertubeClient,
config,
integrityTokenBasedMinter,
metrics,
}: {
instantiatedInnertubeClient: Innertube;
config: Config;
integrityTokenBasedMinter: TokenMinter;
metrics: Metrics | undefined;
}) {
const fetchImpl = getFetchClient(config);
try {
console.log("[INFO] Searching for videos to validate PO token");
const searchResults = await instantiatedInnertubeClient.search("news", {
type: "video",
upload_date: "week",
duration: "medium",
});
// Get all videos that have an id property and shuffle them randomly
const videos = searchResults.videos
.filter((video) =>
video.type === "Video" && "id" in video && video.id
)
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
if (videos.length === 0) {
throw new Error("No videos with valid IDs found in search results");
}
// Try up to 3 random videos to validate the token
const maxAttempts = Math.min(3, videos.length);
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const video = videos[attempt];
try {
// Type guard to ensure video has an id property
if (!("id" in video) || !video.id) {
console.log(
`[WARN] Video at index ${attempt} has no valid ID, trying next video`,
);
continue;
}
console.log(
`[INFO] Validating PO token with video: ${video.id}`,
);
const youtubePlayerResponseJson = await youtubePlayerParsing({
innertubeClient: instantiatedInnertubeClient,
videoId: video.id,
config,
tokenMinter: integrityTokenBasedMinter,
metrics,
overrideCache: true,
});
const videoInfo = youtubeVideoInfo(
instantiatedInnertubeClient,
youtubePlayerResponseJson,
);
const validFormat = videoInfo.streaming_data
?.adaptive_formats[0];
if (!validFormat) {
console.log(
`[WARN] No valid format found for video ${video.id}, trying next video`,
);
continue;
}
const result = await fetchImpl(validFormat?.url, {
method: "HEAD",
});
if (result.status !== 200) {
console.log(
`[WARN] Got status ${result.status} for video ${video.id}, trying next video`,
);
continue;
} else {
console.log(
`[INFO] Successfully validated PO token with video: ${video.id}`,
);
return; // Success
}
} catch (err) {
const videoId = ("id" in video && video.id)
? video.id
: "unknown";
console.log(
`[WARN] Failed to validate with video ${videoId}:`,
{ err },
);
if (attempt === maxAttempts - 1) {
throw new Error(
"Failed to validate PO token with any available videos",
);
}
continue;
}
}
// If we reach here, all attempts failed without throwing an exception
throw new Error(
"Failed to validate PO token: all validation attempts returned non-200 status codes",
);
} catch (err) {
console.log("Failed to validate PO token using search method", { err });
throw err;
}
}