40 Commits

Author SHA1 Message Date
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
f6c6102436 Merge pull request #24 from muleyo/main
Added Voe.sx Domain
2025-03-13 16:08:39 +01:00
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
24 changed files with 5762 additions and 5323 deletions

View File

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

View File

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

View File

@ -9,12 +9,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install nodejs - name: Install nodejs
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 22
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
@ -32,12 +32,12 @@ jobs:
- manifest_version: 3 - manifest_version: 3
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install nodejs - name: Install nodejs
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 22
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
@ -49,7 +49,7 @@ jobs:
run: npm run build run: npm run build
- name: Upload - name: Upload
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: stream-bypass-mv${{ matrix.manifest_version }} name: stream-bypass-mv${{ matrix.manifest_version }}
path: ./dist path: ./dist

101
README.md
View File

@ -3,21 +3,18 @@
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>
<a href="https://discord.gg/gUWwekeNNg">
<img src="https://img.shields.io/discord/915659846836162561?label=Discord&style=flat-square" alt="Discord">
</a>
</p> </p>
<p align="center"> <p align="center">
@ -25,7 +22,9 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<a href="#-installation">Installation 📥</a> <a href="#-installation">Installation 📥</a>
<a href="#-supported-sites">Supported Sites 📜</a> <a href="#-features">Features </a>
<a href="#-supported-websites">Supported Websites 📜</a>
<a href="#%EF%B8%8F-building">Building 🛠️</a> <a href="#%EF%B8%8F-building">Building 🛠️</a>
@ -51,7 +50,8 @@ Additionally, this enables you to download the video by right-clicking it and ju
The best way to install the extension are the official browser extension stores: The best way to install the extension are the official browser extension stores:
- [Firefox Addon Store](https://addons.mozilla.org/de/firefox/addon/stream-bypass/) - [Firefox Addon Store](https://addons.mozilla.org/de/firefox/addon/stream-bypass/) (Firefox for Android is supported too!)
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
### Manual installation ### Manual installation
@ -61,49 +61,52 @@ The best way to install the extension are the official browser extension stores:
- Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory - Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory
- Chromium / Google Chrome - Chromium / Google Chrome
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them > As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
- Download `enhance-crunchyroll-<version>-mv3.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that) - Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that)
- Go into your browser and type `chrome://extensions` in the address bar - Go into your browser and type `chrome://extensions` in the address bar
- Turn on the developer mode by checking the switch in the top right corner - Turn on the developer mode by checking the switch in the top right corner
- Click `Load unpacked` and choose the unzipped directory - Click `Load unpacked` and choose the unzipped directory
## 📜 Supported sites ## ✨ Features
| Site | Supported | Note | | Feature | Firefox | Chrome | Firefox for Android |
| --------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------ | | --------------------------------------------------------------------------------------------------------------------------------- | ------- | ------ | ------------------- |
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔️ | | | Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
| [filemoon.sx](https://filemoon.sx) | ✔ | | | Support websites that are accessed via a redirect | ✔ | ❌ | ✔ |
| [mcloud.to](https://mcloud.to/) | ❌ | Reverse engineering the site costs too much time ([#5](https://github.com/ByteDream/stream-bypass/issues/5)) | | Open video in mpv (with [ff2mpv](https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ❌ |
| [mixdrop.co](https://mixdrop.co) | ✔ | |
| [mp4upload.com](https://mp4upload.com) | ✔ | | - ✔️: Supported.
| [newgrounds.com](https://newgrounds.com) | ✔ | | - ❌: Not supported.
| [streamtape.com](https://streamtape.com) | ✔ | |
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | | ## 📜 Supported websites
| [upstream.to](https://upstream.to) | ✔ | |
| [videovard.sx](https://videovard.sx) | ❌ | Reverse engineering the site costs too much time | | Site | Firefox & Firefox for Android | Chrome & Chromium based |
| [vidoza.net](https://vidoza.net) | ✔ | | | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| [vidstream.pro](https://vidstream.pro) | ❌ | Reverse engineering the site costs too much time ([#5](https://github.com/ByteDream/stream-bypass/issues/5)) | | [dropload.io](https://dropload.io) | ✔ | ✔ |
| [voe.sx](https://voe.sx) | ✔ | | | [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
| [vupload.com](https://vupload.com) | ✔ | | | [filemoon.to](https://filemoon.to) | ✔ | ✔ |
| [kwik.cx](https://kwik.cx) | ✔ | | | [goodstream.uno](https://goodstream.uno) | ✔ | ✔ |
| [dropload.io](https://dropload.io) | ✔ | | | [mixdrop.co](https://mixdrop.co) | ✔ | ✔ |
| [supervideo.tv](https://supervideo.tv) | ✔ | | | [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
| [goodstream.uno](https://goodstream.uno) | ✔ | | | [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) | ✔ | ✔ |
| [kwik.cx](https://kwik.cx) | ✔ | ✔ |
- ✔️: Everything ok. - ✔️: Everything ok.
- ⚠: Included in the addon but will probably not work. See `Note` in this case, an explanation why will stand there in the most cases. - ⚠: Works with limitations.
- ❌: Not included / supported by the addon. This can have various reasons. See `Note` for an explanation. - ❌: Not supported.
_This table might not be 100% accurate, it isn't actively monitored if the addon works for every website!_
Some sites put much effort in obfuscating their code / how they receive the video stream so that it simply cost too much time for me to reverse engineer it and find out how to bypass the native video player of the site. Some sites put much effort in obfuscating their code / how they receive the video stream so that it simply cost too much time for me to reverse engineer it and find out how to bypass the native video player of the site.
<details>
<summary>Hall of dead sites</summary>
<ul>
<li><a href="https://evoload.io">evoload.io</a> - Down</li>
<li><a href="https://vidlox.me">vidlox.me</a> - Reachable but empty</li>
<li><a href="https://vivo.sx">vivo.sx</a> - Down</li>
</ul>
</details>
## 🛠️ Building ## 🛠️ Building
If you want to build the addon from source and not using the [installation](#installation) way, follow the instructions. If you want to build the addon from source and not using the [installation](#installation) way, follow the instructions.
@ -119,16 +122,16 @@ If the requirements are satisfied, you can continue with the following commands:
# install all dependencies # install all dependencies
$ npm install $ npm install
# build the extension source to a build/ directory # build the extension source to the dist/ directory
$ npm run build $ npm run build
# same as build + create a bundle zipfile at dist/ # same as build + more optimizations and browser specific settings at release/
$ npm run bundle $ npm run release:firefox # or "release:chrome" to create a release for chromium based browsers
``` ```
##### 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 [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.
@ -158,11 +161,11 @@ Steps to get it set up:
- Chrome/Chromium: - Chrome/Chromium:
- Go To: Settings -> Extensions - Go To: Settings -> Extensions
- Click on `Details` of the Stream Bypass extension and copy the ID - Click on `Details` of the Stream Bypass extension and copy the ID
- Add `chrome-extension://your-id-here/` to `allowed_origins` -> - Add `chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins` ->
``` ```
"allowed_origins": [ "allowed_origins": [
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/", "chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
"chrome-extension://your-id-her/" "chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/"
] ]
``` ```

40
eslint.config.mjs Normal file
View File

@ -0,0 +1,40 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import ts from 'typescript-eslint';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'no-undef': 'off'
}
},
{
ignores: [
'.DS_Store',
'node_modules',
'dist',
'release',
'.idea',
'.env',
'.env.*',
'!.env.example',
'package-lock.json'
]
}
);

10043
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{ {
"name": "stream-bypass", "name": "stream-bypass",
"version": "3.0.0", "version": "3.1.2",
"displayName": "Stream Bypass", "displayName": "Stream Bypass",
"author": "ByteDream", "author": "bytedream",
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video", "description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
@ -19,34 +19,32 @@
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/ByteDream/stream-bypass.git" "url": "git+https://github.com/bytedream/stream-bypass.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/ByteDream/stream-bypass/issues" "url": "https://github.com/bytedream/stream-bypass/issues"
}, },
"devDependencies": { "devDependencies": {
"@samrum/vite-plugin-web-extension": "^5.0.0", "@samrum/vite-plugin-web-extension": "^5.1.1",
"@sveltejs/vite-plugin-svelte": "^2.1.1", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^4.0.1", "@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.0.228", "@types/chrome": "^0.0.309",
"@types/webextension-polyfill": "^0.10.0", "@types/firefox-webext-browser": "^120.0.4",
"@typescript-eslint/eslint-plugin": "^6.10.0", "eslint": "^9.22.0",
"@typescript-eslint/parser": "^6.10.0", "eslint-config-prettier": "^10.1.1",
"eslint": "^8.53.0", "eslint-plugin-svelte": "^3.1.0",
"eslint-config-prettier": "^9.0.0", "hls.js": "^1.5.20",
"eslint-plugin-svelte": "^2.35.0", "prettier": "^3.5.3",
"hls.js": "^1.4.12", "prettier-plugin-svelte": "^3.3.3",
"prettier": "^3.0.3", "sass": "^1.85.1",
"prettier-plugin-svelte": "^3.1.0", "svelte": "^5.23.0",
"sass": "^1.69.5", "svelte-check": "^4.1.5",
"svelte": "^3.58.0", "svelte-preprocess": "^6.0.3",
"svelte-check": "^3.2.0", "tslib": "^2.8.1",
"svelte-preprocess": "^5.0.3", "typescript": "^5.8.2",
"tslib": "^2.5.0", "typescript-eslint": "^8.26.1",
"typescript": "^5.0.4", "vite": "^6.2.2",
"vite": "~4.3.3", "web-ext": "^8.4.0"
"web-ext": "^7.6.2",
"webextension-polyfill": "^0.10.0"
}, },
"type": "module" "type": "module"
} }

View File

@ -1,24 +0,0 @@
import type { Match } from '~/lib/match';
import { storageDelete, storageGet, storageSet } from '~/lib/settings';
import { getMatch } from '~/lib/match';
chrome.runtime.onMessage.addListener(async (message) => {
if (message.action == 'ff2mpv') {
await chrome.runtime.sendNativeMessage('ff2mpv', { url: message.url });
}
});
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).hostname)) !== undefined) {
await storageSet('redirect', match.id);
}
} else {
await storageDelete('redirect');
}
},
{ urls: ['<all_urls>'], types: ['main_frame', 'sub_frame'] }
);

View File

@ -0,0 +1,40 @@
import './shared';
import type { Match } from '~/lib/match';
import { Redirect, UrlReferer } from '~/lib/settings';
import { getMatch } from '~/lib/match';
chrome.webRequest.onBeforeSendHeaders.addListener(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async (details) => {
const referer = await UrlReferer.get(new URL(details.url).hostname);
if (!referer) return;
await UrlReferer.delete(new URL(details.url).hostname);
details.requestHeaders!.push({
name: 'Referer',
value: `https://${referer}/`
});
return { requestHeaders: details.requestHeaders };
},
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] },
['blocking', 'requestHeaders']
);
chrome.webRequest.onBeforeRedirect.addListener(
async (details) => {
// check if redirects origins from a previous redirect
if ((await Redirect.get()) == null) {
let match: Match | null;
if ((match = await getMatch(new URL(details.url).hostname)) !== null) {
await Redirect.set(match);
}
} else {
await Redirect.delete();
}
},
{ urls: ['<all_urls>'], types: ['main_frame', 'sub_frame'] }
);

View File

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

View File

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

View File

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

View File

@ -2,7 +2,8 @@
import { play } from '~/entries/player/player'; import { play } from '~/entries/player/player';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let errorMessage: string | null = null; let errorMessage: string | null = $state(null);
let videoElem: HTMLVideoElement; let videoElem: HTMLVideoElement;
onMount(async () => { onMount(async () => {
@ -10,13 +11,13 @@
await play(videoElem); await play(videoElem);
videoElem.controls = true; videoElem.controls = true;
} catch (e) { } catch (e) {
errorMessage = e; errorMessage = e as string;
} }
}); });
</script> </script>
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y_media_has_caption -->
<video id="video" bind:this={videoElem} /> <video id="video" bind:this={videoElem}></video>
{#if errorMessage} {#if errorMessage}
<div id="message-container"> <div id="message-container">
<p> <p>

View File

@ -7,8 +7,9 @@
<body> <body>
<script type="module"> <script type="module">
import Player from '~/entries/player/Player.svelte'; import Player from '~/entries/player/Player.svelte';
import { mount } from 'svelte';
new Player({ mount(Player, {
target: document.body target: document.body
}); });
</script> </script>

View File

@ -1,16 +1,23 @@
import { matches } from '~/lib/match'; import { matches } from '~/lib/match';
import Hls from 'hls.js'; import Hls from 'hls.js';
import { UrlReferer } from '~/lib/settings';
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
await UrlReferer.set(new URL(url).hostname, domain);
async function playNative(url: string, videoElem: HTMLVideoElement) {
videoElem.src = url; videoElem.src = url;
} }
async function playHls(url: string, videoElem: HTMLVideoElement) { async function playHls(url: string, domain: string, videoElem: HTMLVideoElement) {
if (videoElem.canPlayType('application/vnd.apple.mpegurl')) { if (videoElem.canPlayType('application/vnd.apple.mpegurl')) {
videoElem.src = url; videoElem.src = url;
} else if (Hls.isSupported()) { } else if (Hls.isSupported()) {
const hls = new Hls({ const hls = new Hls({
enableWorker: false enableWorker: false,
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
await UrlReferer.set(new URL(url).hostname, domain);
xhr.open('GET', url);
}
}); });
hls.loadSource(url); hls.loadSource(url);
hls.attachMedia(videoElem); hls.attachMedia(videoElem);
@ -21,9 +28,9 @@ async function playHls(url: string, videoElem: HTMLVideoElement) {
export async function play(videoElem: HTMLVideoElement) { export async function play(videoElem: HTMLVideoElement) {
const urlQuery = new URLSearchParams(window.location.search); const urlQuery = new URLSearchParams(window.location.search);
const id = urlQuery.get('id'); const id = urlQuery.get('id') as string;
const url = decodeURIComponent(urlQuery.get('url')); const url = decodeURIComponent(urlQuery.get('url') as string);
const domain = urlQuery.get('domain'); const domain = urlQuery.get('domain') as string;
const match = matches[id]; const match = matches[id];
if (match === undefined) { if (match === undefined) {
@ -32,8 +39,8 @@ export async function play(videoElem: HTMLVideoElement) {
document.title = `Stream Bypass (${domain})`; document.title = `Stream Bypass (${domain})`;
if (new URL(url).pathname.endsWith('.m3u8')) { if (new URL(url).pathname.endsWith('.m3u8')) {
await playHls(url, videoElem); await playHls(url, domain, videoElem);
} else { } else {
await playNative(url, videoElem); await playNative(url, domain, videoElem);
} }
} }

View File

@ -1,123 +1,97 @@
<script lang="ts"> <script lang="ts">
import { matches, Reliability } from '~/lib/match'; import { type Match, matches } from '~/lib/match';
import { Hosters, Other } from '~/lib/settings'; import { Hosters, Other } from '~/lib/settings';
import Toggle from './toggle.svelte';
let hostersEnabled: boolean; let hostersEnabled: boolean;
let hosters = []; let hosters: (Match & { active: boolean; disabled: boolean })[] = [];
(async () => { (async () => {
hostersEnabled = !(await Hosters.getAllDisabled()); hostersEnabled = !(await Hosters.getAllDisabled());
const disabled = await Hosters.getDisabled(); const disabled = await Hosters.getDisabled();
hosters = Object.values(matches).map((m) => { hosters = Object.values(matches).map((m: any) => {
m['active'] = disabled.findIndex((p) => p.id == m.id) == -1; m['active'] = disabled.findIndex((p) => p.id == m.id) == -1;
m['disabled'] = !hostersEnabled;
return m; return m;
}); }) as typeof hosters;
})();
let isMobile: boolean;
(async () => {
isMobile = (await browser.runtime.getPlatformInfo()).os === 'android';
})(); })();
let ff2mpvEnabled: boolean; let ff2mpvEnabled: boolean;
(async () => { (async () => {
ff2mpvEnabled = await Other.getFf2mpv(); ff2mpvEnabled = (await Other.getFf2mpv()) as boolean;
})(); })();
</script> </script>
<main> <main
<div> style={isMobile
<h3 class="header">Hosters</h3> ? 'height: 100vh; display: flex; flex-direction: column; align-items: center'
<div class="buttons super-buttons"> : 'height: 500px'}
<button >
class:active={hostersEnabled} <fieldset>
on:click={async () => { <legend>Hoster</legend>
await Hosters.enableAll(); <div class="setting-container" style={isMobile ? 'grid-column-gap: 5rem' : ''}>
hostersEnabled = true; <label for="hosters-enabled">Enabled</label>
hosters = hosters.map((m) => { <div>
m['disabled'] = false; <Toggle
return m; bind:checked={hostersEnabled}
}); id="hosters-enabled"
}}>On</button onChange={() => Hosters.setAll(hostersEnabled)}
> />
<button </div>
class:active={!hostersEnabled} <hr />
on:click={async () => { {#each hosters as hoster, i (hoster.id)}
await Hosters.disableAll(); <label for="hoster-{i}" style="cursor: {hostersEnabled ? 'pointer' : 'default'}"
hostersEnabled = false; >{hoster.name}</label
hosters = hosters.map((m) => { >
m['disabled'] = true; <div>
return m; <Toggle
}); bind:checked={hoster.active}
}}>Off</button disabled={!hostersEnabled}
> id="hoster-{i}"
</div> onChange={async () => {
<table class="setting-table"> if (hoster.active) {
{#each hosters as hoster}
<tr>
<td class="setting-name">
<p
class:reliability-low={hoster.reliability === Reliability.LOW}
class:reliability-normal={hoster.reliability === Reliability.NORMAL}
class:reliability-high={hoster.reliability === Reliability.HIGH}
>
{hoster.name}
</p>
</td>
<td class="buttons">
<button
class:disabled={hoster.disabled}
class:active={hoster.active}
on:click={async () => {
if (hoster.disabled) return;
await Hosters.enable(hoster); await Hosters.enable(hoster);
hoster.active = true; } else {
}}>On</button
>
<button
class:disabled={hoster.disabled}
class:active={!hoster.active}
on:click={async () => {
if (hoster.disabled) return;
await Hosters.disable(hoster); await Hosters.disable(hoster);
hoster.active = false; }
}}>Off</button }}
> ></Toggle>
</td> </div>
</tr>
{/each} {/each}
</table> </div>
</div> </fieldset>
<hr /> {#if !isMobile}
<div> <fieldset>
<h3 class="header">Other</h3> <legend>Other</legend>
<table> <div class="setting-container">
<tr> <label for="ff2mpv">ff2mpv</label>
<td class="setting-name"> <div>
<p>ff2mpv</p> <Toggle
</td> bind:checked={ff2mpvEnabled}
<td class="buttons"> id="ff2mpv"
<button onChange={async () => {
class:active={ff2mpvEnabled} ff2mpvEnabled = !ff2mpvEnabled;
on:click={async () => { if (await browser.permissions.request({ permissions: ['nativeMessaging'] })) {
await Other.setFf2mpv(true); await Other.setFf2mpv(ff2mpvEnabled);
ff2mpvEnabled = true; ff2mpvEnabled = !ff2mpvEnabled;
}}>On</button }
> }}
<button ></Toggle>
class:active={!ff2mpvEnabled}
on:click={async () => {
await Other.setFf2mpv(false);
ff2mpvEnabled = false;
}}>Off</button
>
<a <a
class="info-questionmark"
href="https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams" href="https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams"
>🛈</a >?</a
> >
</td> </div>
</tr> </div>
</table> </fieldset>
</div> {/if}
<hr /> <a id="report-notice" href="https://github.com/ByteDream/stream-bypass/issues"
<a id="bug-notice" href="https://github.com/ByteDream/stream-bypass/issues" >Report issues or requests</a
>Something does not work</a
> >
</main> </main>
@ -127,11 +101,16 @@
background-color: #2b2a33; background-color: #2b2a33;
color: white; color: white;
font-weight: bold; font-weight: bold;
overflow-x: hidden; margin: 0;
overflow-y: auto; padding: 0 8px;
} }
#bug-notice { fieldset {
border-radius: 5px;
border-color: gray;
}
#report-notice {
border: none; border: none;
color: white; color: white;
display: block; display: block;
@ -154,65 +133,33 @@
text-align: center; text-align: center;
} }
.setting-table { .setting-container {
border-collapse: collapse; display: grid;
border-spacing: 0; grid-template-columns: auto auto;
} grid-column-gap: 5px;
grid-row-gap: 4px;
align-items: end;
width: 100%;
.setting-name { & > label {
height: 34px; height: 34px;
p {
margin: 0; margin: 0;
cursor: default; user-select: none;
}
}
.buttons {
display: flex;
flex-direction: row;
height: 34px;
button,
a {
border: 1px solid #281515;
background-color: transparent;
color: white;
cursor: pointer; cursor: pointer;
padding: 5px 8px;
margin: 0;
text-decoration: none;
&.active {
background-color: rgba(255, 65, 65, 0.74);
cursor: default;
}
&.disabled {
background-color: gray;
cursor: not-allowed;
}
}
&.super-buttons {
display: flex; display: flex;
justify-content: center; align-items: center;
gap: 4px; }
& > hr {
grid-column: 1 / span 2;
width: 100%; width: 100%;
margin-bottom: 10px;
} }
} }
.reliability-low { .info-questionmark {
text-decoration: underline; display: inline-block;
text-decoration-color: red; transform: translateX(-40%) translateY(-100%);
} color: black;
.reliability-normal { text-decoration: none;
text-decoration: underline;
text-decoration-color: yellow;
}
.reliability-high {
text-decoration: underline;
text-decoration-color: #00ff00;
} }
</style> </style>

View File

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

View File

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

View File

@ -0,0 +1,68 @@
<!-- https://flowbite.com/docs/forms/toggle/ -->
<script lang="ts">
import type { Snippet } from 'svelte';
type Props = {
checked?: boolean;
disabled?: boolean;
id?: string | null;
onChange?: () => void;
children?: Snippet;
};
let { checked = $bindable(), disabled, id = null, onChange, children }: Props = $props();
</script>
<label class="toggle">
{@render children?.()}
<input type="checkbox" {id} bind:checked {disabled} onchange={onChange} />
<span></span>
</label>
<style lang="scss" global>
.toggle {
display: inline-flex;
align-items: center;
cursor: pointer;
input {
clip: rect(0, 0, 0, 0);
position: absolute;
&:checked + span {
background: limegreen;
&:after {
transform: translateX(100%);
}
}
&:disabled + span {
background: gray;
}
}
span {
position: relative;
width: 2.75rem;
height: 1.5rem;
background: #cf0000;
border-radius: 9999px;
&:after {
content: '';
position: absolute;
top: 2px;
inset-inline-start: 2px;
background: white;
border-radius: 9999px;
height: 1.25rem;
width: 1.25rem;
transition: all 0.15s;
}
}
&:has(input:disabled) {
cursor: default;
}
}
</style>

View File

@ -1,28 +1,20 @@
import { unpack } from '~/lib/utils'; import { unpack } from './utils';
import { Hosters } from '~/lib/settings'; import { Hosters, Redirect, TmpHost } from './settings';
export enum Reliability {
HIGH,
NORMAL,
LOW
}
export interface Match { export interface Match {
name: string; name: string;
id: string; id: string;
reliability: Reliability;
domains: string[]; domains: string[];
replace?: boolean; replace?: boolean;
regex: RegExp; regex: RegExp[];
notice?: string; notice?: string;
match(match: RegExpMatchArray): Promise<string>; match(match: RegExpMatchArray): Promise<string | null>;
} }
export const Doodstream: Match = { export const Doodstream: Match = {
name: 'Doodstream', name: 'Doodstream',
id: 'doodstream', id: 'doodstream',
reliability: Reliability.NORMAL,
domains: [ domains: [
'doodstream.com', 'doodstream.com',
'dood.pm', 'dood.pm',
@ -31,16 +23,20 @@ export const Doodstream: Match = {
'dood.cx', 'dood.cx',
'dood.sh', 'dood.sh',
'dood.watch', 'dood.watch',
'dood.work',
'dood.to', 'dood.to',
'dood.so', 'dood.so',
'dood.la', 'dood.la',
'dood.li',
'dood.re', 'dood.re',
'dood.yt', 'dood.yt',
'doods.pro',
'ds2play.com', 'ds2play.com',
'dooood.com' 'dooood.com',
'd000d.com'
], ],
replace: true, replace: true,
regex: /(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s, regex: [/(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
const response = await fetch(`https://${window.location.host}${match[1]}`, { const response = await fetch(`https://${window.location.host}${match[1]}`, {
@ -58,35 +54,40 @@ export const Doodstream: Match = {
export const DropLoad: Match = { export const DropLoad: Match = {
name: 'Dropload', name: 'Dropload',
id: 'dropload', id: 'dropload',
reliability: Reliability.HIGH,
domains: ['dropload.ui'], domains: ['dropload.ui'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms, regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)[0]; return unpacked.match(/(?<=file:").*(?=")/)![0];
} }
}; };
export const Filemoon: Match = { export const Filemoon: Match = {
name: 'Filemoon', name: 'Filemoon',
id: 'filemoon', id: 'filemoon',
reliability: Reliability.HIGH, domains: ['filemoon.sx', 'filemoon.to', 'filemoon.in'],
domains: ['filemoon.sx', 'filemoon.in'], regex: [/(?<=<iframe\s*src=")\S*(?=")/s, /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms, replace: true,
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
if (window.location.host.startsWith('filemoon')) {
await TmpHost.set(new URL(match[0]).host, Filemoon);
return null;
}
await TmpHost.delete();
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)[0]; return unpacked.match(/(?<=file:")\S*(?=")/)![0];
} }
}; };
export const GoodStream: Match = { export const GoodStream: Match = {
name: 'Goodstream', name: 'Goodstream',
id: 'goodstream', id: 'goodstream',
reliability: Reliability.NORMAL,
domains: ['goodstream.uno'], domains: ['goodstream.uno'],
regex: /(?<=file:\s+").*(?=")/g, regex: [/(?<=file:\s+").*(?=")/g],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
return match[0]; return match[0];
@ -96,26 +97,51 @@ export const GoodStream: Match = {
export const Kwik: Match = { export const Kwik: Match = {
name: 'Kwik', name: 'Kwik',
id: 'kwik', id: 'kwik',
reliability: Reliability.HIGH,
domains: ['kwik.cx'], domains: ['kwik.cx'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms, regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=source=').*(?=')/)[0]; return unpacked.match(/(?<=source=').*(?=')/)![0];
}
};
export const Luluvdo: Match = {
name: 'Luluvdo',
id: 'luluvdo',
domains: ['luluvdo.com'],
regex: [/./gm],
match: async () => {
const post_match = window.location.href.match(/(?<=\/embed\/)\S*(\/.*)?/)!;
const request_body = new FormData();
request_body.set('op', 'embed');
request_body.set('file_code', post_match[0]);
const response = await fetch(`https://${window.location.host}/dl`, {
method: 'POST',
body: request_body,
referrer: window.location.href
});
const eval_match = (await response.text()).match(
/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms
)!;
const unpacked = await unpack(eval_match[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
} }
}; };
export const Mixdrop: Match = { export const Mixdrop: Match = {
name: 'Mixdrop', name: 'Mixdrop',
id: 'mixdrop', id: 'mixdrop',
reliability: Reliability.HIGH,
domains: ['mixdrop.co', 'mixdrop.to', 'mixdrop.ch', 'mixdrop.bz', 'mixdrop.gl'], domains: ['mixdrop.co', 'mixdrop.to', 'mixdrop.ch', 'mixdrop.bz', 'mixdrop.gl'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms, regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
const url = unpacked.match(/(?<=MDCore.wurl=").*(?=")/)[0]; const url = unpacked.match(/(?<=MDCore.wurl=").*(?=")/)![0];
return `https:${url}`; return `https:${url}`;
} }
}; };
@ -123,23 +149,21 @@ export const Mixdrop: Match = {
export const Mp4Upload: Match = { export const Mp4Upload: Match = {
name: 'Mp4Upload', name: 'Mp4Upload',
id: 'mp4upload', id: 'mp4upload',
reliability: Reliability.HIGH,
domains: ['mp4upload.com'], domains: ['mp4upload.com'],
replace: true, replace: true,
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms, regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=player.src\(").*(?=")/)[0]; return unpacked.match(/(?<=player.src\(").*(?=")/)![0];
} }
}; };
export const Newgrounds: Match = { export const Newgrounds: Match = {
name: 'Newgrounds', name: 'Newgrounds',
id: 'newgrounds', id: 'newgrounds',
reliability: Reliability.HIGH,
domains: ['newgrounds.com'], domains: ['newgrounds.com'],
regex: /.*/gm, regex: [/.*/gm],
match: async () => { match: async () => {
const id = window.location.pathname.split('/').slice(-1)[0]; const id = window.location.pathname.split('/').slice(-1)[0];
@ -153,12 +177,26 @@ export const Newgrounds: Match = {
} }
}; };
export const StreamA2z: Match = {
name: 'Stream2Az',
id: 'stream2az',
domains: ['streama2z.com', 'streama2z.xyz'],
regex: [/https?:\/\/\S*m3u8.+(?=['"])/gm],
match: async (match: RegExpMatchArray) => {
if (StreamA2z.domains.indexOf(window.location.hostname) !== -1) {
await Redirect.set(StreamA2z);
return null;
}
return match[0];
}
};
export const Streamtape: Match = { export const Streamtape: Match = {
name: 'Streamtape', name: 'Streamtape',
id: 'streamtape', id: 'streamtape',
reliability: Reliability.HIGH,
domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'], domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'],
regex: /id=.*(?=')/gm, regex: [/id=.*(?=')/gm],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
let i = 0; let i = 0;
@ -176,9 +214,8 @@ export const Streamtape: Match = {
export const Streamzz: Match = { export const Streamzz: Match = {
name: 'Streamzz', name: 'Streamzz',
id: 'streamzz', id: 'streamzz',
reliability: Reliability.LOW,
domains: ['streamzz.to', 'streamz.ws'], domains: ['streamzz.to', 'streamz.ws'],
regex: /(?<=\|)\w{2,}/gm, regex: [/(?<=\|)\w{2,}/gm],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
return `https://get.${location.hostname.split('.')[0]}.tw/getlink-${ return `https://get.${location.hostname.split('.')[0]}.tw/getlink-${
@ -190,35 +227,45 @@ export const Streamzz: Match = {
export const SuperVideo: Match = { export const SuperVideo: Match = {
name: 'Supervideo', name: 'Supervideo',
id: 'supervideo', id: 'supervideo',
reliability: Reliability.HIGH,
domains: ['supervideo.tv'], domains: ['supervideo.tv'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms, regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)[0]; return unpacked.match(/(?<=file:").*(?=")/)![0];
} }
}; };
export const Upstream: Match = { export const Upstream: Match = {
name: 'Upstream', name: 'Upstream',
id: 'upstream', id: 'upstream',
reliability: Reliability.HIGH,
domains: ['upstream.to'], domains: ['upstream.to'],
regex: /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms, regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:").*(?=")/)[0]; return unpacked.match(/(?<=file:").*(?=")/)![0];
}
};
export const Vidmoly: Match = {
name: 'Vidmoly',
id: 'vidmoly',
domains: ['vidmoly.me', 'vidmoly.to'],
regex: [/(?<=file:").+\.m3u8/gm],
replace: true,
match: async (match: RegExpMatchArray) => {
return match[0];
} }
}; };
export const Vidoza: Match = { export const Vidoza: Match = {
name: 'Vidoza', name: 'Vidoza',
id: 'vidoza', id: 'vidoza',
reliability: Reliability.HIGH, domains: ['vidoza.net', 'videzz.net'],
domains: ['vidoza.net'], regex: [/(?<=src:\s?").+?(?=")/gm],
regex: /(?<=src:\s?").+?(?=")/gm, replace: true,
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
return match[0]; return match[0];
@ -228,21 +275,19 @@ export const Vidoza: Match = {
export const Voe: Match = { export const Voe: Match = {
name: 'Voe', name: 'Voe',
id: 'voe', id: 'voe',
reliability: Reliability.HIGH, domains: ['voe.sx', 'maxfinishseveral.com'],
domains: ['voe.sx'], regex: [/(?<='hls':\s*')\S*(?=')/gm],
regex: /https?:\/\/\S*m3u8.+(?=')/gm,
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
return match[0]; return atob(match[0]);
} }
}; };
export const Vupload: Match = { export const Vupload: Match = {
name: 'Vupload', name: 'Vupload',
id: 'vupload', id: 'vupload',
reliability: Reliability.HIGH,
domains: ['vupload.com'], domains: ['vupload.com'],
regex: /(?<=src:\s?").+?(?=")/gm, regex: [/(?<=src:\s?").+?(?=")/gm],
match: async (match: RegExpMatchArray) => { match: async (match: RegExpMatchArray) => {
return match[0]; return match[0];
@ -255,13 +300,16 @@ export const matches = {
[Filemoon.id]: Filemoon, [Filemoon.id]: Filemoon,
[GoodStream.id]: GoodStream, [GoodStream.id]: GoodStream,
[Kwik.id]: Kwik, [Kwik.id]: Kwik,
[Luluvdo.id]: Luluvdo,
[Mixdrop.id]: Mixdrop, [Mixdrop.id]: Mixdrop,
[Mp4Upload.id]: Mp4Upload, [Mp4Upload.id]: Mp4Upload,
[Newgrounds.id]: Newgrounds, [Newgrounds.id]: Newgrounds,
[StreamA2z.id]: StreamA2z,
[Streamtape.id]: Streamtape, [Streamtape.id]: Streamtape,
[Streamzz.id]: Streamzz, [Streamzz.id]: Streamzz,
[SuperVideo.id]: SuperVideo, [SuperVideo.id]: SuperVideo,
[Upstream.id]: Upstream, [Upstream.id]: Upstream,
[Vidmoly.id]: Vidmoly,
[Vidoza.id]: Vidoza, [Vidoza.id]: Vidoza,
[Voe.id]: Voe, [Voe.id]: Voe,
[Vupload.id]: Vupload [Vupload.id]: Vupload
@ -281,5 +329,10 @@ export async function getMatch(domain: string): Promise<Match | null> {
} }
} }
const tmpHost = await TmpHost.get();
if (tmpHost && tmpHost[0] === domain) {
return tmpHost[1];
}
return null; return null;
} }

View File

@ -1,14 +1,13 @@
import browser from 'webextension-polyfill'; import type { Match } from './match';
import type { Match } from '~/lib/match'; import { matches } from './match';
import { matches } from '~/lib/match';
export const Hosters = { export const Hosters = {
getDisabled: async () => { getDisabled: async () => {
const disabled = await storageGet<string[]>('hosters.disabled', []); const disabled = (await storageGet('hosters.disabled', [])) as string[];
return disabled.map((id) => matches[id]).filter((m) => m !== undefined); return disabled.map((id) => matches[id]).filter((m) => m !== undefined);
}, },
disable: async (match: Match) => { disable: async (match: Match) => {
const disabled = await storageGet('hosters.disabled', []); const disabled = (await storageGet('hosters.disabled', [])) as string[];
const index = disabled.indexOf(match.id); const index = disabled.indexOf(match.id);
if (index === -1) { if (index === -1) {
disabled.push(match.id); disabled.push(match.id);
@ -16,7 +15,7 @@ export const Hosters = {
} }
}, },
enable: async (match: Match) => { enable: async (match: Match) => {
const disabled = await storageGet('hosters.disabled', []); const disabled = (await storageGet('hosters.disabled', [])) as string[];
const index = disabled.indexOf(match.id); const index = disabled.indexOf(match.id);
if (index !== -1) { if (index !== -1) {
disabled.splice(index, 1); disabled.splice(index, 1);
@ -26,17 +25,14 @@ export const Hosters = {
getAllDisabled: async () => { getAllDisabled: async () => {
return await storageGet<boolean>('hosters.allDisabled', false); return await storageGet<boolean>('hosters.allDisabled', false);
}, },
disableAll: async () => { setAll: async (enable: boolean) => {
await storageSet('hosters.allDisabled', true); await storageSet('hosters.allDisabled', !enable);
},
enableAll: async () => {
await storageSet('hosters.allDisabled', false);
} }
}; };
export const Redirect = { export const Redirect = {
get: async (): Promise<Match | null> => { get: async (): Promise<Match | null> => {
return matches[await storageGet<string>('redirect')] || null; return matches[(await storageGet('redirect')) as string] || null;
}, },
set: async (match: Match) => { set: async (match: Match) => {
await storageSet('redirect', match.id); await storageSet('redirect', match.id);
@ -46,28 +42,72 @@ export const Redirect = {
} }
}; };
export const TmpHost = {
get: async (): Promise<[string, Match] | null> => {
const tmphost = await storageGet<[string, number]>('tmphost');
if (tmphost === undefined) {
return null;
}
return [tmphost[0], matches[tmphost[1]]];
},
set: async (domain: string, match: Match) => {
await storageSet('tmphost', [domain, match.id]);
},
delete: async () => {
await storageDelete('tmphost');
}
};
export const UrlReferer = {
get: async (url: string): Promise<string | null> => {
return (await storageGet(`urlReferer.${url}`)) || null;
},
set: async (url: string, referer: string) => {
await storageSet(`urlReferer.${url}`, referer);
},
delete: async (url: string) => {
await storageDelete(`urlReferer.${url}`);
}
};
export const Other = { export const Other = {
getFf2mpv: async () => { getFf2mpv: async () => {
return await storageGet('other.ff2mpv', true); return await storageGet('other.ff2mpv', false);
}, },
setFf2mpv: async (enable: boolean) => { setFf2mpv: async (enable: boolean) => {
await storageSet('other.ff2mpv', enable); await storageSet('other.ff2mpv', enable);
} }
}; };
export async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefined> { async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefined> {
const entry = await browser.storage.local.get(key); let resolve: (value: T | undefined) => void;
const value = entry[key]; const promise = new Promise<T | undefined>((r) => (resolve = r));
return value === undefined ? defaultValue : value;
chrome.storage.local.get(key, (entry) => {
const value = entry[key];
resolve(value === undefined ? defaultValue : value);
});
return promise;
} }
export async function storageSet<T>(key: string, value: T) { async function storageSet<T>(key: string, value: T) {
let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r));
const obj = { const obj = {
[key]: value [key]: value
}; };
await browser.storage.local.set(obj); chrome.storage.local.set(obj, () => resolve());
return promise;
} }
export async function storageDelete(key: string) { async function storageDelete(key: string) {
await browser.storage.local.remove(key); let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r));
chrome.storage.local.remove(key, () => resolve());
return promise;
} }

View File

@ -16,10 +16,10 @@ export async function unpack(packed: string): Promise<string> {
${packed} ${packed}
}; };
return packed; return packed;
}' }
`; `;
const res: string = await runInPageContext(toExecute); const res = (await runInPageContext(toExecute)) as string;
return res return res
.replace(/;/g, ';\n') .replace(/;/g, ';\n')
.replace(/{/g, '\n{\n') .replace(/{/g, '\n{\n')
@ -29,11 +29,12 @@ export async function unpack(packed: string): Promise<string> {
} }
// Adapted from: https://github.com/arikw/extension-page-context // Adapted from: https://github.com/arikw/extension-page-context
async function runInPageContext<T>(toExecute: string): Promise<T> { async function runInPageContext<T>(toExecute: string): Promise<T | null> {
// test that we are running with the allow-scripts permission // test that we are running with the allow-scripts permission
try { try {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
window.sessionStorage; window.sessionStorage;
} catch (ignore) { } catch {
return null; return null;
} }
@ -44,28 +45,25 @@ async function runInPageContext<T>(toExecute: string): Promise<T> {
const scriptElm = document.createElement('script'); const scriptElm = document.createElement('script');
scriptElm.setAttribute('type', 'application/javascript'); scriptElm.setAttribute('type', 'application/javascript');
const code = ` // inject the script
scriptElm.textContent = `
( (
async function () { async function () {
const response = {
id: '${resultMessageId}'
};
const response = { try {
id: ${resultMessageId} response.result = JSON.stringify(await (${toExecute})() ); // run script
}; } catch(err) {
response.error = JSON.stringify(err);
try { }
response.result = JSON.stringify(await (${toExecute})() ); // run script
} catch(err) {
response.error = JSON.stringify(err);
}
window.postMessage(response, '*'); window.postMessage(response, '*');
} }
)(); )();
`; `;
// inject the script
scriptElm.textContent = code;
// run the script // run the script
document.documentElement.appendChild(scriptElm); document.documentElement.appendChild(scriptElm);

View File

@ -1,15 +1,17 @@
import pkg from '../package.json'; import pkg from '../package.json';
import { matches } from './lib/match';
const sharedManifest: Partial<chrome.runtime.ManifestBase> = { const sharedManifest: Partial<chrome.runtime.ManifestBase> = {
browser_specific_settings: { browser_specific_settings: {
gecko: { gecko: {
id: '{55dd42e8-3dd9-455a-b4fe-86664881b10c}' id: '{55dd42e8-3dd9-455a-b4fe-86664881b10c}'
} },
gecko_android: {}
}, },
content_scripts: [ content_scripts: [
{ {
all_frames: true, all_frames: true,
matches: ['<all_urls>'], matches: Object.values(matches).flatMap((m) => m.domains.map((d) => `*://${d}/*`)),
js: ['src/entries/contentScript/main.ts'], js: ['src/entries/contentScript/main.ts'],
run_at: 'document_end' run_at: 'document_end'
} }
@ -21,7 +23,8 @@ const sharedManifest: Partial<chrome.runtime.ManifestBase> = {
96: 'icons/stream-bypass@96px.png', 96: 'icons/stream-bypass@96px.png',
128: 'icons/stream-bypass@128px.png' 128: 'icons/stream-bypass@128px.png'
}, },
permissions: ['storage', 'webRequest', 'nativeMessaging', '<all_urls>'] permissions: ['storage'],
optional_permissions: ['nativeMessaging']
}; };
const browserAction = { const browserAction = {
@ -35,18 +38,19 @@ const browserAction = {
const ManifestV2 = { const ManifestV2 = {
...sharedManifest, ...sharedManifest,
background: { background: {
scripts: ['src/entries/background/main.ts'], scripts: ['src/entries/background/mv2.ts'],
persistent: true persistent: true
}, },
content_scripts: [{ ...sharedManifest.content_scripts![0], matches: ['<all_urls>'] }],
browser_action: browserAction, browser_action: browserAction,
permissions: [...sharedManifest.permissions] permissions: [...sharedManifest.permissions, 'webRequest', 'webRequestBlocking', '<all_urls>']
}; };
const ManifestV3 = { const ManifestV3 = {
...sharedManifest, ...sharedManifest,
action: browserAction, action: browserAction,
background: { background: {
service_worker: 'src/entries/background/main.ts' service_worker: 'src/entries/background/mv3.ts'
} }
}; };
@ -65,15 +69,18 @@ export function getManifest(
...manifest, ...manifest,
...ManifestV2, ...ManifestV2,
manifest_version: manifestVersion manifest_version: manifestVersion
}; } as chrome.runtime.ManifestV2;
} }
if (manifestVersion === 3) { if (manifestVersion === 3) {
return { return {
...manifest, ...manifest,
// just like all the adblockers which are unable to fully work under MV3, we need access to every website
// the user enters in order to work correctly, which is forbidden when using MV3
name: `${manifest.name} Lite`,
...ManifestV3, ...ManifestV3,
manifest_version: manifestVersion manifest_version: manifestVersion
}; } as chrome.runtime.ManifestV3;
} }
throw new Error(`Missing manifest definition for manifestVersion ${manifestVersion}`); throw new Error(`Missing manifest definition for manifestVersion ${manifestVersion}`);

View File

@ -3,6 +3,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte';
import webExtension from '@samrum/vite-plugin-web-extension'; import webExtension from '@samrum/vite-plugin-web-extension';
import path from 'path'; import path from 'path';
import { getManifest } from './src/manifest'; import { getManifest } from './src/manifest';
import { matches } from './src/lib/match';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
@ -18,8 +19,10 @@ export default defineConfig(({ mode }) => {
{ {
fileName: 'src/entries/player/player.html', fileName: 'src/entries/player/player.html',
webAccessible: { webAccessible: {
matches: ['<all_urls>'], matches:
excludeEntryFile: true Number(env.MANIFEST_VERSION) === 3
? Object.values(matches).flatMap((m) => m.domains.map((d) => `*://${d}/*`))
: ['<all_urls>']
} }
} }
] ]