Compare commits

..
58 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
101 changed files with 8477 additions and 9460 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@v4
- name: Install nodejs
uses: actions/setup-node@v4
with:
node-version: 22
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@v4
- name: Install nodejs
uses: actions/setup-node@v4
with:
node-version: 22
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@v4
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
+2 -1
View File
@@ -5,7 +5,8 @@ module.exports = {
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 100,
printWidth: 120,
tabWidth: 4,
plugins: ['prettier-plugin-svelte', '@ianvs/prettier-plugin-sort-imports'],
/* prettier-plugin-svelte */
overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }],
+59 -41
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,87 +53,92 @@ 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)
- 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
- Chromium / Google Chrome (mv3)
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
- Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that)
- 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 |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
- ✔: Everything ok.
- ⚠: Works with limitations.
- ✖: 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) |
| [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) |
| [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) |
| [voe.sx](https://voe.sx) | ✔ | (redirect always required) |
| [vupload.com](https://vupload.com) | ✔ | ✔ |
- ✔️: Everything ok.
- ⚠: Works with limitations.
- ❌: Not supported.
_This table might not be 100% accurate, it isn't actively monitored if the addon works for every website!_
Some sites put much effort in obfuscating their code / how they receive the video stream so that it simply cost too much time for me to reverse engineer it and find out how to bypass the native video player of the site.
## 🛠️ 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.
@@ -143,27 +148,38 @@ 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` ->
- 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` ->
- 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/",
@@ -171,6 +187,8 @@ Steps to get it set up:
]
```
</details>
## ⚖ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details.
+10 -17
View File
@@ -1,17 +1,21 @@
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'
]
}
);
-8078
View File
File diff suppressed because it is too large Load Diff
+39 -36
View File
@@ -1,21 +1,9 @@
{
"name": "stream-bypass",
"version": "3.1.5",
"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 . && eslint .",
"format": "prettier --write .",
"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,28 +12,43 @@
"bugs": {
"url": "https://github.com/bytedream/stream-bypass/issues"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@samrum/vite-plugin-web-extension": "^5.1.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.0.315",
"@types/firefox-webext-browser": "^120.0.4",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-svelte": "^3.5.1",
"hls.js": "^1.6.2",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"sass": "^1.86.3",
"svelte": "^5.27.0",
"svelte-check": "^4.1.6",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.0",
"web-ext": "^8.5.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;
}
}
-39
View File
@@ -1,39 +0,0 @@
import './shared';
import { getMatch, type Match } from '~/lib/match';
import { Redirect, UrlReferer } from '~/lib/settings';
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 });
}
});
-104
View File
@@ -1,104 +0,0 @@
import { getMatch, MatchMediaType, type Match } 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;
let urlType: MatchMediaType | null;
try {
const matchResult = await match.match(re);
if (matchResult && typeof matchResult === 'string') {
url = matchResult;
urlType = url.includes('.m3u8') ? MatchMediaType.Hls : MatchMediaType.Native;
} else if (matchResult && typeof matchResult === 'object') {
if (MatchMediaType.Hls in matchResult) {
url = matchResult[MatchMediaType.Hls];
urlType = MatchMediaType.Hls;
} else if (MatchMediaType.Native in matchResult) {
url = matchResult[MatchMediaType.Native];
urlType = MatchMediaType.Native;
}
}
} catch {
return;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!url || !urlType) {
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 && urlType != MatchMediaType.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 = 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
}&type=${urlType}`
)
);
}
}
main();
-65
View File
@@ -1,65 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { play } from '~/entries/player/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 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>
-47
View File
@@ -1,47 +0,0 @@
import Hls from 'hls.js';
import { matches, MatchMediaType } from '~/lib/match';
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 type = urlQuery.get('type') as MatchMediaType;
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 (type === MatchMediaType.Hls) {
await playHls(url, domain, videoElem);
} else if (type === MatchMediaType.Native) {
await playNative(url, domain, videoElem);
}
}
-165
View File
@@ -1,165 +0,0 @@
<script lang="ts">
import { matches, type Match } 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 (hoster.id)}
<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 { mount } from 'svelte';
import Popup from '~/entries/popup/Popup.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 { mount } from 'svelte';
import Player from '~/entries/player/Player.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;
-438
View File
@@ -1,438 +0,0 @@
import { Hosters, Redirect, TmpHost } from './settings';
import { lastPathSegment } from './util/extract';
import { unpack } from './util/userspace';
export interface Match {
name: string;
id: string;
domains: string[];
replace?: boolean;
regex: RegExp[];
notice?: string;
match(
match: RegExpMatchArray
): Promise<
string | { [MatchMediaType.Hls]: string } | { [MatchMediaType.Native]: string } | null
>;
// allow other properties that may be implemented by the objects that use this interface declaration
[other: string]: any;
}
export enum MatchMediaType {
Hls = 'hls',
Native = 'native'
}
export const Doodstream: Match = {
name: 'Doodstream',
id: 'doodstream',
domains: [
'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 `${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 function (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 function (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 function (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 function (match: RegExpMatchArray) {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=source=').*(?=')/)![0];
}
};
export const LoadX: Match = {
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'];
// extension of extracted url is '.txt', so we have to manually specify that it's a hls
return { [MatchMediaType.Hls]: videoSource.replace('\\/', '/') };
}
};
export const Luluvdo: Match = {
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
});
let unpacked;
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
if (evalMatch) {
unpacked = await unpack(evalMatch[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
} else {
unpacked = responseText;
}
return unpacked.match(/(?<=file:").*(?=")/)![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 function (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 function (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 function (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 function (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 function (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 function (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 function (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 function (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 function (match: RegExpMatchArray) {
return match[0];
}
};
export const Voe: Match = {
name: 'Voe',
id: 'voe',
domains: ['voe.sx'],
regex: [
// voe.sx
/(?<=window\.location\.href\s=\s')\S*(?=')/gm,
// whatever site voe.sx redirects to
/(?<=")\S+(@\$|\^\^|~@|%\?|\*~|!!|#&)(?=")/gm
],
match: async function (match: RegExpMatchArray) {
if (window.location.host === 'voe.sx') {
const redirectUrl = new URL(match[0]);
await TmpHost.set(redirectUrl.host, Voe);
return null;
} else {
let deobfuscated = match[0];
deobfuscated = this.rot13(deobfuscated);
deobfuscated = this.removeSpecialSequences(deobfuscated);
deobfuscated = atob(deobfuscated);
deobfuscated = this.shiftString(deobfuscated);
deobfuscated = deobfuscated.split('').reverse().join('');
deobfuscated = atob(deobfuscated);
const payload = JSON.parse(deobfuscated);
return payload['source'];
}
},
rot13: function (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;
},
removeSpecialSequences: function (input: string) {
return input
.replaceAll(/@\$/g, '')
.replaceAll(/\^\^/g, '')
.replaceAll(/~@/g, '')
.replaceAll(/%\?/g, '')
.replaceAll(/\*~/g, '')
.replaceAll(/!!/g, '')
.replaceAll(/#&/g, '');
},
shiftString: function (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 const Vupload: Match = {
name: 'Vupload',
id: 'vupload',
domains: ['vupload.com'],
regex: [/(?<=src:\s?").+?(?=")/gm],
match: async function (match: RegExpMatchArray) {
return match[0];
}
};
export const matches = {
[Doodstream.id]: Doodstream,
[DropLoad.id]: DropLoad,
[Filemoon.id]: Filemoon,
[GoodStream.id]: GoodStream,
[Kwik.id]: Kwik,
[LoadX.id]: LoadX,
[Luluvdo.id]: Luluvdo,
[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;
}
+104 -95
View File
@@ -1,112 +1,121 @@
import { matches, type Match } 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;
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');
constructor(key: Parameters<typeof storage.defineItem>[0], fallback: T) {
this.item = storage.defineItem(key, { fallback });
}
};
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');
}
};
get = () => this.item.getValue();
set = (value: T) => this.item.setValue(value);
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}`);
}
};
update = (fn: (val: T) => T) => this.get().then(fn).then(this.set);
}
export const Other = {
getFf2mpv: async () => {
return await storageGet('other.ff2mpv', false);
},
setFf2mpv: async (enable: boolean) => {
await storageSet('other.ff2mpv', enable);
}
};
export class HostSettings {
/* disabled hosts */
private static disabledHosts = new Setting<HostId[]>('local:disabledHosts', []);
private static allHostsDisabled = new Setting<boolean>('local:allHostsDisabled', false);
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);
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;
});
return promise;
}
static getDisabledHosts = () => this.disabledHosts.get();
static getAllHostsDisabled = () => this.allHostsDisabled.get();
static setAllHostsDisabled = (disabled: boolean) => this.allHostsDisabled.set(disabled);
async function storageSet<T>(key: string, value: T) {
let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r));
/* tmp */
private static temporaryHostDomain = new Setting<Record<string, string>>('local:temporaryHostDomain', {});
const obj = {
[key]: value
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}`);
}
-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": "es2021",
"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
}
}
-36
View File
@@ -1,36 +0,0 @@
import { fileURLToPath } from 'url';
import webExtension from '@samrum/vite-plugin-web-extension';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig, loadEnv, type PluginOption } from 'vite';
import { matches } from './src/lib/match';
import { getManifest } from './src/manifest';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
svelte() as PluginOption,
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>']
}
}
]
}
}) as unknown as PluginOption
],
resolve: {
alias: [{ find: '~', replacement: fileURLToPath(new URL('./src', import.meta.url)) }]
}
};
});
+1
View File
@@ -0,0 +1 @@
/// <reference types="svelte" />

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