Improvements to PO token generation and caching

This commit is contained in:
WardPearce
2025-05-04 11:31:32 +12:00
parent 895e9eac4c
commit 5c032b2dfe
9 changed files with 198 additions and 201 deletions
-1
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+2 -2
View File
@@ -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",
+99 -83
View File
@@ -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('<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>', {
url: 'https://www.youtube.com/',
referrer: 'https://www.youtube.com/',
USER_AGENT
});
const dom = new JSDOM(
'<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>',
{
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)
];
});
return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData));
}
);
+5 -11
View File
@@ -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
)
});
generatePoToken: (bgChallenge: DescrambledChallenge, requestKey: string, visitorData: string) =>
ipcRenderer.invoke('generatePoToken', bgChallenge, requestKey, visitorData)
});
+5 -7
View File
@@ -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<string>;
};
}
}
export { };
export {};
+38 -41
View File
@@ -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<string> {
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)
];
}
return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData));
}
@@ -451,8 +451,6 @@
if (videoFormatId) videoPlaybackAbrRequest.selectedFormatIds.push(videoFormatId);
}
console.log(videoPlaybackAbrRequest);
request.body = Protos.VideoPlaybackAbrRequest.encode(videoPlaybackAbrRequest).finish();
const byteRange = headers.Range
+36 -35
View File
@@ -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
}
];
}
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
// }
];
}
+13 -19
View File
@@ -29,30 +29,24 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
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<string, any> = {
playbackContext: {
contentPlaybackContext: {