60 Commits
v3.1.0 ... main

Author SHA1 Message Date
f9a0197f7f update readme 2025-11-08 23:24:25 +01:00
d8bb70890a update build instructions 2025-11-08 22:26:18 +01:00
45900b2e86 do not upload sources to releases 2025-11-08 22:21:10 +01:00
3058847f31 upload artifacts when publishing 2025-11-08 22:20:35 +01:00
082f30722c do not lint on tag push 2025-11-08 21:52:46 +01:00
39a6fffd50 update version 2025-11-08 20:00:37 +01:00
6109cd5543 fix vidmoly 2025-11-08 19:57:36 +01:00
de38196a77 fix player not accessible when using mv3 2025-11-08 19:56:46 +01:00
d2b6086155 fix player background color 2025-11-08 19:55:54 +01:00
66c638425d fix publish action 2025-11-08 18:38:37 +01:00
b2da03f9a1 fix chrome build artifact 2025-11-08 18:33:19 +01:00
76a4217aab fix readme formatting 2025-11-08 18:29:30 +01:00
cac464cc65 update actions 2025-11-08 18:29:30 +01:00
7bd8f01a1b update readme 2025-11-08 18:29:30 +01:00
b7c5e020b6 update prettier ignored files list 2025-11-08 18:29:30 +01:00
fdd1ca350e update 2025-11-08 18:29:28 +01:00
607326e6d6 Update dependencies and version 2025-05-08 02:02:57 +02:00
47f6de2de8 Fix voe 2025-05-08 01:59:29 +02:00
d159fb0aa4 Add mixdrop domain 2025-05-06 22:21:09 +02:00
290733a85a Add supervideo domain 2025-05-06 22:20:04 +02:00
84eb32ab61 Fix dropload domain 2025-05-06 22:18:19 +02:00
927a4e3ae3 Update dependencies and version 2025-04-17 02:43:13 +02:00
5926177920 Update voe.sx match regex 2025-04-17 02:37:32 +02:00
9a29316641 Fix voe.sx 2025-04-17 02:26:59 +02:00
c300125f64 Set javascript target to es2021 2025-04-17 02:26:41 +02:00
59f2ffec57 Allow arbitrary properties in Match interface 2025-04-17 02:10:18 +02:00
6dff691c25 Use other notation for match function 2025-04-17 02:04:45 +02:00
c4085f9ac8 Update version 2025-04-04 02:13:20 +02:00
422702b16f Add import order prettier plugin 2025-04-04 02:10:51 +02:00
c3dad3681c Fix player media type url query 2025-04-04 02:05:04 +02:00
6eb3259ec5 Remove duplicate supported websites 2025-04-04 01:11:41 +02:00
68e445a738 Update supported websites 2025-04-04 00:58:14 +02:00
e864bc6100 Update dependencies and version 2025-04-04 00:45:03 +02:00
3ac73ac3b4 Update vite config 2025-04-04 00:44:51 +02:00
6aaf960c28 Holy fuck why does vite have a problem with this 2025-04-04 00:43:57 +02:00
8a13dea681 Format 2025-04-04 00:01:21 +02:00
8d575241fe Fix lastPathSegment util 2025-04-04 00:00:38 +02:00
50f400b8b9 Use enum to specify media type 2025-04-03 23:56:18 +02:00
f6fcfd354a Add more utils 2025-04-03 23:48:53 +02:00
c081127d35 Add loadx.ws 2025-04-03 23:43:49 +02:00
416fceba88 Update match function to manually specify media type 2025-04-03 23:43:39 +02:00
a9cf03c176 Fix luluvdo.com 2025-04-03 23:14:31 +02:00
8852318483 Update voe.sx to catch every redirect url 2025-04-03 22:55:24 +02:00
7067aaf4a0 Add luluvdo.com to supported websites table 2025-03-14 18:52:08 +01:00
bf723e2ed6 Update dependencies and version 2025-03-14 18:41:05 +01:00
9c80362a1c Update ci actions 2025-03-14 18:31:29 +01:00
ae738ff32f Add luluvdo.com site 2025-03-14 18:20:08 +01:00
16d4f2956a Add dood.work domain (#23) 2025-03-14 17:43:50 +01:00
bytedream
f6c6102436 Merge pull request #24 from muleyo/main
Added Voe.sx Domain
2025-03-13 16:08:39 +01:00
muleyo
f1525817b1 Added Voe.sx Domain
A domain for Voe.sx was missing
2025-03-10 01:42:23 +01:00
1e1d8cdfa6 Fix tick character 2024-12-19 22:50:59 +01:00
47eed2d12b Update supported websites 2024-12-15 19:08:50 +01:00
67e03eafaa Update dependencies and version 2024-12-15 18:43:06 +01:00
db0ccd9b56 Fix vidoza.net 2024-12-15 18:05:25 +01:00
fd3eda9c61 Fix vidmoly.to 2024-12-15 17:31:13 +01:00
e522916585 Fix filemoon.to 2024-12-15 17:14:46 +01:00
b9514284da Fix voe.sx 2024-12-15 16:16:38 +01:00
06beaa6ebe Update README 2024-07-29 02:35:43 +02:00
6a56c37dd2 Update package-lock.json 2024-07-29 02:31:30 +02:00
018b381732 Update README 2024-07-29 00:00:57 +02:00
93 changed files with 7442 additions and 7013 deletions

1
.env
View File

@@ -1 +0,0 @@
MANIFEST_VERSION=2

View File

@@ -1,9 +0,0 @@
.DS_Store
node_modules
/dist
/release
.env
.env.*
!.env.example
package-lock.json

View File

@@ -1,34 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-undef': 'off'
}
};

40
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: build
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Firefox (mv2)
run: npm run build:firefox
- name: Build Chrome (mv3)
run: npm run build
- name: Upload Firefox (mv2)
uses: actions/upload-artifact@v5
with:
name: stream-bypass-mv2
path: .output/firefox-mv2
- name: Upload Chrome (mv3)
uses: actions/upload-artifact@v5
with:
name: stream-bypass-mv3
path: .output/chrome-mv3

View File

@@ -1,56 +0,0 @@
name: ci
on:
push:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install nodejs
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- manifest_version: 2
- manifest_version: 3
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install nodejs
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
env:
MANIFEST_VERSION: ${{ matrix.manifest_version }}
run: npm run build
- name: Upload
uses: actions/upload-artifact@v3
with:
name: stream-bypass-mv${{ matrix.manifest_version }}
path: ./dist
if-no-files-found: error

28
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: lint
on:
push:
branches:
- '**'
tags-ignore:
- '*'
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint

