127 Commits
v2.1.1 ... 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
696ec22471 Update dependencies and version 2024-07-28 23:43:56 +02:00
e379113aa4 Add firefox android support 2024-07-28 23:33:54 +02:00
84605ceb30 Make nativeMessaging an optional permission 2024-07-28 23:31:25 +02:00
67c492db47 Update popup UI 2024-07-28 23:04:04 +02:00
ae2f196b06 Lint 2024-07-28 18:08:05 +02:00
298e9308ce Add streama2z.com (#19) 2024-07-28 17:54:41 +02:00
3cd4c6b6b7 Fix doodstream.com 2024-07-28 17:54:02 +02:00
6286acf85c Make video replace strategy more aggressive 2024-07-28 17:33:54 +02:00
94eee79218 Fix voe.sx 2024-07-28 16:35:58 +02:00
2f186bda07 Add d000d.com (#18) 2024-07-28 14:46:36 +02:00
1c9f95cebc Fix linting errors 2024-07-14 21:08:47 +02:00
c57cd03407 Fix vidmoly & add it to README 2024-05-20 15:40:38 +02:00
64cf565da3 Update dependencies and version 2024-05-20 15:37:46 +02:00
bf8a7eb602 Remove reliability 2024-05-20 14:11:09 +02:00
c643a39e2d Add new doodstream domain 2024-04-24 01:18:01 +02:00
fcfbc41fdb Fix voe.sx 2024-04-24 00:46:32 +02:00
1251f079f5 Remove discord link 2023-12-10 19:51:35 +01:00
ccfc77167b Add vidmoly.me 2023-12-08 15:50:19 +01:00
a10066458f Add chrome web store link 2023-11-20 12:20:32 +01:00
6a8c705b06 Fix mv2 chrome namespace apis 2023-11-17 16:25:40 +01:00
76a5bac7fd Fix chromium popup height 2023-11-17 16:25:29 +01:00
3aca863a4b Fix invalid release name 2023-11-17 15:06:17 +01:00
fe14edc0ef Make mv3 compatible 2023-11-17 15:06:09 +01:00
fffe23638c Update build instructions 2023-11-11 22:05:39 +01:00
5214228a72 Add linting and build pipeline 2023-11-11 21:48:53 +01:00
7000d5a08b Lint 2023-11-11 21:48:44 +01:00
a21f799e7d Update install instructions 2023-11-11 21:35:24 +01:00
1fb85313dd Use more reliable streamtape match function 2023-11-11 21:31:23 +01:00
6e7410b088 Rewrite 2023-11-11 21:31:20 +01:00
Bharathi
0054f5da0e Update few domains (#15)
* Update matches.ts

* Update matches.ts

* Update matches.ts
2023-08-08 18:58:50 +02:00
48dc192964 Update version 2023-08-08 14:25:48 +02:00
sdaqo
7c45cacd36 add ff2mpv setting + new hosters (#14)
* add Kwik, use unpacker to improve reliabilty

* use packer for filemoon

* use packer for upstream

* add ff2mpv setting + functionality

* Update README.md

* get rid of @types/webextension-polyfill

* Revert "Update README.md"

This reverts commit affb600096.

* Update README.md

* Update info link for ff2mpv

* add kwik to hosters in readme

* removes console.logs

* Delete package-lock.json

* add package-lock.json to .gitignore

* unpack without using eval

* Merge main branch into here

* Add Dropload Hoster

* Add Supervideo Hoster

* Add GoodStream Hoster

* Add hosters to readme and delete console.logs

* Delete package-lock.json

* Fix ff2mpv info url

* Update readme

---------

Co-authored-by: bytedream <bytedream@protonmail.com>
2023-08-08 13:50:18 +02:00
00514e4e81 Update license year 2023-04-29 00:13:39 +02:00
4c1d0d0b63 Update dependencies 2023-04-29 00:12:00 +02:00
sdaqo
a920832945 Add Kwik and improve reliabilty for several other hosters (#12)
* add Kwik, use unpacker to improve reliabilty

* use packer for filemoon

* use packer for upstream

* Update README.md

* Revert "Update README.md"

This reverts commit affb600096.

* add kwik to hosters in readme

* unpack without using eval
2023-04-28 18:19:01 +02:00
ByteDream
7d8d8b6614 Merge pull request #9 from bachig26/master
Update matches.ts
2023-01-19 13:27:04 +01:00
Bharathi
b4052834f2 Update matches.ts
- add dood.re
2023-01-19 05:02:40 +01:00
ByteDream
9900c33243 Merge pull request #7 from bachig26/patch-1
added more domains
2023-01-11 08:10:40 +01:00
Bharathi
98709bc934 added more domains
- additional domains to mixdrop and doodstream
2023-01-11 01:07:48 +01:00
f4cbdd3258 Version 2.1.7 2022-11-10 16:33:48 +01:00
fa41a8de8e Fix voe regex 2022-10-24 23:10:27 +02:00
ce8bc855b9 Update dead sites 2022-10-23 16:56:05 +02:00
03202b2a12 Fix issues link 2022-10-01 12:10:19 +02:00
9f0e1b59ce Update version to 2.1.6 2022-09-30 20:30:42 +02:00
d928d25e09 Update supported websites 2022-09-30 20:29:15 +02:00
266771aa13 Update issues url 2022-09-30 20:26:43 +02:00
d56672d90f Fix mp4upload 2022-09-30 16:02:00 +02:00
a9ea5fe4b2 Add filemoon.sx 2022-09-30 14:59:42 +02:00
bb3f5384d6 Fix url media matching 2022-09-30 14:57:23 +02:00
6989587161 Remove error message when video is not loaded properly 2022-09-12 22:01:50 +02:00
dd9bf71a5c Update dependency versions and bump version to 2.1.5 2022-09-10 18:15:53 +02:00
6da0050df4 Fix false-positive error message 2022-09-10 18:10:48 +02:00
1a7c22ec0e Move from sass to scss for stylesheets 2022-09-10 17:37:12 +02:00
175862b098 Enable error message on non hls streams 2022-08-23 22:07:58 +02:00
fd5a532d0f Change streamzz reliability to low 2022-08-23 21:53:38 +02:00
8c43eedb23 Change doodstream reliability to normal 2022-08-23 21:43:24 +02:00
e027c2e09e Version 2.1.4 2022-08-04 17:06:10 +02:00
f9a0656d4d Optimize redirect functionality 2022-08-04 17:05:50 +02:00
382d8b1268 Add dood.wf as domain for doodstream 2022-08-04 17:03:03 +02:00
5b8639ce6a web development :))))))))))))))))))))))))))) 2022-07-23 17:40:16 +02:00
841c824590 Fix native player error message 2022-07-23 17:28:47 +02:00
2055a3ea81 Update version to 2.1.2 2022-07-23 17:19:31 +02:00
0262d1853c Change error message layout and link color when video cannot be loaded 2022-07-22 13:55:28 +02:00
81da6600e6 Fix error message if video cannot be loaded 2022-07-22 13:55:03 +02:00
817f5b82f9 Remove unused import 2022-07-22 01:49:54 +02:00
9a17fb0d9b Fix domain sometimes not shown in window title when using native player 2022-07-22 01:41:50 +02:00
17f8aab216 Fix mixdrop not working 2022-07-22 01:34:34 +02:00
90 changed files with 10324 additions and 1115 deletions

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

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
build/ logs
dist/ *.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?

