mirror of
https://github.com/younesaassila/ttv-lol-pro.git
synced 2025-05-01 15:04:26 +02:00
Improve architecture
This commit is contained in:
parent
8c9de422e7
commit
714ec4a3e0
@ -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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user