From 7b61513d35e53948868670d713820447aca6e413 Mon Sep 17 00:00:00 2001 From: WardPearce Date: Thu, 1 May 2025 10:59:33 +1200 Subject: [PATCH] Fixed sabr on electron --- materialious/package-lock.json | 34 +-- materialious/package.json | 5 +- .../src/lib/android/http/androidRequests.ts | 96 ++++---- materialious/src/lib/components/Player.svelte | 7 +- materialious/src/lib/patches/youtubejs.ts | 5 +- materialious/src/lib/sabr/sabrUmpParser.ts | 3 +- materialious/src/lib/sabr/shakaHttpPlugin.ts | 4 +- materialious/static/nodejs-android/index.js | 220 ++++++++++-------- 8 files changed, 187 insertions(+), 187 deletions(-) diff --git a/materialious/package-lock.json b/materialious/package-lock.json index eb579c26..eaa7307f 100644 --- a/materialious/package-lock.json +++ b/materialious/package-lock.json @@ -23,7 +23,7 @@ "bgutils-js": "^3.2.0", "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", "fuse.js": "^7.0.0", - "googlevideo": "^2.0.0", + "googlevideo": "^3.0.0", "he": "^1.2.0", "human-number": "^2.0.4", "iso-3166": "^4.3.0", @@ -54,7 +54,6 @@ "@typescript-eslint/eslint-plugin": "^7.16.0", "@typescript-eslint/parser": "^7.18.0", "@vite-pwa/sveltekit": "^0.6.6", - "buffer": "^6.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.45.1", @@ -4620,31 +4619,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -7370,9 +7344,9 @@ "license": "MIT" }, "node_modules/googlevideo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/googlevideo/-/googlevideo-2.0.0.tgz", - "integrity": "sha512-OVlNWZ07TPIelaEII6mH9od+Cxljl7P4AzhEYVNN5d4FhFT9L5otpcLtgvraTE9u69KfVVw+L4pVeczArcD33w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/googlevideo/-/googlevideo-3.0.0.tgz", + "integrity": "sha512-8LGFjbZFG4RCsyhMbjX2+IlU5Suj94FeQr1JandPSsKfAMrqL08pOoQsBYl/5P+bqIFm0wQEVpKIhm5wpiekwg==", "funding": [ "https://github.com/sponsors/LuanRT" ], diff --git a/materialious/package.json b/materialious/package.json index d5975351..70d4019c 100644 --- a/materialious/package.json +++ b/materialious/package.json @@ -26,7 +26,6 @@ "@typescript-eslint/eslint-plugin": "^7.16.0", "@typescript-eslint/parser": "^7.18.0", "@vite-pwa/sveltekit": "^0.6.6", - "buffer": "^6.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.45.1", @@ -55,7 +54,7 @@ "bgutils-js": "^3.2.0", "capacitor-nodejs": "https://github.com/EdenwareApps/Capacitor-NodeJS/releases/download/v1.0.0-beta.7/capacitor6-nodejs.tgz", "fuse.js": "^7.0.0", - "googlevideo": "^2.0.0", + "googlevideo": "^3.0.0", "he": "^1.2.0", "human-number": "^2.0.4", "iso-3166": "^4.3.0", @@ -72,4 +71,4 @@ "terser": "^5.34.1", "youtubei.js": "^13.4.0" } -} \ No newline at end of file +} diff --git a/materialious/src/lib/android/http/androidRequests.ts b/materialious/src/lib/android/http/androidRequests.ts index ff355a23..a3fe52ed 100644 --- a/materialious/src/lib/android/http/androidRequests.ts +++ b/materialious/src/lib/android/http/androidRequests.ts @@ -1,52 +1,56 @@ -import { goto } from "$app/navigation"; -import { Capacitor } from "@capacitor/core"; +import { goto } from '$app/navigation'; +import { Capacitor } from '@capacitor/core'; + +const originalFetch = window.fetch; +const corsProxyUrl: string = 'http://localhost:3000/'; + +function needsProxying(target: string): boolean { + if (!target.startsWith('http')) return false; + return true; +} + +export const androidFetch = async ( + requestInput: string | URL | Request, + requestOptions?: RequestInit +): Promise => { + const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString(); + + if (needsProxying(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; + } + } + + // Use the original fetch with the proxied URL and options + return originalFetch(requestInput, requestOptions); +}; if (Capacitor.getPlatform() === 'android') { - const originalFetch = window.fetch; + window.fetch = androidFetch; - function needsProxying(target: string): boolean { - if (!target.startsWith('http')) return false; - return true; - } + const originalXhrOpen = XMLHttpRequest.prototype.open; - const corsProxyUrl: string = 'http://localhost:3000/'; + XMLHttpRequest.prototype.open = function (...args: any[]): void { + if (needsProxying(args[1])) { + args[1] = corsProxyUrl + args[1]; + } + /* @ts-ignore */ + return originalXhrOpen.apply(this, args); + }; - window.fetch = async (requestInput: string | URL | Request, requestOptions?: RequestInit): Promise => { - const uri = requestInput instanceof Request ? requestInput.url : requestInput.toString(); - - if (needsProxying(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; - } - } - - // Use the original fetch with the proxied URL and options - return originalFetch(requestInput, requestOptions); - }; - - const originalXhrOpen = XMLHttpRequest.prototype.open; - - XMLHttpRequest.prototype.open = function (...args: any[]): void { - if (needsProxying(args[1])) { - args[1] = corsProxyUrl + args[1]; - } - /* @ts-ignore */ - return originalXhrOpen.apply(this, args); - }; - - setTimeout(() => goto('/', { replaceState: true }), 100); + setTimeout(() => goto('/', { replaceState: true }), 100); } diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index fccd8970..6a8cd9c6 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -39,7 +39,7 @@ } from '../store'; import { getDynamicTheme, setStatusBarColor } from '../theme'; import { HttpFetchPlugin, type SabrStreamingContext } from '$lib/sabr/shakaHttpPlugin'; - import { Constants, Innertube, type Misc } from 'youtubei.js'; + import { Constants, type Misc } from 'youtubei.js'; import { fromFormat, fromFormatInitializationMetadata, @@ -174,6 +174,8 @@ return; } + HttpFetchPlugin.cacheManager.clearCache(); + player = new shaka.Player(); playerElement = document.getElementById('player') as HTMLMediaElement; @@ -500,9 +502,8 @@ } } } else if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) { - const innertube = await Innertube.create({ fetch: window.fetch }); const wrapped = {} as Record; - wrapped.context = innertube.session.context; + wrapped.context = data.video.ytjs?.innertube.session.context; wrapped.cpn = data.video.ytjs?.clientPlaybackNonce; wrapped.drmParams = decodeURIComponent(drmParams || ''); wrapped.drmSystem = 'DRM_SYSTEM_WIDEVINE'; diff --git a/materialious/src/lib/patches/youtubejs.ts b/materialious/src/lib/patches/youtubejs.ts index 40553994..baef9927 100644 --- a/materialious/src/lib/patches/youtubejs.ts +++ b/materialious/src/lib/patches/youtubejs.ts @@ -13,7 +13,6 @@ import { fromFormat } from '$lib/sabr/formatKeyUtils'; import { interfaceRegionStore, poTokenCacheStore } from '$lib/store'; import { Capacitor } from '@capacitor/core'; import { USER_AGENT } from 'bgutils-js'; -import { Buffer } from 'buffer'; import { get } from 'svelte/store'; import { Innertube, UniversalCache, Utils, YT, YTNodes } from 'youtubei.js'; @@ -92,13 +91,11 @@ export async function patchYoutubeJs(videoId: string): Promise { let dashUri: string | undefined; if (video.streaming_data) { - video.streaming_data.adaptive_formats = video.streaming_data.adaptive_formats.map((format) => { + video.streaming_data.adaptive_formats.forEach((format) => { const formatKey = fromFormat(format) || ''; format.url = `https://sabr?___key=${formatKey}`; format.signature_cipher = undefined; format.decipher = () => format.url || ''; - - return format; }); if (video.basic_info.is_live) { diff --git a/materialious/src/lib/sabr/sabrUmpParser.ts b/materialious/src/lib/sabr/sabrUmpParser.ts index b439d637..b4318990 100644 --- a/materialious/src/lib/sabr/sabrUmpParser.ts +++ b/materialious/src/lib/sabr/sabrUmpParser.ts @@ -1,4 +1,5 @@ -import { GoogleVideo, Protos, concatenateChunks, Part, PART } from 'googlevideo'; +import { GoogleVideo, Protos, concatenateChunks, PART } from 'googlevideo'; +import type { Part } from 'googlevideo'; import shaka from 'shaka-player/dist/shaka-player.ui'; import { cacheSegment } from './cacheHelper'; diff --git a/materialious/src/lib/sabr/shakaHttpPlugin.ts b/materialious/src/lib/sabr/shakaHttpPlugin.ts index c84e7109..8d85a99b 100644 --- a/materialious/src/lib/sabr/shakaHttpPlugin.ts +++ b/materialious/src/lib/sabr/shakaHttpPlugin.ts @@ -5,6 +5,8 @@ import type { Protos } from 'googlevideo'; import { retrieveCachedSegment } from './cacheHelper'; import { SabrUmpParser } from './sabrUmpParser'; import { CacheManager } from './cacheManager'; +import { Capacitor } from '@capacitor/core'; +import { androidFetch } from '$lib/android/http/androidRequests'; export interface SabrStreamingContext { byteRange?: { start: number; end: number }; @@ -26,7 +28,7 @@ export interface SabrStreamingContext { } export class HttpFetchPlugin { - private static fetch_ = window.fetch; + private static fetch_ = Capacitor.getPlatform() === 'android' ? androidFetch : window.fetch; private static AbortController_ = window.AbortController; private static Headers_ = window.Headers; public static cacheManager = new CacheManager(); diff --git a/materialious/static/nodejs-android/index.js b/materialious/static/nodejs-android/index.js index f3c41640..0e4e3b2d 100644 --- a/materialious/static/nodejs-android/index.js +++ b/materialious/static/nodejs-android/index.js @@ -5,129 +5,151 @@ 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://www.youtube.com' -const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36(KHTML, like Gecko)' +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-goog-authuser', + 'x-user-agent', + 'Accept-Language', + 'X-Goog-FieldMask', + 'Range', + 'Referer', + 'Cookie' +].join(', '); +const CORS_ORIGIN = 'https://www.youtube.com'; +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', CORS_ORIGIN); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', CORS_HEADERS); - res.setHeader('Access-Control-Max-Age', '86400') - res.setHeader('Access-Control-Allow-Credentials', 'true'); + 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', CORS_HEADERS); + res.setHeader('Access-Control-Max-Age', '86400'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); } function fetchWithRedirects(targetUrl, options, redirectCount = 0) { - return new Promise((resolve, reject) => { - const httpClient = targetUrl.protocol.startsWith('https') ? https : http; + return new Promise((resolve, reject) => { + const httpClient = targetUrl.protocol.startsWith('https') ? https : http; - const req = httpClient.request(targetUrl, options, (res) => { - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - if (redirectCount >= MAX_REDIRECTS) { - req.end(); - return reject(new Error('Too many redirects')); - } + const req = httpClient.request(targetUrl, options, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + if (redirectCount >= MAX_REDIRECTS) { + req.end(); + return reject(new Error('Too many redirects')); + } - try { - // Attempt to create the redirect URL - const redirectUrl = new URL(res.headers.location, targetUrl); - return resolve(fetchWithRedirects(redirectUrl, options, redirectCount + 1)); - } catch (error) { - req.end(); - return reject(new Error(`Invalid URL in redirect: ${error.message}`)); - } - } - resolve(res); // Resolve with the final response if not a redirect - }); + try { + // Attempt to create the redirect URL + const redirectUrl = new URL(res.headers.location, targetUrl); + return resolve(fetchWithRedirects(redirectUrl, options, redirectCount + 1)); + } catch (error) { + req.end(); + return reject(new Error(`Invalid URL in redirect: ${error.message}`)); + } + } + resolve(res); // Resolve with the final response if not a redirect + }); - if (options.body && (req.method === 'POST' || req.method === 'PUT')) { - req.write(options.body); - } + if (options.body && (req.method === 'POST' || req.method === 'PUT')) { + req.write(options.body); + } - req.setTimeout(10000, () => reject(new Error('Request timeout')), req.end()); - req.on('error', (error) => reject(error), req.end()); - req.end(); - }); + req.setTimeout(10000, () => reject(new Error('Request timeout')), req.end()); + req.on('error', (error) => reject(error), req.end()); + req.end(); + }); } const server = http.createServer(async (req, res) => { - setCorsHeaders(res); + setCorsHeaders(res); - if (req.method === 'OPTIONS') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - return res.end(); - } + if (req.method === 'OPTIONS') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + return res.end(); + } - if (!req.url || req.url === '/') { - res.writeHead(400, { 'Content-Type': 'text/plain' }); - return res.end('No URL provided to fetch.'); - } + if (!req.url || req.url === '/') { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + return res.end('No URL provided to fetch.'); + } - let targetUrl = req.url.slice(1); // Remove leading '/' - let parsedTarget; - try { - // Ensure protocol (http) is added if missing - if (!targetUrl.startsWith('http')) { - targetUrl = 'http://' + targetUrl; - } - parsedTarget = new URL(targetUrl); - } catch (error) { - res.writeHead(400, { 'Content-Type': 'text/plain' }); - return res.end(`Invalid URL: ${error.message}`); - } + let targetUrl = req.url.slice(1); // Remove leading '/' + let parsedTarget; + try { + // Ensure protocol (http) is added if missing + if (!targetUrl.startsWith('http')) { + targetUrl = 'http://' + targetUrl; + } + parsedTarget = new URL(targetUrl); + } catch (error) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + return res.end(`Invalid URL: ${error.message}`); + } - let body = ''; - req.on('data', chunk => { - body += chunk; - }); + let body = ''; + req.on('data', (chunk) => { + body += chunk; + }); - req.on('end', async () => { - const options = { - method: req.method, - headers: Object.fromEntries( - Object.entries(req.headers).filter(([key]) => ![ - 'host', 'origin', 'referer', 'x-forwarded-for', 'x-requested-with' - ].includes(key.toLowerCase())) - ) - }; + req.on('end', async () => { + const options = { + method: req.method, + headers: Object.fromEntries( + Object.entries(req.headers).filter( + ([key]) => + !['host', 'origin', 'referer', 'x-forwarded-for', 'x-requested-with'].includes( + key.toLowerCase() + ) + ) + ) + }; - options.headers.host = parsedTarget.host; - options.headers.origin = parsedTarget.origin; - options.headers['user-agent'] = USER_AGENT; + 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') { - options.headers['Content-Length'] = Buffer.byteLength(body); - options.body = body; - } + // For POST and PUT methods, pass the body to the outgoing request + if (req.method === 'POST' || req.method === 'PUT') { + options.headers['Content-Length'] = Buffer.byteLength(body); + options.body = body; + } - try { - const proxyRes = await fetchWithRedirects(parsedTarget, options); + try { + const proxyRes = await fetchWithRedirects(parsedTarget, options); - req.on('close', () => { - console.log('Request canceled by the client.'); - proxyRes.destroy(); - }); + req.on('close', () => { + console.log('Request canceled by the client.'); + proxyRes.destroy(); + }); - res.writeHead(proxyRes.statusCode, { - ...proxyRes.headers, - 'Access-Control-Allow-Origin': CORS_ORIGIN, - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': CORS_HEADERS, - 'Access-Control-Allow-Credentials': 'true', - }); + res.writeHead(proxyRes.statusCode, { + ...proxyRes.headers, + 'Access-Control-Allow-Origin': CORS_ORIGIN, + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': CORS_HEADERS, + 'Access-Control-Allow-Credentials': 'true' + }); - // Pipe response data back to the client - proxyRes.pipe(res); - - } catch (error) { - console.error('Proxy error:', error); - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(`Error: ${error.message}`); - } - }); + // Pipe response data back to the client + proxyRes.pipe(res); + } catch (error) { + console.error('Proxy error:', error); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(`Error: ${error.message}`); + } + }); }); server.listen(PORT, () => { - console.log(`Server is running on http://${HOST}:${PORT}`); + console.log(`Server is running on http://${HOST}:${PORT}`); });