import { ApiResponse, Innertube, YT } from "youtubei.js"; import { generateRandomString } from "youtubei.js/Utils"; import { compress, decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts"; import { Store } from "@willsoto/node-konfig-core"; import { failedRequests, retryCount, successfulRequests } from "metrics"; import { innertubeEmbeddedClient } from "../../main.ts"; import type { BG } from "bgutils"; let youtubePlayerReqLocation = "youtubePlayerReq"; if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) { if (Deno.env.has("DENO_COMPILED")) { youtubePlayerReqLocation = Deno.mainModule.replace("src/main.ts", "") + Deno.env.get("YT_PLAYER_REQ_LOCATION"); } else { youtubePlayerReqLocation = Deno.env.get( "YT_PLAYER_REQ_LOCATION", ) as string; } } const { youtubePlayerReq } = await import(youtubePlayerReqLocation); const videoCachePath = Deno.env.get("VIDEO_CACHE_PATH") as string || "/var/tmp/youtubei.js/video_cache"; const maxRetries = Deno.env.get("MAX_PROXY_RETRIES") as string || 3; export let kv: Deno.Kv; if ((Deno.env.get("VIDEO_CACHE_ON_DISK")?.toLowerCase() ?? false) == "true") { console.log("[INFO] Storing video cache on disk"); kv = await Deno.openKv(videoCachePath); } else { kv = await Deno.openKv(); } export const youtubePlayerParsing = async ({ innertubeClient, videoId, konfigStore, tokenMinter, overrideCache = false, }: { innertubeClient: Innertube; videoId: string; konfigStore: Store; tokenMinter: BG.WebPoMinter; overrideCache?: boolean; }): Promise => { const cacheEnabled = overrideCache ? false : konfigStore.get("cache.enabled"); const videoCached = (await kv.get(["video_cache", videoId])) .value as Uint8Array; if (videoCached != null && cacheEnabled) { return JSON.parse(new TextDecoder().decode(decompress(videoCached))); } else { let youtubePlayerResponse = await youtubePlayerReq( innertubeClient, videoId, konfigStore, tokenMinter, ); for (let retries = 1; retries <= (maxRetries as number); retries++) { if ( !youtubePlayerResponse.data.playabilityStatus?.errorScreen ?.playerErrorMessageRenderer?.subreason?.runs?.[0]?.text ?.includes("This helps protect our community") ) { break; } console.log( `[DEBUG] Got 'This helps protect our community', retrying request for ${videoId}. Retry ${retries} of ${maxRetries}`, ); retryCount.inc(); youtubePlayerResponse = await youtubePlayerReq( innertubeClient, videoId, konfigStore, ); } if ( youtubePlayerResponse.data.playabilityStatus.status === "UNPLAYABLE" || youtubePlayerResponse.data.playabilityStatus.status === "LOGIN_REQUIRED" || youtubePlayerResponse.data.playabilityStatus.status === "CONTENT_CHECK_REQUIRED" || youtubePlayerResponse.data.playabilityStatus.reason === "Sign in to confirm your age" ) { innertubeEmbeddedClient.session.context.client.visitorData = innertubeClient.session.context.client.visitorData; if (innertubeClient.session.po_token) { innertubeEmbeddedClient.session.po_token = innertubeClient.session.po_token; } innertubeEmbeddedClient.session.player = innertubeClient.session.player; youtubePlayerResponse = await youtubePlayerReq( innertubeEmbeddedClient, videoId, konfigStore, ); } const videoData = youtubePlayerResponse.data; const video = new YT.VideoInfo( [youtubePlayerResponse], innertubeClient.actions, generateRandomString(16), ); const streamingData = video.streaming_data; // Modify the original YouTube response to include deciphered URLs if (streamingData && videoData && videoData.streamingData) { const ecatcherServiceTracking = videoData.responseContext ?.serviceTrackingParams.find((o: { service: string }) => o.service === "ECATCHER" ); const clientNameUsed = ecatcherServiceTracking?.params?.find(( o: { key: string }, ) => o.key === "client.name"); // no need to decipher on IOS nor ANDROID if ( !clientNameUsed?.value.includes("IOS") && !clientNameUsed?.value.includes("ANDROID") ) { for (const [index, format] of streamingData.formats.entries()) { videoData.streamingData.formats[index].url = format .decipher( innertubeClient.session.player, ); if ( videoData.streamingData.formats[index] .signatureCipher !== undefined ) { delete videoData.streamingData.formats[index] .signatureCipher; } if ( videoData.streamingData.formats[index].url.includes( "alr=yes", ) ) { videoData.streamingData.formats[index].url.replace( "alr=yes", "alr=no", ); } else { videoData.streamingData.formats[index].url += "&alr=no"; } } for ( const [index, adaptive_format] of streamingData .adaptive_formats .entries() ) { videoData.streamingData.adaptiveFormats[index].url = adaptive_format .decipher( innertubeClient.session.player, ); if ( videoData.streamingData.adaptiveFormats[index] .signatureCipher !== undefined ) { delete videoData.streamingData.adaptiveFormats[index] .signatureCipher; } if ( videoData.streamingData.adaptiveFormats[index].url .includes("alr=yes") ) { videoData.streamingData.adaptiveFormats[index].url .replace("alr=yes", "alr=no"); } else { videoData.streamingData.adaptiveFormats[index].url += "&alr=no"; } } } } const videoOnlyNecessaryInfo = (( { captions, playabilityStatus, storyboards, streamingData, videoDetails, microformat, }, ) => ({ captions, playabilityStatus, storyboards, streamingData, videoDetails, microformat, }))(videoData); if (videoData.playabilityStatus?.status == "OK") { successfulRequests.inc(); if ( cacheEnabled == true ) { (async () => { await kv.set( ["video_cache", videoId], compress( new TextEncoder().encode( JSON.stringify(videoOnlyNecessaryInfo), ), ), { expireIn: 1000 * 60 * 60, }, ); })(); } else { failedRequests.inc(); } } return videoOnlyNecessaryInfo; } }; export const youtubeVideoInfo = ( innertubeClient: Innertube, youtubePlayerResponseJson: object, ): YT.VideoInfo => { const playerResponse = { success: true, status_code: 200, data: youtubePlayerResponseJson, } as ApiResponse; return new YT.VideoInfo( [playerResponse], innertubeClient.actions, "", ); };