Compare commits

...
89 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
bytedream 39a6fffd50 update version 2025-11-08 20:00:37 +01:00
bytedream 6109cd5543 fix vidmoly 2025-11-08 19:57:36 +01:00
bytedream de38196a77 fix player not accessible when using mv3 2025-11-08 19:56:46 +01:00
bytedream d2b6086155 fix player background color 2025-11-08 19:55:54 +01:00
bytedream 66c638425d fix publish action 2025-11-08 18:38:37 +01:00
bytedream b2da03f9a1 fix chrome build artifact 2025-11-08 18:33:19 +01:00
bytedream 76a4217aab fix readme formatting 2025-11-08 18:29:30 +01:00
bytedream cac464cc65 update actions 2025-11-08 18:29:30 +01:00
bytedream 7bd8f01a1b update readme 2025-11-08 18:29:30 +01:00
bytedream b7c5e020b6 update prettier ignored files list 2025-11-08 18:29:30 +01:00
bytedream fdd1ca350e update 2025-11-08 18:29:28 +01:00
bytedream 607326e6d6 Update dependencies and version 2025-05-08 02:02:57 +02:00
bytedream 47f6de2de8 Fix voe 2025-05-08 01:59:29 +02:00
bytedream d159fb0aa4 Add mixdrop domain 2025-05-06 22:21:09 +02:00
bytedream 290733a85a Add supervideo domain 2025-05-06 22:20:04 +02:00
bytedream 84eb32ab61 Fix dropload domain 2025-05-06 22:18:19 +02:00
bytedream 927a4e3ae3 Update dependencies and version 2025-04-17 02:43:13 +02:00
bytedream 5926177920 Update voe.sx match regex 2025-04-17 02:37:32 +02:00
bytedream 9a29316641 Fix voe.sx 2025-04-17 02:26:59 +02:00
bytedream c300125f64 Set javascript target to es2021 2025-04-17 02:26:41 +02:00
bytedream 59f2ffec57 Allow arbitrary properties in Match interface 2025-04-17 02:10:18 +02:00
bytedream 6dff691c25 Use other notation for match function 2025-04-17 02:04:45 +02:00
bytedream c4085f9ac8 Update version 2025-04-04 02:13:20 +02:00
bytedream 422702b16f Add import order prettier plugin 2025-04-04 02:10:51 +02:00
bytedream c3dad3681c Fix player media type url query 2025-04-04 02:05:04 +02:00
bytedream 6eb3259ec5 Remove duplicate supported websites 2025-04-04 01:11:41 +02:00
bytedream 68e445a738 Update supported websites 2025-04-04 00:58:14 +02:00
bytedream e864bc6100 Update dependencies and version 2025-04-04 00:45:03 +02:00
bytedream 3ac73ac3b4 Update vite config 2025-04-04 00:44:51 +02:00
bytedream 6aaf960c28 Holy fuck why does vite have a problem with this 2025-04-04 00:43:57 +02:00
bytedream 8a13dea681 Format 2025-04-04 00:01:21 +02:00
bytedream 8d575241fe Fix lastPathSegment util 2025-04-04 00:00:38 +02:00
bytedream 50f400b8b9 Use enum to specify media type 2025-04-03 23:56:18 +02:00
bytedream f6fcfd354a Add more utils 2025-04-03 23:48:53 +02:00
bytedream c081127d35 Add loadx.ws 2025-04-03 23:43:49 +02:00
bytedream 416fceba88 Update match function to manually specify media type 2025-04-03 23:43:39 +02:00
bytedream a9cf03c176 Fix luluvdo.com 2025-04-03 23:14:31 +02:00
bytedream 8852318483 Update voe.sx to catch every redirect url 2025-04-03 22:55:24 +02:00
bytedream 7067aaf4a0 Add luluvdo.com to supported websites table 2025-03-14 18:52:08 +01:00
bytedream bf723e2ed6 Update dependencies and version 2025-03-14 18:41:05 +01:00
bytedream 9c80362a1c Update ci actions 2025-03-14 18:31:29 +01:00
bytedream ae738ff32f Add luluvdo.com site 2025-03-14 18:20:08 +01:00
bytedream 16d4f2956a Add dood.work domain (#23) 2025-03-14 17:43:50 +01:00
bytedreamandGitHub f6c6102436 Merge pull request #24 from muleyo/main
Added Voe.sx Domain
2025-03-13 16:08:39 +01:00
muleyoandGitHub f1525817b1 Added Voe.sx Domain
A domain for Voe.sx was missing
2025-03-10 01:42:23 +01:00
bytedream 1e1d8cdfa6 Fix tick character 2024-12-19 22:50:59 +01:00
bytedream 47eed2d12b Update supported websites 2024-12-15 19:08:50 +01:00
102 changed files with 8513 additions and 9509 deletions
-1
View File
@@ -1 +0,0 @@
MANIFEST_VERSION=2
+44
View File
@@ -0,0 +1,44 @@
name: build
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup pnpm
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
with:
version: 11
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 26
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Firefox (mv2)
run: pnpm run build:firefox
- name: Build Chrome (mv3)
run: pnpm run build
- name: Upload Firefox (mv2)
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: stream-bypass-mv3
path: .output/chrome-mv3
-56
View File
@@ -1,56 +0,0 @@
name: ci
on:
push:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install nodejs
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- manifest_version: 2
- manifest_version: 3
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install nodejs
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
env:
MANIFEST_VERSION: ${{ matrix.manifest_version }}
run: npm run build
- name: Upload
uses: actions/upload-artifact@v3
with:
name: stream-bypass-mv${{ matrix.manifest_version }}
path: ./dist
if-no-files-found: error
+32
View File
@@ -0,0 +1,32 @@
name: lint
on:
push:
branches:
- '**'
tags-ignore:
- '*'
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup pnpm
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
with:
version: 11
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 26
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
+89
View File
@@ -0,0 +1,89 @@
name: publish
on:
push:
tags:
- v*
workflow_dispatch:
inputs:
dry_run:
type: boolean
description: Dry run
default: true
permissions:
contents: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup pnpm
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
with:
version: 11
- name: Setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 26
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Zip extensions
run: |
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@ac75e1407b33421f1368f08e0e6cc567cc4edd25 # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: .output/*mv*.zip
tag: ${{ github.ref }}
overwrite: true
file_glob: true
- name: Submit to stores
env:
DRY_RUN: ${{ github.event.inputs.dry_run }}
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
run: |
pnpm exec wxt submit \
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
--chrome-zip .output/*-mv3.zip
+23 -4
View File
@@ -1,4 +1,23 @@
.idea/
dist/
release/
node_modules/
# Logs
logs
*.log
pnpm-debug.log*
node_modules
.output
stats.html
stats-*.json
.wxt
web-ext.config.ts
# Editor directories and files
.zed
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-2
View File
@@ -1,2 +0,0 @@
auto-install-peers=false
strict-peer-dependencies=false
+9 -6
View File
@@ -1,8 +1,11 @@
.DS_Store
node_modules
/dist
.env
.env.*
!.env.example
.idea
.vscode
.zed
package-lock.json
node_modules
pnpm-lock.yaml
.output
.wxt
.github
-8
View File
@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
+18
View File
@@ -0,0 +1,18 @@
// @ts-check
/** @type {import("prettier").Config} */
module.exports = {
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 120,
tabWidth: 4,
plugins: ['prettier-plugin-svelte', '@ianvs/prettier-plugin-sort-imports'],
/* prettier-plugin-svelte */
overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }],
/* @ianvs/prettier-plugin-sort-imports */
importOrder: ['^~/(.*)$', '^./(.*)$', ''],
importOrderParserPlugins: ['typescript'],
importOrderTypeScriptVersion: '5.0.0',
importOrderCaseSensitive: false
};
+91 -71
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>
@@ -40,7 +40,7 @@ This has the advantage, that no advertising or popups are shown when trying to i
Additionally, this enables you to download the video by right-clicking it and just choose the download option.
<details id="example">
<summary><b>How it's working:</b></summary>
<summary><b>How it works:</b></summary>
<img src="example.gif" alt="">
</details>
@@ -53,55 +53,60 @@ The best way to install the extension are the official browser extension stores:
- [Firefox Addon Store](https://addons.mozilla.org/de/firefox/addon/stream-bypass/) (Firefox for Android is supported too!)
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
### Manual installation
<details>
<summary><h3 id="manual-installation1">Manual installation</h3></summary>
- Firefox
- 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)
- 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
> 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
- Firefox (mv2)
- 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 (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>
## ✨ Features
| Feature | Firefox | Chrome | Firefox for Android |
| --------------------------------------------------------------------------------------------------------------------------------- | ------- | ------ | ------------------- |
| 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.
- ✖: Not supported.
- ✔️: Supported.
- ❌: Not supported.
| 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)) | ✔ | ✔ | ✖ |
## 📜 Supported websites
| Site | Firefox & Firefox for Android | Chrome & Chromium based |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [dropload.io](https://dropload.io) | ✔ | ✔ |
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔️ | ⚠ (redirect probably required) |
| [filemoon.to](https://filemoon.to) | ✔ | ✔ |
| [goodstream.uno](https://goodstream.uno) | ✔ | ✔ |
| [mixdrop.co](https://mixdrop.co) | ✔ ️ | ✔ |
| [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
| [newgrounds.com](https://newgrounds.com) | ✔ | ✔ |
| [streama2z.com](https://streama2z.com) | ✔ | ❌ (redirect always required) |
| [streamtape.com](https://streamtape.com) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) |
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | ✔ |
| [supervideo.tv](https://supervideo.tv) | ✔ | ✔ |
| [upstream.to](https://upstream.to) | ✔ | ✔ |
| [vidmoly.to](https://vidmoly.me) | ✔ | ✔ |
| [vidoza.net](https://vidoza.net) | ⚠ (doesn't always work the first time, retrying/reloading the page one or two times fixes it) | ⚠ (doesn't always work the first time, retrying/reloading the page one or two times fixes it) |
| [voe.sx](https://voe.sx) | ✔ | ❌ (redirect always required) |
| [vupload.com](https://vupload.com) | ✔ | ✔ |
| [kwik.cx](https://kwik.cx) | ✔ | ✔ |
- ✔️: Everything ok.
- ✔: Everything ok.
- ⚠: Works with limitations.
- : Not supported.
- : Not supported.
| 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) | ✔ | ✔ |
| [goodstream.uno](https://goodstream.uno) | ✔ | ✔ |
| [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) | ✔ ️ | ✔ |
| [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
| [newgrounds.com](https://newgrounds.com) | ✔ | ✔ |
| [streama2z.com](https://streama2z.com) | ✔ | ✖ (redirect always required) |
| [streamtape.com](https://streamtape.com) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) |
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | ✔ |
| [supervideo.tv](https://supervideo.tv) | ✔ | ✔ |
| [upstream.to](https://upstream.to) | ✔ | ✔ |
| [vidmoly.to](https://vidmoly.me) | ✔ | ✔ |
| [vidoza.net](https://vidoza.net) | ✔ | ✔ |
| [voe.sx](https://voe.sx) | ✔ | ✖ (redirect always required) |
| [vupload.com](https://vupload.com) | ✔ | ✔ |
_This table might not be 100% accurate, it isn't actively monitored if the addon works for every website!_
@@ -109,29 +114,31 @@ Some sites put much effort in obfuscating their code / how they receive the vide
## 🛠️ Building
If you want to build the addon from source and not using the [installation](#installation) way, follow the instructions.
If you want to build the addon from source and not using the way described in [installation](#-installation), follow the instructions.
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,33 +148,46 @@ When using firefox, use the following:
## ⚙️ Settings
### <ins>ff2mpv: use mpv to directly play streams</ins>
> You reach the settings by pressing the settings icon in the top right corner of the extension popup.
ff2mpv is located at this repository: https://github.com/woodruffw/ff2mpv
### Hosts
Steps to get it set up:
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
[ff2mpv](https://github.com/woodruffw/ff2mpv) allows you to play streams directly in [mpv](https://mpv.io/) instead of the browser.
You can enable or disable this behavior.
<details>
<summary><i>Steps to get it set up</i></summary>
- In the [Usage](https://github.com/woodruffw/ff2mpv#usage) section of the ff2mpv repository pick the installation instruction for your operating system (Linux/Windows/macOS; you do not need the browser addon).
- Scroll down to `Install manually`
- Follow instructions for Firefox/Chrome
- Edit the `ff2mpv.json` you created:
- Firefox: Add `{55dd42e8-3dd9-455a-b4fe-86664881b10c}` to `allowed_extensions` ->
```
"allowed_extensions": [
"ff2mpv@yossarian.net",
"{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
]
```
- Chrome/Chromium:
- Go To: Settings -> Extensions
- Click on `Details` of the Stream Bypass extension and copy the ID
- Add `chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins` ->
```
"allowed_origins": [
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
"chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/"
]
```
- Firefox: Add `{55dd42e8-3dd9-455a-b4fe-86664881b10c}` to `allowed_extensions`:
```
"allowed_extensions": [
"ff2mpv@yossarian.net",
"{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
]
```
- Chrome/Chromium: - Go To: Settings -> Extensions - Click on `Details` of the Stream Bypass extension and copy the ID - Add `chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins`:
```
"allowed_origins": [
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
"chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/"
]
```
</details>
## ⚖ License
+11 -18
View File
@@ -1,17 +1,21 @@
import prettier from 'eslint-config-prettier';
import { fileURLToPath } from 'node:url';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import ts from 'typescript-eslint';
export default ts.config(
const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url));
export default defineConfig(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
...svelte.configs[('flat/recommended', 'flat/prettier')],
prettier,
...svelte.configs['flat/prettier'],
includeIgnoreFile(gitignorePath),
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
@@ -21,20 +25,9 @@ export default ts.config(
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-alert': 'error',
'no-console': 'error',
'no-undef': 'off'
}
},
{
ignores: [
'.DS_Store',
'node_modules',
'dist',
'release',
'.idea',
'.env',
'.env.*',
'!.env.example',
'package-lock.json'
]
}
);
-8249
View File
File diff suppressed because it is too large Load Diff
+39 -35
View File
@@ -1,21 +1,9 @@
{
"name": "stream-bypass",
"version": "3.1.1",
"version": "5.0.0",
"displayName": "Stream Bypass",
"author": "bytedream",
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
"scripts": {
"build": "vite build",
"watch": "vite build --watch --mode development --minify false",
"dev": "vite",
"serve:firefox": "web-ext run --start-url \"about:debugging#/runtime/this-firefox\" --source-dir ./dist/",
"serve:chrome": "web-ext run -t chromium --start-url \"https://example.com\" --source-dir ./dist/",
"check": "svelte-check --tsconfig ./tsconfig.json",
"lint": "prettier --check --plugin prettier-plugin-svelte . && eslint .",
"format": "prettier --write --plugin prettier-plugin-svelte .",
"release:firefox": "MANIFEST_VERSION=2 vite build --outDir release/firefox",
"release:chrome": "MANIFEST_VERSION=3 vite build --outDir release/chrome"
},
"license": "MIT",
"repository": {
"type": "git",
@@ -24,27 +12,43 @@
"bugs": {
"url": "https://github.com/bytedream/stream-bypass/issues"
},
"devDependencies": {
"@samrum/vite-plugin-web-extension": "^5.1.1",
"@sveltejs/vite-plugin-svelte": "^5.0.2",
"@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.0.287",
"@types/firefox-webext-browser": "^120.0.4",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"hls.js": "^1.5.17",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"sass": "^1.83.0",
"svelte": "^5.14.0",
"svelte-check": "^4.1.1",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.0",
"vite": "^6.0.3",
"web-ext": "^8.3.0"
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"postinstall": "wxt prepare",
"format": "prettier --write .",
"lint": "prettier --check . && eslint . && svelte-check --tsconfig ./tsconfig.json"
},
"type": "module"
"devDependencies": {
"@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": "^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.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": "^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

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Before

Width:  |  Height:  |  Size: 713 B

After

Width:  |  Height:  |  Size: 713 B

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Before

Width:  |  Height:  |  Size: 792 B

After

Width:  |  Height:  |  Size: 792 B

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

+21
View File
@@ -0,0 +1,21 @@
@import 'tailwindcss';
@theme {
--color-linux-mint-green: #35a854;
--default-font-family: Arial, Helvetica, sans-serif;
}
@layer base {
body {
background-color: #030712;
color: var(--color-gray-300);
margin: 0;
padding: 0;
}
a {
text-decoration: none;
}
}
-40
View File
@@ -1,40 +0,0 @@
import './shared';
import type { Match } from '~/lib/match';
import { Redirect, UrlReferer } from '~/lib/settings';
import { getMatch } from '~/lib/match';
chrome.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;
await UrlReferer.delete(new URL(details.url).hostname);
details.requestHeaders!.push({
name: 'Referer',
value: `https://${referer}/`
});
return { requestHeaders: details.requestHeaders };
},
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] },
['blocking', 'requestHeaders']
);
chrome.webRequest.onBeforeRedirect.addListener(
async (details) => {
// check if redirects origins from a previous redirect
if ((await Redirect.get()) == null) {
let match: Match | null;
if ((match = await getMatch(new URL(details.url).hostname)) !== null) {
await Redirect.set(match);
}
} else {
await Redirect.delete();
}
},
{ urls: ['<all_urls>'], types: ['main_frame', 'sub_frame'] }
);
-1
View File
@@ -1 +0,0 @@
import './shared';
-5
View File
@@ -1,5 +0,0 @@
chrome.runtime.onMessage.addListener(async (message) => {
if (message.action == 'ff2mpv') {
await chrome.runtime.sendNativeMessage('ff2mpv', { url: message.url });
}
});
-90
View File
@@ -1,90 +0,0 @@
import type { Match } from '~/lib/match';
import { getMatch } from '~/lib/match';
import { Other, Redirect } from '~/lib/settings';
async function main() {
let match: Match | null;
let redirect = false;
if ((match = await getMatch(window.location.host)) === null) {
if ((match = await Redirect.get()) === null) {
return;
}
redirect = true;
}
// some sites have a javascript based redirect, e.g. example.com redirects to example.org by changing
// window.location.href instead of a 3XX http redirect. an empty body is a sign that such a javascript redirect
// occurred
if (document.body == null) {
await Redirect.set(match);
return;
}
let re = null;
for (const regex of match.regex) {
if ((re = document.body.innerHTML.match(regex)) !== null) {
break;
}
}
if (re === null) {
return;
}
if (redirect) {
await Redirect.delete();
}
let url: string | null;
try {
url = await match.match(re);
} catch {
return;
}
if (!url) {
return;
}
// send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application
if (await Other.getFf2mpv()) {
await chrome.runtime.sendMessage({ action: 'ff2mpv', url: url });
}
if (match.replace && !url.includes('.m3u8')) {
// this destroys all intervals that may spawn popups or events
let intervalId = window.setInterval(() => {}, 0);
while (intervalId--) {
clearInterval(intervalId);
}
let timeoutId = window.setTimeout(() => {}, 0);
while (timeoutId--) {
clearTimeout(timeoutId);
}
// clear completed document
document.documentElement.innerHTML = '';
document.body.style.backgroundColor = '#131313';
// video player
const player = document.createElement('video');
player.style.width = '100%';
player.style.height = '100%';
player.controls = true;
player.src = url;
// add video player to document body
document.body.style.margin = '0';
document.body.append(player);
} else {
window.location.assign(
chrome.runtime.getURL(
`src/entries/player/player.html?id=${match.id}&url=${encodeURIComponent(url)}&domain=${
window.location.hostname
}`
)
);
}
}
main();
-65
View File
@@ -1,65 +0,0 @@
<script lang="ts">
import { play } from '~/entries/player/player';
import { onMount } from 'svelte';
let errorMessage: string | null = $state(null);
let videoElem: HTMLVideoElement;
onMount(async () => {
try {
await play(videoElem);
videoElem.controls = true;
} catch (e) {
errorMessage = e as string;
}
});
</script>
<!-- svelte-ignore a11y_media_has_caption -->
<video id="video" bind:this={videoElem}></video>
{#if errorMessage}
<div id="message-container">
<p>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html errorMessage}
</p>
</div>
{/if}
<!-- eslint-disable -->
<style lang="scss" global>
body {
background-color: #131313;
}
html,
body,
video {
width: 100%;
height: 100%;
margin: 0;
}
video {
position: absolute;
top: 0;
left: 0;
}
#message-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: white;
text-align: center;
height: 100%;
& a,
& a:visited {
color: inherit;
text-decoration: underline;
}
}
</style>
-46
View File
@@ -1,46 +0,0 @@
import { matches } from '~/lib/match';
import Hls from 'hls.js';
import { UrlReferer } from '~/lib/settings';
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
await UrlReferer.set(new URL(url).hostname, domain);
videoElem.src = url;
}
async function playHls(url: string, domain: string, videoElem: HTMLVideoElement) {
if (videoElem.canPlayType('application/vnd.apple.mpegurl')) {
videoElem.src = url;
} else if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: false,
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
await UrlReferer.set(new URL(url).hostname, domain);
xhr.open('GET', url);
}
});
hls.loadSource(url);
hls.attachMedia(videoElem);
} else {
throw 'Failed to play m3u8 video (hls is not supported). Try again or create a new issue <a href="https://github.com/ByteDream/stream-bypass/issues/new">here</a>';
}
}
export async function play(videoElem: HTMLVideoElement) {
const urlQuery = new URLSearchParams(window.location.search);
const id = urlQuery.get('id') as string;
const url = decodeURIComponent(urlQuery.get('url') as string);
const domain = urlQuery.get('domain') as string;
const match = matches[id];
if (match === undefined) {
throw `Invalid id: ${id}. Please report this <a href="https://github.com/ByteDream/stream-bypass/issues">here</a>`;
}
document.title = `Stream Bypass (${domain})`;
if (new URL(url).pathname.endsWith('.m3u8')) {
await playHls(url, domain, videoElem);
} else {
await playNative(url, domain, videoElem);
}
}
-165
View File
@@ -1,165 +0,0 @@
<script lang="ts">
import { type Match, matches } from '~/lib/match';
import { Hosters, Other } from '~/lib/settings';
import Toggle from './toggle.svelte';
let hostersEnabled: boolean;
let hosters: (Match & { active: boolean; disabled: boolean })[] = [];
(async () => {
hostersEnabled = !(await Hosters.getAllDisabled());
const disabled = await Hosters.getDisabled();
hosters = Object.values(matches).map((m: any) => {
m['active'] = disabled.findIndex((p) => p.id == m.id) == -1;
return m;
}) as typeof hosters;
})();
let isMobile: boolean;
(async () => {
isMobile = (await browser.runtime.getPlatformInfo()).os === 'android';
})();
let ff2mpvEnabled: boolean;
(async () => {
ff2mpvEnabled = (await Other.getFf2mpv()) as boolean;
})();
</script>
<main
style={isMobile
? 'height: 100vh; display: flex; flex-direction: column; align-items: center'
: 'height: 500px'}
>
<fieldset>
<legend>Hoster</legend>
<div class="setting-container" style={isMobile ? 'grid-column-gap: 5rem' : ''}>
<label for="hosters-enabled">Enabled</label>
<div>
<Toggle
bind:checked={hostersEnabled}
id="hosters-enabled"
onChange={() => Hosters.setAll(hostersEnabled)}
/>
</div>
<hr />
{#each hosters as hoster, i}
<label for="hoster-{i}" style="cursor: {hostersEnabled ? 'pointer' : 'default'}"
>{hoster.name}</label
>
<div>
<Toggle
bind:checked={hoster.active}
disabled={!hostersEnabled}
id="hoster-{i}"
onChange={async () => {
if (hoster.active) {
await Hosters.enable(hoster);
} else {
await Hosters.disable(hoster);
}
}}
></Toggle>
</div>
{/each}
</div>
</fieldset>
{#if !isMobile}
<fieldset>
<legend>Other</legend>
<div class="setting-container">
<label for="ff2mpv">ff2mpv</label>
<div>
<Toggle
bind:checked={ff2mpvEnabled}
id="ff2mpv"
onChange={async () => {
ff2mpvEnabled = !ff2mpvEnabled;
if (await browser.permissions.request({ permissions: ['nativeMessaging'] })) {
await Other.setFf2mpv(ff2mpvEnabled);
ff2mpvEnabled = !ff2mpvEnabled;
}
}}
></Toggle>
<a
class="info-questionmark"
href="https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams"
>?</a
>
</div>
</div>
</fieldset>
{/if}
<a id="report-notice" href="https://github.com/ByteDream/stream-bypass/issues"
>Report issues or requests</a
>
</main>
<!-- eslint-disable -->
<style lang="scss" global>
body {
background-color: #2b2a33;
color: white;
font-weight: bold;
margin: 0;
padding: 0 8px;
}
fieldset {
border-radius: 5px;
border-color: gray;
}
#report-notice {
border: none;
color: white;
display: block;
font-weight: lighter;
font-size: 0.8rem;
text-align: center;
padding: 5px 0;
}
.settings {
display: flex;
flex-direction: column;
align-items: center;
margin: 10px 0;
}
.header {
font-size: 16px;
margin: 5px 0;
text-align: center;
}
.setting-container {
display: grid;
grid-template-columns: auto auto;
grid-column-gap: 5px;
grid-row-gap: 4px;
align-items: end;
width: 100%;
& > label {
height: 34px;
margin: 0;
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
}
& > hr {
grid-column: 1 / span 2;
width: 100%;
}
}
.info-questionmark {
display: inline-block;
transform: translateX(-40%) translateY(-100%);
color: black;
text-decoration: none;
}
</style>
-18
View File
@@ -1,18 +0,0 @@
<!doctype html>
<html style="overflow-y: hidden" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Stream Bypass</title>
</head>
<body style="overflow-y: scroll">
<script type="module">
import Popup from '~/entries/popup/Popup.svelte';
import { mount } from 'svelte';
mount(Popup, {
target: document.body
});
</script>
</body>
</html>
-68
View File
@@ -1,68 +0,0 @@
<!-- https://flowbite.com/docs/forms/toggle/ -->
<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
checked?: boolean;
disabled?: boolean;
id?: string | null;
onChange?: () => void;
children?: Snippet;
};
let { checked = $bindable(), disabled, id = null, onChange, children }: Props = $props();
</script>
<label class="toggle">
{@render children?.()}
<input type="checkbox" {id} bind:checked {disabled} onchange={onChange} />
<span></span>
</label>
<style lang="scss" global>
.toggle {
display: inline-flex;
align-items: center;
cursor: pointer;
input {
clip: rect(0, 0, 0, 0);
position: absolute;
&:checked + span {
background: limegreen;
&:after {
transform: translateX(100%);
}
}
&:disabled + span {
background: gray;
}
}
span {
position: relative;
width: 2.75rem;
height: 1.5rem;
background: #cf0000;
border-radius: 9999px;
&:after {
content: '';
position: absolute;
top: 2px;
inset-inline-start: 2px;
background: white;
border-radius: 9999px;
height: 1.25rem;
width: 1.25rem;
transition: all 0.15s;
}
}
&:has(input:disabled) {
cursor: default;
}
}
</style>
+151
View File
@@ -0,0 +1,151 @@
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(() => {
registerBrowserRuntimeOnMessageListener();
registerBrowserWebRequestOnBeforeRedirectListener();
});
+85
View File
@@ -0,0 +1,85 @@
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}/*`))],
allFrames: true,
runAt: 'document_end',
main
});
async function main() {
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;
for (const regex of host.regex) {
if ((re = document.body.innerHTML.match(regex)) !== null) {
break;
}
}
if (!re) {
return;
}
let hostMatch: HostMatch | null;
try {
hostMatch = await host.match(re);
} catch {
hostMatch = null;
}
if (!hostMatch || !hostMatch.url) return;
// send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application
if (await FF2MPVSettings.getEnabled()) {
await browser.runtime.sendMessage({ action: 'ff2mpv', url: hostMatch.url });
}
if (host.replace && hostMatch.type != HostMatchType.HLS) {
// this destroys all intervals that may spawn popups or events
let intervalId = window.setInterval(() => {}, 0);
while (intervalId--) {
clearInterval(intervalId);
}
let timeoutId = window.setTimeout(() => {}, 0);
while (timeoutId--) {
clearTimeout(timeoutId);
}
// clear completed document
document.documentElement.innerHTML = '';
document.body.style.backgroundColor = '#131313';
// video player
const player = document.createElement('video');
player.style.width = '100%';
player.style.height = '100%';
player.controls = true;
player.src = hostMatch.url;
// add video player to document body
document.body.style.margin = '0';
document.body.append(player);
} else {
window.location.assign(
browser.runtime.getURL(
`/player.html?id=${host.id}&url=${encodeURIComponent(hostMatch.url)}&domain=${window.location.hostname}&type=${hostMatch.type}`
)
);
}
}
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import { onMount } from 'svelte';
import { play } from './player';
let errorMessage: string | null = $state(null);
let videoElem: HTMLVideoElement;
onMount(async () => {
try {
await play(videoElem);
videoElem.controls = true;
} catch (e) {
errorMessage = e as string;
}
});
</script>
<!-- svelte-ignore a11y_media_has_caption -->
<video class="absolute top-0 left-0 w-full h-full m-0" bind:this={videoElem}></video>
{#if errorMessage}
<div class="h-full flex items-center justify-center text-center">
<p>
{errorMessage}&nbsp;<a class="underline" href="https://github.com/bytedream/stream-bypass/issues">here</a>.
</p>
</div>
{/if}
@@ -3,11 +3,20 @@
<head>
<meta charset="UTF-8" />
<title>Stream Bypass</title>
<style>
html,
body,
video {
width: 100%;
height: 100%;
margin: 0;
}
</style>
</head>
<body>
<body style="background-color: #131313; overflow: hidden">
<script type="module">
import Player from '~/entries/player/Player.svelte';
import { mount } from 'svelte';
import Player from './Player.svelte';
mount(Player, {
target: document.body
+87
View File
@@ -0,0 +1,87 @@
import Hls from 'hls.js';
import {
BackgroundMessageType,
listenTabMessages,
sendBackgroundMessage,
sendTabBroadcast,
TabMessageType
} from '@/lib/communication';
import { HostMatchType, hosts } from '@/lib/host';
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
// multiple hosts need to have a correct referer set
await sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
domain: new URL(url).host,
referer: domain
});
videoElem.src = url;
}
async function playHls(url: string, domain: string, videoElem: HTMLVideoElement) {
if (videoElem.canPlayType('application/vnd.apple.mpegurl')) {
videoElem.src = url;
} else if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: false,
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
// multiple hosts need to have a correct referer set
await sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
domain: new URL(url).host,
referer: domain
});
xhr.open('GET', url);
}
});
hls.loadSource(url);
hls.attachMedia(videoElem);
} else {
throw 'Failed to play m3u8 video (hls is not supported). Try again or create a new issue';
}
}
export async function play(videoElem: HTMLVideoElement) {
const urlQuery = new URLSearchParams(window.location.search);
const id = urlQuery.get('id') as string;
const url = decodeURIComponent(urlQuery.get('url') as string);
const domain = urlQuery.get('domain') as string;
const type = urlQuery.get('type') as HostMatchType;
const host = hosts.find((host) => host.id === id);
if (!host) {
throw `Invalid id: ${id}. Please report this`;
}
document.title = `Stream Bypass (${domain})`;
initCommunication(id, url, domain);
switch (type) {
case HostMatchType.NATIVE:
await playNative(url, domain, videoElem);
break;
case HostMatchType.HLS:
await playHls(url, domain, videoElem);
break;
}
}
function initCommunication(id: string, url: string, domain: string) {
const notifyActiveMatch = () =>
sendTabBroadcast(TabMessageType.NotifyActiveMatch, {
id: id,
url: url,
domain: domain
});
// if an extension popup is open, it will be notified that a match/player is now active
notifyActiveMatch();
// if an extension popup is opened, the listener will recognize it's active match request and send the match/player
// data
const cancel = listenTabMessages((type) => {
if (type !== TabMessageType.RequestActiveMatch) return;
notifyActiveMatch();
});
window.onbeforeunload = cancel;
}
+47
View File
@@ -0,0 +1,47 @@
<script lang="ts">
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' | 'site-config';
/* states */
let activePage = $state<Page>('main');
</script>
<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-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-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>
@@ -0,0 +1,3 @@
<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}
@@ -0,0 +1,72 @@
<script lang="ts">
/* types */
interface Props {
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', disabled = false, title }: Props = $props();
let internalChecked = $state($state.snapshot(checked));
/* callbacks */
async function onInputChange() {
if (disabled) return;
internalChecked = !internalChecked;
let approved = false;
if (!onChecked) {
approved = true;
} else {
const ret = onChecked(!internalChecked);
if (typeof ret === 'boolean') {
approved = ret;
} else if (typeof ret === 'object' && 'then' in ret && typeof ret.then === 'function') {
const promiseRet = await ret;
if (typeof promiseRet === 'undefined') approved = true;
else approved = promiseRet;
}
}
if (approved) {
internalChecked = !internalChecked;
checked = internalChecked;
}
}
</script>
<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}
{disabled}
/>
<div
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
class:w-8={size === 'sm'}
class:h-4={size === 'sm'}
class:w-10={size === 'md'}
class:h-5={size === 'md'}
></div>
<div
class="absolute flex items-center justify-center transition bg-white rounded-full dot left-0 top-0 peer-checked:translate-x-full"
class:w-4={size === 'sm'}
class:h-4={size === 'sm'}
class:w-5={size === 'md'}
class:h-5={size === 'md'}
></div>
</div>
</label>
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stream Bypass</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div id="app" class="contents h-full"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
import { mount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {
target: document.getElementById('app')!
});
export default app;
@@ -0,0 +1,5 @@
<script lang="ts"></script>
<div class="h-full flex items-center justify-center">
<p class="text-[1.05rem]">Extension disabled</p>
</div>
@@ -0,0 +1,74 @@
<script lang="ts">
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
import { isMobile, showToast } from '@/entrypoints/popup/state';
/* types */
interface Props {
url: string;
domain: string;
}
type UrlType = 'url' | 'curl';
/* states */
let { url, domain }: Props = $props();
let urlOutputType: UrlType = $state('url');
let urlOutput = $derived(getUrl(urlOutputType));
/* functions */
function getUrl(type: UrlType) {
switch (type) {
case 'url':
return url;
case 'curl':
return `curl -H "Referer: https://${domain}/" "${encodeURI(url)}"`;
}
}
/* callbacks */
async function copyUrl() {
try {
await navigator.clipboard.writeText(urlOutput);
showToast('URL copied');
} catch {
showToast('Failed to copy');
}
}
</script>
<div>
<div class="flex gap-2 items-center pb-1">
<p class="mt-0.5 text-sm">Show video as</p>
<select
class="w-fit text-xs border text-slate-200 border-gray-500 rounded cursor-pointer pt-1 pb-0.5 pl-1"
bind:value={urlOutputType}
>
<option value="url">URL</option>
<option value="curl">cURL</option>
</select>
{#if urlOutputType === 'url'}
<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"
>You may have to send the referer header <code class="select-text">Referer: {domain}</code> when accessing
the url</span
>
</div>
{/if}
</div>
<!-- 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-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"
>
<button class="cursor-pointer" title="Copy to clipboard" onclick={copyUrl}>
<Icon src={Clipboard} size="1rem" />
</button>
</div>
</div>
</div>
@@ -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>
@@ -0,0 +1,51 @@
<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';
/* types */
interface Props {
allHostsDisabled: boolean;
onSettingsClick: () => void;
}
/* states */
let { allHostsDisabled = $bindable(), onSettingsClick }: Props = $props();
/* effects */
$effect(() => {
HostSettings.setAllHostsDisabled(allHostsDisabled);
browser.browserAction.setIcon({
path: allHostsDisabled
? {
16: 'icon/stream-bypass_disabled@16px.png',
32: 'icon/stream-bypass_disabled@32px.png',
48: 'icon/stream-bypass_disabled@48px.png',
128: 'icon/stream-bypass_disabled@128px.png'
}
: {
16: 'icon/stream-bypass@16px.png',
32: 'icon/stream-bypass@32px.png',
48: 'icon/stream-bypass@48px.png',
128: 'icon/stream-bypass@128px.png'
}
});
});
</script>
<PageHeader title="stream-bypass" subtitle="v{import.meta.env.VERSION}">
{#snippet actionsRight()}
{#key allHostsDisabled}
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
{/key}
<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>
@@ -0,0 +1,94 @@
<script lang="ts">
import '@/assets/base.css';
import { onDestroy } from 'svelte';
import Divider from '@/entrypoints/popup/components/Divider.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 { 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, 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 = 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);
</script>
<div class="w-full shrink-0">
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
<Divider />
</div>
<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>
{: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>
* {
user-select: none;
}
</style>
@@ -0,0 +1,16 @@
<script lang="ts">
import { type Host } from '@/lib/host';
/* types */
interface Props {
host: Host;
}
/* states */
const { host }: Props = $props();
</script>
<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>
@@ -0,0 +1,43 @@
<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';
import { FF2MPVSettings } from '@/lib/settings';
/* states */
let enabled = $state(false);
FF2MPVSettings.getEnabled().then((val) => (enabled = val));
/* callbacks */
async function onEnableChange(enabled: boolean) {
if (!enabled) return true;
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 justify-between">
<div class="flex flex-col gap-0.5">
<p class="text-sm font-semibold text-gray-100">Communication enabled</p>
<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
bind:checked={() => enabled, (v) => FF2MPVSettings.setEnabled(v)}
onChecked={onEnableChange}
size="sm"
/>
{/key}
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
/* types */
interface Props {
onBackClick: () => void;
}
/* states */
let { onBackClick }: Props = $props();
</script>
<PageHeader title="Settings" subtitle="Manage hosts and integrations">
{#snippet actionsLeft()}
<BackButton onclick={onBackClick} />
{/snippet}
</PageHeader>
@@ -0,0 +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 { 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="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>
{/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>
@@ -0,0 +1,53 @@
<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 {
onSettingsCloseRequest: () => void;
}
/* 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-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}
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
<Ff2mpv />
</Section>
{/if}
</div>
<style>
* {
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>
+22
View File
@@ -0,0 +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);
}
+71
View File
@@ -0,0 +1,71 @@
// --- tab communication --- //
export enum TabMessageType {
RequestActiveMatch = 10,
NotifyActiveMatch
}
export type TabMessageData<T extends TabMessageType> = {
[TabMessageType.RequestActiveMatch]: undefined;
[TabMessageType.NotifyActiveMatch]: {
id: string;
url: string;
domain: string;
};
}[T];
export async function sendTabBroadcast<T extends TabMessageType>(message: T, data: TabMessageData<T>) {
await browser.runtime.sendMessage({ type: message, data: data });
}
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 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);
}
// --- 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;
}
+42
View File
@@ -0,0 +1,42 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Doodstream',
id: 'doodstream',
domains: [
'do7go.com',
'doodstream.com',
'dood.pm',
'dood.ws',
'dood.wf',
'dood.cx',
'dood.sh',
'dood.watch',
'dood.work',
'dood.to',
'dood.so',
'dood.la',
'dood.li',
'dood.re',
'dood.yt',
'doods.pro',
'ds2play.com',
'dooood.com',
'd000d.com'
],
replace: true,
regex: [/(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s],
match: async function (match: RegExpMatchArray) {
const response = await fetch(`https://${window.location.host}${match[1]}`, {
headers: {
Range: 'bytes=0-'
},
referrer: `https://${window.location.host}/e/${window.location.pathname.split('/').slice(-1)[0]}`
});
return {
type: HostMatchType.NATIVE,
url: `${await response.text()}1234567890${match[2]}${Date.now()}`
};
}
} satisfies Host;
+17
View File
@@ -0,0 +1,17 @@
import { HostMatchType, type Host } from '@/lib/host';
import { unpack } from '@/utils/content';
export default {
name: 'Dropload',
id: 'dropload',
domains: ['dropload.io'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async function (match: RegExpMatchArray) {
const unpacked = await unpack(match[0]);
return {
type: HostMatchType.HLS,
url: unpacked.match(/(?<=file:").*(?=")/)![0]
};
}
} satisfies Host;
+24
View File
@@ -0,0 +1,24 @@
import { HostMatchType, type Host } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
import { unpack } from '@/utils/content';
export default {
name: 'Filemoon',
id: 'filemoon',
domains: ['filemoon.sx', 'filemoon.to', 'filemoon.in'],
regex: [/(?<=<iframe\s*src=")\S*(?=")/s, /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
replace: true,
match: async function (match: RegExpMatchArray) {
if (window.location.host.startsWith('filemoon')) {
await HostSettings.addTemporaryHostDomain(this.id, new URL(match[0]).host);
return null;
}
const unpacked = await unpack(match[0]);
return {
type: HostMatchType.HLS,
url: unpacked.match(/(?<=file:")\S*(?=")/)![0]
};
}
} satisfies Host;
+15
View File
@@ -0,0 +1,15 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Goodstream',
id: 'goodstream',
domains: ['goodstream.uno'],
regex: [/(?<=file:\s+").*(?=")/g],
match: async function (match: RegExpMatchArray) {
return {
type: HostMatchType.HLS,
url: match[0]
};
}
} satisfies Host;
+105
View File
@@ -0,0 +1,105 @@
import Doodstream from './doodstream';
import Dropload from './dropload';
import Filemoon from './filemoon';
import Goodstream from './goodstream';
import Kwik from './kwik';
import Loadx from './loadx';
import Luluvdo from './luluvdo';
import Mixdrop from './mixdrop';
import Mp4Upload from './mp4upload';
import Newgrounds from './newgrounds';
import StreamA2z from './streama2z';
import Streamtape from './streamtape';
import Streamzz from './streamzz';
import SuperVideo from './supervideo';
import Upstream from './upstream';
import Vidmoly from './vidmoly';
import Vidoza from './vidoza';
import Voe from './voe';
import Vupload from './vupload';
import { HostSettings, PerDomainSettings } from '@/lib/settings';
export enum HostMatchType {
NATIVE = 'native',
HLS = 'hls'
}
export interface HostMatch {
type: HostMatchType;
/** If null, it's interpreted that a url should be present but isn't, probably because the website broke */
url: string | null;
}
export type HostId = string;
export interface Host {
name: string;
id: HostId;
domains: string[];
replace?: boolean;
regex: RegExp[];
notice?: string;
match(match: RegExpMatchArray): Promise<HostMatch | null>;
}
export const hosts = [
Doodstream,
Dropload,
Filemoon,
Goodstream,
Kwik,
Loadx,
Luluvdo,
Mixdrop,
Mp4Upload,
Newgrounds,
StreamA2z,
Streamtape,
Streamzz,
SuperVideo,
Upstream,
Vidmoly,
Vidoza,
Voe,
Vupload
];
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;
for (const host of hosts) {
if (host.domains.includes(domain)) {
// 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).then(getHostFromId);
}
+17
View File
@@ -0,0 +1,17 @@
import { HostMatchType, type Host } from '@/lib/host';
import { unpack } from '@/utils/content';
export default {
name: 'Kwik',
id: 'kwik',
domains: ['kwik.cx'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async function (match: RegExpMatchArray) {
const unpacked = await unpack(match[0]);
return {
type: HostMatchType.HLS,
url: unpacked.match(/(?<=source=').*(?=')/)![0]
};
}
} satisfies Host;
+27
View File
@@ -0,0 +1,27 @@
import { HostMatchType, type Host } from '@/lib/host';
import { lastPathSegment } from '@/utils/extract';
export default {
name: 'LoadX',
id: 'loadx',
domains: ['loadx.ws'],
regex: [/./gm],
match: async () => {
const hash = encodeURIComponent(lastPathSegment(window.location.href));
const response = await fetch(`https://${window.location.host}/player/index.php?data=${hash}&do=getVideo`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const responseJson = await response.json();
const videoSource: string = responseJson['videoSource'];
return {
type: HostMatchType.HLS,
url: videoSource.replace('\\/', '/')
};
}
} satisfies Host;
+32
View File
@@ -0,0 +1,32 @@
import { HostMatchType, type Host } from '@/lib/host';
import { unpack } from '@/utils/content';
import { lastPathSegment } from '@/utils/extract';
export default {
name: 'Luluvdo',
id: 'luluvdo',
domains: ['luluvdo.com'],
regex: [/./gm],
match: async () => {
const requestBody = new FormData();
requestBody.set('op', 'embed');
requestBody.set('file_code', lastPathSegment(window.location.href));
const response = await fetch(`https://${window.location.host}/dl`, {
method: 'POST',
body: requestBody,
referrer: window.location.href
});
const responseText = await response.text();
const evalMatch = responseText.match(/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms)!;
// sometimes is packed, sometimes it's not. looks like someone forgets to obfuscate the code when pushing to
// production
const unpacked = evalMatch ? await unpack(evalMatch[0]) : responseText;
return {
type: HostMatchType.HLS,
url: unpacked.match(/(?<=file:").*(?=")/)![0]
};
}
} satisfies Host;
+18
View File
@@ -0,0 +1,18 @@
import { HostMatchType, type Host } from '@/lib/host';
import { unpack } from '@/utils/content';
export default {
name: 'Mixdrop',
id: 'mixdrop',
domains: ['mixdrop.bz', 'mixdrop.ch', 'mixdrop.co', 'mixdrop.gl', 'mixdrop.my', 'mixdrop.to'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async function (match: RegExpMatchArray) {
const unpacked = await unpack(match[0]);
const url = unpacked.match(/(?<=MDCore.wurl=").*(?=")/)![0];
return {
type: HostMatchType.NATIVE,
url: `https:${url}`
};
}
} satisfies Host;
+18
View File
@@ -0,0 +1,18 @@
import { HostMatchType, type Host } from '@/lib/host';
import { unpack } from '@/utils/content';
export default {
name: 'Mp4Upload',
id: 'mp4upload',
domains: ['mp4upload.com'],
replace: true,
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async function (match: RegExpMatchArray) {
const unpacked = await unpack(match[0]);
return {
type: HostMatchType.NATIVE,
url: unpacked.match(/(?<=player.src\(").*(?=")/)![0]
};
}
} satisfies Host;
+22
View File
@@ -0,0 +1,22 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Newgrounds',
id: 'newgrounds',
domains: ['newgrounds.com'],
regex: [/.*/gm],
match: async () => {
const id = window.location.pathname.split('/').slice(-1)[0];
const response = await fetch(`https://www.newgrounds.com/portal/video/${id}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const json = await response.json();
return {
type: HostMatchType.HLS,
url: decodeURI(json['sources'][Object.keys(json['sources'])[0]][0]['src'])
};
}
} satisfies Host;
+20
View File
@@ -0,0 +1,20 @@
import { HostMatchType, type Host } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
export default {
name: 'Stream2Az',
id: 'stream2az',
domains: ['streama2z.com', 'streama2z.xyz'],
regex: [/https?:\/\/\S*m3u8.+(?=['"])/gm],
match: async function (match: RegExpMatchArray) {
if (this.domains.indexOf(window.location.hostname) !== -1) {
await HostSettings.addTemporaryHostDomain(this.id, window.location.hostname);
return null;
}
return {
type: HostMatchType.HLS,
url: match[0]
};
}
} satisfies Host;
+26
View File
@@ -0,0 +1,26 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Streamtape',
id: 'streamtape',
domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'],
regex: [/id=.*(?=')/gm],
match: async function (match: RegExpMatchArray) {
let i = 0;
while (i < match.length) {
if (match[++i - 1] == match[i]) {
return {
type: HostMatchType.HLS,
url: `https://streamtape.com/get_video?${match[i]}`
};
}
}
return {
type: HostMatchType.HLS,
// use the old method as fallback
url: `https://streamtape.com/get_video?${match.reverse()[0]}`
};
}
} satisfies Host;
+17
View File
@@ -0,0 +1,17 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Streamzz',
id: 'streamzz',
domains: ['streamzz.to', 'streamz.ws'],
regex: [/(?<=\|)\w{2,}/gm],
match: async function (match: RegExpMatchArray) {
return {
type: HostMatchType.HLS,
url: `https://get.${location.hostname.split('.')[0]}.tw/getlink-${
match.sort((a, b) => b.length - a.length)[0]
}.dll`
};
}
} satisfies Host;
+17
View File
@@ -0,0 +1,17 @@
import { HostMatchType, type Host } from '@/lib/host';
import { unpack } from '@/utils/content';
export default {
name: 'Supervideo',
id: 'supervideo',
domains: ['supervideo.cc', 'supervideo.tv'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async function (match: RegExpMatchArray) {
const unpacked = await unpack(match[0]);
return {
type: HostMatchType.HLS,
url: unpacked.match(/(?<=file:").*(?=")/)![0]
};
}
} satisfies Host;
+17
View File
@@ -0,0 +1,17 @@
import { HostMatchType, type Host } from '@/lib/host';
import { unpack } from '@/utils/content';
export default {
name: 'Upstream',
id: 'upstream',
domains: ['upstream.to'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async function (match: RegExpMatchArray) {
const unpacked = await unpack(match[0]);
return {
type: HostMatchType.HLS,
url: unpacked.match(/(?<=file:").*(?=")/)![0]
};
}
} satisfies Host;
+19
View File
@@ -0,0 +1,19 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Vidmoly',
id: 'vidmoly',
// 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[1]
};
}
} satisfies Host;
+16
View File
@@ -0,0 +1,16 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Vidoza',
id: 'vidoza',
domains: ['vidoza.net', 'videzz.net'],
regex: [/(?<=src:\s?").+?(?=")/gm],
replace: true,
match: async function (match: RegExpMatchArray) {
return {
type: HostMatchType.NATIVE,
url: match[0]
};
}
} satisfies Host;
+74
View File
@@ -0,0 +1,74 @@
import { HostMatchType, type Host } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
function rot13(encrypted: string) {
let decrypted = '';
for (let i = 0; i < encrypted.length; i++) {
let char = encrypted.charCodeAt(i);
if (char >= 65 && char <= 90) {
char = ((char - 65 + 13) % 26) + 65;
} else if (char >= 97 && char <= 122) {
char = ((char - 97 + 13) % 26) + 97;
}
decrypted += String.fromCharCode(char);
}
return decrypted;
}
function removeSpecialSequences(input: string) {
return input
.replaceAll(/@\$/g, '')
.replaceAll(/\^\^/g, '')
.replaceAll(/~@/g, '')
.replaceAll(/%\?/g, '')
.replaceAll(/\*~/g, '')
.replaceAll(/!!/g, '')
.replaceAll(/#&/g, '');
}
function shiftString(input: string) {
let shifted = '';
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i);
const shiftedChar = char - 3;
shifted += String.fromCharCode(shiftedChar);
}
return shifted;
}
export default {
name: 'Voe',
id: 'voe',
domains: ['voe.sx'],
regex: [
// voe.sx
/(?<=window\.location\.href\s=\s')\S*(?=')/gm,
// whatever site voe.sx redirects to
/(?<=<script type="application\/json">).*(?=<\/script>)/m
],
match: async function (match: RegExpMatchArray) {
if (window.location.host === 'voe.sx') {
await HostSettings.addTemporaryHostDomain(this.id, new URL(match[0]).host);
return null;
} else {
let json = match[0];
json = JSON.parse(json);
let deobfuscated = json[0];
deobfuscated = rot13(deobfuscated);
deobfuscated = removeSpecialSequences(deobfuscated);
deobfuscated = atob(deobfuscated);
deobfuscated = shiftString(deobfuscated);
deobfuscated = deobfuscated.split('').reverse().join('');
deobfuscated = atob(deobfuscated);
const payload = JSON.parse(deobfuscated);
return {
type: HostMatchType.HLS,
url: payload['source']
};
}
}
} satisfies Host;
+15
View File
@@ -0,0 +1,15 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Vupload',
id: 'vupload',
domains: ['vupload.com'],
regex: [/(?<=src:\s?").+?(?=")/gm],
match: async function (match: RegExpMatchArray) {
return {
type: HostMatchType.HLS,
url: match[0]
};
}
} satisfies Host;
-309
View File
@@ -1,309 +0,0 @@
import { unpack } from './utils';
import { Hosters, Redirect, TmpHost } from './settings';
export interface Match {
name: string;
id: string;
domains: string[];
replace?: boolean;
regex: RegExp[];
notice?: string;
match(match: RegExpMatchArray): Promise<string | null>;
}
export const Doodstream: Match = {
name: 'Doodstream',
id: 'doodstream',
domains: [
'doodstream.com',
'dood.pm',
'dood.ws',
'dood.wf',
'dood.cx',
'dood.sh',
'dood.watch',
'dood.to',
'dood.so',
'dood.la',
'dood.li',
'dood.re',
'dood.yt',
'doods.pro',
'ds2play.com',
'dooood.com',
'd000d.com'
],
replace: true,
regex: [/(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s],
match: async (match: RegExpMatchArray) => {
const response = await fetch(`https://${window.location.host}${match[1]}`, {
headers: {
Range: 'bytes=0-'
},
referrer: `https://${window.location.host}/e/${
window.location.pathname.split('/').slice(-1)[0]
}`
});
return `${await response.text()}1234567890${match[2]}${Date.now()}`;
}
};
export const DropLoad: Match = {
name: 'Dropload',
id: 'dropload',
domains: ['dropload.ui'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const Filemoon: Match = {
name: 'Filemoon',
id: 'filemoon',
domains: ['filemoon.sx', 'filemoon.to', 'filemoon.in'],
regex: [/(?<=<iframe\s*src=")\S*(?=")/s, /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
replace: true,
match: async (match: RegExpMatchArray) => {
if (window.location.host.startsWith('filemoon')) {
await TmpHost.set(new URL(match[0]).host, Filemoon);
return null;
}
await TmpHost.delete();
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:")\S*(?=")/)![0];
}
};
export const GoodStream: Match = {
name: 'Goodstream',
id: 'goodstream',
domains: ['goodstream.uno'],
regex: [/(?<=file:\s+").*(?=")/g],
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const Kwik: Match = {
name: 'Kwik',
id: 'kwik',
domains: ['kwik.cx'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=source=').*(?=')/)![0];
}
};
export const Mixdrop: Match = {
name: 'Mixdrop',
id: 'mixdrop',
domains: ['mixdrop.co', 'mixdrop.to', 'mixdrop.ch', 'mixdrop.bz', 'mixdrop.gl'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
const url = unpacked.match(/(?<=MDCore.wurl=").*(?=")/)![0];
return `https:${url}`;
}
};
export const Mp4Upload: Match = {
name: 'Mp4Upload',
id: 'mp4upload',
domains: ['mp4upload.com'],
replace: true,
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=player.src\(").*(?=")/)![0];
}
};
export const Newgrounds: Match = {
name: 'Newgrounds',
id: 'newgrounds',
domains: ['newgrounds.com'],
regex: [/.*/gm],
match: async () => {
const id = window.location.pathname.split('/').slice(-1)[0];
const response = await fetch(`https://www.newgrounds.com/portal/video/${id}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const json = await response.json();
return decodeURI(json['sources'][Object.keys(json['sources'])[0]][0]['src']);
}
};
export const StreamA2z: Match = {
name: 'Stream2Az',
id: 'stream2az',
domains: ['streama2z.com', 'streama2z.xyz'],
regex: [/https?:\/\/\S*m3u8.+(?=['"])/gm],
match: async (match: RegExpMatchArray) => {
if (StreamA2z.domains.indexOf(window.location.hostname) !== -1) {
await Redirect.set(StreamA2z);
return null;
}
return match[0];
}
};
export const Streamtape: Match = {
name: 'Streamtape',
id: 'streamtape',
domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'],
regex: [/id=.*(?=')/gm],
match: async (match: RegExpMatchArray) => {
let i = 0;
while (i < match.length) {
if (match[++i - 1] == match[i]) {
return `https://streamtape.com/get_video?${match[i]}`;
}
}
// use the old method as fallback
return `https://streamtape.com/get_video?${match.reverse()[0]}`;
}
};
export const Streamzz: Match = {
name: 'Streamzz',
id: 'streamzz',
domains: ['streamzz.to', 'streamz.ws'],
regex: [/(?<=\|)\w{2,}/gm],
match: async (match: RegExpMatchArray) => {
return `https://get.${location.hostname.split('.')[0]}.tw/getlink-${
match.sort((a, b) => b.length - a.length)[0]
}.dll`;
}
};
export const SuperVideo: Match = {
name: 'Supervideo',
id: 'supervideo',
domains: ['supervideo.tv'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const Upstream: Match = {
name: 'Upstream',
id: 'upstream',
domains: ['upstream.to'],
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const Vidmoly: Match = {
name: 'Vidmoly',
id: 'vidmoly',
domains: ['vidmoly.me', 'vidmoly.to'],
regex: [/(?<=file:").+\.m3u8/gm],
replace: true,
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const Vidoza: Match = {
name: 'Vidoza',
id: 'vidoza',
domains: ['vidoza.net', 'videzz.net'],
regex: [/(?<=src:\s?").+?(?=")/gm],
replace: true,
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const Voe: Match = {
name: 'Voe',
id: 'voe',
domains: ['voe.sx'],
regex: [/(?<='hls':\s*')\S*(?=')/gm],
match: async (match: RegExpMatchArray) => {
return atob(match[0]);
}
};
export const Vupload: Match = {
name: 'Vupload',
id: 'vupload',
domains: ['vupload.com'],
regex: [/(?<=src:\s?").+?(?=")/gm],
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const matches = {
[Doodstream.id]: Doodstream,
[DropLoad.id]: DropLoad,
[Filemoon.id]: Filemoon,
[GoodStream.id]: GoodStream,
[Kwik.id]: Kwik,
[Mixdrop.id]: Mixdrop,
[Mp4Upload.id]: Mp4Upload,
[Newgrounds.id]: Newgrounds,
[StreamA2z.id]: StreamA2z,
[Streamtape.id]: Streamtape,
[Streamzz.id]: Streamzz,
[SuperVideo.id]: SuperVideo,
[Upstream.id]: Upstream,
[Vidmoly.id]: Vidmoly,
[Vidoza.id]: Vidoza,
[Voe.id]: Voe,
[Vupload.id]: Vupload
};
export async function getMatch(domain: string): Promise<Match | null> {
if (await Hosters.getAllDisabled()) {
return null;
}
for (const match of Object.values(matches)) {
if (
match.domains.indexOf(domain) !== -1 &&
!(await Hosters.getDisabled().then((d) => d.find((p) => p.id == match.id)))
) {
return match;
}
}
const tmpHost = await TmpHost.get();
if (tmpHost && tmpHost[0] === domain) {
return tmpHost[1];
}
return null;
}
+108 -100
View File
@@ -1,113 +1,121 @@
import type { Match } from './match';
import { matches } from './match';
import { storage } from '#imports';
import { BackgroundMessageType, sendBackgroundMessage } from './communication';
import type { HostId } from '@/lib/host';
export const Hosters = {
getDisabled: async () => {
const disabled = (await storageGet('hosters.disabled', [])) as string[];
return disabled.map((id) => matches[id]).filter((m) => m !== undefined);
},
disable: async (match: Match) => {
const disabled = (await storageGet('hosters.disabled', [])) as string[];
const index = disabled.indexOf(match.id);
if (index === -1) {
disabled.push(match.id);
await storageSet('hosters.disabled', disabled);
}
},
enable: async (match: Match) => {
const disabled = (await storageGet('hosters.disabled', [])) as string[];
const index = disabled.indexOf(match.id);
if (index !== -1) {
disabled.splice(index, 1);
await storageSet('hosters.disabled', disabled);
}
},
getAllDisabled: async () => {
return await storageGet<boolean>('hosters.allDisabled', false);
},
setAll: async (enable: boolean) => {
await storageSet('hosters.allDisabled', !enable);
class Setting<T> {
private item;
constructor(key: Parameters<typeof storage.defineItem>[0], fallback: T) {
this.item = storage.defineItem(key, { fallback });
}
};
export const Redirect = {
get: async (): Promise<Match | null> => {
return matches[(await storageGet('redirect')) as string] || null;
},
set: async (match: Match) => {
await storageSet('redirect', match.id);
},
delete: async () => {
await storageDelete('redirect');
}
};
get = () => this.item.getValue();
set = (value: T) => this.item.setValue(value);
export const TmpHost = {
get: async (): Promise<[string, Match] | null> => {
const tmphost = await storageGet<[string, number]>('tmphost');
if (tmphost === undefined) {
return null;
}
return [tmphost[0], matches[tmphost[1]]];
},
set: async (domain: string, match: Match) => {
await storageSet('tmphost', [domain, match.id]);
},
delete: async () => {
await storageDelete('tmphost');
}
};
export const UrlReferer = {
get: async (url: string): Promise<string | null> => {
return (await storageGet(`urlReferer.${url}`)) || null;
},
set: async (url: string, referer: string) => {
await storageSet(`urlReferer.${url}`, referer);
},
delete: async (url: string) => {
await storageDelete(`urlReferer.${url}`);
}
};
export const Other = {
getFf2mpv: async () => {
return await storageGet('other.ff2mpv', false);
},
setFf2mpv: async (enable: boolean) => {
await storageSet('other.ff2mpv', enable);
}
};
async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefined> {
let resolve: (value: T | undefined) => void;
const promise = new Promise<T | undefined>((r) => (resolve = r));
chrome.storage.local.get(key, (entry) => {
const value = entry[key];
resolve(value === undefined ? defaultValue : value);
});
return promise;
update = (fn: (val: T) => T) => this.get().then(fn).then(this.set);
}
async function storageSet<T>(key: string, value: T) {
let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r));
export class HostSettings {
/* disabled hosts */
private static disabledHosts = new Setting<HostId[]>('local:disabledHosts', []);
private static allHostsDisabled = new Setting<boolean>('local:allHostsDisabled', false);
const obj = {
[key]: value
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;
});
static getDisabledHosts = () => this.disabledHosts.get();
static getAllHostsDisabled = () => this.allHostsDisabled.get();
static setAllHostsDisabled = (disabled: boolean) => this.allHostsDisabled.set(disabled);
/* tmp */
private static temporaryHostDomain = new Setting<Record<string, string>>('local:temporaryHostDomain', {});
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;
});
return Promise.all([backgroundScriptInject, temporaryHostDomainUpdate]);
};
chrome.storage.local.set(obj, () => resolve());
return promise;
static checkTemporaryHostDomain = (domain: string) => this.temporaryHostDomain.get().then((val) => val[domain]);
}
async function storageDelete(key: string) {
let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r));
export class FF2MPVSettings {
private static ff2mpvEnabled = new Setting<boolean>('local:ff2mpv', false);
chrome.storage.local.remove(key, () => resolve());
static getEnabled = () => this.ff2mpvEnabled.get();
static setEnabled = (enabled: boolean) => this.ff2mpvEnabled.set(enabled);
}
return promise;
export interface PerDomainConfig {
allDisabled: boolean;
disabledHostIds: HostId[];
}
export class PerDomainSettings {
private static storage = new Setting<Record<string, PerDomainConfig>>('local:perDomainSettings', {});
private static ensureDomain = (val: Record<string, PerDomainConfig>, domain: string) => {
if (!val[domain]) val[domain] = { allDisabled: false, disabledHostIds: [] };
return val[domain];
};
static get = (domain: string): Promise<PerDomainConfig> =>
this.storage.get().then((val) => val[domain] ?? { allDisabled: false, disabledHostIds: [] });
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;
});
}
-87
View File
@@ -1,87 +0,0 @@
import pkg from '../package.json';
import { matches } from './lib/match';
const sharedManifest: Partial<chrome.runtime.ManifestBase> = {
browser_specific_settings: {
gecko: {
id: '{55dd42e8-3dd9-455a-b4fe-86664881b10c}'
},
gecko_android: {}
},
content_scripts: [
{
all_frames: true,
matches: Object.values(matches).flatMap((m) => m.domains.map((d) => `*://${d}/*`)),
js: ['src/entries/contentScript/main.ts'],
run_at: 'document_end'
}
],
icons: {
16: 'icons/stream-bypass@16px.png',
32: 'icons/stream-bypass@32px.png',
48: 'icons/stream-bypass@48px.png',
96: 'icons/stream-bypass@96px.png',
128: 'icons/stream-bypass@128px.png'
},
permissions: ['storage'],
optional_permissions: ['nativeMessaging']
};
const browserAction = {
default_icon: {
16: 'icons/stream-bypass@16px.png',
32: 'icons/stream-bypass@32px.png'
},
default_popup: 'src/entries/popup/index.html'
};
const ManifestV2 = {
...sharedManifest,
background: {
scripts: ['src/entries/background/mv2.ts'],
persistent: true
},
content_scripts: [{ ...sharedManifest.content_scripts![0], matches: ['<all_urls>'] }],
browser_action: browserAction,
permissions: [...sharedManifest.permissions, 'webRequest', 'webRequestBlocking', '<all_urls>']
};
const ManifestV3 = {
...sharedManifest,
action: browserAction,
background: {
service_worker: 'src/entries/background/mv3.ts'
}
};
export function getManifest(
manifestVersion: number
): chrome.runtime.ManifestV2 | chrome.runtime.ManifestV3 {
const manifest = {
author: pkg.author,
description: pkg.description,
name: pkg.displayName ?? pkg.name,
version: pkg.version
};
if (manifestVersion === 2) {
return {
...manifest,
...ManifestV2,
manifest_version: manifestVersion
} as chrome.runtime.ManifestV2;
}
if (manifestVersion === 3) {
return {
...manifest,
// just like all the adblockers which are unable to fully work under MV3, we need access to every website
// the user enters in order to work correctly, which is forbidden when using MV3
name: `${manifest.name} Lite`,
...ManifestV3,
manifest_version: manifestVersion
} as chrome.runtime.ManifestV3;
}
throw new Error(`Missing manifest definition for manifestVersion ${manifestVersion}`);
}
+6
View File
@@ -0,0 +1,6 @@
export function lastPathSegment(path: string): string {
while (path.endsWith('/')) {
path = path.slice(0, -1);
}
return path.substring(path.lastIndexOf('/') + 1);
}
-3
View File
@@ -1,3 +0,0 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />
/// <reference types="@samrum/vite-plugin-web-extension/client" />
-7
View File
@@ -1,7 +0,0 @@
import sveltePreprocess from 'svelte-preprocess';
export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: sveltePreprocess()
};
+3 -15
View File
@@ -1,18 +1,6 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"target": "es2019",
"module": "esnext",
"useDefineForClassFields": true,
"resolveJsonModule": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
},
"allowJs": true,
"checkJs": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
"useDefineForClassFields": true
}
}
-38
View File
@@ -1,38 +0,0 @@
import { defineConfig, loadEnv } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import webExtension from '@samrum/vite-plugin-web-extension';
import path from 'path';
import { getManifest } from './src/manifest';
import { matches } from './src/lib/match';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
svelte(),
webExtension({
manifest: getManifest(Number(env.MANIFEST_VERSION)),
additionalInputs: {
html: [
{
fileName: 'src/entries/player/player.html',
webAccessible: {
matches:
Number(env.MANIFEST_VERSION) === 3
? Object.values(matches).flatMap((m) => m.domains.map((d) => `*://${d}/*`))
: ['<all_urls>']
}
}
]
}
})
],
resolve: {
alias: {
'~': path.resolve(__dirname, './src')
}
}
};
});

Some files were not shown because too many files have changed in this diff Show More