mirror of
https://github.com/younesaassila/ttv-lol-pro.git
synced 2025-04-29 22:14:27 +02:00
🚧 WIP
This commit is contained in:
parent
4b5bd5fa2c
commit
fc67664985
@ -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, {
|
||||
|
@ -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.
|
||||
}
|
34
src/background/handlers/onBeforeRequest.ts
Normal file
34
src/background/handlers/onBeforeRequest.ts
Normal file
@ -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;
|
||||
});
|
||||
}
|
@ -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.
|
||||
}
|
@ -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.
|
||||
}
|
||||
|
@ -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() || "<empty>"
|
||||
}`
|
||||
);
|
||||
|
||||
if (proxyInfoArray.length === 0) return { type: "direct" };
|
||||
return proxyInfoArray;
|
||||
}
|
||||
|
@ -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() || "<empty>"
|
||||
}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
9
src/common/ts/findChannelFromVideoWeaverUrl.ts
Normal file
9
src/common/ts/findChannelFromVideoWeaverUrl.ts
Normal file
@ -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;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default function log(...args: any[]) {
|
||||
console.log("[TTV LOL PRO]", ...args);
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -38,6 +38,7 @@
|
||||
"proxy",
|
||||
"storage",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"https://*.ttvnw.net/*",
|
||||
"https://www.twitch.tv/*",
|
||||
"https://m.twitch.tv/*",
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -64,29 +64,6 @@
|
||||
</small>
|
||||
</section>
|
||||
|
||||
<section id="privacy-section" class="section" hidden>
|
||||
<h2>Privacy</h2>
|
||||
<ul class="options-list">
|
||||
<li>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="disable-vod-redirect-checkbox"
|
||||
id="disable-vod-redirect-checkbox"
|
||||
/>
|
||||
<label for="disable-vod-redirect-checkbox">
|
||||
Disable VOD proxying
|
||||
</label>
|
||||
<br />
|
||||
<small>
|
||||
TTV LOL's API requires your Twitch token (containing sensitive
|
||||
information) to remove ads from VODs. To protect your privacy, and
|
||||
since most ad blockers (like uBlock Origin) already remove ads
|
||||
from VODs, this option is enabled by default.
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="ignored-channel-subscriptions-section"
|
||||
class="section"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import $ from "../common/ts/$";
|
||||
import { TWITCH_URL_REGEX } from "../common/ts/regexes";
|
||||
import { twitchWatchPageUrlRegex } from "../common/ts/regexes";
|
||||
import store from "../store";
|
||||
import type { StreamStatus } from "../types";
|
||||
|
||||
@ -28,7 +28,7 @@ async function main() {
|
||||
const activeTab = tabs[0];
|
||||
if (!activeTab || !activeTab.url) return;
|
||||
|
||||
const match = TWITCH_URL_REGEX.exec(activeTab.url);
|
||||
const match = twitchWatchPageUrlRegex.exec(activeTab.url);
|
||||
if (!match) return;
|
||||
const [, streamId] = match;
|
||||
if (!streamId) return;
|
||||
|
@ -3,12 +3,12 @@ import type { State } from "./types";
|
||||
export default function getDefaultState() {
|
||||
return {
|
||||
checkForUpdates: false, // No need to check for updates on startup for CRX and XPI installs. The default value is set in the store initializer.
|
||||
disableVodRedirect: true, // Most ad-blockers already remove ads from VODs (VOD proxying requires a Twitch token).
|
||||
ignoredChannelSubscriptions: [], // Some channels might show ads even if you're subscribed to them.
|
||||
isUpdateAvailable: false,
|
||||
lastUpdateCheck: 0,
|
||||
servers: [],
|
||||
streamStatuses: {},
|
||||
videoWeaverUrlsByChannel: {},
|
||||
whitelistedChannels: [],
|
||||
} as State;
|
||||
}
|
||||
|
@ -6,12 +6,12 @@ export type StorageAreaName = "local" | "managed" | "sync";
|
||||
|
||||
export interface State {
|
||||
checkForUpdates: boolean;
|
||||
disableVodRedirect: boolean;
|
||||
ignoredChannelSubscriptions: string[];
|
||||
isUpdateAvailable: boolean;
|
||||
lastUpdateCheck: number;
|
||||
servers: string[];
|
||||
streamStatuses: Record<string, StreamStatus>;
|
||||
videoWeaverUrlsByChannel: Record<string, string[]>;
|
||||
whitelistedChannels: string[];
|
||||
}
|
||||
|
||||
|
45
src/types.ts
45
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";
|
||||
|
Loading…
x
Reference in New Issue
Block a user