diff --git a/package-lock.json b/package-lock.json index c14bbafa..3212fcc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "GPL-3.0", "dependencies": { "bowser": "^2.11.0", - "ip": "^1.1.8" + "ip": "^1.1.8", + "m3u8-parser": "^7.1.0" }, "devDependencies": { "@parcel/config-webextension": "^2.10.3", @@ -189,6 +190,22 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", + "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/@lezer/common": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.1.2.tgz", @@ -2211,6 +2228,20 @@ "integrity": "sha512-10ql7A0qzBmFB+F+qAke/nP1PIonS0TXZAOMVOxEUsm+lGSW6uwVcISFNa0I4Oyj0884TZVWGGMIWeXOVSNFHw==", "dev": true }, + "node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/abortcontroller-polyfill": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", @@ -2687,6 +2718,11 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -2844,6 +2880,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", @@ -3358,6 +3403,16 @@ "node": ">=10" } }, + "node_modules/m3u8-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.1.0.tgz", + "integrity": "sha512-7N+pk79EH4oLKPEYdgRXgAsKDyA/VCo0qCHlUwacttQA0WqsjZQYmNfywMvjlY9MpEBVZEt0jKFd73Kv15EBYQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -3379,6 +3434,14 @@ "node": ">=8.6" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3813,6 +3876,14 @@ } } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", @@ -4131,6 +4202,11 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "node_modules/utility-types": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", diff --git a/package.json b/package.json index 44971587..b3ccf7c8 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "license": "GPL-3.0", "dependencies": { "bowser": "^2.11.0", - "ip": "^1.1.8" + "ip": "^1.1.8", + "m3u8-parser": "^7.1.0" }, "devDependencies": { "@parcel/config-webextension": "^2.10.3", diff --git a/src/m3u8-parser.d.ts b/src/m3u8-parser.d.ts new file mode 100644 index 00000000..f7079285 --- /dev/null +++ b/src/m3u8-parser.d.ts @@ -0,0 +1,41 @@ +declare module "m3u8-parser" { + export class Parser { + constructor(); + push(chunk: string): void; + end(): void; + manifest: Manifest; + } +} + +interface Manifest { + allowCache: boolean; + discontinuityStarts: any[]; + dateRanges: any[]; + segments: any[]; + mediaGroups?: { + AUDIO: {}; + VIDEO: {}; + "CLOSED-CAPTIONS": {}; + SUBTITLES: {}; + }; + playlists?: Playlist[]; +} + +interface Playlist { + attributes: Attributes; + uri: string; + timeline: number; +} + +interface Attributes { + "FRAME-RATE": number; + VIDEO: string; + CODECS: string; + RESOLUTION: Resolution; + BANDWIDTH: number; +} + +interface Resolution { + width: number; + height: number; +} diff --git a/src/page/getFetch.ts b/src/page/getFetch.ts index 5db6a138..30c7c4f2 100644 --- a/src/page/getFetch.ts +++ b/src/page/getFetch.ts @@ -1,3 +1,4 @@ +import * as m3u8Parser from "m3u8-parser"; import acceptFlag from "../common/ts/acceptFlag"; import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl"; import findChannelFromUsherUrl from "../common/ts/findChannelFromUsherUrl"; @@ -44,8 +45,23 @@ export function getFetch(options: FetchOptions): typeof fetch { let cachedPlaybackTokenRequestHeaders: Map | null = null; let cachedPlaybackTokenRequestBody: string | null = null; let cachedUsherRequestUrl: string | null = null; - let currentVideoWeaverUrls: string[] | null = null; - let replacementVideoWeaverUrls: string[] | null = null; + + let currentVideoWeaversMap: Map | null = null; + let replacementVideoWeaversMap: Map | null = null; + + function usherResponseToMap(response: string): Map | null { + const parser = new m3u8Parser.Parser(); + parser.push(response); + parser.end(); + const parsedManifest = parser.manifest; + if (parsedManifest.playlists == null) return null; + return new Map( + parsedManifest.playlists.map(playlist => [ + playlist.attributes.VIDEO, + playlist.uri, + ]) + ); + } if (options.shouldWaitForStore) { setTimeout(() => { @@ -76,17 +92,17 @@ export function getFetch(options: FetchOptions): typeof fetch { setTimeout(async () => { try { console.log("[TTV LOL PRO] 🔄 Checking for new Video Weaver URLs…"); - const newVideoWeaverUrls = await fetchReplacementVideoWeaverUrls( + const newUsherManifest = await fetchReplacementUsherManifest( cachedUsherRequestUrl ); - if (newVideoWeaverUrls == null) { + if (newUsherManifest == null) { console.log("[TTV LOL PRO] 🔄 No new Video Weaver URLs found."); return; } - replacementVideoWeaverUrls = newVideoWeaverUrls; + replacementVideoWeaversMap = usherResponseToMap(newUsherManifest); console.log( "[TTV LOL PRO] 🔄 Found new Video Weaver URLs:", - replacementVideoWeaverUrls + Object.fromEntries(replacementVideoWeaversMap?.entries() ?? []) ); } catch (error) { console.error( @@ -212,22 +228,18 @@ export function getFetch(options: FetchOptions): typeof fetch { console.debug(`[TTV LOL PRO] 🥅 Caught Video Weaver request '${url}'.`); // TODO: Implement replacement limit if the ad is a preroll to avoid infinite loops. let videoWeaverUrl = url; - if (replacementVideoWeaverUrls != null) { - // const index = currentVideoWeaverUrls?.findIndex( - // url => - // url.split("playlist/")[1] === videoWeaverUrl.split("playlist/")[1] - // ); - // console.log("index = ", index); - // if (index != null && index >= 0) { - // videoWeaverUrl = - // replacementVideoWeaverUrls[ - // Math.min(index, replacementVideoWeaverUrls.length - 1) - // ]; - // console.log( - // `[TTV LOL PRO] 🔄 Replaced Video Weaver URL '${url}' with '${videoWeaverUrl}'.` - // ); - // } - videoWeaverUrl = replacementVideoWeaverUrls[0]; + if ( + replacementVideoWeaversMap != null && + replacementVideoWeaversMap.size > 0 + ) { + const video = [...(currentVideoWeaversMap?.entries() ?? [])].find( + ([key, value]) => value === url + )?.[0]; + if (video != null && replacementVideoWeaversMap.has(video)) { + videoWeaverUrl = replacementVideoWeaversMap.get(video)!; + } else { + videoWeaverUrl = [...replacementVideoWeaversMap.values()][0]; + } console.log( `[TTV LOL PRO] 🔄 Replaced Video Weaver URL '${url}' with '${videoWeaverUrl}'.` ); @@ -279,11 +291,9 @@ export function getFetch(options: FetchOptions): typeof fetch { // Usher responses. if (host != null && usherHostRegex.test(host)) { responseBody = await readResponseBody(); - currentVideoWeaverUrls = responseBody - .split("\n") - .filter(line => videoWeaverUrlRegex.test(line)); - replacementVideoWeaverUrls = null; console.debug("[TTV LOL PRO] 🥅 Caught Usher response."); + currentVideoWeaversMap = usherResponseToMap(responseBody); + replacementVideoWeaversMap = null; const videoWeaverUrls = responseBody .split("\n") .filter(line => videoWeaverUrlRegex.test(line)); @@ -552,9 +562,9 @@ function getReplacementUsherUrl( } } -async function fetchReplacementVideoWeaverUrls( +async function fetchReplacementUsherManifest( cachedUsherRequestUrl: string | null -): Promise { +): Promise { if (cachedUsherRequestUrl == null) return null; try { const newPlaybackAccessToken = await sendMessageToPageScript({ @@ -574,10 +584,7 @@ async function fetchReplacementVideoWeaverUrls( } const response = await NATIVE_FETCH(newUsherUrl); const text = await response.text(); - const videoWeaverUrls = text - .split("\n") - .filter(line => videoWeaverUrlRegex.test(line)); - return videoWeaverUrls; + return text; } catch { return null; }