Improve architecture

This commit is contained in:
Younes Aassila 2023-12-22 23:00:31 +01:00
parent 8c9de422e7
commit 714ec4a3e0

View File

@ -8,7 +8,6 @@ import {
twitchGqlHostRegex, twitchGqlHostRegex,
usherHostRegex, usherHostRegex,
videoWeaverHostRegex, videoWeaverHostRegex,
videoWeaverUrlRegex,
} from "../common/ts/regexes"; } from "../common/ts/regexes";
import { State } from "../store/types"; import { State } from "../store/types";
@ -21,6 +20,11 @@ export interface FetchOptions {
state?: State; state?: State;
sendMessageToWorkers?: (message: any) => void; sendMessageToWorkers?: (message: any) => void;
} }
export interface VideoWeaver {
assigned: Map<string, string>; // E.g. "720p60" -> "https://video-weaver.fra02.hls.ttvnw.net/v1/playlist/..."
replacement: Map<string, string> | null; // Same as above, but with new URLs.
consecutiveMidrollResponses: number; // Used to avoid infinite loops.
}
export interface PlaybackAccessToken { export interface PlaybackAccessToken {
value: string; value: string;
signature: string; signature: string;
@ -36,18 +40,12 @@ enum MessageType {
} }
export function getFetch(options: FetchOptions): typeof fetch { export function getFetch(options: FetchOptions): typeof fetch {
// TODO: Clear variables on navigation. // TODO: What happens when the user navigates to another channel?
const knownVideoWeaverUrls = new Set<string>(); let videoWeavers: VideoWeaver[] = [];
const videoWeaverUrlsToFlag = new Map<string, number>(); // Video Weaver URLs to flag -> number of times flagged. let proxiedVideoWeaverUrls = new Set<string>(); // Used to proxy only the first request to each Video Weaver URL.
let cachedPlaybackTokenRequestHeaders: Map<string, string> | null = null; // Cached by page script.
// TODO: Again, what happens when the user navigates to another channel? let cachedPlaybackTokenRequestBody: string | null = null; // Cached by page script.
let cachedPlaybackTokenRequestHeaders: Map<string, string> | null = null; let cachedUsherRequestUrl: string | null = null; // Cached by worker script.
let cachedPlaybackTokenRequestBody: string | null = null;
let cachedUsherRequestUrl: string | null = null;
let assignedVideoWeaversMap: Map<string, string> | null = null;
let replacementVideoWeaversMap: Map<string, string> | null = null;
let consecutiveMidrollResponses = 0;
if (options.shouldWaitForStore) { if (options.shouldWaitForStore) {
setTimeout(() => { setTimeout(() => {
@ -73,7 +71,7 @@ export function getFetch(options: FetchOptions): typeof fetch {
}); });
} }
async function setReplacementVideoWeaversMap() { async function setVideoWeaverReplacementMap(videoWeaver: VideoWeaver) {
try { try {
console.log("[TTV LOL PRO] 🔄 Checking for new Video Weaver URLs…"); console.log("[TTV LOL PRO] 🔄 Checking for new Video Weaver URLs…");
const newUsherManifest = await fetchReplacementUsherManifest( const newUsherManifest = await fetchReplacementUsherManifest(
@ -83,14 +81,26 @@ export function getFetch(options: FetchOptions): typeof fetch {
console.log("[TTV LOL PRO] 🔄 No new Video Weaver URLs found."); console.log("[TTV LOL PRO] 🔄 No new Video Weaver URLs found.");
return; return;
} }
replacementVideoWeaversMap = videoWeaver.replacement =
getVideoWeaversMapFromUsherResponse(newUsherManifest); getVideoWeaverMapFromUsherResponse(newUsherManifest);
console.log( console.log(
"[TTV LOL PRO] 🔄 Found new Video Weaver URLs:", "[TTV LOL PRO] 🔄 Found new Video Weaver URLs:",
Object.fromEntries(replacementVideoWeaversMap?.entries() ?? []) Object.fromEntries(videoWeaver.replacement?.entries() ?? [])
); );
// Send replacement Video Weaver URLs to content script.
const videoWeaverUrls = [...(videoWeaver.replacement?.values() ?? [])];
if (cachedUsherRequestUrl != null && videoWeaverUrls.length > 0) {
// Send Video Weaver URLs to content script.
sendMessageToContentScript(options.scope, {
type: "UsherResponse",
channel: findChannelFromUsherUrl(cachedUsherRequestUrl),
videoWeaverUrls,
proxyCountry:
/USER-COUNTRY="([A-Z]+)"/i.exec(newUsherManifest)?.[1] || null,
});
}
} catch (error) { } catch (error) {
replacementVideoWeaversMap = null; videoWeaver.replacement = null;
console.error( console.error(
"[TTV LOL PRO] 🔄 Failed to get new Video Weaver URLs:", "[TTV LOL PRO] 🔄 Failed to get new Video Weaver URLs:",
error error
@ -100,7 +110,7 @@ export function getFetch(options: FetchOptions): typeof fetch {
// // TEST CODE // // TEST CODE
// if (options.scope === "worker") { // if (options.scope === "worker") {
// setTimeout(setReplacementVideoWeaversMap, 30000); // setTimeout(() => setVideoWeaverReplacementMap(videoWeavers[0]), 30000);
// } // }
return async function fetch( return async function fetch(
@ -135,7 +145,7 @@ export function getFetch(options: FetchOptions): typeof fetch {
// Twitch GraphQL requests. // Twitch GraphQL requests.
if (host != null && twitchGqlHostRegex.test(host)) { if (host != null && twitchGqlHostRegex.test(host)) {
requestBody = await readRequestBody(); requestBody ??= await readRequestBody();
// Integrity requests. // Integrity requests.
if (url === "https://gql.twitch.tv/integrity") { if (url === "https://gql.twitch.tv/integrity") {
console.debug( console.debug(
@ -215,19 +225,28 @@ export function getFetch(options: FetchOptions): typeof fetch {
// Video Weaver requests. // Video Weaver requests.
if (host != null && videoWeaverHostRegex.test(host)) { if (host != null && videoWeaverHostRegex.test(host)) {
console.debug(`[TTV LOL PRO] 🥅 Caught Video Weaver request '${url}'.`); const videoWeaver = videoWeavers.find(videoWeaver =>
[...(videoWeaver.assigned.values() ?? [])].includes(url)
);
if (videoWeaver == null) {
console.warn(
"[TTV LOL PRO] 🥅 Caught Video Weaver request, but no associated Video Weaver found."
);
}
let videoWeaverUrl = url; let videoWeaverUrl = url;
if (replacementVideoWeaversMap != null) {
const video = [...(assignedVideoWeaversMap?.entries() ?? [])].find( if (videoWeaver?.replacement != null) {
([key, value]) => value === url const video = [...(videoWeaver.assigned.entries() ?? [])].find(
([, url]) => url === videoWeaverUrl
)?.[0]; )?.[0];
if (video != null && replacementVideoWeaversMap.has(video)) { // Replace Video Weaver URL with replacement URL.
videoWeaverUrl = replacementVideoWeaversMap.get(video)!; if (video != null && videoWeaver.replacement.has(video)) {
videoWeaverUrl = videoWeaver.replacement.get(video)!;
console.log( console.log(
`[TTV LOL PRO] 🔄 Replaced Video Weaver URL '${url}' with '${videoWeaverUrl}'.` `[TTV LOL PRO] 🔄 Replaced Video Weaver URL '${url}' with '${videoWeaverUrl}'.`
); );
} else if (replacementVideoWeaversMap.size > 0) { } else if (videoWeaver.replacement.size > 0) {
videoWeaverUrl = [...replacementVideoWeaversMap.values()][0]; videoWeaverUrl = [...videoWeaver.replacement.values()][0];
console.log( console.log(
`[TTV LOL PRO] 🔄 Replaced Video Weaver URL '${url}' with '${videoWeaverUrl}' (fallback).` `[TTV LOL PRO] 🔄 Replaced Video Weaver URL '${url}' with '${videoWeaverUrl}' (fallback).`
); );
@ -238,36 +257,26 @@ export function getFetch(options: FetchOptions): typeof fetch {
} }
} }
const isNewUrl = !knownVideoWeaverUrls.has(videoWeaverUrl); // Flag first request to each Video Weaver URL.
const isFlaggedUrl = videoWeaverUrlsToFlag.has(videoWeaverUrl); if (!proxiedVideoWeaverUrls.has(videoWeaverUrl)) {
proxiedVideoWeaverUrls.add(videoWeaverUrl);
if (isNewUrl || isFlaggedUrl) {
console.log( console.log(
`[TTV LOL PRO] 🥅 Caught ${ `[TTV LOL PRO] 🥅 Caught first request to Video Weaver URL. Flagging…`
isNewUrl
? "first request to Video Weaver URL"
: "Video Weaver request to flag"
}. Flagging`
); );
flagRequest(headersMap); flagRequest(headersMap);
// videoWeaverUrlsToFlag.set(
// videoWeaverUrl,
// (videoWeaverUrlsToFlag.get(videoWeaverUrl) ?? 0) + 1
// );
if (isNewUrl) knownVideoWeaverUrls.add(videoWeaverUrl);
} }
response = await NATIVE_FETCH(videoWeaverUrl, { response ??= await NATIVE_FETCH(videoWeaverUrl, {
...init,
headers: Object.fromEntries(headersMap),
});
} else {
response = await NATIVE_FETCH(input, {
...init, ...init,
headers: Object.fromEntries(headersMap), headers: Object.fromEntries(headersMap),
}); });
} }
response ??= await NATIVE_FETCH(input, {
...init,
headers: Object.fromEntries(headersMap),
});
//#endregion //#endregion
// Reading the response body can be expensive, so we only do it if we need to. // Reading the response body can be expensive, so we only do it if we need to.
@ -282,14 +291,17 @@ export function getFetch(options: FetchOptions): typeof fetch {
// Usher responses. // Usher responses.
if (host != null && usherHostRegex.test(host)) { if (host != null && usherHostRegex.test(host)) {
responseBody = await readResponseBody(); responseBody ??= await readResponseBody();
console.debug("[TTV LOL PRO] 🥅 Caught Usher response."); console.log("[TTV LOL PRO] 🥅 Caught Usher response.");
assignedVideoWeaversMap = const videoWeaverMap = getVideoWeaverMapFromUsherResponse(responseBody);
getVideoWeaversMapFromUsherResponse(responseBody); if (videoWeaverMap != null) {
replacementVideoWeaversMap = null; videoWeavers.push({
const videoWeaverUrls = responseBody assigned: videoWeaverMap,
.split("\n") replacement: null,
.filter(line => videoWeaverUrlRegex.test(line)); consecutiveMidrollResponses: 0,
});
}
const videoWeaverUrls = [...(videoWeaverMap?.values() ?? [])];
// Send Video Weaver URLs to content script. // Send Video Weaver URLs to content script.
sendMessageToContentScript(options.scope, { sendMessageToContentScript(options.scope, {
type: "UsherResponse", type: "UsherResponse",
@ -299,12 +311,21 @@ export function getFetch(options: FetchOptions): typeof fetch {
/USER-COUNTRY="([A-Z]+)"/i.exec(responseBody)?.[1] || null, /USER-COUNTRY="([A-Z]+)"/i.exec(responseBody)?.[1] || null,
}); });
// Remove all Video Weaver URLs from known URLs. // Remove all Video Weaver URLs from known URLs.
videoWeaverUrls.forEach(url => knownVideoWeaverUrls.delete(url)); videoWeaverUrls.forEach(url => proxiedVideoWeaverUrls.delete(url));
} }
// Video Weaver responses. // Video Weaver responses.
if (host != null && videoWeaverHostRegex.test(host)) { if (host != null && videoWeaverHostRegex.test(host)) {
responseBody = await readResponseBody(); responseBody ??= await readResponseBody();
const videoWeaver = videoWeavers.find(videoWeaver =>
[...(videoWeaver.assigned.values() ?? [])].includes(url)
);
if (videoWeaver == null) {
console.warn(
"[TTV LOL PRO] 🥅 Caught Video Weaver response, but no associated Video Weaver found."
);
return response;
}
// Check if response contains midroll ad. // Check if response contains midroll ad.
if ( if (
@ -314,16 +335,18 @@ export function getFetch(options: FetchOptions): typeof fetch {
console.log( console.log(
"[TTV LOL PRO] 🥅 Caught Video Weaver response containing ad." "[TTV LOL PRO] 🥅 Caught Video Weaver response containing ad."
); );
consecutiveMidrollResponses += 1; videoWeaver.consecutiveMidrollResponses += 1;
// Avoid infinite loops. // Avoid infinite loops.
if (consecutiveMidrollResponses <= 2) { if (videoWeaver.consecutiveMidrollResponses <= 2) {
await setReplacementVideoWeaversMap(); await setVideoWeaverReplacementMap(videoWeaver);
cancelRequest();
} else { } else {
replacementVideoWeaversMap = null; videoWeaver.replacement = null;
} }
} else { } else {
// No ad, clear attempts. // No ad, clear attempts.
consecutiveMidrollResponses = 0; videoWeaver.consecutiveMidrollResponses = 0;
console.debug("[TTV LOL PRO] Caught Video Weaver response WITHOUT ad.");
} }
} }
@ -489,6 +512,7 @@ async function fetchReplacementPlaybackAccessToken(
method: "POST", method: "POST",
headers: { headers: {
// TODO: Find unnecessary headers. // TODO: Find unnecessary headers.
// TODO: Flag this request!!
Accept: "*/*", Accept: "*/*",
"Accept-Language": "en-US", "Accept-Language": "en-US",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
@ -572,7 +596,7 @@ async function fetchReplacementUsherManifest(
} }
} }
function getVideoWeaversMapFromUsherResponse( function getVideoWeaverMapFromUsherResponse(
response: string response: string
): Map<string, string> | null { ): Map<string, string> | null {
const parser = new m3u8Parser.Parser(); const parser = new m3u8Parser.Parser();