250 lines
8.7 KiB
TypeScript
250 lines
8.7 KiB
TypeScript
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<object> => {
|
|
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,
|
|
"",
|
|
);
|
|
};
|