85
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
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@v5
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Zip extensions
run: |
npm run zip:firefox
npm 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@v5
with:
name: ${{ steps.paths.outputs.mv2_name }}
path: ${{ steps.paths.outputs.mv2_path }}
- name: Upload mv3 as artifact
uses: actions/upload-artifact@v5
with:
name: ${{ steps.paths.outputs.mv3_name }}
path: ${{ steps.paths.outputs.mv3_path }}
- name: Upload to latest release
if: github.event.inputs.dry_run != 'true'
uses: svenstaro/upload-release-action@v2
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: |
npx wxt submit \
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
--chrome-zip .output/*-mv3.zip

31
.gitignore vendored
View File

@@ -1,4 +1,27 @@
.idea/ # Logs
dist/ logs
release/ *.log
node_modules/ npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-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
.npmrc
View File

@@ -1,2 +0,0 @@
auto-install-peers=false
strict-peer-dependencies=false

View File

@@ -1,8 +1,11 @@
.DS_Store .DS_Store
node_modules .idea
/dist .vscode
.env .zed
.env.*
!.env.example
node_modules
package-lock.json package-lock.json
.output
.wxt
.github

View File

@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

18
.prettierrc.cjs Normal file
View File

@@ -0,0 +1,18 @@
// @ts-check
/** @type {import("prettier").Config} */
module.exports = {
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 120,
tabWidth: 4,
plugins: ['prettier-plugin-svelte', '@ianvs/prettier-plugin-sort-imports'],
/* prettier-plugin-svelte */
overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }],
/* @ianvs/prettier-plugin-sort-imports */
importOrder: ['^~/(.*)$', '^./(.*)$', ''],
importOrderParserPlugins: ['typescript'],
importOrderTypeScriptVersion: '5.0.0',
importOrderCaseSensitive: false
};

178
README.md
View File

