Merge pull request #1343 from Materialious/update/1.13.9

Update/1.13.9
This commit is contained in:
Ward
2026-01-25 13:54:28 +00:00
committed by GitHub
18 changed files with 188 additions and 140 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-05T05:19:59.214925354Z">
<DropdownSelection timestamp="2026-01-25T13:45:42.378164664Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/ward/.android/avd/Television_4K.avd" />
<DeviceId pluginId="LocalEmulator" identifier="path=/home/ward/.android/avd/Pixel_9.avd" />
</handle>
</Target>
</DropdownSelection>
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "us.materialio.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 192
versionName "1.13.8"
versionCode 193
versionName "1.13.9"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -91,7 +91,11 @@
<release version="1.13.8" date="2026-1-22">
<release version="1.13.9" date="2026-1-26">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.9</url>
</release>
<release version="1.13.8" date="2026-1-22">
<url>https://github.com/Materialious/Materialious/releases/tag/1.13.8</url>
</release>
<release version="1.13.7" date="2026-1-21">
+32 -3
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.13.8",
"version": "1.13.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.13.8",
"version": "1.13.9",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
@@ -19,7 +19,8 @@
"electron-updater": "^6.3.4",
"electron-window-state": "^5.0.3",
"jsdom": "^27.4.0",
"jsonfile": "^6.2.0"
"jsonfile": "^6.2.0",
"youtubei.js": "^16.0.1"
},
"devDependencies": {
"electron": "^40.0.0",
@@ -84,6 +85,12 @@
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"license": "MIT"
},
"node_modules/@bufbuild/protobuf": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@capacitor-community/electron": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@capacitor-community/electron/-/electron-5.0.1.tgz",
@@ -4607,6 +4614,15 @@
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"license": "CC0-1.0"
},
"node_modules/meriyah": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz",
"integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==",
"license": "ISC",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
@@ -6589,6 +6605,19 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/youtubei.js": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-16.0.1.tgz",
"integrity": "sha512-3802bCAGkBc2/G5WUTc0l/bO5mPYJbQAHL04d9hE9PnrDHoBUT8MN721Yqt4RCNncAXdHcfee9VdJy3Fhq1r5g==",
"funding": [
"https://github.com/sponsors/LuanRT"
],
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"meriyah": "^6.1.4"
}
}
}
}
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "Materialious",
"version": "1.13.8",
"version": "1.13.9",
"description": "Modern material design for Invidious.",
"author": {
"name": "Ward Pearce",
@@ -32,7 +32,8 @@
"electron-updater": "^6.3.4",
"electron-window-state": "^5.0.3",
"jsdom": "^27.4.0",
"jsonfile": "^6.2.0"
"jsonfile": "^6.2.0",
"youtubei.js": "^16.0.1"
},
"devDependencies": {
"electron": "^40.0.0",
@@ -44,4 +45,4 @@
"capacitor",
"electron"
]
}
}
+80 -74
View File
@@ -8,6 +8,7 @@ import electronIsDev from 'electron-is-dev';
import unhandled from 'electron-unhandled';
import { autoUpdater } from 'electron-updater';
import { JSDOM } from 'jsdom';
import type { IGetChallengeResponse } from 'youtubei.js';
import BG, { buildURL, GOOG_API_KEY, USER_AGENT, WebPoSignalOutput } from 'bgutils-js';
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
@@ -74,91 +75,96 @@ app.on('activate', async function () {
});
// Place all ipc or other electron api calls and custom functionality under this line
ipcMain.handle('generatePoToken', async (_, requestKey: string, visitorData: string) => {
const youtubeUrl = 'https://www.youtube.com/';
ipcMain.handle(
'generatePoToken',
async (_, requestKey: string, visitorData: string, challenge: IGetChallengeResponse) => {
const youtubeUrl = 'https://www.youtube.com/';
const dom = new JSDOM(
'<!DOCTYPE html><html lang="en"><head><title>YouTube</title></head><body></body></html>',
{
url: youtubeUrl,
referrer: youtubeUrl,
origin: youtubeUrl,
USER_AGENT
const dom = new JSDOM(
'<!DOCTYPE html><html lang="en"><head><title>YouTube</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
});
if (!Reflect.has(globalThis, 'navigator')) {
Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator });
}
);
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
location: dom.window.location,
origin: dom.window.origin
});
if (!challenge.bg_challenge) {
throw new Error('No BotGuard Token');
}
if (!Reflect.has(globalThis, 'navigator')) {
Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator });
}
const interpreterUrl =
challenge.bg_challenge.interpreter_url
.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
const interpreterHash = challenge.bg_challenge.interpreter_hash;
const challengeResponse = await fetch(buildURL('Create', 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])
});
if (!interpreterUrl || !interpreterHash) {
throw new Error(
`Could not get integrity token. Interpreter Hash: ${challenge.bg_challenge?.interpreter_hash}`
);
}
const challengeResponseData = await challengeResponse.json();
const bgChallenge = BG.Challenge.parseChallengeData(challengeResponseData);
const interpreterResponse = await fetch(`https:${interpreterUrl}`, {
headers: {
'user-agent': USER_AGENT
}
});
if (!bgChallenge) throw new Error('Unable to parse challenge data');
if (!interpreterResponse.ok) {
throw new Error('Unable to fetch interpreter');
}
const interpreterJavascript =
bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
const interpreterJavascript = await interpreterResponse.text();
if (!interpreterJavascript) {
throw new Error(
`Could not get integrity token. Interpreter Hash: ${bgChallenge.interpreterHash}`
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const botguardClient = await BG.BotGuardClient.create({
program: challenge.bg_challenge.program,
globalName: challenge.bg_challenge.global_name,
globalObj: globalThis
});
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-javacript/0.1'
},
body: JSON.stringify([requestKey, botguardResponse])
});
const integrityTokenResponseData = await integrityTokenResponse.json();
const integrityToken = integrityTokenResponseData[0] as string | undefined;
if (!integrityToken) {
throw new Error(`Could not get integrity token. Interpreter Hash: ${interpreterHash}`);
}
const integrityTokenBasedMinter = await BG.WebPoMinter.create(
{ integrityToken },
webPoSignalOutput
);
return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData));
}
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const botguardClient = await BG.BotGuardClient.create({
program: bgChallenge.program,
globalName: bgChallenge.globalName,
globalObj: globalThis
});
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-javacript/0.1'
},
body: JSON.stringify([requestKey, botguardResponse])
});
const integrityTokenResponseData = await integrityTokenResponse.json();
const integrityToken = integrityTokenResponseData[0] as string | undefined;
if (!integrityToken) {
throw new Error(
`Could not get integrity token. Interpreter Hash: ${bgChallenge.interpreterHash}`
);
}
const integrityTokenBasedMinter = await BG.WebPoMinter.create(
{ integrityToken },
webPoSignalOutput
);
return await integrityTokenBasedMinter.mintAsWebsafeString(decodeURIComponent(visitorData));
});
);
ipcMain.handle('setAllowInsecureSSL', async (_, allow) => {
allowInsecureSSL = allow;
+2 -2
View File
@@ -2,8 +2,8 @@ const { contextBridge, ipcRenderer } = require('electron');
require('./rt/electron-rt');
contextBridge.exposeInMainWorld('electronAPI', {
generatePoToken: (requestKey: string, visitorData: string) =>
ipcRenderer.invoke('generatePoToken', requestKey, visitorData),
generatePoToken: (requestKey: string, visitorData: string, challenge: any) =>
ipcRenderer.invoke('generatePoToken', requestKey, visitorData, challenge),
setAllowInsecureSSL: async (allow: boolean) => {
return await ipcRenderer.invoke('setAllowInsecureSSL', allow);
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "materialious",
"version": "1.13.8",
"version": "1.13.9",
"private": true,
"scripts": {
"dev": "npm run patch:github && vite dev",
+6 -3
View File
@@ -1,6 +1,5 @@
// See https://kit.svelte.dev/docs/types#app
import type { IGetChallengeResponse } from 'youtubei.js';
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
@@ -11,7 +10,11 @@ declare global {
}
interface Window {
electronAPI: {
generatePoToken: (requestKey: string, visitorData: string) => Promise<string>;
generatePoToken: (
requestKey: string,
visitorData: string,
challenge: IGetChallengeResponse
) => Promise<string>;
setAllowInsecureSSL: (allowInsecureSSL: boolean) => Promoise<boolean>;
doUpdateCheck: (disableAutoUpdate: boolean) => Promise<void>;
};
+30 -28
View File
@@ -1,45 +1,49 @@
import { BG, buildURL, GOOG_API_KEY, type WebPoSignalOutput } from 'bgutils-js';
import { BG, buildURL, GOOG_API_KEY, USER_AGENT, type WebPoSignalOutput } from 'bgutils-js';
import type { IGetChallengeResponse } from 'youtubei.js';
export async function androidPoTokenMinter(
requestKey: string,
visitorData: string
visitorData: string,
challenge: IGetChallengeResponse
): Promise<string> {
const challengeResponse = await fetch(buildURL('Create', 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])
});
if (!challenge.bg_challenge) {
throw new Error('No BotGuard Token');
}
const challengeResponseData = await challengeResponse.json();
const bgChallenge = BG.Challenge.parseChallengeData(challengeResponseData);
const interpreterUrl =
challenge.bg_challenge.interpreter_url
.private_do_not_access_or_else_trusted_resource_url_wrapped_value;
const interpreterHash = challenge.bg_challenge.interpreter_hash;
if (!bgChallenge) throw new Error('Unable to parse challenge data');
const interpreterJavascript =
bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (!interpreterJavascript) {
if (!interpreterUrl || !interpreterHash) {
throw new Error(
`Could not get integrity token. Interpreter Hash: ${bgChallenge.interpreterHash}`
`Could not get integrity token. Interpreter Hash: ${challenge.bg_challenge?.interpreter_hash}`
);
}
if (!document.getElementById(bgChallenge.interpreterHash)) {
if (!document.getElementById(interpreterHash)) {
const interpreterResponse = await fetch(`https:${interpreterUrl}`, {
headers: {
'user-agent': USER_AGENT
}
});
if (!interpreterResponse.ok) {
throw new Error('Unable to fetch interpreter');
}
const interpreterJavascript = await interpreterResponse.text();
const script = document.createElement('script');
script.type = 'text/javascript';
script.id = bgChallenge.interpreterHash;
script.id = interpreterHash;
script.textContent = interpreterJavascript;
document.head.appendChild(script);
}
const botguardClient = await BG.BotGuardClient.create({
globalObj: globalThis,
globalName: bgChallenge.globalName,
program: bgChallenge.program
program: challenge.bg_challenge.program,
globalName: challenge.bg_challenge.global_name,
globalObj: globalThis
});
const webPoSignalOutput: WebPoSignalOutput = [];
@@ -59,9 +63,7 @@ export async function androidPoTokenMinter(
const integrityToken = integrityTokenResponseData[0] as string | undefined;
if (!integrityToken) {
throw new Error(
`Could not get integrity token. Interpreter Hash: ${bgChallenge.interpreterHash}`
);
throw new Error(`Could not get integrity token. Interpreter Hash: ${interpreterHash}`);
}
const integrityTokenBasedMinter = await BG.WebPoMinter.create(
+8 -6
View File
@@ -1,8 +1,8 @@
import type { SynciousProgressModel } from './model';
import type { ApiExntendedProgressModel } from './model';
import { getVideoProgress } from '.';
const videoIds: string[] = [];
const pendingResolves = new Map<string, (result: SynciousProgressModel | undefined) => void>();
const pendingResolves = new Map<string, (result: ApiExntendedProgressModel | undefined) => void>();
let timeout: ReturnType<typeof setTimeout> | null = null;
const DEBOUNCE_MS = 1000;
@@ -15,10 +15,10 @@ async function processBatches(): Promise<void> {
batches.push(videoIds.splice(0, BATCH_SIZE));
}
const results: SynciousProgressModel[] = [];
const results: ApiExntendedProgressModel[] = [];
for (const batch of batches) {
const res: SynciousProgressModel[] = await getVideoProgress(batch.join(','));
const res: ApiExntendedProgressModel[] = await getVideoProgress(batch.join(','));
results.push(...res);
// Resolve pending promises for this batch
@@ -33,10 +33,12 @@ async function processBatches(): Promise<void> {
}
}
export function queueSyncious(videoId: string): Promise<SynciousProgressModel | undefined> {
export function queueGetWatchProgress(
videoId: string
): Promise<ApiExntendedProgressModel | undefined> {
videoIds.push(videoId);
const promise = new Promise<SynciousProgressModel | undefined>((resolve) => {
const promise = new Promise<ApiExntendedProgressModel | undefined>((resolve) => {
pendingResolves.set(videoId, resolve);
});
+5 -5
View File
@@ -1,4 +1,4 @@
import { patchYoutubeJs } from '$lib/patches/youtubejs';
import { getVideoTYjs } from '$lib/api/youtubejs';
import { Capacitor } from '@capacitor/core';
import { get } from 'svelte/store';
import {
@@ -27,7 +27,7 @@ import type {
ReturnYTDislikes,
SearchSuggestion,
Subscription,
SynciousProgressModel,
ApiExntendedProgressModel,
Video,
VideoPlay
} from './model';
@@ -86,13 +86,13 @@ export async function getVideo(
fetchOptions?: RequestInit
): Promise<VideoPlay> {
if (get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) {
return await patchYoutubeJs(videoId);
return await getVideoTYjs(videoId);
}
const resp = await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`)), fetchOptions);
if (!resp.ok && get(playerYouTubeJsFallback) && Capacitor.isNativePlatform()) {
return await patchYoutubeJs(videoId);
return await getVideoTYjs(videoId);
} else {
await fetchErrorHandle(resp);
}
@@ -447,7 +447,7 @@ function buildApiExtendedAuthHeaders(): Record<string, Record<string, string>> {
export async function getVideoProgress(
videoId: string,
fetchOptions: RequestInit = {}
): Promise<SynciousProgressModel[]> {
): Promise<ApiExntendedProgressModel[]> {
const resp = await fetchErrorHandle(
await fetch(`${get(synciousInstanceStore)}/video/${encodeURIComponent(videoId)}`, {
...buildApiExtendedAuthHeaders(),
+1 -1
View File
@@ -285,7 +285,7 @@ export interface DeArrow {
videoDuration: number;
}
export interface SynciousProgressModel {
export interface ApiExntendedProgressModel {
time: number;
video_id: string;
}
@@ -35,7 +35,7 @@ Platform.shim.eval = async (
return new Function(code)();
};
export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
if (!Capacitor.isNativePlatform()) {
throw new Error('Platform not supported');
}
@@ -82,7 +82,8 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
throw new Error('Unable to pull video info from youtube.js');
}
poTokenCacheStore.set(await platformMinter(requestKey, videoId));
const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
poTokenCacheStore.set(await platformMinter(requestKey, videoId, challengeResponse));
let dashUri: string | undefined;
@@ -90,7 +91,7 @@ export async function patchYoutubeJs(videoId: string): Promise<VideoPlay> {
// https://github.com/LuanRT/googlevideo/issues/42
if (video.streaming_data)
video.streaming_data.adaptive_formats = video.streaming_data.adaptive_formats.filter(
(format) => format.xtags !== 'CgcKAnZiEgEx'
(format) => !format.xtags
);
const adaptiveFormats: AdaptiveFormats[] = [];
@@ -44,7 +44,7 @@
synciousStore
} from '../store';
import { setStatusBarColor } from '../theme';
import { patchYoutubeJs } from '$lib/patches/youtubejs';
import { getVideoTYjs } from '$lib/api/youtubejs';
import {
goToNextVideo,
goToPreviousVideo,
@@ -568,7 +568,7 @@
async function reloadVideo() {
showVideoRetry = false;
data.video = await patchYoutubeJs(data.video.videoId);
data.video = await getVideoTYjs(data.video.videoId);
await loadVideo();
}
@@ -22,7 +22,7 @@
synciousInstanceStore,
synciousStore
} from '../store';
import { queueSyncious } from '$lib/api/apiExtended';
import { queueGetWatchProgress } from '$lib/api/apiExtended';
interface Props {
video: VideoBase | Video | Notification | PlaylistPageVideo;
@@ -109,7 +109,7 @@
if (get(synciousStore) && get(synciousInstanceStore) && get(authStore)) {
try {
progress = (await queueSyncious(video.videoId))?.time?.toString() ?? undefined;
progress = (await queueGetWatchProgress(video.videoId))?.time?.toString() ?? undefined;
} catch {
// Continue regardless of error
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import re
from datetime import datetime
LATEST_VERSION = "1.13.8"
LATEST_VERSION = "1.13.9"
RELEASE_DATE = datetime.now().strftime("%Y-%-m-%d") # Format: YYYY-M-D
WORKING_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "materialious")