Update local fallback to latest logic

This commit is contained in:
WardPearce
2025-04-01 13:07:59 +13:00
parent 9863cf74df
commit 52126e5366
7 changed files with 144 additions and 64 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ const config: CapacitorConfig = {
webDir: 'build',
plugins: {
CapacitorNodeJS: {
nodeDir: 'nodejs-android'
nodeDir: 'nodejs-android',
}
},
};
+5
View File
@@ -229,7 +229,12 @@ export function setupContentSecurityPolicy(customScheme: string): void {
});
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
const uri = new URL(details.url);
details.requestHeaders['User-Agent'] = USER_AGENT;
details.requestHeaders['origin'] = uri.origin;
details.requestHeaders['host'] = uri.host;
callback({ requestHeaders: details.requestHeaders });
});
}
@@ -7,6 +7,8 @@ if (Capacitor.getPlatform() === 'android') {
const currentOrigin: string = window.location.protocol + '//' + window.location.host;
function needsProxying(target: string): boolean {
if (!target.startsWith('http')) return false;
const targetOriginMatch = /^https?:\/\/([^\/]+)/i.exec(target);
return (targetOriginMatch && targetOriginMatch[0].toLowerCase()) !== currentOrigin;
}
@@ -14,16 +16,28 @@ if (Capacitor.getPlatform() === 'android') {
const corsProxyUrl: string = 'http://localhost:3000/';
window.fetch = async (requestInput: string | URL | Request, requestOptions?: RequestInit): Promise<Response> => {
const uri = requestInput.toString();
console.log(uri, needsProxying(uri));
const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString();
if (needsProxying(uri)) {
requestInput = corsProxyUrl + uri;
if (requestInput instanceof Request) {
requestInput = new Request(corsProxyUrl + uri, {
method: requestInput.method,
headers: requestInput.headers,
body: requestInput.body,
mode: requestInput.mode,
credentials: requestInput.credentials,
cache: requestInput.cache,
redirect: requestInput.redirect,
referrer: requestInput.referrer,
integrity: requestInput.integrity,
keepalive: requestInput.keepalive,
...(requestInput.body ? { duplex: "half" } : {})
});
} else {
requestInput = corsProxyUrl + uri;
}
}
console.log(requestInput);
// Use the original fetch with the proxied URL and options
return originalFetch(requestInput, requestOptions);
};
@@ -151,7 +151,11 @@
url.searchParams.set('range', request.headers.Range.split('=')[1]);
url.searchParams.set('ump', '1');
url.searchParams.set('srfvp', '1');
url.searchParams.set('pot', get(poTokenCacheStore).poToken);
const cachedPoToken = get(poTokenCacheStore);
if (cachedPoToken) url.searchParams.set('pot', cachedPoToken);
delete request.headers.Range;
}
+103 -51
View File
@@ -2,72 +2,124 @@ import type { AdaptiveFormats, Captions, Image, StoryBoard, Thumbnail, VideoBase
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
import { numberWithCommas } from '$lib/time';
import { Capacitor } from '@capacitor/core';
import { BG, type BgConfig } from 'bgutils-js';
import { BG, buildURL, GOOG_API_KEY, type WebPoSignalOutput } from 'bgutils-js';
import { Buffer } from 'buffer';
import { get } from 'svelte/store';
import { Innertube, UniversalCache } from 'youtubei.js';
import { Innertube, UniversalCache, YT, YTNodes } from 'youtubei.js';
type WebPoMinter = {
integrityTokenBasedMinter?: BG.WebPoMinter;
botguardClient?: BG.BotGuardClient;
};
async function getWebPoMinter(youtube: Innertube): Promise<WebPoMinter> {
const requestKey = 'O43z0dpjhgX20SCx4KAo';
const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
if (!challengeResponse.bg_challenge)
throw new Error('Yt.js: Could not get challenge');
const interpreterUrl = challengeResponse.bg_challenge.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
const bgScriptResponse = await fetch(`https:${interpreterUrl}`);
const interpreterJavascript = await bgScriptResponse.text();
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Yt.js: Could not load VM');
const botguardClient = await BG.BotGuardClient.create({
program: challengeResponse.bg_challenge.program,
globalName: challengeResponse.bg_challenge.global_name,
globalObj: globalThis
});
const webPoSignalOutput: WebPoSignalOutput = [];
const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput });
console.log('botguardResponse', botguardResponse);
const integrityTokenResponse = await fetch(buildURL('GenerateIT', true), {
method: 'POST',
headers: {
'content-type': 'application/json+protobuf',
'x-goog-api-key': GOOG_API_KEY,
'x-user-agent': 'grpc-web-javascript/0.1',
},
body: JSON.stringify([requestKey, botguardResponse])
});
const integrityTokenResponseData = await integrityTokenResponse.json();
console.log(integrityTokenResponseData);
if (typeof integrityTokenResponseData[0] !== 'string')
throw new Error('Yt.js: Could not get integrity token');
const integrityToken = integrityTokenResponseData[0] as string | undefined;
const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput);
return {
integrityTokenBasedMinter,
botguardClient
};
}
export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
if (!Capacitor.isNativePlatform()) {
throw new Error('Platform not supported');
throw new Error('Yt.js: Platform not supported');
}
let youtube: Innertube;
if (!get(poTokenCacheStore)) {
youtube = await Innertube.create({ retrieve_player: false, fetch: fetch });
const requestKey = 'O43z0dpjhgX20SCx4KAo';
const visitorData = youtube.session.context.client.visitorData;
if (!visitorData)
throw new Error('Could not get visitor data');
const bgConfig: BgConfig = {
fetch: (input: string | URL | Request, init?: RequestInit) => fetch(input, init),
globalObj: globalThis,
identifier: visitorData,
requestKey
};
const bgChallenge = await BG.Challenge.create(bgConfig);
if (!bgChallenge)
throw new Error('Could not get challenge');
const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const poTokenResult = await BG.PoToken.generate({
program: bgChallenge.program,
globalName: bgChallenge.globalName,
bgConfig
});
poTokenCacheStore.set({
poToken: poTokenResult.poToken,
visitorData: visitorData
});
}
const cachedPoToken = get(poTokenCacheStore);
youtube = await Innertube.create({
const youtube = await Innertube.create({
fetch: fetch,
generate_session_locally: true,
cache: new UniversalCache(false),
location: get(interfaceRegionStore),
visitor_data: cachedPoToken.visitorData,
po_token: cachedPoToken.poToken
});
const video = await youtube.getInfo(videoId);
let sessionWebPo: string | undefined;
const { integrityTokenBasedMinter } = await getWebPoMinter(youtube);
if (integrityTokenBasedMinter) {
sessionWebPo = await integrityTokenBasedMinter.mintAsWebsafeString(youtube.session.context.client.visitorData ?? '');
}
poTokenCacheStore.set(sessionWebPo);
const extraArgs: Record<string, any> = {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: youtube.session.player?.sts
}
},
contentCheckOk: true,
racyCheckOk: true
};
// Generate content WebPO token.
if (integrityTokenBasedMinter) {
extraArgs.serviceIntegrityDimensions = {
poToken: await integrityTokenBasedMinter.mintAsWebsafeString(videoId)
};
}
const watchEndpoint = new YTNodes.NavigationEndpoint({ watchEndpoint: { videoId } });
const rawPlayerResponse = await watchEndpoint.call(youtube.actions, extraArgs);
const rawNextResponse = await watchEndpoint.call(youtube.actions, {
override_endpoint: '/next',
racyCheckOk: true,
contentCheckOk: true
});
const video = new YT.VideoInfo([rawPlayerResponse, rawNextResponse], youtube!.actions, '');
if (!video.primary_info || !video.secondary_info) {
throw new Error('Unable to pull video info from youtube.js');
throw new Error('Yt.js: Unable to pull video info from youtube.js');
}
let dashUri: string = '';
+1 -1
View File
@@ -101,6 +101,6 @@ export const playlistSettingsStore: Writable<Record<string, { shuffle: boolean;
writable({});
export const poTokenCacheStore: Writable<{ poToken: string, visitorData: string; }> = writable();
export const poTokenCacheStore: Writable<string | undefined> = writable();
export const searchHistoryStore: Writable<string[]> = persisted('searchHistory', []);
+9 -4
View File
@@ -5,10 +5,14 @@ const HOST = 'localhost';
const PORT = 3000;
const MAX_REDIRECTS = 10;
const CORS_HEADERS = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-goog-visitor-id, x-goog-api-key, x-origin, x-youtube-client-version, x-youtube-client-name, x-goog-api-format-version, x-user-agent, Accept-Language, Range, Referer'
const CORS_ORIGIN = 'https://localhost'
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36(KHTML, like Gecko)'
function setCorsHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', '*');
res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS);
res.setHeader('Access-Control-Max-Age', '86400')
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
@@ -89,6 +93,7 @@ const server = http.createServer(async (req, res) => {
options.headers.host = parsedTarget.host;
options.headers.origin = parsedTarget.origin;
options.headers['user-agent'] = USER_AGENT;
// For POST and PUT methods, pass the body to the outgoing request
if (req.method === 'POST' || req.method === 'PUT') {
@@ -106,9 +111,9 @@ const server = http.createServer(async (req, res) => {
res.writeHead(proxyRes.statusCode, {
...proxyRes.headers,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Origin': CORS_ORIGIN,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Headers': CORS_HEADERS,
'Access-Control-Allow-Credentials': 'true',
});