diff --git a/src/background/background.ts b/src/background/background.ts index e0692df..2b65979 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -1,15 +1,13 @@ import browser from "webextension-polyfill"; import isChrome from "../common/ts/isChrome"; -import log from "../common/ts/log"; -import onBeforeHlsRequest from "./handlers/onBeforeHlsRequest"; -import onBeforeVodRequest from "./handlers/onBeforeVodRequest"; +import onBeforeRequest from "./handlers/onBeforeRequest"; import onHeadersReceived from "./handlers/onHeadersReceived"; import onProxyRequest from "./handlers/onProxyRequest"; import onStartupStoreCleanup from "./handlers/onStartupStoreCleanup"; import onStartupUpdateCheck from "./handlers/onStartupUpdateCheck"; import updateProxySettings from "./updateProxySettings"; -log("🚀 Background script running."); +console.info("🚀 Background script running."); // Cleanup the session-related data in the store on startup. browser.runtime.onStartup.addListener(onStartupStoreCleanup); @@ -23,15 +21,14 @@ if (!isChrome) { urls: ["https://*.ttvnw.net/*"], // Filtered to video-weaver requests in the handler. }); - // TODO: Map channel names to HLS playlists. - browser.webRequest.onBeforeRequest.addListener(onBeforeHlsRequest, { - urls: ["https://usher.ttvnw.net/api/channel/hls/*"], - }); - - // TODO: Excluded from proxying. - browser.webRequest.onBeforeRequest.addListener(onBeforeVodRequest, { - urls: ["https://usher.ttvnw.net/vod/*"], - }); + // Map channel names to video-weaver URLs. + browser.webRequest.onBeforeRequest.addListener( + onBeforeRequest, + { + urls: ["https://usher.ttvnw.net/api/channel/hls/*"], + }, + ["blocking"] + ); // Monitor video-weaver responses. browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, { diff --git a/src/background/handlers/onBeforeHlsRequest.ts b/src/background/handlers/onBeforeHlsRequest.ts deleted file mode 100644 index 6f73998..0000000 --- a/src/background/handlers/onBeforeHlsRequest.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { WebRequest } from "webextension-polyfill"; - -export default function onBeforeHlsRequest( - details: WebRequest.OnBeforeRequestDetailsType -): void { - // TODO: Get channel name from URL. - // TODO: Filter response data to HLS playlists. - // TODO: Map channel name to HLS playlists. -} diff --git a/src/background/handlers/onBeforeRequest.ts b/src/background/handlers/onBeforeRequest.ts new file mode 100644 index 0000000..cd1917e --- /dev/null +++ b/src/background/handlers/onBeforeRequest.ts @@ -0,0 +1,34 @@ +import { WebRequest } from "webextension-polyfill"; +import filterResponseDataWrapper from "../../common/ts/filterResponseDataWrapper"; +import { + twitchApiChannelNameRegex, + videoWeaverUrlRegex, +} from "../../common/ts/regexes"; +import store from "../../store"; + +export default function onBeforeRequest( + details: WebRequest.OnBeforeRequestDetailsType +): void | WebRequest.BlockingResponseOrPromise { + const match = twitchApiChannelNameRegex.exec(details.url); + if (!match) return; + const channelName = match[1]?.toLowerCase(); + if (!channelName) return; + + filterResponseDataWrapper(details, text => { + const videoWeaverUrls = text.match(videoWeaverUrlRegex); + if (!videoWeaverUrls) return text; + console.log( + `📺 Found ${videoWeaverUrls.length} video-weaver URLs for ${channelName}.` + ); + const existingVideoWeaverUrls = + store.state.videoWeaverUrlsByChannel[channelName] ?? []; + const newVideoWeaverUrls = videoWeaverUrls.filter( + url => !existingVideoWeaverUrls.includes(url) + ); + store.state.videoWeaverUrlsByChannel[channelName] = [ + ...existingVideoWeaverUrls, + ...newVideoWeaverUrls, + ]; + return text; + }); +} diff --git a/src/background/handlers/onBeforeVodRequest.ts b/src/background/handlers/onBeforeVodRequest.ts deleted file mode 100644 index 3b63eff..0000000 --- a/src/background/handlers/onBeforeVodRequest.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { WebRequest } from "webextension-polyfill"; - -export default function onBeforeVodRequest( - details: WebRequest.OnBeforeRequestDetailsType -): void { - // TODO: Filter response data to HLS playlists. - // TODO: Exclude HLS playlists from proxying. -} diff --git a/src/background/handlers/onHeadersReceived.ts b/src/background/handlers/onHeadersReceived.ts index f67c531..1b79026 100644 --- a/src/background/handlers/onHeadersReceived.ts +++ b/src/background/handlers/onHeadersReceived.ts @@ -7,16 +7,18 @@ export default function onHeadersReceived( details: WebRequest.OnHeadersReceivedDetailsType & { proxyInfo?: ProxyInfo; } -): void { +): void | WebRequest.BlockingResponseOrPromise { // Filter to video-weaver responses. const host = getHostFromUrl(details.url); if (!host || !videoWeaverHostRegex.test(host)) return; const proxyInfo = details.proxyInfo; // Firefox only. if (!proxyInfo || proxyInfo.type === "direct") - return console.log(`❌ Failed to proxy ${details.url}`); + return console.log(`❌ Did not proxy ${details.url}`); console.log( `✅ Proxied ${details.url} through ${proxyInfo.host}:${proxyInfo.port} (${proxyInfo.type})` ); + + // TODO: Stream status. } diff --git a/src/background/handlers/onProxyRequest.ts b/src/background/handlers/onProxyRequest.ts index 2f060b4..75a036f 100644 --- a/src/background/handlers/onProxyRequest.ts +++ b/src/background/handlers/onProxyRequest.ts @@ -1,4 +1,5 @@ import { Proxy } from "webextension-polyfill"; +import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl"; import getHostFromUrl from "../../common/ts/getHostFromUrl"; import { videoWeaverHostRegex } from "../../common/ts/regexes"; import store from "../../store"; @@ -11,6 +12,19 @@ export default function onProxyRequest( const host = getHostFromUrl(details.url); if (!host || !videoWeaverHostRegex.test(host)) return { type: "direct" }; + // Check if the channel is whitelisted. + const channelName = findChannelFromVideoWeaverUrl(details.url); + const isWhitelisted = (channelName: string) => { + const whitelistedChannelsLower = store.state.whitelistedChannels.map( + channel => channel.toLowerCase() + ); + return whitelistedChannelsLower.includes(channelName.toLowerCase()); + }; + if (channelName != null && isWhitelisted(channelName)) { + console.log(`✋ Channel ${channelName} is whitelisted.`); + return { type: "direct" }; + } + const proxies = store.state.servers; const proxyInfoArray: ProxyInfo[] = proxies.map(host => { const [hostname, port] = host.split(":"); @@ -20,11 +34,11 @@ export default function onProxyRequest( port: Number(port) ?? 3128, } as ProxyInfo; }); - console.log( - `🔄 Proxying ${details.url} through one of: [${proxies.toString()}]` + `⌛ Proxying ${details.url} (${channelName ?? "unknown"}) through one of: ${ + proxies.toString() || "" + }` ); - if (proxyInfoArray.length === 0) return { type: "direct" }; return proxyInfoArray; } diff --git a/src/background/updateProxySettings.ts b/src/background/updateProxySettings.ts index 655623f..45204f1 100644 --- a/src/background/updateProxySettings.ts +++ b/src/background/updateProxySettings.ts @@ -23,7 +23,9 @@ export default function updateProxySettings() { }; chrome.proxy.settings.set({ value: config, scope: "regular" }, function () { console.log( - `Proxying video-weaver requests through one of: [${store.state.servers.toString()}]` + `⚙️ Proxying video-weaver requests through one of: ${ + store.state.servers.toString() || "" + }` ); }); } diff --git a/src/common/ts/findChannelFromVideoWeaverUrl.ts b/src/common/ts/findChannelFromVideoWeaverUrl.ts new file mode 100644 index 0000000..0cfcfdf --- /dev/null +++ b/src/common/ts/findChannelFromVideoWeaverUrl.ts @@ -0,0 +1,9 @@ +import store from "../../store"; + +export default function findChannelFromVideoWeaverUrl(videoWeaverUrl: string) { + const channelName = Object.keys(store.state.videoWeaverUrlsByChannel).find( + channelName => + store.state.videoWeaverUrlsByChannel[channelName].includes(videoWeaverUrl) + ); + return channelName ?? null; +} diff --git a/src/common/ts/log.ts b/src/common/ts/log.ts deleted file mode 100644 index 7ca3454..0000000 --- a/src/common/ts/log.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function log(...args: any[]) { - console.log("[TTV LOL PRO]", ...args); -} diff --git a/src/common/ts/regexes.ts b/src/common/ts/regexes.ts index 93c35c9..8002da1 100644 --- a/src/common/ts/regexes.ts +++ b/src/common/ts/regexes.ts @@ -1,5 +1,6 @@ -export const TWITCH_URL_REGEX = - /^https?:\/\/(?:(?:www|m)\.)?twitch\.tv\/(?:videos\/)?([A-Z0-9][A-Z0-9_]*)/i; -export const TWITCH_API_URL_REGEX = /\/(hls|vod)\/(.+)\.m3u8(?:\?(.*))?$/i; -export const TTV_LOL_API_URL_REGEX = /\/(?:playlist|vod)\/(.+)\.m3u8/i; +export const twitchApiChannelNameRegex = /\/hls\/(.+)\.m3u8/i; +export const twitchWatchPageUrlRegex = + /^https?:\/\/(?:(?:www|m)\.)?twitch\.tv\/(?:videos\/)?(\w+)/i; export const videoWeaverHostRegex = /^video-weaver\.\w+\.hls\.ttvnw\.net$/i; +export const videoWeaverUrlRegex = + /^https?:\/\/video-weaver\.\w+\.hls\.ttvnw\.net\/v1\/playlist\/.+\.m3u8$/gim; diff --git a/src/content/content.ts b/src/content/content.ts index 68be3b2..ec9d0b9 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -1,15 +1,14 @@ -import log from "../common/ts/log"; -import { TWITCH_URL_REGEX } from "../common/ts/regexes"; +import { twitchWatchPageUrlRegex } from "../common/ts/regexes"; import store from "../store"; -log("🚀 Content script running."); +console.info("[TTV LOL PRO] 🚀 Content script running."); if (store.readyState === "complete") clearErrors(); else store.addEventListener("load", clearErrors); // Clear errors for stream on page load/reload. function clearErrors() { - const match = TWITCH_URL_REGEX.exec(location.href); + const match = twitchWatchPageUrlRegex.exec(location.href); if (!match) return; const [, streamId] = match; if (!streamId) return; diff --git a/src/manifest.json b/src/manifest.json index 41bc877..2505322 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -38,6 +38,7 @@ "proxy", "storage", "webRequest", + "webRequestBlocking", "https://*.ttvnw.net/*", "https://www.twitch.tv/*", "https://m.twitch.tv/*", diff --git a/src/options/options.ts b/src/options/options.ts index 5047a83..eaab6e2 100644 --- a/src/options/options.ts +++ b/src/options/options.ts @@ -36,10 +36,6 @@ const whitelistedChannelsListElement = $( $; // Proxies const serversListElement = $("#servers-list") as HTMLOListElement; -// Privacy -const disableVodRedirectCheckboxElement = $( - "#disable-vod-redirect-checkbox" -) as HTMLInputElement; // Ignored channel subscriptions const ignoredChannelSubscriptionsListElement = $( "#ignored-channel-subscriptions-list" @@ -84,24 +80,6 @@ function main() { const checkbox = e.target as HTMLInputElement; store.state.checkForUpdates = checkbox.checked; }); - // Disable VOD proxying - disableVodRedirectCheckboxElement.checked = store.state.disableVodRedirect; - disableVodRedirectCheckboxElement.addEventListener("change", e => { - const checkbox = e.target as HTMLInputElement; - if (checkbox.checked) { - store.state.disableVodRedirect = checkbox.checked; - } else { - // Ask for confirmation before enabling VOD proxying. - const consent = confirm( - "Are you sure?\n\nYour Twitch token (containing sensitive information) will be sent to TTV LOL's API server when watching VODs." - ); - if (consent) { - store.state.disableVodRedirect = checkbox.checked; - } else { - checkbox.checked = true; - } - } - }); // Server list listInit(serversListElement, "servers", store.state.servers, { getPromptPlaceholder: insertMode => { @@ -295,9 +273,7 @@ exportButtonElement.addEventListener("click", () => { "ttv-lol-pro_backup.json", JSON.stringify({ checkForUpdates: store.state.checkForUpdates, - disableVodRedirect: store.state.disableVodRedirect, ignoredChannelSubscriptions: store.state.ignoredChannelSubscriptions, - resetPlayerOnMidroll: store.state.resetPlayerOnMidroll, servers: store.state.servers, whitelistedChannels: store.state.whitelistedChannels, }), diff --git a/src/options/page.html b/src/options/page.html index de7045b..54a43cf 100644 --- a/src/options/page.html +++ b/src/options/page.html @@ -64,29 +64,6 @@ - -
; + videoWeaverUrlsByChannel: Record; whitelistedChannels: string[]; } diff --git a/src/types.ts b/src/types.ts index 61d600f..23efa02 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,51 +15,6 @@ export interface StreamStatusError { status: number; } -export const enum PlaylistType { - Playlist = "playlist", - VOD = "vod", -} - -export interface Token { - adblock?: boolean; - authorization: { - forbidden: boolean; - reason: string; - }; - blackout_enabled?: boolean; - channel?: string; - channel_id?: number; - chansub: { - restricted_bitrates?: string[]; - view_until: number; - }; - ci_gb?: boolean; - geoblock_reason?: string; - device_id?: string; - expires: number; - extended_history_allowed?: boolean; - game?: string; - hide_ads?: boolean; - https_required: boolean; - mature?: boolean; - partner?: boolean; - platform?: string; - player_type?: string; - private?: { - allowed_to_view: boolean; - }; - privileged: boolean; - role?: string; - server_ads?: boolean; - show_ads?: boolean; - subscriber?: boolean; - turbo?: boolean; - user_id?: number; - user_ip?: string; - version: number; - vod_id?: number; -} - // From https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo export type ProxyInfo = { type: "direct" | "http" | "https" | "socks" | "socks4";