Compare commits

..
42 Commits
Author SHA1 Message Date
bytedream 8e22e92f89 update dependencies 2026-07-10 16:23:16 +02:00
bytedream f7cfe41211 only listen to send headers and redirect request where it's necessary 2026-07-10 16:10:24 +02:00
bytedream d7e1cbfb48 update text 2026-07-10 14:38:08 +02:00
bytedream 3541c7d2f0 re-inject content script on 3XX redirects 2026-07-07 18:25:48 +02:00
bytedream df3c66a24c fix vidmoly 2026-07-07 17:36:04 +02:00
bytedream b929f0956d disallow alert 2026-07-07 17:36:04 +02:00
bytedream 4c9825ebd5 redirect dynamically via injected content script instead of checking
each request
2026-07-07 17:36:01 +02:00
bytedream 71593f6762 clean empty per site settings 2026-07-07 14:16:13 +02:00
bytedream ddcb13013a add option to disable all hosts via settings page 2026-07-07 14:09:41 +02:00
bytedream 8cf14fb17a update dependencies and version 2026-07-07 13:53:20 +02:00
bytedream 3e3e2f5e75 replace slop dash with normal minus 2026-07-07 13:51:51 +02:00
bytedream c5deca1b2c unify headers 2026-07-07 13:50:05 +02:00
bytedream c4fea538f0 fix per site settings being set for wrong domain 2026-07-07 13:34:49 +02:00
bytedream 0552df008d update popup main page layout 2026-07-07 13:32:51 +02:00
bytedream e437d5436a fix per domain disable not working if host is in iframe 2026-07-07 13:26:33 +02:00
bytedream c09cf1188d show popup when ff2mpv settings are requested 2026-07-07 12:31:53 +02:00
bytedream eef560f224 add per domain settings (#30) 2026-07-07 12:26:54 +02:00
bytedream 9857ba6416 update settings layout 2026-07-02 11:11:56 +02:00
bytedream c08e071c88 fix tailwind classes 2026-06-28 20:46:27 +02:00
bytedream dbec92375c add toast when copying url to clipboard 2026-06-28 20:22:35 +02:00
bytedream a1328e8908 use host id instead of host in settings 2026-06-28 20:20:37 +02:00
bytedream 0455b53992 add svelte check to lint script 2026-06-28 16:42:56 +02:00
bytedream c9f5b594ff cleanup settings 2026-06-28 16:37:52 +02:00
bytedreamandMistral AI 9639f148f9 add no-console rule and remove console.log calls
Co-authored-by: Mistral AI <mistral@mistral.ai>
2026-06-25 18:21:24 +02:00
bytedream 85f4a3b776 update pnpm lockfile and workspace settings to v11 2026-06-22 23:12:49 +02:00
bytedreamandMistral Vibe 30e079d665 add pnpm/action-setup to workflows
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 23:02:20 +02:00
bytedreamandMistral Vibe e499adb4ac update action node version
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 22:46:06 +02:00
bytedreamandMistral Vibe aa6827b059 update github actions to latest versions
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 15:46:33 +02:00
bytedreamandMistral Vibe 5a86625f3f switch to pnpm
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 15:24:00 +02:00
bytedream a44f71f056 fix incorrect mv version in readme 2026-06-22 14:49:03 +02:00
bytedream 652d0588fc update dependencies and version 2026-01-11 21:26:52 +01:00
bytedream a2a7132d0b format 2026-01-11 21:25:53 +01:00
bytedream 0696165932 fix vidoza 2026-01-11 21:20:18 +01:00
bytedream 8cb9a83b2c fix doodstream 2026-01-11 21:17:42 +01:00
bytedream f200312ad5 fix background listeners not working 2026-01-11 21:17:30 +01:00
bytedream 1cf4940453 minor refactor 2026-01-11 21:17:10 +01:00
bytedream 67dbb9c685 fix vidmoly 2026-01-08 00:50:00 +01:00
bytedream f9a0197f7f update readme 2025-11-08 23:24:25 +01:00
bytedream d8bb70890a update build instructions 2025-11-08 22:26:18 +01:00
bytedream 45900b2e86 do not upload sources to releases 2025-11-08 22:21:10 +01:00
bytedream 3058847f31 upload artifacts when publishing 2025-11-08 22:20:35 +01:00
bytedream 082f30722c do not lint on tag push 2025-11-08 21:52:46 +01:00
50 changed files with 7136 additions and 8715 deletions
+14 -10
View File
@@ -10,31 +10,35 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup node
uses: actions/setup-node@v6
- name: Setup pnpm
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
with:
node-version: 22
cache: 'npm'
version: 11
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 26
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
- name: Build Firefox (mv2)
run: npm run build:firefox
run: pnpm run build:firefox
- name: Build Chrome (mv3)
run: npm run build
run: pnpm run build
- name: Upload Firefox (mv2)
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: stream-bypass-mv2
path: .output/firefox-mv2
- name: Upload Chrome (mv3)
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: stream-bypass-mv3
path: .output/chrome-mv3
+17 -9
View File
@@ -1,24 +1,32 @@
name: lint
on:
push:
pull_request:
push:
branches:
- '**'
tags-ignore:
- '*'
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup node
uses: actions/setup-node@v6
- name: Setup pnpm
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
with:
node-version: 22
cache: 'npm'
version: 11
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 26
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
- name: Lint
run: npm run lint
run: pnpm run lint
+39 -11
View File
@@ -19,28 +19,56 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup node
uses: actions/setup-node@v6
- name: Setup pnpm
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
with:
node-version: 22
cache: 'npm'
version: 11
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 26
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
- name: Zip extensions
run: |
npm run zip:firefox
npm run zip
pnpm run zip:firefox
pnpm run zip
- name: Resolve zip extension paths
id: paths
run: |
MV2_PATH=$(find .output -type f -name "*-mv2.zip")
MV2_NAME=$(basename $MV2_PATH)
MV3_PATH=$(find .output -type f -name "*-mv3.zip")
MV3_NAME=$(basename $MV3_PATH)
echo "mv2_path=$MV2_PATH" >> "$GITHUB_OUTPUT"
echo "mv2_name=$MV2_NAME" >> "$GITHUB_OUTPUT"
echo "mv3_path=$MV3_PATH" >> "$GITHUB_OUTPUT"
echo "mv3_name=$MV3_NAME" >> "$GITHUB_OUTPUT"
- name: Upload mv2 as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.paths.outputs.mv2_name }}
path: ${{ steps.paths.outputs.mv2_path }}
- name: Upload mv3 as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.paths.outputs.mv3_name }}
path: ${{ steps.paths.outputs.mv3_path }}
- name: Upload to latest release
if: github.event.inputs.dry_run != 'true'
uses: svenstaro/upload-release-action@v2
uses: svenstaro/upload-release-action@ac75e1407b33421f1368f08e0e6cc567cc4edd25 # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: .output/*.zip
file: .output/*mv*.zip
tag: ${{ github.ref }}
overwrite: true
file_glob: true
@@ -56,6 +84,6 @@ jobs:
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
run: |
npx wxt submit \
pnpm exec wxt submit \
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
--chrome-zip .output/*-mv3.zip
-4
View File
@@ -1,11 +1,7 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.output
+1 -1
View File
@@ -4,7 +4,7 @@
.zed
node_modules
package-lock.json
pnpm-lock.yaml
.output
.wxt
+31 -20
View File
@@ -4,7 +4,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<p align="center">
<a href="https://github.com/bytedream/stream-bypass/releases/latest">
<img src="https://img.shields.io/github/v/release/ByteDream/stream-bypass?label=Version&style=flat-square" alt="Version">
<img src="https://img.shields.io/github/v/release/bytedream/stream-bypass?label=Version&style=flat-square" alt="Version">
</a>
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/">
<img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Users&style=flat-square" alt="Firefox Addon Store">
@@ -13,7 +13,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store">
</a>
<a href="https://github.com/bytedream/stream-bypass/releases/latest">
<img src="https://img.shields.io/github/downloads/ByteDream/stream-bypass/total?label=GitHub%20Downloads&style=flat-square" alt="GitHub Downloads">
<img src="https://img.shields.io/github/downloads/bytedream/stream-bypass/total?label=GitHub%20Downloads&style=flat-square" alt="GitHub Downloads">
</a>
</p>
@@ -54,13 +54,18 @@ The best way to install the extension are the official browser extension stores:
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
<details>
<summary><h3>Manual installation</h3></summary>
<summary><h3 id="manual-installation1">Manual installation</h3></summary>
- Firefox (mv2)
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that)
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. with [7zip](https://www.7-zip.org/))
- Go into your browser and type `about:debugging#/runtime/this-firefox` in the address bar
- Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory
- Chromium / Google Chrome (mv3) > As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them - Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that) - Go into your browser and type `chrome://extensions` in the address bar - Turn on the developer mode by checking the switch in the top right corner - Click `Load unpacked` and choose the unzipped directory
- Chromium / Google Chrome (mv3)
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
- Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. [7zip](https://www.7-zip.org/))
- Go into your browser and type `chrome://extensions` in the address bar
- Turn on the developer mode by checking the switch in the top right corner
- Click `Load unpacked` and choose the unzipped directory
</details>
@@ -71,9 +76,9 @@ The best way to install the extension are the official browser extension stores:
| Feature | Firefox (mv2) | Chrome (mv3) | Firefox for Android (mv2) |
| --------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------------------- |
| Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
| Support websites that are accessed via a redirect | ✔ | ✖ | ✔ |
| Open video in mpv (with [ff2mpv](https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ✖ |
| Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
| Support websites that are accessed via a redirect | ✔ | ✖ | ✔ |
| Open video in mpv (with [ff2mpv](https://github.com/bytedream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ✖ |
## 📜 Supported websites
@@ -81,8 +86,8 @@ The best way to install the extension are the official browser extension stores:
- ⚠: Works with limitations.
- ✖: Not supported.
| Site | Firefox & Firefox for Android (mv2) | Chrome & Chromium based (mv2) |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Site | Firefox & Firefox for Android (mv2) | Chrome & Chromium based (mv3) |
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| [dropload.io](https://dropload.io) | ✔ | ✔ |
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
| [filemoon.to](https://filemoon.to) | ✔ | ✔ |
@@ -90,7 +95,7 @@ The best way to install the extension are the official browser extension stores:
| [kwik.cx](https://kwik.cx) | ✔ | ✔ |
| [loadx.ws](https://loadx.ws) | ✔ | ✖ (background request always required) |
| [luluvdo.com](https://luluvdo.com) | ✔ | ✖ (background request always required) |
| [mixdrop.co](https://mixdrop.co) | ✔ ️ | ✔ |
| [mixdrop.co](https://mixdrop.co) | ✔ ️ | ✔ |
| [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
| [newgrounds.com](https://newgrounds.com) | ✔ | ✔ |
| [streama2z.com](https://streama2z.com) | ✔ | ✖ (redirect always required) |
@@ -113,25 +118,27 @@ If you want to build the addon from source and not using the way described in [i
Requirements:
- `npm` installed.
- `pnpm` installed.
- A copy of this repository and a shell / console open in the copied directory.
If the requirements are satisfied, you can continue with the following commands:
```shell
# install all dependencies
$ npm install
$ pnpm i
# build the extension source to the dist/ directory
$ npm run build
# build the extension and start it in a new firefox instance
$ pnpm run dev:firefox
# same as build + more optimizations and browser specific settings at release/
$ npm run release:firefox # or "release:chrome" to create a release for chromium based browsers
# build the extension with optimizations to the .output/firefox-mv2 directory
$ pnpm run build:firefox
```
You can omit the `:firefox` suffix, then it's built for Chrome.
##### Install
If you want to use the addon in Chromium or any browser which is based on it, follow the steps in [installation](#-installation).
If you want to use the addon in Chromium or any browser which is based on it, follow the steps in the [manual installation](#-installation).
When using firefox, use the following:
1. Type `about:debugging` in the browser's address bar.
@@ -141,11 +148,15 @@ When using firefox, use the following:
## ⚙️ Settings
> You reach the settings by pressing the tree dots (⋮) in the top right corner of the extension popup.
> You reach the settings by pressing the settings icon in the top right corner of the extension popup.
### Hosts
You can enable or disabled for which hosts the extension should redirect.
You can enable or disable for which hosts the extension should work.
### Per-site disables
Herer you can disable specific hosts (or all of them) for the current site only - useful when a site uses multiple hosts and you only want to bypass a few, or when a host misbehaves on a particular site.
### ff2mpv
+2
View File
@@ -25,6 +25,8 @@ export default defineConfig(
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-alert': 'error',
'no-console': 'error',
'no-undef': 'off'
}
}
-8355
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -1,6 +1,6 @@
{
"name": "stream-bypass",
"version": "4.0.0",
"version": "5.0.0",
"displayName": "Stream Bypass",
"author": "bytedream",
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
@@ -20,35 +20,35 @@
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"check": "svelte-check --tsconfig ./tsconfig.json",
"postinstall": "wxt prepare",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
"lint": "prettier --check . && eslint . && svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@eslint/compat": "^1.4.1",
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
"@steeze-ui/heroicons": "^2.4.2",
"@steeze-ui/svelte-icon": "^1.6.2",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.16",
"@tsconfig/svelte": "^5.0.5",
"@wxt-dev/module-svelte": "^2.0.4",
"eslint": "^9.38.0",
"@sveltejs/vite-plugin-svelte": "^7.2.0",
"@tailwindcss/vite": "^4.3.2",
"@tsconfig/svelte": "^5.0.8",
"@wxt-dev/module-svelte": "^2.0.5",
"eslint": "^10.6.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.5",
"hls.js": "^1.6.13",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"sass": "^1.93.2",
"svelte": "^5.43.2",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.16",
"eslint-plugin-svelte": "^3.20.0",
"hls.js": "^1.6.16",
"prettier": "^3.9.4",
"prettier-plugin-svelte": "^4.1.1",
"sass": "^1.101.0",
"svelte": "^5.56.4",
"svelte-check": "^4.7.2",
"tailwindcss": "^4.3.2",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.12",
"web-ext": "^9.1.0",
"wxt": "^0.20.11"
"typescript": "^6.0.3",
"typescript-eslint": "^8.63.0",
"vite": "^8.1.4",
"web-ext": "^10.4.0",
"wxt": "^0.20.27"
}
}
+5724
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
allowBuilds:
'@parcel/watcher': true
esbuild: true
spawn-sync: false
+148 -27
View File
@@ -1,30 +1,151 @@
import { UrlReferer } from '@/lib/settings';
import { BackgroundMessageData, BackgroundMessageType } from '@/lib/communication';
import { getHost, hosts } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
const temporaryReferers: Record<string, string> = {};
const allHostDomains = hosts.flatMap((host) => host.domains);
function domainToMatch(domain: string) {
return `*://${domain}/*`;
}
function domainsToMatch(domains: string[]) {
return domains.map(domainToMatch);
}
async function reInjectContentScript(domain: string) {
const contentScript: Browser.scripting.RegisteredContentScript = {
id: 'temporary-hosts',
js: ['content-scripts/content.js'],
matches: [domainToMatch(domain)],
persistAcrossSessions: false,
allFrames: true,
runAt: 'document_end'
};
const contentScripts = await browser.scripting.getRegisteredContentScripts({ ids: [contentScript.id] });
if (contentScripts.length !== 0) {
const registeredContentScript = contentScripts[0];
// do not double register for same domain
if (registeredContentScript.matches!.indexOf(contentScript.matches![0]) !== -1) return;
contentScript.matches!.push(...contentScripts[0].matches!);
await browser.scripting.updateContentScripts([contentScript]);
} else {
await browser.scripting.registerContentScripts([contentScript]);
}
}
/* --- listeners --- */
async function browserRuntimeOnMessageListener(message: any, sender: Browser.runtime.MessageSender) {
const type = message.type as BackgroundMessageType;
let data = message.data;
let response;
switch (type) {
case BackgroundMessageType.Ff2mpv: {
data = data as BackgroundMessageData<typeof type>;
await browser.runtime.sendNativeMessage('ff2mpv', { url: data.url });
break;
}
case BackgroundMessageType.RequestTabUrl: {
response = sender.tab?.url ?? null;
break;
}
case BackgroundMessageType.RegisterContentScript: {
// requires "dynamic" host permissions, which is only available in mv2
if (import.meta.env.MANIFEST_VERSION === 3) break;
data = data as BackgroundMessageData<typeof type>;
await reInjectContentScript(data.domain);
break;
}
case BackgroundMessageType.RegisterTemporaryReferer: {
// requires "dynamic" host permissions, which is only available in mv2
if (import.meta.env.MANIFEST_VERSION === 3) break;
data = data as BackgroundMessageData<typeof type>;
if (data.domain in temporaryReferers) break;
temporaryReferers[data.domain] = data.referer;
// remount before send headers listener which to also listen on the new temporary referer domain
registerBrowserWebRequestOnBeforeSendHeadersListener();
break;
}
default:
break;
}
return response;
}
function registerBrowserRuntimeOnMessageListener() {
browser.runtime.onMessage.addListener(browserRuntimeOnMessageListener);
}
function browserWebRequestOnBeforeSendHeadersListener(
details: Browser.webRequest.OnBeforeSendHeadersDetails
): Browser.webRequest.BlockingResponse | undefined {
const referer = temporaryReferers[new URL(details.url).host];
if (!referer) return;
details.requestHeaders!.push({
name: 'Referer',
value: `https://${referer}/`
});
return { requestHeaders: details.requestHeaders };
}
function registerBrowserWebRequestOnBeforeSendHeadersListener() {
// only available in mv2
if (import.meta.env.MANIFEST_VERSION === 3) return;
// background crashes when a listener with empty url is registered
else if (Object.keys(temporaryReferers).length === 0) return;
browser.webRequest.onBeforeSendHeaders.removeListener(browserWebRequestOnBeforeSendHeadersListener);
browser.webRequest.onBeforeSendHeaders.addListener(
browserWebRequestOnBeforeSendHeadersListener,
{
urls: domainsToMatch(Object.keys(temporaryReferers)),
types: ['xmlhttprequest']
},
['blocking', 'requestHeaders']
);
}
async function browserWebRequestOnBeforeRedirectListener(details: Browser.webRequest.OnBeforeRedirectDetails) {
const domain = new URL(details.url).host;
const host = await getHost(domain);
if (!host) return;
const newDomain = new URL(details.redirectUrl).host;
await Promise.all([
HostSettings.addTemporaryHostDomain(host.id, newDomain, { registerContentScript: false }),
// must be called manually, a message sent from a background script is only received on non-background pages
reInjectContentScript(newDomain)
]);
// add new domain to internal list of supported hosts and remount listener to also listen on the new domain
if (allHostDomains.indexOf(newDomain) === -1) allHostDomains.push(newDomain);
queueMicrotask(registerBrowserWebRequestOnBeforeRedirectListener);
}
function registerBrowserWebRequestOnBeforeRedirectListener() {
// only available in mv2
if (import.meta.env.MANIFEST_VERSION === 3) return;
browser.webRequest.onBeforeRedirect.removeListener(browserWebRequestOnBeforeRedirectListener);
browser.webRequest.onBeforeRedirect.addListener(browserWebRequestOnBeforeRedirectListener, {
urls: domainsToMatch(allHostDomains),
types: ['main_frame', 'sub_frame']
});
}
export default defineBackground(() => {
browser.runtime.onMessage.addListener(async (message) => {
if (message.action == 'ff2mpv') {
await browser.runtime.sendNativeMessage('ff2mpv', { url: message.url });
}
// the following listener is only available in mv2
if (import.meta.env.MANIFEST_VERSION === 3) return;
browser.webRequest.onBeforeSendHeaders.addListener(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async (details) => {
const referer = await UrlReferer.get(new URL(details.url).hostname);
if (!referer) return;
details.requestHeaders!.push({
name: 'Referer',
value: `https://${referer}/`
});
return { requestHeaders: details.requestHeaders };
},
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] },
['blocking', 'requestHeaders']
);
});
registerBrowserRuntimeOnMessageListener();
registerBrowserWebRequestOnBeforeRedirectListener();
});
+19 -12
View File
@@ -1,21 +1,28 @@
import { getHost, hosts, type Host, type HostMatch } from '@/lib/host';
import { FF2MPVSettings } from '@/lib/settings';
import { BackgroundMessageType, sendBackgroundMessage } from '@/lib/communication';
import { getHost, HostMatchType, hosts, type HostMatch } from '@/lib/host';
import { FF2MPVSettings, PerDomainSettings } from '@/lib/settings';
export default defineContentScript({
matches: [
...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`)),
// only mv2 allows to match all urls
...(import.meta.env.MANIFEST_VERSION === 2 ? ['<all_urls>'] : [])
],
matches: [...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`))],
allFrames: true,
runAt: 'document_end',
main
});
async function main() {
let host: Host | null;
if ((host = await getHost(window.location.host)) === null) {
return;
const host = await getHost(window.location.host);
if (!host) return;
// if current windows is an iframe, check if the parent has any disabled hosts
if (window !== window.top) {
const tabUrl = await sendBackgroundMessage(BackgroundMessageType.RequestTabUrl, undefined);
if (tabUrl) {
const domain = new URL(tabUrl).host;
const perDomainSetting = await PerDomainSettings.get(domain);
if (perDomainSetting.allDisabled) return;
else if (perDomainSetting.disabledHostIds.indexOf(host.id) !== -1) return;
}
}
let re = null;
@@ -24,7 +31,7 @@ async function main() {
break;
}
}
if (re === null) {
if (!re) {
return;
}
@@ -42,7 +49,7 @@ async function main() {
await browser.runtime.sendMessage({ action: 'ff2mpv', url: hostMatch.url });
}
if (host.replace && hostMatch.type != 'hls') {
if (host.replace && hostMatch.type != HostMatchType.HLS) {
// this destroys all intervals that may spawn popups or events
let intervalId = window.setInterval(() => {}, 0);
while (intervalId--) {
+19 -7
View File
@@ -1,11 +1,19 @@
import Hls from 'hls.js';
import { listenMessages, MessageType, sendMessage } from '@/lib/communication';
import {
BackgroundMessageType,
listenTabMessages,
sendBackgroundMessage,
sendTabBroadcast,
TabMessageType
} from '@/lib/communication';
import { HostMatchType, hosts } from '@/lib/host';
import { UrlReferer } from '@/lib/settings';
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
// multiple hosts need to have a correct referer set
await UrlReferer.addTemporary(new URL(url).hostname, domain);
await sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
domain: new URL(url).host,
referer: domain
});
videoElem.src = url;
}
@@ -18,7 +26,11 @@ async function playHls(url: string, domain: string, videoElem: HTMLVideoElement)
enableWorker: false,
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
// multiple hosts need to have a correct referer set
await UrlReferer.addTemporary(new URL(url).hostname, domain);
await sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
domain: new URL(url).host,
referer: domain
});
xhr.open('GET', url);
}
});
@@ -56,7 +68,7 @@ export async function play(videoElem: HTMLVideoElement) {
function initCommunication(id: string, url: string, domain: string) {
const notifyActiveMatch = () =>
sendMessage(MessageType.NotifyActiveMatch, {
sendTabBroadcast(TabMessageType.NotifyActiveMatch, {
id: id,
url: url,
domain: domain
@@ -67,8 +79,8 @@ function initCommunication(id: string, url: string, domain: string) {
// if an extension popup is opened, the listener will recognize it's active match request and send the match/player
// data
const cancel = listenMessages((type) => {
if (type !== MessageType.RequestActiveMatch) return;
const cancel = listenTabMessages((type) => {
if (type !== TabMessageType.RequestActiveMatch) return;
notifyActiveMatch();
});
window.onbeforeunload = cancel;
+19 -5
View File
@@ -2,32 +2,46 @@
import '@/assets/base.css';
import { fly } from 'svelte/transition';
import Toast from '@/entrypoints/popup/components/Toast.svelte';
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
import SiteConfig from '@/entrypoints/popup/pages/site-config/SiteConfig.svelte';
import { isMobile } from '@/entrypoints/popup/state.js';
/* state init */
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
/* types */
type Page = 'main' | 'settings';
type Page = 'main' | 'settings' | 'site-config';
/* states */
let activePage = $state<Page>('main');
</script>
<div class="flex w-[350px] overflow-hidden" class:w-screen={$isMobile}>
<div class="flex w-78 overflow-hidden" class:w-screen={$isMobile}>
<Toast />
{#if activePage === 'main'}
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-[300px] flex-1 flex flex-col">
<Main onSettingsOpenRequest={() => (activePage = 'settings')} />
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-80 flex-1 flex flex-col">
<Main
onSettingsOpenRequest={() => (activePage = 'settings')}
onSiteConfigOpenRequest={() => (activePage = 'site-config')}
/>
</div>
{:else if activePage === 'settings'}
<div
transition:fly={{ x: 300, duration: 150 }}
class="min-w-full w-full h-[300px] flex-1 flex flex-col"
class="min-w-full w-full h-120 flex-1 flex flex-col"
class:h-screen={$isMobile}
>
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
</div>
{:else if activePage === 'site-config'}
<div
transition:fly={{ x: 300, duration: 150 }}
class="min-w-full w-full h-120 flex-1 flex flex-col"
class:h-screen={$isMobile}
>
<SiteConfig onBackClick={() => (activePage = 'main')} />
</div>
{/if}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import { ArrowLeft } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
/* types */
interface Props {
onclick: () => void;
}
/* states */
let { onclick }: Props = $props();
</script>
<button
type="button"
class="shrink-0 flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
{onclick}
>
<Icon src={ArrowLeft} size="1.1rem" />
</button>
@@ -1 +1,3 @@
<div class="w-full border-b-[1px] border-gray-400"></div>
<script lang="ts"></script>
<div class="w-full border-b border-gray-400"></div>
@@ -0,0 +1,63 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { Host } from '@/lib/host';
/* types */
interface Props {
host: Host;
enabled?: boolean;
badge?: string;
onclick?: () => void;
actions: Snippet;
}
/* states */
let { host, enabled, badge, onclick, actions }: Props = $props();
let rowClass = $derived(
`flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${onclick ? 'hover:bg-gray-800/40 cursor-pointer' : ''}`
);
/* functions */
function handleKeydown(event: KeyboardEvent) {
if (!onclick) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onclick();
}
}
</script>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div
class={rowClass}
role={onclick ? 'button' : undefined}
tabindex={onclick ? 0 : undefined}
{onclick}
onkeydown={handleKeydown}
>
<span
class="w-2 h-2 rounded-full shrink-0"
class:bg-linux-mint-green={enabled === true}
class:bg-gray-500={enabled === false}
class:bg-transparent={enabled === undefined}
></span>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<p class="text-sm font-semibold text-gray-100 truncate">{host.name}</p>
{#if badge}
<span
class="shrink-0 text-[0.65rem] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-linux-mint-green/20 text-linux-mint-green"
>
{badge}
</span>
{/if}
</div>
<p class="text-xs text-gray-400 truncate" title={host.domains.join(', ')}>
{host.domains.join(', ')}
</p>
</div>
<div class="shrink-0">
{@render actions()}
</div>
</div>
@@ -0,0 +1,39 @@
<script lang="ts">
import { ExclamationTriangle, InformationCircle } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
/* types */
interface Props {
level: 'warning' | 'info';
title?: string;
description?: string;
}
/* states */
let { level, title, description }: Props = $props();
const icons = {
warning: ExclamationTriangle,
info: InformationCircle
};
const containerStyles = {
warning: 'bg-yellow-900/30 border-yellow-700/50 text-yellow-200',
info: 'bg-blue-900/30 border-blue-700/50 text-blue-200'
};
const iconStyles = {
warning: 'text-yellow-400',
info: 'text-blue-400'
};
</script>
<div class="mx-2 mt-1 flex items-start gap-2 px-2.5 py-2 border rounded-md text-xs {containerStyles[level]}">
<Icon src={icons[level]} size="0.95rem" class={iconStyles[level]} />
<div class="leading-snug">
{#if title}
<p class="font-semibold">{title}</p>
{/if}
{#if description}
<p class="opacity-80">{description}</p>
{/if}
</div>
</div>
@@ -0,0 +1,35 @@
<script lang="ts">
import type { Snippet } from 'svelte';
/* types */
interface Props {
title: string;
subtitle?: string;
actionsLeft?: Snippet;
actionsRight?: Snippet;
}
/* states */
let { title, subtitle, actionsLeft, actionsRight }: Props = $props();
</script>
<div class="flex items-center gap-2 px-3 py-2.5">
{#if actionsLeft}
<div class="shrink-0 flex items-center gap-2">
{@render actionsLeft()}
</div>
{/if}
<div class="min-w-0 flex-1">
<h1 class="text-lg font-semibold text-gray-100 leading-tight truncate">{title}</h1>
{#if subtitle}
<p class="text-xs text-gray-400 leading-tight truncate" title={subtitle}>{subtitle}</p>
{/if}
</div>
{#if actionsRight}
<div class="shrink-0 flex items-center gap-2">
{@render actionsRight()}
</div>
{/if}
</div>
@@ -0,0 +1,34 @@
<script lang="ts">
import { MagnifyingGlass, XMark } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
/* types */
interface Props {
value: string;
placeholder?: string;
}
/* states */
let { value = $bindable(''), placeholder = 'Search…' }: Props = $props();
</script>
<div
class="flex items-center gap-2 px-2.5 py-1.5 bg-gray-900/60 border border-gray-700 rounded-md focus-within:border-linux-mint-green transition-colors"
>
<Icon src={MagnifyingGlass} size="0.95rem" class="text-gray-400 shrink-0" />
<input
type="text"
bind:value
{placeholder}
class="flex-1 bg-transparent text-sm text-gray-100 placeholder:text-gray-500 outline-none min-w-0"
/>
{#if value}
<button
type="button"
class="text-gray-400 hover:text-gray-100 cursor-pointer shrink-0"
onclick={() => (value = '')}
>
<Icon src={XMark} size="0.95rem" />
</button>
{/if}
</div>
@@ -0,0 +1,58 @@
<script lang="ts">
import { ChevronDown } from '@steeze-ui/heroicons';
import { Icon, type IconSource } from '@steeze-ui/svelte-icon';
import { untrack, type Snippet } from 'svelte';
import { slide } from 'svelte/transition';
/* types */
interface Props {
title: string;
description?: string;
icon?: IconSource;
defaultOpen?: boolean;
children: Snippet;
}
/* states */
let { title, description, icon, defaultOpen = false, children }: Props = $props();
let open = $state(untrack(() => defaultOpen));
</script>
<div class="bg-gray-800/40 border border-gray-700/60 rounded-lg max-h-full flex flex-col">
<button
type="button"
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 cursor-pointer hover:bg-gray-800/60 transition-colors"
onclick={() => (open = !open)}
>
<div class="flex items-center gap-2 min-w-0">
{#if icon}
<Icon src={icon} size="1.1rem" class="text-gray-400 shrink-0" />
{/if}
<div class="min-w-0 text-left">
<h3 class="text-sm font-semibold text-gray-100 leading-tight">{title}</h3>
{#if description}
<p class="text-xs text-gray-400 mt-0.5 leading-tight">{description}</p>
{/if}
</div>
</div>
<span class="text-gray-400 shrink-0 inline-flex chevron" class:rotated={open}>
<Icon src={ChevronDown} size="1rem" />
</span>
</button>
{#if open}
<div class="px-3 py-3 flex overflow-hidden" transition:slide={{ duration: 150 }}>
<div class="w-full">
{@render children()}
</div>
</div>
{/if}
</div>
<style>
.chevron {
transition: transform 150ms ease;
}
.rotated {
transform: rotate(180deg);
}
</style>
@@ -0,0 +1,13 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { toast } from '@/entrypoints/popup/state';
</script>
{#if $toast}
<div
class="absolute top-2 left-1/2 -translate-x-1/2 z-20 w-max max-w-11/12 px-3 py-1.5 bg-gray-700 text-gray-100 text-sm rounded shadow-lg pointer-events-none"
transition:fly={{ y: -20, duration: 200 }}
>
{$toast.message}
</div>
{/if}
+18 -3
View File
@@ -4,14 +4,17 @@
checked: boolean;
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
size?: 'sm' | 'md';
disabled?: boolean;
title?: string;
}
/* states */
let { checked = $bindable(), onChecked, size = 'md' }: Props = $props();
let { checked = $bindable(), onChecked, size = 'md', disabled = false, title }: Props = $props();
let internalChecked = $state($state.snapshot(checked));
/* callbacks */
async function onInputChange() {
if (disabled) return;
internalChecked = !internalChecked;
let approved = false;
@@ -36,9 +39,21 @@
}
</script>
<label class="flex items-center cursor-pointer">
<label
class="flex items-center"
class:cursor-pointer={!disabled}
class:cursor-not-allowed={disabled}
class:opacity-50={disabled}
{title}
>
<div class="relative">
<input type="checkbox" class="peer sr-only" bind:checked={internalChecked} onchange={onInputChange} />
<input
type="checkbox"
class="peer sr-only"
bind:checked={internalChecked}
onchange={onInputChange}
{disabled}
/>
<div
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
class:w-8={size === 'sm'}
@@ -1,3 +1,5 @@
<script lang="ts"></script>
<div class="h-full flex items-center justify-center">
<p class="text-[1.05rem]">Extension disabled</p>
</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
import { isMobile } from '@/entrypoints/popup/state';
import { isMobile, showToast } from '@/entrypoints/popup/state';
/* types */
interface Props {
@@ -28,8 +28,13 @@
}
/* callbacks */
function copyUrl() {
navigator.clipboard.writeText(urlOutput);
async function copyUrl() {
try {
await navigator.clipboard.writeText(urlOutput);
showToast('URL copied');
} catch {
showToast('Failed to copy');
}
}
</script>
@@ -47,7 +52,7 @@
<div class="relative group h-4 flex justify-center items-center">
<button class="text-sm peer"><Icon src={InformationCircle} size="1rem" /></button>
<span
class="z-10 absolute w-58 bottom-5/4 p-1 bg-gray-800 text-xs invisible opacity-0 group-hover:visible group-hover:opacity-100 peer-focus:visible peer-focus:opacity-100 transition-[opacity]"
class="z-10 absolute w-58 bottom-5/4 p-1 bg-gray-800 text-xs invisible opacity-0 group-hover:visible group-hover:opacity-100 peer-focus:visible peer-focus:opacity-100 transition-opacity"
>You may have to send the referer header <code class="select-text">Referer: {domain}</code> when accessing
the url</span
>
@@ -57,7 +62,7 @@
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div class="relative group w-full" tabindex={$isMobile ? 0 : undefined}>
<pre
class="w-full h-20 overflow-y-scroll text-[0.8rem] wrap-anywhere text-wrap select-text rounded bg-gray-900 py-[0.25rem] px-1.5">{urlOutput}</pre>
class="w-full h-20 overflow-y-scroll text-[0.8rem] wrap-anywhere text-wrap select-text rounded bg-gray-900 py-1 px-1.5">{urlOutput}</pre>
<div
class="absolute top-2 right-2 transition-opacity duration-100 opacity-0 group-hover:opacity-100 group-focus:opacity-100 h-full"
>
@@ -0,0 +1,29 @@
<script lang="ts">
import { AdjustmentsHorizontal, ChevronRight, GlobeAlt } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
/* types */
interface Props {
domain: string;
onConfigureClick: () => void;
}
/* states */
let { domain, onConfigureClick }: Props = $props();
</script>
<div class="flex items-center justify-between gap-2 px-3 py-1.5 text-sm text-gray-300">
<span class="flex items-center gap-2 min-w-0">
<Icon src={GlobeAlt} size="0.95rem" class="text-gray-400 shrink-0" />
<span class="truncate" title={domain}>{domain}</span>
</span>
<button
type="button"
class="flex items-center gap-1 shrink-0 text-xs text-gray-400 hover:text-gray-200 px-1.5 py-0.5 rounded hover:bg-gray-800/60 cursor-pointer transition-colors"
onclick={onConfigureClick}
>
<Icon src={AdjustmentsHorizontal} size="0.85rem" />
<span>Configure</span>
<Icon src={ChevronRight} size="0.75rem" />
</button>
</div>
+14 -9
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import { Cog6Tooth } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
import { HostSettings } from '@/lib/settings';
@@ -32,15 +35,17 @@
});
</script>
<div class="flex justify-between items-center p-2">
<div class="flex items-baseline gap-2">
<h1>stream-bypass</h1>
<span class="text-xs text-gray-400">v{import.meta.env.VERSION}</span>
</div>
<div class="flex items-center gap-2">
<PageHeader title="stream-bypass" subtitle="v{import.meta.env.VERSION}">
{#snippet actionsRight()}
{#key allHostsDisabled}
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
{/key}
<button class="font-bold cursor-pointer" onclick={() => onSettingsClick()}>⋮</button>
</div>
</div>
<button
type="button"
class="shrink-0 flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
onclick={() => onSettingsClick()}
>
<Icon src={Cog6Tooth} size="1.1rem" />
</button>
{/snippet}
</PageHeader>
+51 -22
View File
@@ -3,63 +3,92 @@
import { onDestroy } from 'svelte';
import Divider from '@/entrypoints/popup/components/Divider.svelte';
import AllDisabled from '@/entrypoints/popup/pages/main/AllDisabled.svelte';
import Notice from '@/entrypoints/popup/components/Notice.svelte';
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
import CurrentSite from '@/entrypoints/popup/pages/main/CurrentSite.svelte';
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
import NoMatch from '@/entrypoints/popup/pages/main/NoMatch.svelte';
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
import { listenTabMessages, sendTabMessageToActiveTab, TabMessageType } from '@/lib/communication';
import { hosts, type Host } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
/* types */
interface Props {
onSettingsOpenRequest: () => void;
onSiteConfigOpenRequest: () => void;
}
/* states */
let { onSettingsOpenRequest }: Props = $props();
let { onSettingsOpenRequest, onSiteConfigOpenRequest }: Props = $props();
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
let currentDomain = $state<string | null>(null);
let allHostsDisabled = $state(false);
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
/* lifecycle */
const cancel = listenMessages((type, data) => {
if (type !== MessageType.NotifyActiveMatch) return;
currentMatch = {
const cancel = listenTabMessages((type, data) => {
if (type !== TabMessageType.NotifyActiveMatch) return;
const match = {
host: hosts.find((host) => host.id === data.id)!,
url: data.url,
domain: data.domain
};
currentMatch = match;
});
sendTabMessageToActiveTab(TabMessageType.RequestActiveMatch, undefined);
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
const url = tabs[0]?.url;
if (!url) return;
currentDomain = new URL(url).hostname;
});
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
onDestroy(cancel);
</script>
<div class="w-full">
<div class="w-full shrink-0">
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
<Divider />
</div>
<div class="px-2 h-full">
{#if allHostsDisabled}
<AllDisabled />
{:else if !currentMatch}
<NoMatch />
{:else}
<div class="flex flex-col justify-between h-full pb-2">
<Match host={currentMatch.host} domain={currentMatch.domain} />
<div class="divider border-dashed"></div>
<div class="mt-2">
<div class="grid grid-cols-1 grid-rows-[min-content_auto_min-content] gap-2 h-full">
<div class="px-2 pt-1.5 shrink-0">
{#if currentDomain}
<CurrentSite domain={currentDomain} onConfigureClick={onSiteConfigOpenRequest} />
{/if}
</div>
<div>
{#if allHostsDisabled}
<Notice
level="warning"
title="Extension disabled"
description="Configurations are still saved and take effect the when extension is re-enabled."
/>
{:else if currentMatch}
<div class="flex flex-col justify-between h-full px-2">
<Match host={currentMatch.host} />
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
</div>
</div>
{/if}
{:else}
<div class="flex-1 min-h-0 flex flex-col">
<Notice level="info" title="No match found" description="No redirectable host found on this site" />
</div>
{/if}
</div>
<div class="px-2 pb-0.5">
<p class="text-[.65rem] text-gray-400 text-center">
Suggestions or bugs can be submitted <a
class="underline"
href="https://github.com/bytedream/stream-bypass/issues">here</a
>
</p>
</div>
</div>
<style>
* {
@apply select-none;
user-select: none;
}
</style>
@@ -4,17 +4,13 @@
/* types */
interface Props {
host: Host;
domain: string;
}
/* states */
const { host, domain }: Props = $props();
const { host }: Props = $props();
</script>
<div class="flex flex-col items-center justify-center w-full h-full">
<p class="text-lg">Match found:</p>
<div class="[&>*]:select-text">
<span class="underline text-green-400 text-2xl font-bold">{host.name}</span>
<span class="text-xs text-slate-300">({domain})</span>
</div>
<div class="flex flex-col items-center justify-center">
<p class="text-md text-gray-400">Match found:</p>
<span class="underline text-green-400 text-xl font-bold select-text">{host.name}</span>
</div>
@@ -1,13 +0,0 @@
<div class="relative h-full">
<div class="h-full flex items-center justify-center">
<p class="text-[1.05rem]">No supported video found on this site</p>
</div>
<div class="absolute bottom-0.5">
<p class="text-xs text-gray-400">
Suggestions or bugs can be submitted <a
class="underline"
href="https://github.com/bytedream/stream-bypass/issues">here</a
>
</p>
</div>
</div>
@@ -1,4 +1,5 @@
<script lang="ts">
import { showToast } from '../../state';
import { InformationCircle } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
@@ -9,21 +10,28 @@
FF2MPVSettings.getEnabled().then((val) => (enabled = val));
/* callbacks */
function onEnableChange(enabled: boolean) {
async function onEnableChange(enabled: boolean) {
if (!enabled) return true;
return browser.permissions.request({ permissions: ['nativeMessaging'] });
const id = setTimeout(() => showToast('Accept the permission request to allow communication with mpv'), 100);
const confirmed = await browser.permissions.request({ permissions: ['nativeMessaging'] });
clearTimeout(id);
return confirmed;
}
</script>
<div class="flex items-center gap-2">
<div class="relative mr-3">
<span>Communication enabled</span>
<div class="flex items-center justify-between">
<div class="flex flex-col gap-0.5">
<p class="text-sm font-semibold text-gray-100">Communication enabled</p>
<a
class="absolute -top-1 -right-4 text-sm"
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv-use-mpv-to-directly-play-streams"
target="_blank"><Icon src={InformationCircle} size="1rem" /></a
class="inline-flex items-center gap-1 text-xs text-gray-400 hover:text-gray-200 transition-colors"
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv"
target="_blank"
>
<Icon src={InformationCircle} size="0.85rem" />
<span>Learn more</span>
</a>
</div>
{#key enabled}
<Toggle
@@ -1,4 +1,7 @@
<script lang="ts">
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
/* types */
interface Props {
onBackClick: () => void;
@@ -8,9 +11,8 @@
let { onBackClick }: Props = $props();
</script>
<div class="flex justify-between items-center p-2">
<div class="flex items-baseline gap-2">
<button class="cursor-pointer" onclick={() => onBackClick()}>←</button>
<h1>Settings</h1>
</div>
</div>
<PageHeader title="Settings" subtitle="Manage hosts and integrations">
{#snippet actionsLeft()}
<BackButton onclick={onBackClick} />
{/snippet}
</PageHeader>
@@ -1,41 +1,47 @@
<script lang="ts">
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
import SearchInput from '@/entrypoints/popup/components/SearchInput.svelte';
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
import { hosts } from '@/lib/host';
import { filterHosts, hosts } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
/* types */
interface Props {
searchQuery: string;
}
/* states */
let { searchQuery = $bindable('') }: Props = $props();
let disabledHostIds = $state<Array<string>>([]);
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
let filtered = $derived(filterHosts(hosts, searchQuery));
</script>
<div class="grid grid-cols-[35%_43%_22%] gap-y-0.75">
<p class="font-bold">Host</p>
<p class="font-bold">Domains</p>
<p class="font-bold">Enabled</p>
{#each hosts as host (host.id)}
{@const domainList = host.domains.join(', ')}
<p>{host.name}</p>
<div>
<label for={host.id}>
<input id={host.id} type="checkbox" class="peer hidden" checked />
<p
title={domainList}
class="cursor-pointer overflow-hidden peer-checked:text-ellipsis peer-checked:text-nowrap"
>
{domainList}
</p>
</label>
<div class="flex flex-col gap-2 max-h-full">
<SearchInput bind:value={searchQuery} placeholder="Filter hosts…" />
{#if filtered.length === 0}
<p class="text-xs text-gray-400 text-center py-2">No hosts match "{searchQuery}"</p>
{:else}
<div class="flex flex-col divide-y divide-gray-700/40 -mx-1 max-h-full overflow-y-scroll">
{#each filtered as host (host.id)}
<HostRow {host} enabled={!disabledHostIds.includes(host.id)}>
{#snippet actions()}
{#key disabledHostIds}
<Toggle
bind:checked={
() => !disabledHostIds.includes(host.id),
(v) =>
v
? HostSettings.removeDisabledHost(host.id)
: HostSettings.addDisabledHost(host.id)
}
size="sm"
/>
{/key}
{/snippet}
</HostRow>
{/each}
</div>
<div class="mt-[.2rem]">
{#key disabledHostIds}
<Toggle
bind:checked={
() => !disabledHostIds.includes(host.id),
(v) => (v ? HostSettings.removeDisabledHost(host) : HostSettings.addDisabledHost(host))
}
size="sm"
></Toggle>
{/key}
</div>
{/each}
{/if}
</div>
@@ -0,0 +1,207 @@
<script lang="ts">
import { showToast } from '../../state';
import { Trash } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
import { getHostFromId, hosts } from '@/lib/host';
import { PerDomainSettings, type PerDomainConfig } from '@/lib/settings';
/* constants */
const ALL_HOSTS = '__all__';
/* states */
let perDomainMap = $state<Record<string, PerDomainConfig>>({});
let expanded = $state<string | null>(null);
let newDomainInput = $state<string>('');
let selectedHostId = $state<string>('');
loadMap();
/* functions */
async function loadMap() {
perDomainMap = await PerDomainSettings.getMap();
}
function parseDomain(input: string): string | null {
let d = input.trim().toLowerCase();
if (!d) return null;
d = d.replace(/^https?:\/\//, '');
d = d.split('/')[0];
d = d.split(':')[0];
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/.test(d)) {
return null;
}
return d;
}
/* callbacks */
async function onClear(domain: string) {
await PerDomainSettings.clear(domain);
await loadMap();
if (expanded === domain) expanded = null;
}
async function onAllRemove(domain: string) {
await PerDomainSettings.setAllDisabled(domain, false);
await loadMap();
}
async function onHostRemove(domain: string, hostId: string) {
await PerDomainSettings.setHostDisabled(domain, hostId, false);
await loadMap();
}
async function onHostAdd(domain: string, hostId: string) {
if (!hostId) return;
if (hostId === ALL_HOSTS) {
await PerDomainSettings.setAllDisabled(domain, true);
} else {
await PerDomainSettings.setHostDisabled(domain, hostId, true);
}
await loadMap();
}
async function onDomainAdd(input: string) {
const domain = parseDomain(input);
if (!domain) {
showToast('Invalid domain');
return;
}
await PerDomainSettings.add(domain);
await loadMap();
expanded = domain;
}
function toggleExpand(domain: string) {
expanded = expanded === domain ? null : domain;
}
</script>
<div class="flex flex-col gap-1 h-full overflow-y-scroll">
<div class="flex items-center gap-2 pb-1.5">
<input
type="text"
bind:value={newDomainInput}
placeholder="Add domain…"
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-2 py-1 text-gray-100 placeholder:text-gray-500 focus:border-linux-mint-green focus:outline-none transition-colors"
onkeydown={(e) => {
if (e.key === 'Enter') onDomainAdd(newDomainInput).then(() => (newDomainInput = ''));
}}
/>
<button
type="button"
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
disabled={!newDomainInput.trim()}
onclick={() => onDomainAdd(newDomainInput).then(() => (newDomainInput = ''))}
>
Add
</button>
</div>
{#if Object.keys(perDomainMap).length === 0}
<p class="text-xs text-gray-400 text-center py-2">
No per-site disables configured. Use the popup on a streaming site to configure it.
</p>
{:else}
<div class="flex flex-col gap-1.5">
{#each Object.entries(perDomainMap) as [domain, settings] (domain)}
{@const isOpen = expanded === domain}
<div class="border border-gray-700/60 rounded-md overflow-hidden">
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="w-full flex items-center justify-between gap-2 px-2.5 py-1.5 bg-gray-900/40 hover:bg-gray-800/60 cursor-pointer transition-colors"
role="button"
tabindex={0}
onclick={() => toggleExpand(domain)}
>
<div class="min-w-0">
<p class="text-sm font-semibold text-gray-100 truncate">{domain}</p>
<p class="text-[0.7rem] text-gray-400">
{#if settings.allDisabled}
All hosts disabled
{:else}
{settings.disabledHostIds.length}
{settings.disabledHostIds.length === 1 ? 'host' : 'hosts'} disabled
{/if}
</p>
</div>
<button
type="button"
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
onclick={(e) => {
e.stopPropagation();
onClear(domain);
}}
>
<Icon src={Trash} size="0.9rem" />
</button>
</div>
{#if isOpen}
{@const availableHosts = hosts.filter((h) => !settings.disabledHostIds.includes(h.id))}
<div class="px-2 py-2 border-t border-gray-700/60 flex flex-col gap-2">
{#if settings.allDisabled}
<div class="flex items-center justify-between gap-2 px-2 py-1.5 bg-gray-900/40 rounded">
<p class="text-xs text-gray-300">All hosts disabled</p>
<button
type="button"
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
onclick={() => onAllRemove(domain)}
>
<Icon src={Trash} size="0.9rem" />
</button>
</div>
{/if}
{#if settings.disabledHostIds.length > 0}
<div class="flex flex-col -mx-1 max-h-60 overflow-y-auto">
{#each settings.disabledHostIds as hostId (hostId)}
{@const host = getHostFromId(hostId)}
{#if host}
<HostRow {host} enabled={false}>
{#snippet actions()}
<button
type="button"
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
onclick={() => onHostRemove(domain, hostId)}
>
<Icon src={Trash} size="0.9rem" />
</button>
{/snippet}
</HostRow>
{/if}
{/each}
</div>
{/if}
{#if !settings.allDisabled && settings.disabledHostIds.length === 0}
<p class="text-xs text-gray-400 text-center py-1">No overrides set</p>
{/if}
<div class="flex items-center gap-2 pt-1">
<select
bind:value={selectedHostId}
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-1.5 py-1 text-gray-100 cursor-pointer focus:border-linux-mint-green focus:outline-none transition-colors"
>
<option value="" disabled>Add host…</option>
{#if !settings.allDisabled}
<option value={ALL_HOSTS}>All hosts</option>
<option disabled>─────────</option>
{/if}
{#each availableHosts as host (host.id)}
<option value={host.id}>{host.name}</option>
{/each}
</select>
<button
type="button"
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
disabled={!selectedHostId}
onclick={() => onHostAdd(domain, selectedHostId).then(() => (selectedHostId = ''))}
>
{selectedHostId === ALL_HOSTS ? 'Disable all' : 'Add'}
</button>
</div>
</div>
{/if}
</div>
{/each}
</div>
{/if}
</div>
@@ -1,9 +1,13 @@
<script lang="ts">
import { CommandLine, MapPin, ServerStack } from '@steeze-ui/heroicons';
import Divider from '@/entrypoints/popup/components/Divider.svelte';
import Section from '@/entrypoints/popup/components/Section.svelte';
import Ff2mpv from '@/entrypoints/popup/pages/settings/Ff2mpv.svelte';
import Header from '@/entrypoints/popup/pages/settings/Header.svelte';
import HostsTable from '@/entrypoints/popup/pages/settings/HostsTable.svelte';
import PerSiteDisables from '@/entrypoints/popup/pages/settings/PerSiteDisables.svelte';
import { isMobile } from '@/entrypoints/popup/state';
import { PerDomainSettings } from '@/lib/settings';
/* types */
interface Props {
@@ -12,49 +16,38 @@
/* states */
let { onSettingsCloseRequest }: Props = $props();
let searchQuery = $state('');
onDestroy(PerDomainSettings.cleanEmpty);
</script>
<div class="w-full">
<Header onBackClick={onSettingsCloseRequest} />
<Divider />
</div>
<div class="flex flex-col gap-y-1 pt-1 h-full mx-2 my-1 overflow-y-scroll">
<details class="details" open>
<summary>Hosts</summary>
<HostsTable />
</details>
<div class="flex flex-col gap-2 p-2 h-full overflow-y-scroll">
<div class="max-h-full">
<Section
title="Hosts"
description="Enable or disable support for specific streaming providers"
icon={ServerStack}
defaultOpen
>
<HostsTable bind:searchQuery />
</Section>
</div>
<Section title="Per-site disables" description="Disable hosts for specific websites" icon={MapPin}>
<PerSiteDisables />
</Section>
{#if !$isMobile}
<details class="details">
<summary>ff2mpv</summary>
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
<Ff2mpv />
</details>
</Section>
{/if}
</div>
<style>
* {
@apply select-none;
}
.details {
/* using normal css instead of tailwind in the following blocks.
for some reason tailwind fails to resolve many references */
&::before,
&::after {
content: '';
display: block;
width: 100%;
border-top: 1px solid var(--color-gray-600);
margin: 0.25rem 0;
}
& > summary {
cursor: pointer;
}
&[open] > summary {
margin-bottom: 0.5rem;
}
user-select: none;
}
</style>
@@ -0,0 +1,19 @@
<script lang="ts">
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
/* types */
interface Props {
domain: string;
onBackClick: () => void;
}
/* states */
let { domain, onBackClick }: Props = $props();
</script>
<PageHeader title="Site settings" subtitle={domain}>
{#snippet actionsLeft()}
<BackButton onclick={onBackClick} />
{/snippet}
</PageHeader>
@@ -0,0 +1,98 @@
<script lang="ts">
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
import { hosts, type Host } from '@/lib/host';
import { PerDomainSettings } from '@/lib/settings';
/* types */
interface Props {
domain: string;
matchedHostId: string | null;
}
/* states */
let { domain, matchedHostId }: Props = $props();
let perDomain = $state<{ allDisabled: boolean; disabledHostIds: string[] }>({
allDisabled: false,
disabledHostIds: []
});
// `domain` is updated after this component is active, and thus we must react to this update
$effect(() => {
PerDomainSettings.get(domain).then((val) => (perDomain = val));
});
let orderedHosts = $derived(getOrderedHosts(matchedHostId));
/* functions */
function getOrderedHosts(matchedId: string | null): Host[] {
const matched = matchedId ? hosts.find((h) => h.id === matchedId) : null;
const rest = hosts
.filter((h) => !matched || h.id !== matched.id)
.slice()
.sort((a, b) => a.name.localeCompare(b.name));
return matched ? [matched, ...rest] : rest;
}
/* callbacks */
async function onHostToggle(hostId: string, enabled: boolean) {
await PerDomainSettings.setHostDisabled(domain, hostId, !enabled);
perDomain = await PerDomainSettings.get(domain);
}
async function onAllToggle(enabled: boolean) {
await PerDomainSettings.setAllDisabled(domain, !enabled);
perDomain = await PerDomainSettings.get(domain);
}
</script>
<div class="flex flex-col gap-2 px-2 pb-2 h-full overflow-hidden">
<div class="flex-1 min-h-0 overflow-y-auto -mx-1 divide-y divide-gray-700/40">
{#each orderedHosts as host (host.id)}
{@const isInDisabledList = perDomain.disabledHostIds.includes(host.id)}
<HostRow
{host}
enabled={!isInDisabledList && !perDomain.allDisabled}
badge={host.id === matchedHostId ? 'Match' : undefined}
>
{#snippet actions()}
{#key perDomain.allDisabled + perDomain.disabledHostIds.join(',')}
<Toggle
disabled={perDomain.allDisabled}
title={perDomain.allDisabled ? 'All hosts are disabled for this site' : undefined}
bind:checked={
() => !isInDisabledList,
(v) => {
onHostToggle(host.id, v);
return v;
}
}
size="sm"
/>
{/key}
{/snippet}
</HostRow>
{/each}
</div>
<div
class="flex items-center justify-between gap-2 px-3 py-2 mt-1 bg-gray-800/40 border border-gray-700/60 rounded-md shrink-0"
>
<div class="min-w-0">
<p class="text-sm font-semibold text-gray-100">Disable all on this site</p>
<p class="text-xs text-gray-400 truncate">Turn off all hosts for {domain}</p>
</div>
{#key perDomain.allDisabled}
<Toggle
bind:checked={
() => !perDomain.allDisabled,
(v) => {
onAllToggle(v);
return v;
}
}
size="sm"
/>
{/key}
</div>
</div>
@@ -0,0 +1,58 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import Divider from '@/entrypoints/popup/components/Divider.svelte';
import Header from '@/entrypoints/popup/pages/site-config/Header.svelte';
import PerSiteHostsList from '@/entrypoints/popup/pages/site-config/PerSiteHostsList.svelte';
import { listenTabMessages, sendTabMessageToActiveTab, TabMessageType } from '@/lib/communication';
import { hosts, type Host } from '@/lib/host';
import { PerDomainSettings } from '@/lib/settings';
/* types */
interface Props {
onBackClick: () => void;
}
/* states */
let { onBackClick }: Props = $props();
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
let currentDomain = $state<string | null>(null);
/* lifecycle */
const cancel = listenTabMessages((type, data) => {
if (type !== TabMessageType.NotifyActiveMatch) return;
const match = {
host: hosts.find((host) => host.id === data.id)!,
url: data.url,
domain: data.domain
};
currentMatch = match;
});
sendTabMessageToActiveTab(TabMessageType.RequestActiveMatch, undefined);
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
const url = tabs[0]?.url;
if (!url) return;
currentDomain = new URL(url).hostname;
});
onDestroy(() => {
cancel();
PerDomainSettings.cleanEmpty();
});
</script>
<div class="w-full">
<Header domain={currentDomain ?? ''} {onBackClick} />
<Divider />
</div>
<div class="flex flex-col h-full overflow-hidden">
{#if currentDomain}
<PerSiteHostsList domain={currentDomain} matchedHostId={currentMatch?.host.id ?? null} />
{:else}
<div class="flex items-center justify-center h-full text-sm text-gray-400 p-4 text-center">
No active website detected.
</div>
{/if}
</div>
+19
View File
@@ -1,3 +1,22 @@
import { writable } from 'svelte/store';
/* --- global init --- */
export const isMobile = writable(false);
/* --- toast --- */
interface Toast {
id: number;
message: string;
}
export const toast = writable<Toast | null>(null);
let toastId = 0;
export function showToast(message: string, duration = 2000) {
const id = ++toastId;
toast.set({ id, message });
setTimeout(() => {
toast.update((current) => (current?.id === id ? null : current));
}, duration);
}
+46 -11
View File
@@ -1,36 +1,71 @@
export enum MessageType {
RequestActiveMatch,
// --- tab communication --- //
export enum TabMessageType {
RequestActiveMatch = 10,
NotifyActiveMatch
}
export type MessageData<T extends MessageType> = {
[MessageType.RequestActiveMatch]: undefined;
[MessageType.NotifyActiveMatch]: {
export type TabMessageData<T extends TabMessageType> = {
[TabMessageType.RequestActiveMatch]: undefined;
[TabMessageType.NotifyActiveMatch]: {
id: string;
url: string;
domain: string;
};
}[T];
export async function sendMessage<T extends MessageType>(message: T, data: MessageData<T>) {
export async function sendTabBroadcast<T extends TabMessageType>(message: T, data: TabMessageData<T>) {
await browser.runtime.sendMessage({ type: message, data: data });
}
export async function sendMessageToActiveTab<T extends MessageType>(message: T, data: MessageData<T>) {
export async function sendTabMessageToActiveTab<T extends TabMessageType>(message: T, data: TabMessageData<T>) {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
await browser.tabs.sendMessage(tabs[0].id!, { type: message, data: data }).catch(() => {});
}
export function listenMessages(listener: (type: MessageType, data: any) => void): () => void {
const callback = (callbackData: { type: MessageType; data: any }) => {
export function listenTabMessages(listener: (type: TabMessageType, data: any) => void): () => void {
const callback = (callbackData: { type: TabMessageType; data: any }) => {
const { type, data } = callbackData;
listener(type, data);
};
browser.runtime.onMessage.addListener(callback);
return () => browser.runtime.onMessage.removeListener(callback);
}
return () => {
browser.runtime.onMessage.removeListener(callback);
// --- background script communication --- //
export enum BackgroundMessageType {
Ff2mpv = 20,
RequestTabUrl,
RegisterContentScript,
RegisterTemporaryReferer
}
export type BackgroundMessageData<T extends BackgroundMessageType> = {
[BackgroundMessageType.Ff2mpv]: {
url: string;
};
[BackgroundMessageType.RequestTabUrl]: undefined;
[BackgroundMessageType.RegisterContentScript]: {
domain: string;
};
[BackgroundMessageType.RegisterTemporaryReferer]: {
domain: string;
referer: string;
};
}[T];
export type BackgroundMessageReply<T extends BackgroundMessageType> = {
[BackgroundMessageType.Ff2mpv]: undefined;
[BackgroundMessageType.RequestTabUrl]: string | null;
[BackgroundMessageType.RegisterContentScript]: undefined;
[BackgroundMessageType.RegisterTemporaryReferer]: undefined;
}[T];
export async function sendBackgroundMessage<T extends BackgroundMessageType>(
message: T,
data: BackgroundMessageData<T>
): Promise<BackgroundMessageReply<T>> {
const response = await browser.runtime.sendMessage({ type: message, data: data });
return response;
}
+2 -1
View File
@@ -4,6 +4,7 @@ export default {
name: 'Doodstream',
id: 'doodstream',
domains: [
'do7go.com',
'doodstream.com',
'dood.pm',
'dood.ws',
@@ -34,7 +35,7 @@ export default {
referrer: `https://${window.location.host}/e/${window.location.pathname.split('/').slice(-1)[0]}`
});
return {
type: HostMatchType.HLS,
type: HostMatchType.NATIVE,
url: `${await response.text()}1234567890${match[2]}${Date.now()}`
};
}
+1 -1
View File
@@ -11,7 +11,7 @@ export default {
match: async function (match: RegExpMatchArray) {
if (window.location.host.startsWith('filemoon')) {
await HostSettings.addTemporaryHostDomain(this, new URL(match[0]).host);
await HostSettings.addTemporaryHostDomain(this.id, new URL(match[0]).host);
return null;
}
+35 -8
View File
@@ -17,7 +17,7 @@ import Vidmoly from './vidmoly';
import Vidoza from './vidoza';
import Voe from './voe';
import Vupload from './vupload';
import { HostSettings } from '@/lib/settings';
import { HostSettings, PerDomainSettings } from '@/lib/settings';
export enum HostMatchType {
NATIVE = 'native',
@@ -30,9 +30,11 @@ export interface HostMatch {
url: string | null;
}
export type HostId = string;
export interface Host {
name: string;
id: string;
id: HostId;
domains: string[];
replace?: boolean;
regex: RegExp[];
@@ -63,16 +65,41 @@ export const hosts = [
Vupload
];
export async function getHost(domain: string) {
if (await HostSettings.getAllHostsDisabled()) return null;
export function getHostFromId(id: HostId): Host | null {
return hosts.find((host) => host.id === id) ?? null;
}
export function filterHosts(list: Host[], query: string): Host[] {
const q = query.trim().toLowerCase();
if (!q) return list;
return list.filter((host) => {
if (host.name.toLowerCase().includes(q)) return true;
return host.domains.some((domain) => domain.toLowerCase().includes(q));
});
}
export async function getHost(domain: string): Promise<Host | null> {
const [allHostsDisabled, disabledHosts, perDomainSetting] = await Promise.all([
HostSettings.getAllHostsDisabled(),
HostSettings.getDisabledHosts(),
PerDomainSettings.get(domain)
]);
// extension is disabled via popup
if (allHostsDisabled) return null;
// domain is disabled via popup
else if (perDomainSetting.allDisabled) return null;
const disabledIds = await HostSettings.getDisabledHosts();
for (const host of hosts) {
if (host.domains.includes(domain)) {
if (!disabledIds.includes(host.id)) return host;
else return null;
// host is generally disabled via popup
if (disabledHosts.includes(host.id)) return null;
// host is disabled for this domain via popup
else if (perDomainSetting.disabledHostIds.includes(host.id)) return null;
return host;
}
}
return HostSettings.checkTemporaryHostDomain(domain);
return HostSettings.checkTemporaryHostDomain(domain).then(getHostFromId);
}
+1 -1
View File
@@ -9,7 +9,7 @@ export default {
match: async function (match: RegExpMatchArray) {
if (this.domains.indexOf(window.location.hostname) !== -1) {
await HostSettings.addTemporaryHostDomain(this, window.location.hostname);
await HostSettings.addTemporaryHostDomain(this.id, window.location.hostname);
return null;
}
return {
+7 -3
View File
@@ -3,13 +3,17 @@ import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Vidmoly',
id: 'vidmoly',
domains: ['vidmoly.me', 'vidmoly.net', 'vidmoly.to'],
regex: [/(?<=file:").+\.m3u8.*(?=")/gm],
// vidmoly has multiple domains, but regardless of the domain, the actual videos are loaded via an iframe which
// always points to vidmoly.net. the "outer" vidmoly site also has a link to some video, which, would
// be preferred to use, as the whole site could be replaced by the native video player instead of just the iframe,
// but said link doesn't always point the same video as the iframe
domains: ['vidmoly.net', 'vidmoly.biz'],
regex: [/file:\s?'(http.*)'/],
match: async function (match: RegExpMatchArray) {
return {
type: HostMatchType.HLS,
url: match[0]
url: match[1]
};
}
} satisfies Host;
+1 -1
View File
@@ -9,7 +9,7 @@ export default {
match: async function (match: RegExpMatchArray) {
return {
type: HostMatchType.HLS,
type: HostMatchType.NATIVE,
url: match[0]
};
}
+1 -1
View File
@@ -49,7 +49,7 @@ export default {
match: async function (match: RegExpMatchArray) {
if (window.location.host === 'voe.sx') {
await HostSettings.addTemporaryHostDomain(this, new URL(match[0]).host);
await HostSettings.addTemporaryHostDomain(this.id, new URL(match[0]).host);
return null;
} else {
let json = match[0];
+100 -67
View File
@@ -1,88 +1,121 @@
import { storage } from '#imports';
import { hosts, type Host } from '@/lib/host';
import { BackgroundMessageType, sendBackgroundMessage } from './communication';
import type { HostId } from '@/lib/host';
class Setting<T> {
private item;
constructor(key: Parameters<typeof storage.defineItem>[0], fallback: T) {
this.item = storage.defineItem(key, { fallback });
}
get = () => this.item.getValue();
set = (value: T) => this.item.setValue(value);
update = (fn: (val: T) => T) => this.get().then(fn).then(this.set);
}
export class HostSettings {
/* disabled hosts */
private static disabledHosts = storage.defineItem<string[]>('local:disabledHosts', { fallback: [] });
private static allHostsDisabled = storage.defineItem<boolean>('local:allHostsDisabled', { fallback: false });
private static disabledHosts = new Setting<HostId[]>('local:disabledHosts', []);
private static allHostsDisabled = new Setting<boolean>('local:allHostsDisabled', false);
static async addDisabledHost(host: Host) {
const ids = await this.disabledHosts.getValue();
static addDisabledHost = (hostId: HostId) =>
this.disabledHosts.update((val) => {
if (!val.includes(hostId)) val.push(hostId);
return val;
});
static removeDisabledHost = (hostId: HostId) =>
this.disabledHosts.update((val) => {
const index = val.indexOf(hostId);
if (index !== -1) val.splice(index, 1);
return val;
});
const index = ids.indexOf(host.id);
if (index === -1) {
ids.push(host.id);
await this.disabledHosts.setValue(ids);
}
}
static async removeDisabledHost(host: Host) {
const ids = await this.disabledHosts.getValue();
const index = ids.indexOf(host.id);
if (index !== -1) {
ids.splice(index, 1);
await this.disabledHosts.setValue(ids);
}
}
static async getDisabledHosts() {
return await this.disabledHosts.getValue();
}
static async getAllHostsDisabled() {
return await this.allHostsDisabled.getValue();
}
static async setAllHostsDisabled(disabled: boolean) {
await this.allHostsDisabled.setValue(disabled);
}
static getDisabledHosts = () => this.disabledHosts.get();
static getAllHostsDisabled = () => this.allHostsDisabled.get();
static setAllHostsDisabled = (disabled: boolean) => this.allHostsDisabled.set(disabled);
/* tmp */
private static temporaryHostDomain = storage.defineItem<Record<string, string>>('local:temporaryHostDomain', {
fallback: {}
});
private static temporaryHostDomain = new Setting<Record<string, string>>('local:temporaryHostDomain', {});
static async addTemporaryHostDomain(host: Host, domain: string) {
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
static addTemporaryHostDomain = async (
hostId: HostId,
domain: string,
options = {
// only has an effect with mv2
registerContentScript: import.meta.env.MANIFEST_VERSION === 2
}
) => {
const backgroundScriptInject = options.registerContentScript
? sendBackgroundMessage(BackgroundMessageType.RegisterContentScript, { domain: domain })
: Promise.resolve();
const temporaryHostDomainUpdate = this.temporaryHostDomain.update((val) => {
val[domain] = hostId;
return val;
});
temporaryHostDomains[domain] = host.id;
await this.temporaryHostDomain.setValue(temporaryHostDomains);
console.log(await this.temporaryHostDomain.getValue());
}
static async checkTemporaryHostDomain(domain: string) {
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
const hostId = temporaryHostDomains[domain];
return hostId ? (hosts.find((host) => host.id === hostId) ?? null) : null;
}
return Promise.all([backgroundScriptInject, temporaryHostDomainUpdate]);
};
static checkTemporaryHostDomain = (domain: string) => this.temporaryHostDomain.get().then((val) => val[domain]);
}
export class FF2MPVSettings {
private static ff2mpvEnabled = storage.defineItem<boolean>('local:ff2mpv', { fallback: false });
private static ff2mpvEnabled = new Setting<boolean>('local:ff2mpv', false);
static async getEnabled() {
return await this.ff2mpvEnabled.getValue();
}
static async setEnabled(enabled: boolean) {
console.log('set', enabled);
await this.ff2mpvEnabled.setValue(enabled);
}
static getEnabled = () => this.ff2mpvEnabled.get();
static setEnabled = (enabled: boolean) => this.ff2mpvEnabled.set(enabled);
}
export class UrlReferer {
private static temporaryUrlReferer = storage.defineItem<Record<string, string>>('local:temporaryUrlReferer', {
fallback: {}
});
export interface PerDomainConfig {
allDisabled: boolean;
disabledHostIds: HostId[];
}
static async addTemporary(hostname: string, referer: string) {
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
export class PerDomainSettings {
private static storage = new Setting<Record<string, PerDomainConfig>>('local:perDomainSettings', {});
tmpUrlReferer[hostname] = referer;
await this.temporaryUrlReferer.setValue(tmpUrlReferer);
}
private static ensureDomain = (val: Record<string, PerDomainConfig>, domain: string) => {
if (!val[domain]) val[domain] = { allDisabled: false, disabledHostIds: [] };
return val[domain];
};
static async get(hostname: string) {
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
static get = (domain: string): Promise<PerDomainConfig> =>
this.storage.get().then((val) => val[domain] ?? { allDisabled: false, disabledHostIds: [] });
return tmpUrlReferer[hostname] ?? null;
}
static getMap = () => this.storage.get();
static setAllDisabled = (domain: string, disabled: boolean) =>
this.storage.update((val) => {
this.ensureDomain(val, domain).allDisabled = disabled;
return val;
});
static setHostDisabled = (domain: string, hostId: HostId, disabled: boolean) =>
this.storage.update((val) => {
const entry = this.ensureDomain(val, domain);
if (!disabled) entry.disabledHostIds = entry.disabledHostIds.filter((id) => id !== hostId);
else if (!entry.disabledHostIds.includes(hostId)) entry.disabledHostIds.push(hostId);
return val;
});
static add = (domain: string) =>
this.storage.update((val) => {
this.ensureDomain(val, domain);
return val;
});
static clear = (domain: string) =>
this.storage.update((val) => {
delete val[domain];
return val;
});
static cleanEmpty = () =>
this.storage.update((val) => {
for (const [domain, settings] of Object.entries(val)) {
if (!settings.allDisabled && settings.disabledHostIds.length === 0) delete val[domain];
}
return val;
});
}
+4 -1
View File
@@ -26,7 +26,10 @@ export default defineConfig({
gecko_android: {}
}
: undefined,
permissions: ['storage', ...(manifestVersion === 2 ? ['webRequest', 'webRequestBlocking', '<all_urls>'] : [])],
permissions: [
'storage',
...(manifestVersion === 2 ? ['scripting', 'webRequest', 'webRequestBlocking', '<all_urls>'] : [])
],
optional_permissions: ['nativeMessaging'],
web_accessible_resources: [
{