From 5c032b2dfeeeb7b7ecfbd5f0299673e214ff938a Mon Sep 17 00:00:00 2001 From: WardPearce Date: Sun, 4 May 2025 11:31:32 +1200 Subject: [PATCH] Improvements to PO token generation and caching --- materialious/android/.idea/misc.xml | 1 - materialious/electron/package-lock.json | 4 +- materialious/electron/src/index.ts | 182 ++++++++++-------- materialious/electron/src/preload.ts | 16 +- materialious/src/app.d.ts | 12 +- .../src/lib/android/youtube/minter.ts | 79 ++++---- materialious/src/lib/components/Player.svelte | 2 - materialious/src/lib/navPages.ts | 71 +++---- materialious/src/lib/patches/youtubejs.ts | 32 ++- 9 files changed, 198 insertions(+), 201 deletions(-) diff --git a/materialious/android/.idea/misc.xml b/materialious/android/.idea/misc.xml index 74dd639e..b2c751a3 100644 --- a/materialious/android/.idea/misc.xml +++ b/materialious/android/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/materialious/electron/package-lock.json b/materialious/electron/package-lock.json index d7bc18b9..8693089f 100644 --- a/materialious/electron/package-lock.json +++ b/materialious/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "Materialious", - "version": "1.8.1", + "version": "1.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Materialious", - "version": "1.8.1", + "version": "1.8.2", "license": "MIT", "dependencies": { "@capacitor-community/electron": "^5.0.0", diff --git a/materialious/electron/src/index.ts b/materialious/electron/src/index.ts index 7cabcc30..92b385cb 100644 --- a/materialious/electron/src/index.ts +++ b/materialious/electron/src/index.ts @@ -1,5 +1,8 @@ import type { CapacitorElectronConfig } from '@capacitor-community/electron'; -import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron'; +import { + getCapacitorElectronConfig, + setupElectronDeepLinking +} from '@capacitor-community/electron'; import type { MenuItemConstructorOptions } from 'electron'; import { app, ipcMain, MenuItem } from 'electron'; import electronIsDev from 'electron-is-dev'; @@ -7,17 +10,25 @@ import unhandled from 'electron-unhandled'; import { autoUpdater } from 'electron-updater'; import { JSDOM } from 'jsdom'; -import BG, { buildURL, GOOG_API_KEY, USER_AGENT, WebPoSignalOutput } from 'bgutils-js'; +import BG, { + buildURL, + DescrambledChallenge, + GOOG_API_KEY, + USER_AGENT, + WebPoSignalOutput +} from 'bgutils-js'; import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup'; // Graceful handling of unhandled errors. unhandled(); // Define our menu templates (these are optional) -const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })]; +const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ + new MenuItem({ label: 'Quit App', role: 'quit' }) +]; const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ - { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, - { role: 'viewMenu' }, + { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, + { role: 'viewMenu' } ]; // Get Config options from capacitor.config @@ -25,116 +36,121 @@ const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig( // Initialize our app. You can pass menu templates into the app here. // const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig); -const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate); +const myCapacitorApp = new ElectronCapacitorApp( + capacitorFileConfig, + trayMenuTemplate, + appMenuBarMenuTemplate +); // If deeplinking is enabled then we will set it up here. if (capacitorFileConfig.electron?.deepLinkingEnabled) { - setupElectronDeepLinking(myCapacitorApp, { - customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp', - }); + setupElectronDeepLinking(myCapacitorApp, { + customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp' + }); } // If we are in Dev mode, use the file watcher components. if (electronIsDev) { - setupReloadWatcher(myCapacitorApp); + setupReloadWatcher(myCapacitorApp); } // Run Application (async () => { - // Wait for electron app to be ready. - await app.whenReady(); - // Security - Set Content-Security-Policy based on whether or not we are in dev mode. - setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); - // Initialize our app, build windows, and load content. - await myCapacitorApp.init(); - // Check for updates if we are in a packaged app. - autoUpdater.checkForUpdatesAndNotify(); + // Wait for electron app to be ready. + await app.whenReady(); + // Security - Set Content-Security-Policy based on whether or not we are in dev mode. + setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme()); + // Initialize our app, build windows, and load content. + await myCapacitorApp.init(); + // Check for updates if we are in a packaged app. + autoUpdater.checkForUpdatesAndNotify(); })(); // Handle when all of our windows are close (platforms have their own expectations). app.on('window-all-closed', function () { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } + // On OS X it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') { + app.quit(); + } }); // When the dock icon is clicked. app.on('activate', async function () { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (myCapacitorApp.getMainWindow().isDestroyed()) { - await myCapacitorApp.init(); - } + // On OS X it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (myCapacitorApp.getMainWindow().isDestroyed()) { + await myCapacitorApp.init(); + } }); // Place all ipc or other electron api calls and custom functionality under this line -ipcMain.handle('generatePoToken', async ( - _, - challengeResponse: any, - requestKey: string, - visitorData: string, - videoId: string -) => { - if (!challengeResponse.bg_challenge) - throw new Error('Yt.js: Could not get challenge'); +ipcMain.handle( + 'generatePoToken', + async (_, bgChallenge: any, requestKey: string, visitorData: string) => { + const youtubeUrl = 'https://www.youtube.com/'; - const dom = new JSDOM('', { - url: 'https://www.youtube.com/', - referrer: 'https://www.youtube.com/', - USER_AGENT - }); + const dom = new JSDOM( + '', + { + url: youtubeUrl, + referrer: youtubeUrl, + origin: youtubeUrl, + USER_AGENT + } + ); - Object.assign(globalThis, { - window: dom.window, - document: dom.window.document, - location: dom.window.location, - origin: dom.window.origin - }); + Object.assign(globalThis, { + window: dom.window, + document: dom.window.document, + location: dom.window.location, + origin: dom.window.origin + }); - if (!Reflect.has(globalThis, 'navigator')) { - Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator }); - } + if (!Reflect.has(globalThis, 'navigator')) { + Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator }); + } - 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(); + const interpreterUrl = + bgChallenge.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('Could not load VM'); + if (interpreterJavascript) { + new Function(interpreterJavascript)(); + } else throw new Error('Could not load VM'); - const botguardClient = await BG.BotGuardClient.create({ - program: challengeResponse.bg_challenge.program, - globalName: challengeResponse.bg_challenge.global_name, - globalObj: globalThis - }); + const botguardClient = await BG.BotGuardClient.create({ + program: bgChallenge.program, + globalName: bgChallenge.global_name, + globalObj: globalThis + }); - const webPoSignalOutput: WebPoSignalOutput = []; - const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput }); + const webPoSignalOutput: WebPoSignalOutput = []; + const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput }); - 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 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(); + const integrityTokenResponseData = await integrityTokenResponse.json(); - if (typeof integrityTokenResponseData[0] !== 'string') - throw new Error('Yt.js: Could not get integrity token'); + if (typeof integrityTokenResponseData[0] !== 'string') + throw new Error('Could not get integrity token'); - const integrityToken = integrityTokenResponseData[0]; + const integrityToken = integrityTokenResponseData[0]; - const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput); + const integrityTokenBasedMinter = await BG.WebPoMinter.create( + { integrityToken }, + webPoSignalOutput + ); - return [ - await integrityTokenBasedMinter.mintAsWebsafeString(visitorData), - await integrityTokenBasedMinter.mintAsWebsafeString(videoId) - ]; -}); \ No newline at end of file + return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData)); + } +); diff --git a/materialious/electron/src/preload.ts b/materialious/electron/src/preload.ts index 0789db91..e54e2e00 100644 --- a/materialious/electron/src/preload.ts +++ b/materialious/electron/src/preload.ts @@ -1,15 +1,9 @@ +import { DescrambledChallenge } from 'bgutils-js'; + const { contextBridge, ipcRenderer } = require('electron'); require('./rt/electron-rt'); contextBridge.exposeInMainWorld('electronAPI', { - generatePoToken: (challengeResponse: any, - requestKey: string, - visitorData: string, - videoId: string) => ipcRenderer.invoke( - 'generatePoToken', - challengeResponse, - requestKey, - visitorData, - videoId - ) -}); \ No newline at end of file + generatePoToken: (bgChallenge: DescrambledChallenge, requestKey: string, visitorData: string) => + ipcRenderer.invoke('generatePoToken', bgChallenge, requestKey, visitorData) +}); diff --git a/materialious/src/app.d.ts b/materialious/src/app.d.ts index 7d625e6a..0ce56bce 100644 --- a/materialious/src/app.d.ts +++ b/materialious/src/app.d.ts @@ -1,6 +1,6 @@ // See https://kit.svelte.dev/docs/types#app -import type { IGetChallengeResponse } from "youtubei.js"; +import type { IGetChallengeResponse } from 'youtubei.js'; // for information about these interfaces declare global { @@ -14,14 +14,12 @@ declare global { interface Window { electronAPI: { generatePoToken: ( - challengeResponse: IGetChallengeResponse, + bgChallenge: IGetChallengeResponse, requestKey: string, - visitorData: string, - videoId: string - ) => Promise<[string, string]>; + visitorData: string + ) => Promise; }; } } -export { }; - +export {}; diff --git a/materialious/src/lib/android/youtube/minter.ts b/materialious/src/lib/android/youtube/minter.ts index 2f7678c3..d24db38a 100644 --- a/materialious/src/lib/android/youtube/minter.ts +++ b/materialious/src/lib/android/youtube/minter.ts @@ -1,53 +1,50 @@ import { BG, buildURL, GOOG_API_KEY, type WebPoSignalOutput } from 'bgutils-js'; -import { type IGetChallengeResponse } from 'youtubei.js'; +import type { IBotguardChallenge } from 'youtubei.js'; export async function androidPoTokenMinter( - challengeResponse: IGetChallengeResponse, - requestKey: string, - visitorData: string, - videoId: string -): Promise<[string, string]> { - if (!challengeResponse.bg_challenge) - throw new Error('Yt.js: Could not get challenge'); + bgChallenge: IBotguardChallenge, + requestKey: string, + visitorData: string +): Promise { + const interpreterUrl = + bgChallenge.interpreter_url.private_do_not_access_or_else_trusted_resource_url_wrapped_value; + const bgScriptResponse = await fetch(`https:${interpreterUrl}`); + const interpreterJavascript = await bgScriptResponse.text(); - 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('Could not load VM'); - if (interpreterJavascript) { - new Function(interpreterJavascript)(); - } else throw new Error('Could not load VM'); + const botguardClient = await BG.BotGuardClient.create({ + program: bgChallenge.program, + globalName: bgChallenge.global_name, + globalObj: globalThis + }); - 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 }); - const webPoSignalOutput: WebPoSignalOutput = []; - const botguardResponse = await botguardClient.snapshot({ webPoSignalOutput }); + 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 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(); - const integrityTokenResponseData = await integrityTokenResponse.json(); + if (typeof integrityTokenResponseData[0] !== 'string') + throw new Error('Could not get integrity token'); - if (typeof integrityTokenResponseData[0] !== 'string') - throw new Error('Yt.js: Could not get integrity token'); + const integrityToken = integrityTokenResponseData[0]; - const integrityToken = integrityTokenResponseData[0]; + const integrityTokenBasedMinter = await BG.WebPoMinter.create( + { integrityToken }, + webPoSignalOutput + ); - const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken }, webPoSignalOutput); - - return [ - await integrityTokenBasedMinter.mintAsWebsafeString(visitorData), - await integrityTokenBasedMinter.mintAsWebsafeString(videoId) - ]; -} \ No newline at end of file + return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData)); +} diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 7e00fa2a..1cbcae5d 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -451,8 +451,6 @@ if (videoFormatId) videoPlaybackAbrRequest.selectedFormatIds.push(videoFormatId); } - console.log(videoPlaybackAbrRequest); - request.body = Protos.VideoPlaybackAbrRequest.encode(videoPlaybackAbrRequest).finish(); const byteRange = headers.Range diff --git a/materialious/src/lib/navPages.ts b/materialious/src/lib/navPages.ts index 28d14a14..d63fccb9 100644 --- a/materialious/src/lib/navPages.ts +++ b/materialious/src/lib/navPages.ts @@ -1,39 +1,40 @@ import { _ } from 'svelte-i18n'; import { get } from 'svelte/store'; - // Must be a func do to how i18n is loaded -export function getPages(): { icon: string, href: string, name: string, requiresAuth: boolean; }[] { - return [ - { - icon: 'home', - href: '/', - name: get(_)('pages.home'), - requiresAuth: false - }, - { - icon: 'whatshot', - href: '/trending', - name: get(_)('pages.trending'), - requiresAuth: false - }, - { - icon: 'subscriptions', - href: '/subscriptions', - name: get(_)('pages.subscriptions'), - requiresAuth: true - }, - { - icon: 'video_library', - href: '/playlists', - name: get(_)('pages.playlists'), - requiresAuth: true - }, - { - icon: 'history', - href: '/history', - name: get(_)('pages.history'), - requiresAuth: true - } - ]; -} \ No newline at end of file +export function getPages(): { icon: string; href: string; name: string; requiresAuth: boolean }[] { + return [ + { + icon: 'home', + href: '/', + name: get(_)('pages.home'), + requiresAuth: false + }, + { + icon: 'whatshot', + href: '/trending', + name: get(_)('pages.trending'), + requiresAuth: false + }, + { + icon: 'subscriptions', + href: '/subscriptions', + name: get(_)('pages.subscriptions'), + requiresAuth: true + }, + { + icon: 'video_library', + href: '/playlists', + name: get(_)('pages.playlists'), + requiresAuth: true + } + // Temporarily disable history page due to limitations with + // Invidious's API + // { + // icon: 'history', + // href: '/history', + // name: get(_)('pages.history'), + // requiresAuth: true + // } + ]; +} diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index baef9927..88cdf7bd 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -29,30 +29,24 @@ export async function patchYoutubeJs(videoId: string): Promise { enable_session_cache: false }); - let sessionPoToken: string; - let contentPoToken: string; + if (!get(poTokenCacheStore)) { + const visitorData = youtube.session.context.client.visitorData ?? ''; - const requestKey = 'O43z0dpjhgX20SCx4KAo'; - const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND'); + const requestKey = 'O43z0dpjhgX20SCx4KAo'; + const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND'); - if (Capacitor.getPlatform() === 'android') { - [sessionPoToken, contentPoToken] = await androidPoTokenMinter( - challengeResponse, - requestKey, - youtube.session.context.client.visitorData ?? '', - videoId - ); - } else { - [sessionPoToken, contentPoToken] = await window.electronAPI.generatePoToken( - challengeResponse, - requestKey, - youtube.session.context.client.visitorData ?? '', - videoId + if (!challengeResponse.bg_challenge) throw new Error('Could not get challenge'); + + const platformMinter = + Capacitor.getPlatform() === 'android' + ? androidPoTokenMinter + : window.electronAPI.generatePoToken; + + poTokenCacheStore.set( + await platformMinter(challengeResponse.bg_challenge, requestKey, visitorData) ); } - poTokenCacheStore.set(sessionPoToken); - const extraArgs: Record = { playbackContext: { contentPlaybackContext: {