@@ -4,7 +4,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<p align="center"> <p align="center">
<a href="https://github.com/bytedream/stream-bypass/releases/latest"> <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>
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/"> <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"> <img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Users&style=flat-square" alt="Firefox Addon Store">
@@ -12,11 +12,8 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<a href="https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb"> <a href="https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb">
<img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store"> <img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store">
</a> </a>
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/">
<img src="https://img.shields.io/amo/stars/stream-bypass?label=Firefox%20Store%20Stars&style=flat-square" alt="Firefox Addon Stars">
</a>
<a href="https://github.com/bytedream/stream-bypass/releases/latest"> <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> </a>
</p> </p>
@@ -25,7 +22,9 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<a href="#-installation">Installation 📥</a> <a href="#-installation">Installation 📥</a>
<a href="#-supported-sites">Supported Sites 📜</a> <a href="#-features">Features </a>
<a href="#-supported-websites">Supported Websites 📜</a>
<a href="#%EF%B8%8F-building">Building 🛠️</a> <a href="#%EF%B8%8F-building">Building 🛠️</a>
@@ -41,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. Additionally, this enables you to download the video by right-clicking it and just choose the download option.
<details id="example"> <details id="example">
<summary><b>How it's working:</b></summary> <summary><b>How it works:</b></summary>
<img src="example.gif" alt=""> <img src="example.gif" alt="">
</details> </details>
@@ -51,65 +50,71 @@ Additionally, this enables you to download the video by right-clicking it and ju
The best way to install the extension are the official browser extension stores: 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 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) - [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
### Manual installation <details>
<summary><h3 id="manual-installation1">Manual installation</h3></summary>
- Firefox - Firefox (mv2)
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that) - Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. with [7zip](https://www.7-zip.org/))
- Go into your browser and type `about:debugging#/runtime/this-firefox` in the address bar - 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 - 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 > 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 - 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 - Turn on the developer mode by checking the switch in the top right corner
- Click `Load unpacked` and choose the unzipped directory - Click `Load unpacked` and choose the unzipped directory
## 📜 Supported sites </details>
| Site | Supported | Note | ## ✨ Features
| --------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
| [dropload.io](https://dropload.io) | ✔ | |
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔️ | |
| [filemoon.sx](https://filemoon.sx) | ✔ | |
| [goodstream.uno](https://goodstream.uno) | ✔ | |
| [mcloud.to](https://mcloud.to/) | ❌ | Reverse engineering the site costs too much time ([#5](https://github.com/ByteDream/stream-bypass/issues/5)) |
| [mixdrop.co](https://mixdrop.co) | ✔ | |
| [mp4upload.com](https://mp4upload.com) | ✔ | |
| [newgrounds.com](https://newgrounds.com) | ✔ | |
| [streama2s.com](https://streama2z.com) | ✔ |
| [streamtape.com](https://streamtape.com) | ✔ | |
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | |
| [supervideo.tv](https://supervideo.tv) | ✔ | |
| [upstream.to](https://upstream.to) | ✔ | |
| [videovard.sx](https://videovard.sx) | ❌ | Reverse engineering the site costs too much time |
| [vidmoly.me](https://vidmoly.me) | ✔ | |
| [vidoza.net](https://vidoza.net) | ✔ | |
| [vidstream.pro](https://vidstream.pro) | ❌ | Reverse engineering the site costs too much time ([#5](https://github.com/ByteDream/stream-bypass/issues/5)) |
| [voe.sx](https://voe.sx) | ✔ | |
| [vupload.com](https://vupload.com) | ✔ | |
| [kwik.cx](https://kwik.cx) | ✔ | |
-: Everything ok. - ✔: Supported.
- : Included in the addon but will probably not work. See `Note` in this case, an explanation why will stand there in the most cases. - : Not supported.
- ❌: Not included / supported by the addon. This can have various reasons. See `Note` for an explanation.
| 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
- ✔: Everything ok.
- ⚠: Works with limitations.
- ✖: Not supported.
| Site | Firefox & Firefox for Android (mv2) | Chrome & Chromium based (mv2) |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| [dropload.io](https://dropload.io) | ✔ | ✔ |
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
| [filemoon.to](https://filemoon.to) | ✔ | ✔ |
| [goodstream.uno](https://goodstream.uno) | ✔ | ✔ |
| [kwik.cx](https://kwik.cx) | ✔ | ✔ |
| [loadx.ws](https://loadx.ws) | ✔ | ✖ (background request always required) |
| [luluvdo.com](https://luluvdo.com) | ✔ | ✖ (background request always required) |
| [mixdrop.co](https://mixdrop.co) | ✔ | ✔ |
| [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
| [newgrounds.com](https://newgrounds.com) | ✔ | ✔ |
| [streama2z.com](https://streama2z.com) | ✔ | ✖ (redirect always required) |
| [streamtape.com](https://streamtape.com) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) |
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | ✔ |
| [supervideo.tv](https://supervideo.tv) | ✔ | ✔ |
| [upstream.to](https://upstream.to) | ✔ | ✔ |
| [vidmoly.to](https://vidmoly.me) | ✔ | ✔ |
| [vidoza.net](https://vidoza.net) | ✔ | ✔ |
| [voe.sx](https://voe.sx) | ✔ | ✖ (redirect always required) |
| [vupload.com](https://vupload.com) | ✔ | ✔ |
_This table might not be 100% accurate, it isn't actively monitored if the addon works for every website!_
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. 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.
<details>
<summary>Hall of dead sites</summary>
<ul>
<li><a href="https://evoload.io">evoload.io</a> - Down</li>
<li><a href="https://vidlox.me">vidlox.me</a> - Reachable but empty</li>
<li><a href="https://vivo.sx">vivo.sx</a> - Down</li>
</ul>
</details>
## 🛠️ Building ## 🛠️ 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: Requirements:
@@ -120,18 +125,20 @@ If the requirements are satisfied, you can continue with the following commands:
```shell ```shell
# install all dependencies # install all dependencies
$ npm install $ npm i
# build the extension source to the dist/ directory # build the extension and start it in a new firefox instance
$ npm run build $ npm run dev:firefox
# same as build + more optimizations and browser specific settings at release/ # build the extension with optimizations to the .output/firefox-mv2 directory
$ npm run release:firefox # or "release:chrome" to create a release for chromium based browsers $ npm run build:firefox
``` ```
You can omit the `:firefox` suffix, then it's built for Chrome.
##### Install ##### 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: When using firefox, use the following:
1. Type `about:debugging` in the browser's address bar. 1. Type `about:debugging` in the browser's address bar.
@@ -141,33 +148,42 @@ When using firefox, use the following:
## ⚙️ Settings ## ⚙️ Settings
### <ins>ff2mpv: use mpv to directly play streams</ins> > You reach the settings by pressing the tree dots (⋮) 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 disabled for which hosts the extension should redirect.
### 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). - 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` - Scroll down to `Install manually`
- Follow instructions for Firefox/Chrome - Follow instructions for Firefox/Chrome
- Edit the `ff2mpv.json` you created: - 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", "allowed_extensions": [
"{55dd42e8-3dd9-455a-b4fe-86664881b10c}" "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 - 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`:
- Add `chrome-extension://your-id-here/` to `allowed_origins` -> ```
``` "allowed_origins": [
"allowed_origins": [ "chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/", "chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/"
"chrome-extension://your-id-her/" ]
] ```
```
</details>
## ⚖ License ## ⚖ License

31
eslint.config.mjs Normal file
View File

@@ -0,0 +1,31 @@
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';
const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url));
export default defineConfig(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs[('flat/recommended', 'flat/prettier')],
prettier,
includeIgnoreFile(gitignorePath),
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-undef': 'off'
}
}
);

11373
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,9 @@
{ {
"name": "stream-bypass", "name": "stream-bypass",
"version": "3.1.0", "version": "4.0.0",
"displayName": "Stream Bypass", "displayName": "Stream Bypass",
"author": "bytedream", "author": "bytedream",
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video", "description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
"scripts": {
"build": "vite build",
"watch": "vite build --watch --mode development --minify false",
"dev": "vite",
"serve:firefox": "web-ext run --start-url \"about:debugging#/runtime/this-firefox\" --source-dir ./dist/",
"serve:chrome": "web-ext run -t chromium --start-url \"https://example.com\" --source-dir ./dist/",
"check": "svelte-check --tsconfig ./tsconfig.json",
"lint": "prettier --check --plugin prettier-plugin-svelte . && eslint .",
"format": "prettier --write --plugin prettier-plugin-svelte .",
"release:firefox": "MANIFEST_VERSION=2 vite build --outDir release/firefox",
"release:chrome": "MANIFEST_VERSION=3 vite build --outDir release/chrome"
},
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -24,28 +12,43 @@
"bugs": { "bugs": {
"url": "https://github.com/bytedream/stream-bypass/issues" "url": "https://github.com/bytedream/stream-bypass/issues"
}, },
"devDependencies": { "type": "module",
"@samrum/vite-plugin-web-extension": "^5.1.0", "scripts": {
"@sveltejs/vite-plugin-svelte": "^2.5.3", "dev": "wxt",
"@tsconfig/svelte": "^5.0.4", "dev:firefox": "wxt -b firefox",
"@types/chrome": "^0.0.269", "build": "wxt build",
"@types/firefox-webext-browser": "^120.0.4", "build:firefox": "wxt build -b firefox",
"@typescript-eslint/eslint-plugin": "^7.17.0", "zip": "wxt zip",
"@typescript-eslint/parser": "^7.17.0", "zip:firefox": "wxt zip -b firefox",
"eslint": "^8.57.0", "check": "svelte-check --tsconfig ./tsconfig.json",
"eslint-config-prettier": "^9.1.0", "postinstall": "wxt prepare",
"eslint-plugin-svelte": "^2.43.0", "format": "prettier --write .",
"hls.js": "^1.5.13", "lint": "prettier --check . && eslint ."
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"sass": "^1.77.8",
"svelte": "^4.2.18",
"svelte-check": "^3.8.4",
"svelte-preprocess": "^6.0.2",
"tslib": "^2.6.3",
"typescript": "^5.5.4",
"vite": "^4.5.3",
"web-ext": "^8.2.0"
}, },
"type": "module" "devDependencies": {
"@eslint/compat": "^1.4.1",
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@steeze-ui/heroicons": "^2.4.2",
"@steeze-ui/svelte-icon": "^1.6.2",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.16",
"@tsconfig/svelte": "^5.0.5",
"@wxt-dev/module-svelte": "^2.0.4",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.5",
"hls.js": "^1.6.13",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"sass": "^1.93.2",
"svelte": "^5.43.2",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.16",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.12",
"web-ext": "^9.1.0",
"wxt": "^0.20.11"
}
} }

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 713 B

After

Width:  |  Height:  |  Size: 713 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 792 B

After

Width:  |  Height:  |  Size: 792 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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
src/assets/base.css Normal file
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;
}
}

View File

@@ -1,40 +0,0 @@
import './shared';
import type { Match } from '~/lib/match';
import { Redirect, storageDelete, storageGet } from '~/lib/settings';
import { getMatch } from '~/lib/match';
chrome.webRequest.onBeforeSendHeaders.addListener(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async (details) => {
const referer: { domain: string } | undefined = await storageGet('referer');
if (referer === undefined) return;
details.requestHeaders!.push({
name: 'Referer',
value: `https://${referer.domain}/`
});
await storageDelete('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'] }
);

View File

@@ -1 +0,0 @@
import './shared';

View File

@@ -1,5 +0,0 @@
chrome.runtime.onMessage.addListener(async (message) => {
if (message.action == 'ff2mpv') {
await chrome.runtime.sendNativeMessage('ff2mpv', { url: message.url });
}
});

View File

@@ -1,82 +0,0 @@
import type { Match } from '~/lib/match';
import { getMatch } from '~/lib/match';
import { Other, Redirect } from '~/lib/settings';
async function main() {
let match: Match | null;
let redirect = false;
if ((match = await getMatch(window.location.host)) === null) {
if ((match = await Redirect.get()) === null) {
return;
}
redirect = true;
}
// some sites have a javascript based redirect, e.g. example.com redirects to example.org by changing
// window.location.href instead of a 3XX http redirect. an empty body is a sign that such a javascript redirect
// occurred
if (document.body == null) {
await Redirect.set(match);
return;
}
const re = document.body.innerHTML.match(match.regex);
if (re === null) {
return;
}
if (redirect) {
await Redirect.delete();
}
let url: string | null;
try {
url = await match.match(re);
} catch (e) {
return;
}
if (!url) {
return;
}
// send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application
if (await Other.getFf2mpv()) {
await chrome.runtime.sendMessage({ action: 'ff2mpv', url: url });
}
if (match.replace && !url.includes('.m3u8')) {
// this destroys all intervals that may spawn popups or events
let intervalId = window.setInterval(() => {}, 0);
while (intervalId--) {
clearInterval(intervalId);
}
let timeoutId = window.setTimeout(() => {}, 0);
while (timeoutId--) {
clearTimeout(timeoutId);
}
// clear completed document
document.documentElement.innerHTML = '';
// video player
const player = document.createElement('video');
player.style.width = '100%';
player.style.height = '100%';
player.controls = true;
player.src = url;
// add video player to document body
document.body.style.margin = '0';
document.body.append(player);
} else {
window.location.assign(
chrome.runtime.getURL(
`src/entries/player/player.html?id=${match.id}&url=${encodeURIComponent(url)}&domain=${
window.location.hostname
}`
)
);
}
}
main();

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import { play } from '~/entries/player/player';
import { onMount } from 'svelte';
let errorMessage: string | null = null;
let videoElem: HTMLVideoElement;
onMount(async () => {
try {
await play(videoElem);
videoElem.controls = true;
} catch (e) {
errorMessage = e;
}
});
</script>
<!-- svelte-ignore a11y-media-has-caption -->
<video id="video" bind:this={videoElem} />
{#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>

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Stream Bypass</title>
</head>
<body>
<script type="module">
import Player from '~/entries/player/Player.svelte';
new Player({
target: document.body
});
</script>
</body>
</html>

View File

@@ -1,42 +0,0 @@
import { matches } from '~/lib/match';
import Hls from 'hls.js';
import { storageSet } from '~/lib/settings';
async function playNative(url: string, videoElem: HTMLVideoElement) {
videoElem.src = url;
}
async function playHls(url: string, videoElem: HTMLVideoElement) {
if (videoElem.canPlayType('application/vnd.apple.mpegurl')) {
videoElem.src = url;
} else if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: false
});
hls.loadSource(url);
hls.attachMedia(videoElem);
} else {
throw 'Failed to play m3u8 video (hls is not supported). Try again or create a new issue <a href="https://github.com/ByteDream/stream-bypass/issues/new">here</a>';
}
}
export async function play(videoElem: HTMLVideoElement) {
const urlQuery = new URLSearchParams(window.location.search);
const id = urlQuery.get('id') as string;
const url = decodeURIComponent(urlQuery.get('url') as string);
const domain = urlQuery.get('domain') as string;
const match = matches[id];
if (match === undefined) {
throw `Invalid id: ${id}. Please report this <a href="https://github.com/ByteDream/stream-bypass/issues">here</a>`;
}
document.title = `Stream Bypass (${domain})`;
await storageSet('referer', { domain: domain });
if (new URL(url).pathname.endsWith('.m3u8')) {
await playHls(url, videoElem);
} else {
await playNative(url, videoElem);
}
}

View File

@@ -1,165 +0,0 @@
<script lang="ts">
import { type Match, matches } from '~/lib/match';
import { Hosters, Other } from '~/lib/settings';
import Toggle from './toggle.svelte';
let hostersEnabled: boolean;
let hosters: (Match & { active: boolean; disabled: boolean })[] = [];
(async () => {
hostersEnabled = !(await Hosters.getAllDisabled());
const disabled = await Hosters.getDisabled();
hosters = Object.values(matches).map((m: any) => {
m['active'] = disabled.findIndex((p) => p.id == m.id) == -1;
return m;
}) as typeof hosters;
})();
let isMobile: boolean;
(async () => {
isMobile = (await browser.runtime.getPlatformInfo()).os === 'android';
})();
let ff2mpvEnabled: boolean;
(async () => {
ff2mpvEnabled = (await Other.getFf2mpv()) as boolean;
})();
</script>
<main
style={isMobile
? 'height: 100vh; display: flex; flex-direction: column; align-items: center'
: 'height: 500px'}
>
<fieldset>
<legend>Hoster</legend>
<div class="setting-container" style={isMobile ? 'grid-column-gap: 5rem' : ''}>
<label for="hosters-enabled">Enabled</label>
<div>
<Toggle
bind:checked={hostersEnabled}
id="hosters-enabled"
on:change={() => Hosters.setAll(hostersEnabled)}
/>
</div>
<hr />
{#each hosters as hoster, i}
<label for="hoster-{i}" style="cursor: {hostersEnabled ? 'pointer' : 'default'}"
>{hoster.name}</label
>
<div>
<Toggle
bind:checked={hoster.active}
disabled={!hostersEnabled}
id="hoster-{i}"
on:change={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"
on:change={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>

View File

@@ -1,17 +0,0 @@
<!doctype html>
<html style="overflow-y: hidden" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Stream Bypass</title>
</head>
<body style="overflow-y: scroll">
<script type="module">
import Popup from '~/entries/popup/Popup.svelte';
new Popup({
target: document.body
});
</script>
</body>
</html>

View File

@@ -1,5 +0,0 @@
import App from './Popup.svelte';
new App({
target: document.getElementById('app') as Element
});

View File

@@ -1,65 +0,0 @@
<!-- https://flowbite.com/docs/forms/toggle/ -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let checked = false;
export let disabled = false;
export let id: string | null = null;
const dispatch = createEventDispatcher();
</script>
<label class="toggle">
<slot />
<input type="checkbox" {id} bind:checked {disabled} on:change={(e) => dispatch('change', e)} />
<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>

View File

@@ -0,0 +1,30 @@
import { UrlReferer } from '@/lib/settings';
export default defineBackground(() => {
browser.runtime.onMessage.addListener(async (message) => {
if (message.action == 'ff2mpv') {
await browser.runtime.sendNativeMessage('ff2mpv', { url: message.url });
}
// the following listener is only available in mv2
if (import.meta.env.MANIFEST_VERSION === 3) return;
browser.webRequest.onBeforeSendHeaders.addListener(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async (details) => {
const referer = await UrlReferer.get(new URL(details.url).hostname);
if (!referer) return;
details.requestHeaders!.push({
name: 'Referer',
value: `https://${referer}/`
});
return { requestHeaders: details.requestHeaders };
},
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] },
['blocking', 'requestHeaders']
);
});
});

View File

@@ -0,0 +1,78 @@
import { getHost, hosts, type Host, type HostMatch } from '@/lib/host';
import { FF2MPVSettings } from '@/lib/settings';
export default defineContentScript({
matches: [
...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`)),
// only mv2 allows to match all urls
...(import.meta.env.MANIFEST_VERSION === 2 ? ['<all_urls>'] : [])
],
allFrames: true,
runAt: 'document_end',
main
});
async function main() {
let host: Host | null;
if ((host = await getHost(window.location.host)) === null) {
return;
}
let re = null;
for (const regex of host.regex) {
if ((re = document.body.innerHTML.match(regex)) !== null) {
break;
}
}
if (re === null) {
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 != '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}`
)
);
}
}

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}

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Stream Bypass</title>
<style>
html,
body,
video {
width: 100%;
height: 100%;
margin: 0;
}
</style>
</head>
<body style="background-color: #131313; overflow: hidden">
<script type="module">
import { mount } from 'svelte';
import Player from './Player.svelte';
mount(Player, {
target: document.body
});
</script>
</body>
</html>

View File

@@ -0,0 +1,75 @@
import Hls from 'hls.js';
import { listenMessages, MessageType, sendMessage } from '@/lib/communication';
import { HostMatchType, hosts } from '@/lib/host';
import { UrlReferer } from '@/lib/settings';
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
// multiple hosts need to have a correct referer set
await UrlReferer.addTemporary(new URL(url).hostname, domain);
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 UrlReferer.addTemporary(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';
}
}
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 = () =>
sendMessage(MessageType.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 = listenMessages((type) => {
if (type !== MessageType.RequestActiveMatch) return;
notifyActiveMatch();
});
window.onbeforeunload = cancel;
}

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import '@/assets/base.css';
import { fly } from 'svelte/transition';
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
import { isMobile } from '@/entrypoints/popup/state.js';
/* state init */
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
/* types */
type Page = 'main' | 'settings';
/* states */
let activePage = $state<Page>('main');
</script>
<div class="flex w-[350px] overflow-hidden" class:w-screen={$isMobile}>
{#if activePage === 'main'}
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-[300px] flex-1 flex flex-col">
<Main onSettingsOpenRequest={() => (activePage = 'settings')} />
</div>
{:else if activePage === 'settings'}
<div
transition:fly={{ x: 300, duration: 150 }}
class="min-w-full w-full h-[300px] flex-1 flex flex-col"
class:h-screen={$isMobile}
>
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
</div>
{/if}
</div>

View File

@@ -0,0 +1 @@
<div class="w-full border-b-[1px] border-gray-400"></div>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
/* types */
interface Props {
checked: boolean;
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
size?: 'sm' | 'md';
}
/* states */
let { checked = $bindable(), onChecked, size = 'md' }: Props = $props();
let internalChecked = $state($state.snapshot(checked));
/* callbacks */
async function onInputChange() {
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 cursor-pointer">
<div class="relative">
<input type="checkbox" class="peer sr-only" bind:checked={internalChecked} onchange={onInputChange} />
<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>

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>

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;

View File

@@ -0,0 +1,3 @@
<div class="h-full flex items-center justify-center">
<p class="text-[1.05rem]">Extension disabled</p>
</div>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
import { Icon } from '@steeze-ui/svelte-icon';
import { isMobile } 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 */
function copyUrl() {
navigator.clipboard.writeText(urlOutput);
}
</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-[0.25rem] 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>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
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>
<div class="flex justify-between items-center p-2">
<div class="flex items-baseline gap-2">
<h1>stream-bypass</h1>
<span class="text-xs text-gray-400">v{import.meta.env.VERSION}</span>
</div>
<div class="flex items-center gap-2">
{#key allHostsDisabled}
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
{/key}
<button class="font-bold cursor-pointer" onclick={() => onSettingsClick()}>⋮</button>
</div>
</div>

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import '@/assets/base.css';
import { onDestroy } from 'svelte';
import Divider from '@/entrypoints/popup/components/Divider.svelte';
import AllDisabled from '@/entrypoints/popup/pages/main/AllDisabled.svelte';
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
import NoMatch from '@/entrypoints/popup/pages/main/NoMatch.svelte';
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
import { hosts, type Host } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
/* types */
interface Props {
onSettingsOpenRequest: () => void;
}
/* states */
let { onSettingsOpenRequest }: Props = $props();
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
let allHostsDisabled = $state(false);
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
/* lifecycle */
const cancel = listenMessages((type, data) => {
if (type !== MessageType.NotifyActiveMatch) return;
currentMatch = {
host: hosts.find((host) => host.id === data.id)!,
url: data.url,
domain: data.domain
};
});
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
onDestroy(cancel);
</script>
<div class="w-full">
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
<Divider />
</div>
<div class="px-2 h-full">
{#if allHostsDisabled}
<AllDisabled />
{:else if !currentMatch}
<NoMatch />
{:else}
<div class="flex flex-col justify-between h-full pb-2">
<Match host={currentMatch.host} domain={currentMatch.domain} />
<div class="divider border-dashed"></div>
<div class="mt-2">
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
</div>
</div>
{/if}
</div>
<style>
* {
@apply select-none;
}
</style>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { type Host } from '@/lib/host';
/* types */
interface Props {
host: Host;
domain: string;
}
/* states */
const { host, domain }: Props = $props();
</script>
<div class="flex flex-col items-center justify-center w-full h-full">
<p class="text-lg">Match found:</p>
<div class="[&>*]:select-text">
<span class="underline text-green-400 text-2xl font-bold">{host.name}</span>
<span class="text-xs text-slate-300">({domain})</span>
</div>
</div>

View File

@@ -0,0 +1,13 @@
<div class="relative h-full">
<div class="h-full flex items-center justify-center">
<p class="text-[1.05rem]">No supported video found on this site</p>
</div>
<div class="absolute bottom-0.5">
<p class="text-xs text-gray-400">
Suggestions or bugs can be submitted <a
class="underline"
href="https://github.com/bytedream/stream-bypass/issues">here</a
>
</p>
</div>
</div>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
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 */
function onEnableChange(enabled: boolean) {
if (!enabled) return true;
return browser.permissions.request({ permissions: ['nativeMessaging'] });
}
</script>
<div class="flex items-center gap-2">
<div class="relative mr-3">
<span>Communication enabled</span>
<a
class="absolute -top-1 -right-4 text-sm"
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv-use-mpv-to-directly-play-streams"
target="_blank"><Icon src={InformationCircle} size="1rem" /></a
>
</div>
{#key enabled}
<Toggle
bind:checked={() => enabled, (v) => FF2MPVSettings.setEnabled(v)}
onChecked={onEnableChange}
size="sm"
/>
{/key}
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
/* types */
interface Props {
onBackClick: () => void;
}
/* states */
let { onBackClick }: Props = $props();
</script>
<div class="flex justify-between items-center p-2">
<div class="flex items-baseline gap-2">
<button class="cursor-pointer" onclick={() => onBackClick()}>←</button>
<h1>Settings</h1>
</div>
</div>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
import { hosts } from '@/lib/host';
import { HostSettings } from '@/lib/settings';
/* states */
let disabledHostIds = $state<Array<string>>([]);
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
</script>
<div class="grid grid-cols-[35%_43%_22%] gap-y-0.75">
<p class="font-bold">Host</p>
<p class="font-bold">Domains</p>
<p class="font-bold">Enabled</p>
{#each hosts as host (host.id)}
{@const domainList = host.domains.join(', ')}
<p>{host.name}</p>
<div>
<label for={host.id}>
<input id={host.id} type="checkbox" class="peer hidden" checked />
<p
title={domainList}
class="cursor-pointer overflow-hidden peer-checked:text-ellipsis peer-checked:text-nowrap"
>
{domainList}
</p>
</label>
</div>
<div class="mt-[.2rem]">
{#key disabledHostIds}
<Toggle
bind:checked={
() => !disabledHostIds.includes(host.id),
(v) => (v ? HostSettings.removeDisabledHost(host) : HostSettings.addDisabledHost(host))
}
size="sm"
></Toggle>
{/key}
</div>
{/each}
</div>

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import Divider from '@/entrypoints/popup/components/Divider.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 { isMobile } from '@/entrypoints/popup/state';
/* types */
interface Props {
onSettingsCloseRequest: () => void;
}
/* states */
let { onSettingsCloseRequest }: Props = $props();
</script>
<div class="w-full">
<Header onBackClick={onSettingsCloseRequest} />
<Divider />
</div>
<div class="flex flex-col gap-y-1 pt-1 h-full mx-2 my-1 overflow-y-scroll">
<details class="details" open>
<summary>Hosts</summary>
<HostsTable />
</details>
{#if !$isMobile}
<details class="details">
<summary>ff2mpv</summary>
<Ff2mpv />
</details>
{/if}
</div>
<style>
* {
@apply select-none;
}
.details {
/* using normal css instead of tailwind in the following blocks.
for some reason tailwind fails to resolve many references */
&::before,
&::after {
content: '';
display: block;
width: 100%;
border-top: 1px solid var(--color-gray-600);
margin: 0.25rem 0;
}
& > summary {
cursor: pointer;
}
&[open] > summary {
margin-bottom: 0.5rem;
}
}
</style>

View File

@@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const isMobile = writable(false);

36
src/lib/communication.ts Normal file
View File

@@ -0,0 +1,36 @@
export enum MessageType {
RequestActiveMatch,
NotifyActiveMatch
}
export type MessageData<T extends MessageType> = {
[MessageType.RequestActiveMatch]: undefined;
[MessageType.NotifyActiveMatch]: {
id: string;
url: string;
domain: string;
};
}[T];
export async function sendMessage<T extends MessageType>(message: T, data: MessageData<T>) {
await browser.runtime.sendMessage({ type: message, data: data });
}
export async function sendMessageToActiveTab<T extends MessageType>(message: T, data: MessageData<T>) {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
await browser.tabs.sendMessage(tabs[0].id!, { type: message, data: data }).catch(() => {});
}
export function listenMessages(listener: (type: MessageType, data: any) => void): () => void {
const callback = (callbackData: { type: MessageType; data: any }) => {
const { type, data } = callbackData;
listener(type, data);
};
browser.runtime.onMessage.addListener(callback);
return () => {
browser.runtime.onMessage.removeListener(callback);
};
}

View File

@@ -0,0 +1,41 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
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 {
type: HostMatchType.HLS,
url: `${await response.text()}1234567890${match[2]}${Date.now()}`
};
}
} satisfies Host;

17
src/lib/host/dropload.ts Normal file
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
src/lib/host/filemoon.ts Normal file
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, 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;

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;

78
src/lib/host/index.ts Normal file
View File

@@ -0,0 +1,78 @@
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 } 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 interface Host {
name: string;
id: string;
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 async function getHost(domain: string) {
if (await HostSettings.getAllHostsDisabled()) return null;
const disabledIds = await HostSettings.getDisabledHosts();
for (const host of hosts) {
if (host.domains.includes(domain)) {
if (!disabledIds.includes(host.id)) return host;
else return null;
}
}
return HostSettings.checkTemporaryHostDomain(domain);
}

17
src/lib/host/kwik.ts Normal file
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
src/lib/host/loadx.ts Normal file
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
src/lib/host/luluvdo.ts Normal file
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
src/lib/host/mixdrop.ts Normal file
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
src/lib/host/mp4upload.ts Normal file
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;

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
src/lib/host/streama2z.ts Normal file
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, window.location.hostname);
return null;
}
return {
type: HostMatchType.HLS,
url: match[0]
};
}
} satisfies Host;

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
src/lib/host/streamzz.ts Normal file
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;

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
src/lib/host/upstream.ts Normal file
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;

15
src/lib/host/vidmoly.ts Normal file
View File

@@ -0,0 +1,15 @@
import { HostMatchType, type Host } from '@/lib/host';
export default {
name: 'Vidmoly',
id: 'vidmoly',
domains: ['vidmoly.me', 'vidmoly.net', 'vidmoly.to'],
regex: [/(?<=file:").+\.m3u8.*(?=")/gm],
match: async function (match: RegExpMatchArray) {
return {
type: HostMatchType.HLS,
url: match[0]
};
}
} satisfies Host;

16
src/lib/host/vidoza.ts Normal file
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.HLS,
url: match[0]
};
}
} satisfies Host;

74
src/lib/host/voe.ts Normal file
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, 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
src/lib/host/vupload.ts Normal file
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;

View File

@@ -1,296 +0,0 @@
import { unpack } from './utils';
import { Hosters, Redirect } from './settings';
export interface Match {
name: string;
id: string;
domains: string[];
replace?: boolean;
regex: RegExp;
notice?: string;
match(match: RegExpMatchArray): Promise<string | null>;
}
export const Doodstream: Match = {
name: 'Doodstream',
id: 'doodstream',
domains: [
'doodstream.com',
'dood.pm',
'dood.ws',
'dood.wf',
'dood.cx',
'dood.sh',
'dood.watch',
'dood.to',
'dood.so',
'dood.la',
'dood.li',
'dood.re',
'dood.yt',
'doods.pro',
'ds2play.com',
'dooood.com',
'd000d.com'
],
replace: true,
regex: /(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s,
match: async (match: RegExpMatchArray) => {
const response = await fetch(`https://${window.location.host}${match[1]}`, {
headers: {
Range: 'bytes=0-'
},
referrer: `https://${window.location.host}/e/${
window.location.pathname.split('/').slice(-1)[0]
}`
});
return `${await response.text()}1234567890${match[2]}${Date.now()}`;
}
};
export const DropLoad: Match = {
name: 'Dropload',
id: 'dropload',
domains: ['dropload.ui'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms,
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const Filemoon: Match = {
name: 'Filemoon',
id: 'filemoon',
domains: ['filemoon.sx', 'filemoon.in'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms,
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const GoodStream: Match = {
name: 'Goodstream',
id: 'goodstream',
domains: ['goodstream.uno'],
regex: /(?<=file:\s+").*(?=")/g,
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const Kwik: Match = {
name: 'Kwik',
id: 'kwik',
domains: ['kwik.cx'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms,
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=source=').*(?=')/)![0];
}
};
export const Mixdrop: Match = {
name: 'Mixdrop',
id: 'mixdrop',
domains: ['mixdrop.co', 'mixdrop.to', 'mixdrop.ch', 'mixdrop.bz', 'mixdrop.gl'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms,
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
const url = unpacked.match(/(?<=MDCore.wurl=").*(?=")/)![0];
return `https:${url}`;
}
};
export const Mp4Upload: Match = {
name: 'Mp4Upload',
id: 'mp4upload',
domains: ['mp4upload.com'],
replace: true,
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms,
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=player.src\(").*(?=")/)![0];
}
};
export const Newgrounds: Match = {
name: 'Newgrounds',
id: 'newgrounds',
domains: ['newgrounds.com'],
regex: /.*/gm,
match: async () => {
const id = window.location.pathname.split('/').slice(-1)[0];
const response = await fetch(`https://www.newgrounds.com/portal/video/${id}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const json = await response.json();
return decodeURI(json['sources'][Object.keys(json['sources'])[0]][0]['src']);
}
};
export const StreamA2z: Match = {
name: 'Stream2Az',
id: 'stream2az',
domains: ['streama2z.com', 'streama2z.xyz'],
regex: /https?:\/\/\S*m3u8.+(?=['"])/gm,
match: async (match: RegExpMatchArray) => {
if (StreamA2z.domains.indexOf(window.location.hostname) !== -1) {
await Redirect.set(StreamA2z);
return null;
}
return match[0];
}
};
export const Streamtape: Match = {
name: 'Streamtape',
id: 'streamtape',
domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'],
regex: /id=.*(?=')/gm,
match: async (match: RegExpMatchArray) => {
let i = 0;
while (i < match.length) {
if (match[++i - 1] == match[i]) {
return `https://streamtape.com/get_video?${match[i]}`;
}
}
// use the old method as fallback
return `https://streamtape.com/get_video?${match.reverse()[0]}`;
}
};
export const Streamzz: Match = {
name: 'Streamzz',
id: 'streamzz',
domains: ['streamzz.to', 'streamz.ws'],
regex: /(?<=\|)\w{2,}/gm,
match: async (match: RegExpMatchArray) => {
return `https://get.${location.hostname.split('.')[0]}.tw/getlink-${
match.sort((a, b) => b.length - a.length)[0]
}.dll`;
}
};
export const SuperVideo: Match = {
name: 'Supervideo',
id: 'supervideo',
domains: ['supervideo.tv'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms,
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const Upstream: Match = {
name: 'Upstream',
id: 'upstream',
domains: ['upstream.to'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms,
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const Vidmoly: Match = {
name: 'Vidmoly',
id: 'vidmoly',
domains: ['vidmoly.me', 'vidmoly.to'],
regex: /(?<=file:").+\.m3u8/gm,
match: async (match: RegExpMatchArray) => {
alert('a');
return match[0];
}
};
export const Vidoza: Match = {
name: 'Vidoza',
id: 'vidoza',
domains: ['vidoza.net'],
regex: /(?<=src:\s?").+?(?=")/gm,
replace: true,
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const Voe: Match = {
name: 'Voe',
id: 'voe',
domains: ['voe.sx'],
regex: /https?:\/\/\S*m3u8.+(?=['"])/gm,
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const Vupload: Match = {
name: 'Vupload',
id: 'vupload',
domains: ['vupload.com'],
regex: /(?<=src:\s?").+?(?=")/gm,
match: async (match: RegExpMatchArray) => {
return match[0];
}
};
export const matches = {
[Doodstream.id]: Doodstream,
[DropLoad.id]: DropLoad,
[Filemoon.id]: Filemoon,
[GoodStream.id]: GoodStream,
[Kwik.id]: Kwik,
[Mixdrop.id]: Mixdrop,
[Mp4Upload.id]: Mp4Upload,
[Newgrounds.id]: Newgrounds,
[StreamA2z.id]: StreamA2z,
[Streamtape.id]: Streamtape,
[Streamzz.id]: Streamzz,
[SuperVideo.id]: SuperVideo,
[Upstream.id]: Upstream,
[Vidmoly.id]: Vidmoly,
[Vidoza.id]: Vidoza,
[Voe.id]: Voe,
[Vupload.id]: Vupload
};
export async function getMatch(domain: string): Promise<Match | null> {
if (await Hosters.getAllDisabled()) {
return null;
}
for (const match of Object.values(matches)) {
if (
match.domains.indexOf(domain) !== -1 &&
!(await Hosters.getDisabled().then((d) => d.find((p) => p.id == match.id)))
) {
return match;
}
}
return null;
}

View File

@@ -1,85 +1,88 @@
import type { Match } from './match'; import { storage } from '#imports';
import { matches } from './match'; import { hosts, type Host } from '@/lib/host';
export const Hosters = { export class HostSettings {
getDisabled: async () => { /* disabled hosts */
const disabled = (await storageGet('hosters.disabled', [])) as string[]; private static disabledHosts = storage.defineItem<string[]>('local:disabledHosts', { fallback: [] });
return disabled.map((id) => matches[id]).filter((m) => m !== undefined); private static allHostsDisabled = storage.defineItem<boolean>('local:allHostsDisabled', { fallback: false });
},
disable: async (match: Match) => { static async addDisabledHost(host: Host) {
const disabled = (await storageGet('hosters.disabled', [])) as string[]; const ids = await this.disabledHosts.getValue();
const index = disabled.indexOf(match.id);
const index = ids.indexOf(host.id);
if (index === -1) { if (index === -1) {
disabled.push(match.id); ids.push(host.id);
await storageSet('hosters.disabled', disabled); await this.disabledHosts.setValue(ids);
} }
}, }
enable: async (match: Match) => { static async removeDisabledHost(host: Host) {
const disabled = (await storageGet('hosters.disabled', [])) as string[]; const ids = await this.disabledHosts.getValue();
const index = disabled.indexOf(match.id);
const index = ids.indexOf(host.id);
if (index !== -1) { if (index !== -1) {
disabled.splice(index, 1); ids.splice(index, 1);
await storageSet('hosters.disabled', disabled); await this.disabledHosts.setValue(ids);
} }
},
getAllDisabled: async () => {
return await storageGet<boolean>('hosters.allDisabled', false);
},
setAll: async (enable: boolean) => {
await storageSet('hosters.allDisabled', !enable);
} }
}; static async getDisabledHosts() {
return await this.disabledHosts.getValue();
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');
} }
};
export const Other = { static async getAllHostsDisabled() {
getFf2mpv: async () => { return await this.allHostsDisabled.getValue();
return await storageGet('other.ff2mpv', false);
},
setFf2mpv: async (enable: boolean) => {
await storageSet('other.ff2mpv', enable);
} }
};
export async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefined> { static async setAllHostsDisabled(disabled: boolean) {
let resolve: (value: T | undefined) => void; await this.allHostsDisabled.setValue(disabled);
const promise = new Promise<T | undefined>((r) => (resolve = r)); }
chrome.storage.local.get(key, (entry) => { /* tmp */
const value = entry[key]; private static temporaryHostDomain = storage.defineItem<Record<string, string>>('local:temporaryHostDomain', {
resolve(value === undefined ? defaultValue : value); fallback: {}
}); });
return promise; static async addTemporaryHostDomain(host: Host, domain: string) {
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
temporaryHostDomains[domain] = host.id;
await this.temporaryHostDomain.setValue(temporaryHostDomains);
console.log(await this.temporaryHostDomain.getValue());
}
static async checkTemporaryHostDomain(domain: string) {
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
const hostId = temporaryHostDomains[domain];
return hostId ? (hosts.find((host) => host.id === hostId) ?? null) : null;
}
} }
export async function storageSet<T>(key: string, value: T) { export class FF2MPVSettings {
let resolve: () => void; private static ff2mpvEnabled = storage.defineItem<boolean>('local:ff2mpv', { fallback: false });
const promise = new Promise<void>((r) => (resolve = r));
const obj = { static async getEnabled() {
[key]: value return await this.ff2mpvEnabled.getValue();
}; }
chrome.storage.local.set(obj, () => resolve()); static async setEnabled(enabled: boolean) {
console.log('set', enabled);
return promise; await this.ff2mpvEnabled.setValue(enabled);
}
} }
export async function storageDelete(key: string) { export class UrlReferer {
let resolve: () => void; private static temporaryUrlReferer = storage.defineItem<Record<string, string>>('local:temporaryUrlReferer', {
const promise = new Promise<void>((r) => (resolve = r)); fallback: {}
});
chrome.storage.local.remove(key, () => resolve()); static async addTemporary(hostname: string, referer: string) {
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
return promise; tmpUrlReferer[hostname] = referer;
await this.temporaryUrlReferer.setValue(tmpUrlReferer);
}
static async get(hostname: string) {
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
return tmpUrlReferer[hostname] ?? null;
}
} }

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}`);
}

View File

@@ -16,7 +16,7 @@ export async function unpack(packed: string): Promise<string> {
${packed} ${packed}
}; };
return packed; return packed;
}' }
`; `;
const res = (await runInPageContext(toExecute)) as string; const res = (await runInPageContext(toExecute)) as string;
@@ -32,8 +32,9 @@ export async function unpack(packed: string): Promise<string> {
async function runInPageContext<T>(toExecute: string): Promise<T | null> { async function runInPageContext<T>(toExecute: string): Promise<T | null> {
// test that we are running with the allow-scripts permission // test that we are running with the allow-scripts permission
try { try {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
window.sessionStorage; window.sessionStorage;
} catch (ignore) { } catch {
return null; return null;
} }
@@ -48,18 +49,17 @@ async function runInPageContext<T>(toExecute: string): Promise<T | null> {
scriptElm.textContent = ` scriptElm.textContent = `
( (
async function () { async function () {
const response = {
id: '${resultMessageId}'
};
const response = { try {
id: ${resultMessageId} response.result = JSON.stringify(await (${toExecute})() ); // run script
}; } catch(err) {
response.error = JSON.stringify(err);
}
try { window.postMessage(response, '*');
response.result = JSON.stringify(await (${toExecute})() ); // run script
} catch(err) {
response.error = JSON.stringify(err);
}
window.postMessage(response, '*');
} }
)(); )();
`; `;

6
src/utils/extract.ts Normal file
View File

@@ -0,0 +1,6 @@
export function lastPathSegment(path: string): string {
while (path.endsWith('/')) {
path = path.slice(0, -1);
}
return path.substring(path.lastIndexOf('/') + 1);
}

3
src/vite-env.d.ts vendored
View File

@@ -1,3 +0,0 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />
/// <reference types="@samrum/vite-plugin-web-extension/client" />

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()
};

View File

@@ -1,18 +1,6 @@
{ {
"extends": "@tsconfig/svelte/tsconfig.json", "extends": "./.wxt/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true, "useDefineForClassFields": true
"target": "es2019", }
"module": "esnext",
"useDefineForClassFields": true,
"resolveJsonModule": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
},
"allowJs": true,
"checkJs": true
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
} }

View File

@@ -1,38 +0,0 @@
import { defineConfig, loadEnv } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import webExtension from '@samrum/vite-plugin-web-extension';
import path from 'path';
import { getManifest } from './src/manifest';
import { matches } from './src/lib/match';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
svelte(),
webExtension({
manifest: getManifest(Number(env.MANIFEST_VERSION)),
additionalInputs: {
html: [
{
fileName: 'src/entries/player/player.html',
webAccessible: {
matches:
Number(env.MANIFEST_VERSION) === 3
? Object.values(matches).flatMap((m) => m.domains.map((d) => `*://${d}/*`))
: ['<all_urls>']
}
}
]
}
})
],
resolve: {
alias: {
'~': path.resolve(__dirname, './src')
}
}
};
});

1
wxt-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="svelte" />

58
wxt.config.ts Normal file
View File

@@ -0,0 +1,58 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'wxt';
import { version } from './package.json';
export default defineConfig({
srcDir: 'src',
modules: ['@wxt-dev/module-svelte'],
manifest: ({ browser, manifestVersion }) => ({
name: manifestVersion === 2 ? 'Stream Bypass' : 'Stream Bypass Lite',
icons: {
16: 'icon/stream-bypass@16px.png',
32: 'icon/stream-bypass@32px.png',
48: 'icon/stream-bypass@48px.png',
128: 'icon/stream-bypass@128px.png'
},
browser_specific_settings:
browser === 'firefox'
? {
gecko: {
id: '{55dd42e8-3dd9-455a-b4fe-86664881b10c}',
data_collection_permissions: {
required: ['none']
}
},
gecko_android: {}
}
: undefined,
permissions: ['storage', ...(manifestVersion === 2 ? ['webRequest', 'webRequestBlocking', '<all_urls>'] : [])],
optional_permissions: ['nativeMessaging'],
web_accessible_resources: [
{
resources: ['player.html'],
// TODO: Replace this with all hosts domains if target manifest version is 3.
// This isn't working atm because importing '@/lib/host' fails. Ahhhh I love the whole fucking JS/TS
// environment. Maybe I'm also overlooking something and the fix is easy, but that itsn't working out of the
// box is once again terrible DX
matches: ['<all_urls>']
}
]
}),
zip: {
artifactTemplate: '{{name}}-{{version}}-{{manifestVersion}}.zip'
},
vite: () => ({
define: {
'import.meta.env.VERSION': JSON.stringify(version)
},
plugins: [tailwindcss()]
}),
svelte: {
vite: {
preprocess: vitePreprocess({})
}
}
});