11
.prettierignore Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
.idea
.vscode
.zed
node_modules
package-lock.json
.output
.wxt
.github

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

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2022 ByteDream Copyright (c) 2022-NOW ByteDream
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

176
README.md
View File

@@ -3,20 +3,17 @@
A multi-browser addon / extension for multiple streaming providers which redirects directly to the source video. A multi-browser addon / extension for multiple streaming providers which redirects directly to the source video.
<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%20Store%20Downloads&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">
</a> </a>
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/"> <a href="https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb">
<img src="https://img.shields.io/amo/stars/stream-bypass?label=Firefox%20Store%20Stars&style=flat-square" alt="Firefox Addon Stars"> <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://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 href="https://discord.gg/gUWwekeNNg">
<img src="https://img.shields.io/discord/915659846836162561?label=Discord&style=flat-square" alt="Discord">
</a> </a>
</p> </p>
@@ -25,9 +22,13 @@ 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="#%EF%B8%8F-building">Building ⚙️</a> <a href="#-supported-websites">Supported Websites 📜</a>
<a href="#%EF%B8%8F-building">Building 🛠️</a>
<a href="#%EF%B8%8F-settings">Settings ⚙️</a>
<a href="#-license">License ⚖</a> <a href="#-license">License ⚖</a>
</p> </p>
@@ -39,86 +40,151 @@ 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>
## 📥 Installation ## 📥 Installation
### Firefox ### Official browser stores
Install the addon directly from the [firefox addon store](https://addons.mozilla.org/de/firefox/addon/stream-bypass/). The best way to install the extension are the official browser extension stores:
### Chromium / Google Chrome - [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)
1. Download the zipfile from the [latest release](https://smartrelease.bytedream.org/github/ByteDream/stream-bypass/stream_bypass-{tag}.zip) and unzip it (with [7zip](https://www.7-zip.org/) or something like that). <details>
2. Go into your browser and type `chrome://extensions` in the address bar. <summary><h3 id="manual-installation1">Manual installation</h3></summary>
3. Turn the developer mode in the top right corner on.
4. Click Load unpacked.
5. Choose the cloned / unzipped directory.
### Opera - Firefox (mv2)
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. with [7zip](https://www.7-zip.org/))
- Go into your browser and type `about:debugging#/runtime/this-firefox` in the address bar
- Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory
- Chromium / Google Chrome (mv3)
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
- Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. [7zip](https://www.7-zip.org/))
- Go into your browser and type `chrome://extensions` in the address bar
- Turn on the developer mode by checking the switch in the top right corner
- Click `Load unpacked` and choose the unzipped directory
1. Download the zipfile from the [latest release](https://smartrelease.bytedream.org/github/ByteDream/stream-bypass/stream_bypass-{tag}.zip) and unzip it (with [7zip](https://www.7-zip.org/) or something like that). </details>
2. Go into your browser and type `opera://extensions` in the address bar.
3. Turn the developer mode in the top right corner on.
4. Click Load unpacked.
5. Choose the cloned / unzipped directory.
## 📜 Supported sites ## ✨ Features
| Site | Supported | Note | - ✔: Supported.
|-----------------------------------------------------------------------|-----------|--------------------------------------------------| - ✖: Not supported.
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔️ | |
| [evoload.io](https://evoload.io) | ✔️ | |
| [mixdrop.co](https://mixdrop.co) | ✔ | |
| [mp4upload.com](https://mp4upload.com) | ✔ | |
| [newgrounds.com](https://newgrounds.com) | ✔ | |
| [streamtape.com](https://streamtape.com) | ✔ | |
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | |
| [upstream.to](https://upstream.to) | ✔ | |
| [videovard.sx](https://videovard.sx) | ❌ | Reverse engineering the site costs too much time |
| [vidlox.me](https://vidlox.me) | ⚠ | Website down / Timeout |
| [vidoza.net](https://vidoza.net) | ✔ | |
| [vivo.sx](https://vivo.sx) | ⚠️ | Website down / Timeout |
| [voe.sx](https://voe.sx) / [voeunblk.com](https://voeunblk.com) | ✔ | |
| [vupload.com](https://vupload.com) | ✔ | |
- ✔️: Everything ok. | Feature | Firefox (mv2) | Chrome (mv3) | Firefox for Android (mv2) |
- ⚠: 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 included / supported by the addon. This can have various reasons. See `Note` for an explanation. | 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.
## 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:
- `npm` installed. - `npm` installed.
- A copy of this repository and a shell / console open in the copied directory. - A copy of this repository and a shell / console open in the copied directory.
If the requirements are satisfied, you can continue with the following commands: 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 a build/ directory # build the extension and start it in a new firefox instance
$ npm run build $ npm run dev:firefox
# same as build + create a bundle zipfile at dist/ # build the extension with optimizations to the .output/firefox-mv2 directory
$ npm run bundle $ 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 (almost every other, Google Chrome, Opera, ...), 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.
2. Select 'This Firefox' tab (maybe named different, depending on your language). 2. Select 'This Firefox' tab (maybe named different, depending on your language).
3. Under `Temporary Extensions`, click `Load Temporary Add-on`. 3. Under `Temporary Extensions`, click `Load Temporary Add-on`.
4. Choose any file in the directory where the compiled sources are. 4. Choose any file in the directory where the compiled sources are.
## ⚙️ Settings
> You reach the settings by pressing the tree dots (⋮) in the top right corner of the extension popup.
### Hosts
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).
- Scroll down to `Install manually`
- Follow instructions for Firefox/Chrome
- Edit the `ff2mpv.json` you created:
- Firefox: Add `{55dd42e8-3dd9-455a-b4fe-86664881b10c}` to `allowed_extensions`:
```
"allowed_extensions": [
"ff2mpv@yossarian.net",
"{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
]
```
- Chrome/Chromium: - Go To: Settings -> Extensions - Click on `Details` of the Stream Bypass extension and copy the ID - Add `chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins`:
```
"allowed_origins": [
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
"chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/"
]
```
</details>
## ⚖ License ## ⚖ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details.

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

8355
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,54 @@
{ {
"name": "stream-bypass", "name": "stream-bypass",
"version": "2.1.1", "version": "4.0.0",
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video", "displayName": "Stream Bypass",
"main": "src/index.ts", "author": "bytedream",
"scripts": { "description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
"build": "node tasks/build.ts", "license": "MIT",
"bundle": "node tasks/build.ts && web-ext build -s build -a dist -o", "repository": {
"bundle:all": "node tasks/build.ts && web-ext build -s build -a dist -o && web-ext sign -s build -a dist", "type": "git",
"check": "web-ext lint -s build" "url": "git+https://github.com/bytedream/stream-bypass.git"
}, },
"repository": { "bugs": {
"type": "git", "url": "https://github.com/bytedream/stream-bypass/issues"
"url": "git+https://github.com/ByteDream/stream-bypass.git" },
}, "type": "module",
"author": "ByteDream", "scripts": {
"license": "MIT", "dev": "wxt",
"bugs": { "dev:firefox": "wxt -b firefox",
"url": "https://github.com/ByteDream/stream-bypass/issues" "build": "wxt build",
}, "build:firefox": "wxt build -b firefox",
"homepage": "https://github.com/ByteDream/stream-bypass#readme", "zip": "wxt zip",
"devDependencies": { "zip:firefox": "wxt zip -b firefox",
"@rollup/plugin-commonjs": "^22.0.0", "check": "svelte-check --tsconfig ./tsconfig.json",
"@rollup/plugin-node-resolve": "^13.3.0", "postinstall": "wxt prepare",
"@rollup/plugin-replace": "^4.0.0", "format": "prettier --write .",
"@rollup/plugin-typescript": "^8.3.3", "lint": "prettier --check . && eslint ."
"@typescript-eslint/eslint-plugin": "^5.27.1", },
"@typescript-eslint/parser": "^5.27.1", "devDependencies": {
"@types/chrome": "^0.0.190", "@eslint/compat": "^1.4.1",
"@types/node-sass": "^4.11.2", "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@types/yazl": "^2.4.2", "@steeze-ui/heroicons": "^2.4.2",
"eslint": "^8.17.0", "@steeze-ui/svelte-icon": "^1.6.2",
"hls.js": "^1.1.5", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"node-sass": "^7.0.1", "@tailwindcss/vite": "^4.1.16",
"node-sass-package-importer": "^5.3.2", "@tsconfig/svelte": "^5.0.5",
"rollup": "^2.75.6", "@wxt-dev/module-svelte": "^2.0.4",
"tippy.js": "^6.3.7", "eslint": "^9.38.0",
"typescript": "^4.7.3", "eslint-config-prettier": "^10.1.8",
"web-ext": "^7.0.0" "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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="738.248" height="738.248" viewBox="0 0 195.328 195.328"><path d="M108.902 30.525c-53.858 0-97.664 43.806-97.664 97.664 0 53.86 43.806 97.665 97.664 97.665 53.86 0 97.664-43.806 97.664-97.665 0-53.858-43.805-97.664-97.664-97.664zm0 13.434c46.6 0 84.23 37.632 84.23 84.23 0 46.6-37.63 84.23-84.23 84.23-46.599 0-84.232-37.63-84.232-84.23 0-46.598 37.633-84.23 84.232-84.23z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="translate(-11.238 -30.525)"/><path d="M8.986 105.297v96.926l83.94-48.463-7.7-4.446zm10.266 17.781 53.143 30.682-53.143 30.681z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="matrix(1.30853 0 0 1.30853 44.795 -103.534)"/><path d="M345.131 686.085c-122.49-10.29-225.617-86.136-271.115-199.397-9.596-23.888-15.72-47.197-20.05-76.315-2.763-18.58-2.76-64.116.004-82.784 7.5-50.64 23.648-93.74 49.956-133.33 39.979-60.166 96.484-103.56 164.472-126.311 53.98-18.063 110.354-21.415 165.476-9.838 82.83 17.396 153.434 65.363 200.69 136.344 10.702 16.073 25.836 46.006 32.68 64.634 6.703 18.242 13.106 43.275 16.28 63.647 2.098 13.461 2.487 20.483 2.534 45.734.06 31.25-.585 38.592-5.576 63.53-26.78 133.796-136.647 235.614-272.394 252.435-17.246 2.137-47.675 2.935-62.957 1.651zm77.476-197.054c113.33-65.435 206.175-119.33 206.322-119.767.277-.827-411.864-239.171-414.659-239.8-1.374-.31-1.586 31.672-1.586 239.605 0 227.996.097 239.938 1.934 239.45 1.063-.283 94.658-54.052 207.989-119.487z" style="fill:#fff;stroke:#000;stroke-width:5.46616" transform="scale(.26458)"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="738.248" height="738.248" viewBox="0 0 195.328 195.328"><path d="M108.902 30.525c-53.858 0-97.664 43.806-97.664 97.664 0 53.86 43.806 97.665 97.664 97.665 53.86 0 97.664-43.806 97.664-97.665 0-53.858-43.805-97.664-97.664-97.664zm0 13.434c46.6 0 84.23 37.632 84.23 84.23 0 46.6-37.63 84.23-84.23 84.23-46.599 0-84.232-37.63-84.232-84.23 0-46.598 37.633-84.23 84.232-84.23z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="translate(-11.238 -30.525)"/><path d="M8.986 105.297v96.926l83.94-48.463-7.7-4.446zm10.266 17.781 53.143 30.682-53.143 30.681z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="matrix(1.30853 0 0 1.30853 44.795 -103.534)"/><path d="M345.131 686.085c-122.49-10.29-225.617-86.136-271.115-199.397-9.596-23.888-15.72-47.197-20.05-76.315-2.763-18.58-2.76-64.116.004-82.784 7.5-50.64 23.648-93.74 49.956-133.33 39.979-60.166 96.484-103.56 164.472-126.311 53.98-18.063 110.354-21.415 165.476-9.838 82.83 17.396 153.434 65.363 200.69 136.344 10.702 16.073 25.836 46.006 32.68 64.634 6.703 18.242 13.106 43.275 16.28 63.647 2.098 13.461 2.487 20.483 2.534 45.734.06 31.25-.585 38.592-5.576 63.53-26.78 133.796-136.647 235.614-272.394 252.435-17.246 2.137-47.675 2.935-62.957 1.651zm77.476-197.054c113.33-65.435 206.175-119.33 206.322-119.767.277-.827-411.864-239.171-414.659-239.8-1.374-.31-1.586 31.672-1.586 239.605 0 227.996.097 239.938 1.934 239.45 1.063-.283 94.658-54.052 207.989-119.487z" style="fill:gray;stroke:#000;stroke-width:5.46616" transform="scale(.26458)"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 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,16 +0,0 @@
import {getMatch} from "./match/match";
import {storageGet, storageSet} from "./store/store";
import {Match} from "./match/matches";
chrome.webRequest.onBeforeRedirect.addListener(async details => {
// check if redirects origins from a previous redirect
if (await storageGet('redirect') === undefined) {
let match: Match
if ((match = await getMatch(new URL(details.url).host)) !== undefined) {
await storageSet('redirect', match.id)
}
}
}, {
urls: ['<all_urls>'],
types: ['main_frame', 'sub_frame']
})

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

View File

@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="195.32812mm"
height="195.32812mm"
viewBox="0 0 195.32812 195.32812"
version="1.1"
id="svg5"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="logo-disabled.svg"
inkscape:export-filename="disabled_128.png"
inkscape:export-xdpi="16.644812"
inkscape:export-ydpi="16.644812"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.90509668"
inkscape:cx="428.68349"
inkscape:cy="362.94465"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="0"
inkscape:window-y="36"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<inkscape:path-effect
effect="powerstroke"
id="path-effect3140"
is_visible="true"
lpeversion="1"
offset_points="3,0"
not_jump="false"
sort_points="true"
interpolator_type="CubicBezierJohan"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
linejoin_type="extrp_arc"
miter_limit="4"
scale_width="1"
end_linecap_type="zerowidth" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.238281,-30.525391)">
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none"
d="m 108.90234,30.525391 c -53.858772,0 -97.664059,43.805286 -97.664059,97.664059 0,53.85878 43.805287,97.66407 97.664059,97.66407 53.85878,0 97.66407,-43.80529 97.66407,-97.66407 0,-53.858773 -43.80529,-97.664059 -97.66407,-97.664059 z m 0,13.433593 c 46.59895,0 84.23047,37.631526 84.23047,84.230466 0,46.59895 -37.63152,84.23047 -84.23047,84.23047 -46.59894,0 -84.232418,-37.63152 -84.232418,-84.23047 0,-46.59894 37.633478,-84.230466 84.232418,-84.230466 z"
id="path2592" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none"
d="m 8.9863281,105.29688 v 96.92578 l 83.9394529,-48.46289 -7.699219,-4.44532 z m 10.2656249,17.78125 53.142578,30.68164 -53.142578,30.68164 z"
id="path2646"
transform="matrix(1.3085265,0,0,1.3085265,56.033246,-73.00885)" />
<path
style="fill:#808080;stroke:#000000;stroke-width:5.46616"
d="M 345.13114,686.08528 C 222.64086,675.79533 119.51392,599.94853 74.016249,486.68834 64.419992,462.79978 58.296197,439.49115 53.966274,410.37335 51.203358,391.79333 51.205528,346.25661 53.970219,327.58931 61.4703,276.94849 77.618278,233.84988 103.92601,194.25835 143.90493,134.09253 200.40964,90.698198 268.39759,67.947934 322.37862,49.884696 378.75198,46.533168 433.87378,58.109989 c 82.82951,17.396066 153.43381,65.362801 200.69115,136.344381 10.701,16.07313 25.83502,46.0053 32.67962,64.63397 6.70243,18.2417 13.10531,43.27443 16.2797,63.64715 2.0974,13.46078 2.48648,20.4825 2.53407,45.73343 0.0589,31.24975 -0.58565,38.59247 -5.57665,63.52913 -26.77909,133.797 -136.64661,235.61462 -272.39375,252.4357 -17.24628,2.13706 -47.67501,2.9353 -62.95678,1.65153 z M 422.60679,489.0315 C 535.93722,423.59631 628.78214,369.70078 628.92885,369.26367 629.20612,368.43746 217.06471,130.09256 214.27041,129.46316 c -1.37426,-0.30954 -1.58595,31.67289 -1.58595,239.60604 0,227.99566 0.0965,239.93763 1.9335,239.44933 1.06342,-0.28267 94.65839,-54.05183 207.98883,-119.48703 z"
id="path2606"
transform="matrix(0.26458333,0,0,0.26458333,11.238281,30.525391)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="195.32812mm"
height="195.32812mm"
viewBox="0 0 195.32812 195.32812"
version="1.1"
id="svg5"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="logo.svg"
inkscape:export-filename="logo_128.png"
inkscape:export-xdpi="16.644812"
inkscape:export-ydpi="16.644812"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.90509668"
inkscape:cx="198.32136"
inkscape:cy="397.19514"
inkscape:window-width="1920"
inkscape:window-height="1015"
inkscape:window-x="1920"
inkscape:window-y="36"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<inkscape:path-effect
effect="powerstroke"
id="path-effect3140"
is_visible="true"
lpeversion="1"
offset_points="3,0"
not_jump="false"
sort_points="true"
interpolator_type="CubicBezierJohan"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
linejoin_type="extrp_arc"
miter_limit="4"
scale_width="1"
end_linecap_type="zerowidth" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-11.238281,-30.525391)">
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none"
d="m 108.90234,30.525391 c -53.858772,0 -97.664059,43.805286 -97.664059,97.664059 0,53.85878 43.805287,97.66407 97.664059,97.66407 53.85878,0 97.66407,-43.80529 97.66407,-97.66407 0,-53.858773 -43.80529,-97.664059 -97.66407,-97.664059 z m 0,13.433593 c 46.59895,0 84.23047,37.631526 84.23047,84.230466 0,46.59895 -37.63152,84.23047 -84.23047,84.23047 -46.59894,0 -84.232418,-37.63152 -84.232418,-84.23047 0,-46.59894 37.633478,-84.230466 84.232418,-84.230466 z"
id="path2592" />
<path
style="color:#000000;fill:#000000;-inkscape-stroke:none"
d="m 8.9863281,105.29688 v 96.92578 l 83.9394529,-48.46289 -7.699219,-4.44532 z m 10.2656249,17.78125 53.142578,30.68164 -53.142578,30.68164 z"
id="path2646"
transform="matrix(1.3085265,0,0,1.3085265,56.033246,-73.00885)" />
<path
style="fill:#ffffff;stroke:#000000;stroke-width:5.46616"
d="M 345.13114,686.08528 C 222.64086,675.79533 119.51392,599.94853 74.016249,486.68834 64.419992,462.79978 58.296197,439.49115 53.966274,410.37335 51.203358,391.79333 51.205528,346.25661 53.970219,327.58931 61.4703,276.94849 77.618278,233.84988 103.92601,194.25835 143.90493,134.09253 200.40964,90.698198 268.39759,67.947933 322.37862,49.884696 378.75198,46.533168 433.87378,58.109989 c 82.82951,17.396066 153.43381,65.362801 200.69115,136.344371 10.701,16.07314 25.83502,46.00531 32.67962,64.63398 6.70243,18.2417 13.10531,43.27443 16.2797,63.64715 2.0974,13.46078 2.48648,20.4825 2.53407,45.73343 0.0589,31.24975 -0.58565,38.59247 -5.57665,63.52913 -26.7791,133.797 -136.64661,235.61462 -272.39375,252.43569 -17.24628,2.13707 -47.67501,2.9353 -62.95678,1.65154 z M 422.60679,489.0315 C 535.93722,423.59631 628.78214,369.70078 628.92885,369.26367 629.20612,368.43746 217.06471,130.09256 214.27041,129.46316 c -1.37426,-0.30954 -1.58595,31.67289 -1.58595,239.60604 0,227.99566 0.0965,239.93763 1.9335,239.44933 1.06342,-0.28267 94.65839,-54.05183 207.98883,-119.48703 z"
id="path374"
transform="matrix(0.26458333,0,0,0.26458333,11.238281,30.525391)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,39 +0,0 @@
import {getMatch} from "./match/match";
import {storageDelete, storageGet} from "./store/store";
import {Match, matches} from "./match/matches";
import play = chrome.cast.media.timeout.play;
async function main() {
let match: Match;
if ((match = await getMatch(window.location.host)) === undefined) {
let id: string
if ((id = await storageGet('redirect')) !== undefined) {
match = matches.find(m => m.id === id)
await storageDelete('redirect')
} else {
return
}
}
const re = document.body.innerHTML.match(match.regex)
if (re === null) {
return
}
const url = await match.match(re)
if (match.replace && !url.endsWith('.m3u8')) {
const player = document.createElement('video')
player.style.width = '100%'
player.style.height = '100%'
player.controls = true
player.src = url
document.body.innerHTML = ''
document.body.append(player)
} else {
window.location.assign(chrome.runtime.getURL(`ui/player/player.html?id=${match.id}&url=${encodeURIComponent(url)}&domains=${window.location.host}`))
}
}
main()

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;

88
src/lib/settings.ts Normal file
View File

@@ -0,0 +1,88 @@
import { storage } from '#imports';
import { hosts, type Host } from '@/lib/host';
export class HostSettings {
/* disabled hosts */
private static disabledHosts = storage.defineItem<string[]>('local:disabledHosts', { fallback: [] });
private static allHostsDisabled = storage.defineItem<boolean>('local:allHostsDisabled', { fallback: false });
static async addDisabledHost(host: Host) {
const ids = await this.disabledHosts.getValue();
const index = ids.indexOf(host.id);
if (index === -1) {
ids.push(host.id);
await this.disabledHosts.setValue(ids);
}
}
static async removeDisabledHost(host: Host) {
const ids = await this.disabledHosts.getValue();
const index = ids.indexOf(host.id);
if (index !== -1) {
ids.splice(index, 1);
await this.disabledHosts.setValue(ids);
}
}
static async getDisabledHosts() {
return await this.disabledHosts.getValue();
}
static async getAllHostsDisabled() {
return await this.allHostsDisabled.getValue();
}
static async setAllHostsDisabled(disabled: boolean) {
await this.allHostsDisabled.setValue(disabled);
}
/* tmp */
private static temporaryHostDomain = storage.defineItem<Record<string, string>>('local:temporaryHostDomain', {
fallback: {}
});
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 class FF2MPVSettings {
private static ff2mpvEnabled = storage.defineItem<boolean>('local:ff2mpv', { fallback: false });
static async getEnabled() {
return await this.ff2mpvEnabled.getValue();
}
static async setEnabled(enabled: boolean) {
console.log('set', enabled);
await this.ff2mpvEnabled.setValue(enabled);
}
}
export class UrlReferer {
private static temporaryUrlReferer = storage.defineItem<Record<string, string>>('local:temporaryUrlReferer', {
fallback: {}
});
static async addTemporary(hostname: string, referer: string) {
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
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,50 +0,0 @@
{
"manifest_version": 2,
"name": "Stream Bypass",
"author": "ByteDream",
"description": "A multi-browser addon / extension for multiple streaming providers which redirects directly to the source video.",
"version": "2.1.0",
"homepage_url": "https://github.com/ByteDream/stream-bypass",
"browser_specific_settings": {
"gecko": {
"id": "{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
}
},
"content_scripts": [
{
"all_frames": true,
"matches": [
"<all_urls>"
],
"js": [
"index.js"
],
"run_at": "document_end"
}
],
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"storage",
"webRequest",
"<all_urls>"
],
"web_accessible_resources": [
"ui/player/*"
],
"browser_action": {
"default_icon": {
"48": "icons/logo_48.png",
"128": "icons/logo_128.png"
},
"default_title": "Stream Bypass",
"default_popup": "ui/popup/popup.html"
},
"icons": {
"48": "icons/logo_48.png",
"128": "icons/logo_128.png"
}
}

View File

@@ -1,14 +0,0 @@
import {Match, matches} from "./matches";
import {getAllDisabled, getDisabled} from "../store/store";
export async function getMatch(host: string): Promise<Match | undefined> {
if (await getAllDisabled()) {
return undefined
}
for (const match of matches) {
if (match.domains.some(v => host.indexOf(v) !== -1) && !((await getDisabled()).some(v => v === match))) {
return match
}
}
}

View File

@@ -1,258 +0,0 @@
export enum Reliability {
HIGH = 1,
NORMAL,
LOW,
}
export abstract class Match {
name: string
id: string
reliability: Reliability
domains: string[]
replace?: boolean
regex: RegExp
abstract match(match: RegExpMatchArray): Promise<string>
notice?: string
}
class Doodstream implements Match {
name = 'Doodstream'
id = 'doodstream'
reliability = Reliability.HIGH
domains = [
'doodstream.com',
'dood.pm',
'dood.ws'
]
replace = true
regex = new RegExp(/(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s)
async match(match: RegExpMatchArray): Promise<string> {
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()}`
}
}
class Evoload implements Match {
name = 'Evoload'
id = 'evoload'
reliability = Reliability.NORMAL
domains = [
'evoload.io'
]
regex = new RegExp(/.*/gm)
async match(match: RegExpMatchArray): Promise<string> {
const code = window.location.pathname.split('/').slice(-1)[0]
const response = await fetch('https://evoload.io/SecurePlayer', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({code: code})
})
const json = await response.json()
return json['stream']['src']
}
}
class Mixdrop implements Match {
name = 'Mixdrop'
id = 'mixdrop'
reliability = Reliability.HIGH
domains = [
'mixdrop.co'
]
regex = new RegExp(/(?<=\|)\w{2,}/gm)
async match(match: RegExpMatchArray): Promise<string> {
return `https://a-${match[1]}.${match[4]}.${match[5]}/v/${match[2]}.${match[6]}?s=${match[12]}&e=${match[13]}`
}
}
class Mp4Upload implements Match {
name = 'Mp4Upload'
id = 'mp4upload'
reliability = Reliability.NORMAL
domains = [
'mp4upload.com'
]
replace = true
regex = new RegExp(/(?<=\|)\w{2,}/gm)
async match(match: RegExpMatchArray): Promise<string> {
return `https://${match[34]}.mp4upload.com:${match[89]}/d/${match[88]}/video.mp4`
}
}
class Newgrounds implements Match {
name = 'Newgrounds'
id = 'newgrounds'
reliability = Reliability.HIGH
domains = [
'newgrounds.com'
]
regex = new RegExp(/.*/gm)
async match(match: RegExpMatchArray): Promise<string> {
let id = window.location.pathname.split('/').slice(-1)[0]
let response = await fetch(`https://www.newgrounds.com/portal/video/${id}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
let json = await response.json()
return decodeURI(json['sources'][Object.keys(json['sources'])[0]][0]['src'])
}
}
class Streamtape implements Match {
name = 'Streamtape'
id = 'streamtape'
reliability = Reliability.NORMAL
domains = [
'streamtape.com'
]
regex = new RegExp(/id=.*(?=')/gm)
async match(match: RegExpMatchArray): Promise<string> {
return `https://streamtape.com/get_video?${match.reverse()[0]}`
}
}
class Streamzz implements Match {
name = 'Streamzz'
id = 'streamzz'
reliability = Reliability.NORMAL
domains = [
'streamzz.to',
'streamz.ws'
]
regex = new RegExp(/(?<=\|)\w{2,}/gm)
async match(match: RegExpMatchArray): Promise<string> {
return `https://get.${document.domain.split('.')[0]}.tw/getlink-${match.sort((a, b) => b.length - a.length)[0]}.dll`
}
}
class Upstream implements Match {
name = 'Upstream'
id = 'upstream'
reliability = Reliability.NORMAL
domains = [
'upstream.to'
]
regex = new RegExp(/(?<=\|)\w{2,}/gm)
async match(match: RegExpMatchArray): Promise<string> {
return `https://${match[49]}.upstreamcdn.co/hls/${match[148]}/master.m3u8`
}
}
class Vidlox implements Match {
name = 'Vidlox'
id = 'vidlox'
reliability = Reliability.LOW
domains = [
'vidlox.me'
]
regex = new RegExp(/(?<=\[")\S+?(?=")/gm)
async match(match: RegExpMatchArray): Promise<string> {
return match[0]
}
}
class Vidoza implements Match {
name = 'Vidoza'
id = 'vidoza'
reliability = Reliability.HIGH
domains = [
'vidoza.net'
]
regex = new RegExp(/(?<=src:\s?").+?(?=")/gm)
async match(match: RegExpMatchArray): Promise<string> {
return match[0]
}
}
class Vivo implements Match {
name = 'Vivo'
id = 'vivo'
reliability = Reliability.LOW
domains = [
'vivo.sx'
]
regex = new RegExp(/(?<=source:\s')(\S+)(?=')/gms)
async match(match: RegExpMatchArray): Promise<string> {
return this.rot47(decodeURIComponent(match[0]))
}
// decrypts a string with the rot47 algorithm (https://en.wikipedia.org/wiki/ROT13#Variants)
rot47(encoded: string): string {
const s = []
for(let i = 0; i < encoded.length; i++) {
const j = encoded.charCodeAt(i)
if((j >= 33) && (j <= 126)) {
s[i] = String.fromCharCode(33+((j+ 14)%94))
} else {
s[i] = String.fromCharCode(j)
}
}
return s.join('')
}
}
class Voe implements Match {
name = 'Voe'
id = 'voe'
reliability = Reliability.HIGH
domains = [
'voe.sx'
]
regex = new RegExp(/https?:\/\/\S*m3u8(?=")/gm)
async match(match: RegExpMatchArray): Promise<string> {
return match[0]
}
}
class Vupload implements Match {
name = 'Vupload'
id = 'vupload'
reliability = Reliability.HIGH
domains = [
'vupload.com'
]
regex = new RegExp(/(?<=src:\s?").+?(?=")/gm)
async match(match: RegExpMatchArray): Promise<string> {
return match[0]
}
}
export const matches = [
new Doodstream(),
new Evoload(),
new Mixdrop(),
new Mp4Upload(),
new Newgrounds(),
new Streamtape(),
new Streamzz(),
new Upstream(),
new Vidlox(),
new Vidoza(),
new Vivo(),
new Voe(),
new Vupload()
]

View File

@@ -1,68 +0,0 @@
import {Match, matches} from "../match/matches";
export async function storageGet(key: string): Promise<any> {
return new Promise((resolve) => {
chrome.storage.local.get(key, (value) => {
resolve(value[key])
})
})
}
export async function storageSet(key: string, value: any) {
const obj = {}
obj[key] = value
await chrome.storage.local.set(obj)
}
export async function storageDelete(key: string) {
await chrome.storage.local.remove(key)
}
export async function getDisabled(): Promise<Match[]> {
const localMatches = []
for (const disabled of (await storageGet('disabled') as string[]) || []) {
let m: Match
if ((m = matches.find((v) => v.id === disabled)) !== undefined) {
localMatches.push(m)
}
}
return localMatches
}
export async function getAllDisabled(): Promise<boolean> {
const value = await storageGet('all')
return value !== undefined ? String(value).toLowerCase() === 'true' : false
}
export async function enableAll() {
await storageSet('all', false)
await chrome.browserAction.setIcon({
path: null
})
}
export async function disableAll() {
await storageSet('all', true)
await chrome.browserAction.setIcon({
path: {
48: chrome.runtime.getURL('icons/disabled_48.png'),
128: chrome.runtime.getURL('icons/disabled_128.png')
}
})
}
export async function enable(match: Match) {
const disabled = await storageGet('disabled') || []
disabled.splice(disabled.indexOf(match.id))
await storageSet('disabled', disabled)
}
export async function disable(match: Match) {
const disabled = await storageGet('disabled') as string[] || []
if (disabled.indexOf(match.id) !== undefined) {
disabled.push(match.id)
await storageSet('disabled', disabled)
}
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"target": "es2019",
"removeComments": true,
"esModuleInterop": true,
"lib": [
"dom",
"es5",
"scripthost",
"es2015.collection",
"es2015.promise"
],
},
"exclude": [
"../node_modules"
]
}

View File

@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stream Bypass</title>
<link rel="stylesheet" href="player.css">
<script src="player.js" defer></script>
</head>
<body>
<video id="video"></video>
<div id="message-container">
<p id="message" hidden></p>
</div>
</body>
</html>

View File

@@ -1,22 +0,0 @@
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
height: 100%
#message
color: white
text-align: center

View File

@@ -1,80 +0,0 @@
import {Match, matches, Reliability} from "../../match/matches";
// @ts-ignore
import Hls from "hls.js";
function show_message(message: string) {
document.getElementById('message').innerText = message
document.getElementById('message-container').hidden = false
document.getElementById('video').hidden = true
}
async function play_native(url: string, match: Match) {
const video = document.getElementById('video') as HTMLVideoElement
video.controls = true
video.src = url
}
async function play_hls(url: string, match: Match) {
const video = document.getElementById('video') as HTMLVideoElement
video.controls = true
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url
} else if (Hls.isSupported()) {
const hls = new Hls({
enableWorker: false
})
hls.loadSource(url)
hls.attachMedia(video)
const loaded = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(false)
}, match.reliability * 3000)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
resolve(true)
})
})
if (!loaded) {
let message: string
switch (match.reliability) {
case Reliability.LOW:
message = `The reliability for this domain is low, errors like this are common.
Try to choose another streaming provider (if existent) or deactivate the addon for this provider (${match.name}) and try again`
break
case Reliability.NORMAL:
message = `The reliability for this domain is normal, errors like this can occur but are not very common. Try to refresh the page`
break
case Reliability.HIGH:
message = `The reliability for this domains is high, errors like this are very unlikely to happen.
Try to refresh the page and if the error still exists you might want to open a new issue <a href="https://github.com/ByteDream/stream-bypass/issues/new">here</a>.
When you're using <a href="https://www.torproject.org/">Tor</a> such errors have a slight chance to occur more often,
so if this is the case just try to reload the page and see if it's working then`
break
}
show_message(`Could not load HLS video. ${message}`)
}
} else {
show_message('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>')
}
}
async function main() {
const urlQuery = new URLSearchParams(window.location.search)
const id = urlQuery.get('id')
const url = urlQuery.get('url')
const domain = urlQuery.get('domain')
const match = matches.find((m) => m.id === id)
if (match === undefined) {
show_message(`Invalid id: ${id}. Please report this <a href="https://github.com/ByteDream/stream-bypass/issues/new">here</a>`)
return
}
document.title = `Stream Bypass (${domain})`
url.endsWith('.m3u8') ? await play_hls(url, match) : await play_native(url, match)
}
main()

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Stream-Bypass</title>
<link rel="stylesheet" href="popup.css">
<script src="popup.js" defer></script>
</head>
<body>
<div id="container">
<div id="all">
<div class="buttons">
<a>On</a>
<a>Off</a>
</div>
</div>
<hr>
<table id="sub-container">
</table>
<a id="error" href="https://github.com/ByteDream/stream-bypass/issues/new">Something does not work</a>
</div>
</body>
</html>

View File

@@ -1,56 +0,0 @@
body
background-color: #2b2a33
font-weight: bold
max-height: 500px
overflow-x: hidden
overflow-y: auto
a, p
color: white
font-size: 16px
margin: 5px 0
cursor: default
a
border: 1px solid #281515
cursor: pointer
font-weight: normal
padding: 5px 8px
&.active
background-color: rgba(255, 65, 65, 0.74)
cursor: default
&.disabled
background-color: grey
cursor: not-allowed
&#error
border: none
display: block
font-weight: lighter
font-size: 0.8rem
text-align: center
padding: 10px 0 5px 0
hr
margin: 3px 0
#all
display: flex
justify-content: center
margin: 10px 0
.low-reliability
text-decoration: underline
text-decoration-color: rgb(255, 0, 0)
.normal-reliability
text-decoration: underline
text-decoration-color: yellow
.high-reliability
text-decoration: underline
text-decoration-color: #00ff00

View File

@@ -1,96 +0,0 @@
import {getDisabled, disable, enable, getAllDisabled, enableAll, disableAll} from "../../store/store";
import {matches, Reliability} from "../../match/matches";
async function main() {
const disabled = await getDisabled()
const subContainer = document.getElementById('sub-container')
for (const m of matches) {
const row = document.createElement('tr')
const name = document.createElement('td')
const nameValue = document.createElement('p')
nameValue.innerText = m.name
switch (m.reliability) {
case Reliability.LOW:
nameValue.classList.add('low-reliability')
break
case Reliability.NORMAL:
nameValue.classList.add('normal-reliability')
break
case Reliability.HIGH:
nameValue.classList.add('high-reliability')
break
}
const buttons = document.createElement('td')
buttons.classList.add('buttons')
const on = document.createElement('a')
on.innerText = 'On'
const off = document.createElement('a')
off.innerText = 'Off'
disabled.find((v) => v.id === m.id) === undefined ? on.classList.add('active') : off.classList.add('active')
on.onclick = async function () {
if (!on.classList.contains('disabled')) {
await enable(m)
on.classList.add('active')
off.classList.remove('active')
}
}
off.onclick = async function () {
if (!off.classList.contains('disabled')) {
await disable(m)
on.classList.remove('active')
off.classList.add('active')
}
}
name.append(nameValue)
buttons.append(on, off)
row.append(name, buttons)
subContainer.append(row)
}
const allOnButton = document.getElementById('all').getElementsByTagName('a')[0]
const allOffButton = document.getElementById('all').getElementsByTagName('a')[1]
if (await getAllDisabled()) {
const allBtns = document.getElementById('sub-container').getElementsByTagName('a')
for (let i = 0; i < allBtns.length; i++) {
allBtns[i].classList.add('disabled')
}
allOffButton.classList.add('active')
} else {
allOnButton.classList.add('active')
}
allOnButton.onclick = async () => {
if (!allOnButton.classList.contains('active')) {
allOnButton.classList.add('active')
allOffButton.classList.remove('active')
const allBtns = document.getElementById('sub-container').getElementsByTagName('a')
for (let i = 0; i < allBtns.length; i++) {
allBtns[i].classList.remove('disabled')
}
await enableAll()
}
}
allOffButton.onclick = async () => {
if (!allOffButton.classList.contains('active')) {
allOffButton.classList.add('active')
allOnButton.classList.remove('active')
const allBtns = document.getElementById('sub-container').getElementsByTagName('a')
for (let i = 0; i < allBtns.length; i++) {
allBtns[i].classList.add('disabled')
}
await disableAll()
}
}
}
main()

92
src/utils/content.ts Normal file
View File

@@ -0,0 +1,92 @@
// Adapted from http://matthewfl.com/unPacker.html by matthew@matthewfl.com
export async function unpack(packed: string): Promise<string> {
const context = `
{
eval: function (c) {
packed = c;
},
window: {},
document: {}
}
`;
const toExecute = `
function() {
let packed = "";
with(${context}) {
${packed}
};
return packed;
}
`;
const res = (await runInPageContext(toExecute)) as string;
return res
.replace(/;/g, ';\n')
.replace(/{/g, '\n{\n')
.replace(/}/g, '\n}\n')
.replace(/\n;\n/g, ';\n')
.replace(/\n\\n/g, '\n');
}
// Adapted from: https://github.com/arikw/extension-page-context
async function runInPageContext<T>(toExecute: string): Promise<T | null> {
// test that we are running with the allow-scripts permission
try {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
window.sessionStorage;
} catch {
return null;
}
// returned value container
const resultMessageId = crypto.randomUUID();
// prepare script container
const scriptElm = document.createElement('script');
scriptElm.setAttribute('type', 'application/javascript');
// inject the script
scriptElm.textContent = `
(
async function () {
const response = {
id: '${resultMessageId}'
};
try {
response.result = JSON.stringify(await (${toExecute})() ); // run script
} catch(err) {
response.error = JSON.stringify(err);
}
window.postMessage(response, '*');
}
)();
`;
// run the script
document.documentElement.appendChild(scriptElm);
// clean up script element
scriptElm.remove();
let resolve: (value: T) => void;
let reject: (value: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
function onResult(event: MessageEvent) {
if (event.data.id === resultMessageId) {
window.removeEventListener('message', onResult);
if (event.data.error !== undefined) {
return reject(JSON.parse(event.data.error));
}
return resolve(event.data.result !== undefined ? JSON.parse(event.data.result) : undefined);
}
}
window.addEventListener('message', onResult);
return await promise;
}

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

View File

@@ -1,107 +0,0 @@
const fs = require('fs')
const path = require('path')
const rollup = require('rollup')
const rollupPluginCommonJs = require('@rollup/plugin-commonjs')
const rollupPluginNodeResolve = require('@rollup/plugin-node-resolve').default
const rollupPluginReplace = require('@rollup/plugin-replace')
const rollupPluginTypescript = require('@rollup/plugin-typescript')
const sass = require('node-sass')
const sassPluginNodeImport = require('node-sass-package-importer')
const typescript = require('typescript')
async function buildManifest() {
const manifest = JSON.parse(fs.readFileSync('src/manifest.json'))
manifest['version'] = process.env.npm_package_version
fs.writeFileSync('src/manifest.json', JSON.stringify(manifest, null, 2))
}
async function buildMisc() {
const files = {
'src/manifest.json': 'build/manifest.json',
'src/icons/logo_48.png': 'build/icons/logo_48.png',
'src/icons/logo_128.png': 'build/icons/logo_128.png',
'src/icons/disabled_48.png': 'build/icons/disabled_48.png',
'src/icons/disabled_128.png': 'build/icons/disabled_128.png',
}
for (const [src, dst] of Object.entries(files)) {
fs.mkdirSync(path.dirname(dst), {recursive: true})
fs.copyFileSync(src, dst)
}
}
async function buildHtml() {
const files = {
'src/ui/popup/popup.html': 'build/ui/popup/popup.html',
'src/ui/player/player.html': 'build/ui/player/player.html'
}
for (const [src, dst] of Object.entries(files)) {
fs.mkdirSync(path.dirname(dst), {recursive: true})
fs.copyFileSync(src, dst)
}
}
async function buildCss() {
const files = {
'src/ui/popup/popup.sass': 'build/ui/popup/popup.css',
'src/ui/player/player.sass': 'build/ui/player/player.css'
}
for (const [src, dst] of Object.entries(files)) {
const compiled = sass.renderSync({
file: src,
importer: sassPluginNodeImport()
})
fs.mkdirSync(path.dirname(dst), {recursive: true})
fs.writeFileSync(dst, compiled.css)
}
}
async function buildJs() {
const files = {
'src/ui/popup/popup.ts': 'build/ui/popup/popup.js',
'src/ui/player/player.ts': 'build/ui/player/player.js',
'src/index.ts': 'build/index.js',
'src/background.ts': 'build/background.js'
}
for (const [src, dst] of Object.entries(files)) {
const bundle = await rollup.rollup({
input: src,
plugins: [
rollupPluginNodeResolve({
browser: true
}),
rollupPluginReplace({
'process.env.NODE_ENV': JSON.stringify('production')
}),
rollupPluginTypescript({
typescript,
tsconfig: 'src/tsconfig.json'
}),
rollupPluginCommonJs({
extensions: ['.js', '.ts']
})
]
})
await bundle.write({
file: dst,
strict: true
})
}
}
async function build() {
await buildManifest()
await buildMisc()
await buildHtml()
await buildCss()
await buildJs()
}
build()

6
tsconfig.json Normal file
View File

@@ -0,0 +1,6 @@
{
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"useDefineForClassFields": true
}
}

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