Compare commits

..

No commits in common. "main" and "v3.0.1" have entirely different histories.
main ... v3.0.1

25 changed files with 5008 additions and 6149 deletions

9
.eslintignore Normal file
View File

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

33
.eslintrc.cjs Normal file
View File

@ -0,0 +1,33 @@
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@v4 uses: actions/checkout@v3
- name: Install nodejs - name: Install nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: 22 node-version: 18
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@v4 uses: actions/checkout@v3
- name: Install nodejs - name: Install nodejs
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: 22 node-version: 18
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@v4 uses: actions/upload-artifact@v3
with: with:
name: stream-bypass-mv${{ matrix.manifest_version }} name: stream-bypass-mv${{ matrix.manifest_version }}
path: ./dist path: ./dist

8
.prettierrc Normal file
View File

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

View File

@ -1,17 +0,0 @@
// @ts-check
/** @type {import("prettier").Config} */
module.exports = {
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 100,
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

@ -3,16 +3,19 @@
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%20Users&style=flat-square" alt="Firefox Addon Store"> <img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Store%20Downloads&style=flat-square" alt="Firefox Addon Store">
</a>
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/">
<img src="https://img.shields.io/amo/stars/stream-bypass?label=Firefox%20Store%20Stars&style=flat-square" alt="Firefox Addon Stars">
</a> </a>
<a href="https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb"> <a href="https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb">
<img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store"> <img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Users">
</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>
</p> </p>
@ -22,9 +25,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<a href="#-installation">Installation 📥</a> <a href="#-installation">Installation 📥</a>
<a href="#-features">Features ✨</a> <a href="#-supported-sites">Supported Sites 📜</a>
<a href="#-supported-websites">Supported Websites 📜</a>
<a href="#%EF%B8%8F-building">Building 🛠️</a> <a href="#%EF%B8%8F-building">Building 🛠️</a>
@ -50,7 +51,7 @@ 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 for Android is supported too!) - [Firefox Addon Store](https://addons.mozilla.org/de/firefox/addon/stream-bypass/)
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb) - [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
### Manual installation ### Manual installation
@ -66,49 +67,45 @@ The best way to install the extension are the official browser extension stores:
- 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
## ✨ Features ## 📜 Supported sites
| Feature | Firefox | Chrome | Firefox for Android | | Site | Supported | Note |
| --------------------------------------------------------------------------------------------------------------------------------- | ------- | ------ | ------------------- | | --------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
| Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ | | [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔️ | |
| Support websites that are accessed via a redirect | ✔ | ❌ | ✔ | | [filemoon.sx](https://filemoon.sx) | ✔ | |
| Open video in mpv (with [ff2mpv](https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ❌ | | [mcloud.to](https://mcloud.to/) | ❌ | Reverse engineering the site costs too much time ([#5](https://github.com/ByteDream/stream-bypass/issues/5)) |
| [mixdrop.co](https://mixdrop.co) | ✔ | |
- ✔️: Supported. | [mp4upload.com](https://mp4upload.com) | ✔ | |
- ❌: Not supported. | [newgrounds.com](https://newgrounds.com) | ✔ | |
| [streamtape.com](https://streamtape.com) | ✔ | |
## 📜 Supported websites | [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | |
| [upstream.to](https://upstream.to) | ✔ | |
| Site | Firefox & Firefox for Android | Chrome & Chromium based | | [videovard.sx](https://videovard.sx) | ❌ | Reverse engineering the site costs too much time |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | [vidmoly.me](https://vidmoly.me) | ✔ | |
| [dropload.io](https://dropload.io) | ✔ | ✔ | | [vidoza.net](https://vidoza.net) | ✔ | |
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) | | [vidstream.pro](https://vidstream.pro) | ❌ | Reverse engineering the site costs too much time ([#5](https://github.com/ByteDream/stream-bypass/issues/5)) |
| [filemoon.to](https://filemoon.to) | ✔ | ✔ | | [voe.sx](https://voe.sx) | ✔ | |
| [goodstream.uno](https://goodstream.uno) | ✔ | ✔ | | [vupload.com](https://vupload.com) | ✔ | |
| [kwik.cx](https://kwik.cx) | ✔ | ✔ | | [kwik.cx](https://kwik.cx) | ✔ | |
| [loadx.ws](https://loadx.ws) | ✔ | ❌ (background request always required) | | [dropload.io](https://dropload.io) | ✔ | |
| [luluvdo.com](https://luluvdo.com) | ✔ | ❌ (background request always required) | | [supervideo.tv](https://supervideo.tv) | ✔ | |
| [mixdrop.co](https://mixdrop.co) | ✔ | ✔ | | [goodstream.uno](https://goodstream.uno) | ✔ | |
| [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) | ✔ | ✔ |
- ✔️: Everything ok. - ✔️: Everything ok.
- ⚠: Works with limitations. - ⚠: Included in the addon but will probably not work. See `Note` in this case, an explanation why will stand there in the most cases.
- ❌: Not supported. - ❌: Not included / supported by the addon. This can have various reasons. See `Note` for an explanation.
_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.
@ -163,11 +160,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://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins` -> - Add `chrome-extension://your-id-here/` to `allowed_origins` ->
``` ```
"allowed_origins": [ "allowed_origins": [
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/", "chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
"chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/" "chrome-extension://your-id-her/"
] ]
``` ```

View File

@ -1,40 +0,0 @@
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
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'
]
}
);

10029
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.1.6", "version": "3.0.1",
"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",
@ -11,41 +11,40 @@
"serve:firefox": "web-ext run --start-url \"about:debugging#/runtime/this-firefox\" --source-dir ./dist/", "serve:firefox": "web-ext run --start-url \"about:debugging#/runtime/this-firefox\" --source-dir ./dist/",
"serve:chrome": "web-ext run -t chromium --start-url \"https://example.com\" --source-dir ./dist/", "serve:chrome": "web-ext run -t chromium --start-url \"https://example.com\" --source-dir ./dist/",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-check --tsconfig ./tsconfig.json",
"lint": "prettier --check . && eslint .", "lint": "prettier --check --plugin prettier-plugin-svelte . && eslint .",
"format": "prettier --write .", "format": "prettier --write --plugin prettier-plugin-svelte .",
"release:firefox": "MANIFEST_VERSION=2 vite build --outDir release/firefox", "release:firefox": "MANIFEST_VERSION=2 vite build --outDir release/firefox",
"release:chrome": "MANIFEST_VERSION=3 vite build --outDir release/chrome" "release:chrome": "MANIFEST_VERSION=3 vite build --outDir release/chrome"
}, },
"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": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.1", "@samrum/vite-plugin-web-extension": "^5.1.0",
"@samrum/vite-plugin-web-extension": "^5.1.1", "@sveltejs/vite-plugin-svelte": "^2.5.3",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.0.320", "@types/chrome": "^0.0.268",
"@types/firefox-webext-browser": "^120.0.4", "@typescript-eslint/eslint-plugin": "^7.9.0",
"eslint": "^9.26.0", "@typescript-eslint/parser": "^7.9.0",
"eslint-config-prettier": "^10.1.3", "eslint": "^8.57.0",
"eslint-plugin-svelte": "^3.5.1", "eslint-config-prettier": "^9.1.0",
"hls.js": "^1.6.2", "eslint-plugin-svelte": "^2.39.0",
"prettier": "^3.5.3", "hls.js": "^1.5.8",
"prettier-plugin-svelte": "^3.3.3", "prettier": "^3.2.5",
"sass": "^1.87.0", "prettier-plugin-svelte": "^3.2.3",
"svelte": "^5.28.2", "sass": "^1.77.2",
"svelte-check": "^4.1.7", "svelte": "^4.2.17",
"svelte-preprocess": "^6.0.3", "svelte-check": "^3.7.1",
"tslib": "^2.8.1", "svelte-preprocess": "^5.1.4",
"typescript": "^5.8.3", "tslib": "^2.6.2",
"typescript-eslint": "^8.32.0", "typescript": "^5.4.5",
"vite": "^6.3.5", "vite": "^4.5.3",
"web-ext": "^8.6.0" "web-ext": "^7.11.0"
}, },
"type": "module" "type": "module"
} }

View File

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

View File

@ -1,8 +1,9 @@
import { getMatch, MatchMediaType, type Match } from '~/lib/match'; import type { Match } from '~/lib/match';
import { getMatch } from '~/lib/match';
import { Other, Redirect } from '~/lib/settings'; import { Other, Redirect } from '~/lib/settings';
async function main() { async function main() {
let match: Match | null; let match: Match;
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) {
@ -11,91 +12,36 @@ async function main() {
redirect = true; redirect = true;
} }
// some sites have a javascript based redirect, e.g. example.com redirects to example.org by changing const re = document.body.innerHTML.match(match.regex);
// 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();
} }
let url: string | null; const url = await match.match(re);
let urlType: MatchMediaType | null;
try {
const matchResult = await match.match(re);
if (matchResult && typeof matchResult === 'string') {
url = matchResult;
urlType = url.includes('.m3u8') ? MatchMediaType.Hls : MatchMediaType.Native;
} else if (matchResult && typeof matchResult === 'object') {
if (MatchMediaType.Hls in matchResult) {
url = matchResult[MatchMediaType.Hls];
urlType = MatchMediaType.Hls;
} else if (MatchMediaType.Native in matchResult) {
url = matchResult[MatchMediaType.Native];
urlType = MatchMediaType.Native;
}
}
} catch {
return;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!url || !urlType) {
return;
}
// send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application // send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application
if (await Other.getFf2mpv()) { if (await Other.getFf2mpv()) {
await chrome.runtime.sendMessage({ action: 'ff2mpv', url: url }); await chrome.runtime.sendMessage({ action: 'ff2mpv', url: url });
} }
if (match.replace && urlType != MatchMediaType.Hls) { 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;
// add video player to document body document.body.innerHTML = '';
document.body.style.margin = '0';
document.body.append(player); document.body.append(player);
} else { } else {
window.location.assign( window.location.assign(
chrome.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
}&type=${urlType}` }`
) )
); );
} }

View File

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

View File

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

View File

@ -1,23 +1,17 @@
import { matches } from '~/lib/match';
import Hls from 'hls.js'; import Hls from 'hls.js';
import { matches, MatchMediaType } from '~/lib/match'; import { storageSet } from '~/lib/settings';
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, domain: string, videoElem: HTMLVideoElement) { async function playHls(url: 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);
@ -28,10 +22,9 @@ async function playHls(url: string, domain: 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') as string; const id = urlQuery.get('id');
const url = decodeURIComponent(urlQuery.get('url') as string); const url = decodeURIComponent(urlQuery.get('url'));
const domain = urlQuery.get('domain') as string; const domain = urlQuery.get('domain');
const type = urlQuery.get('type') as MatchMediaType;
const match = matches[id]; const match = matches[id];
if (match === undefined) { if (match === undefined) {
@ -39,9 +32,11 @@ export async function play(videoElem: HTMLVideoElement) {
} }
document.title = `Stream Bypass (${domain})`; document.title = `Stream Bypass (${domain})`;
if (type === MatchMediaType.Hls) { await storageSet('referer', { domain: domain });
await playHls(url, domain, videoElem);
} else if (type === MatchMediaType.Native) { if (new URL(url).pathname.endsWith('.m3u8')) {
await playNative(url, domain, videoElem); await playHls(url, videoElem);
} else {
await playNative(url, videoElem);
} }
} }

View File

@ -1,97 +1,117 @@
<script lang="ts"> <script lang="ts">
import { matches, type Match } from '~/lib/match'; import { 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: (Match & { active: boolean; disabled: boolean })[] = []; let hosters = [];
(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: any) => { hosters = Object.values(matches).map((m) => {
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()) as boolean; ff2mpvEnabled = await Other.getFf2mpv();
})(); })();
</script> </script>
<main <main>
style={isMobile
? 'height: 100vh; display: flex; flex-direction: column; align-items: center'
: 'height: 500px'}
>
<fieldset>
<legend>Hoster</legend>
<div class="setting-container" style={isMobile ? 'grid-column-gap: 5rem' : ''}>
<label for="hosters-enabled">Enabled</label>
<div> <div>
<Toggle <h3 class="header">Hosters</h3>
bind:checked={hostersEnabled} <div class="buttons super-buttons">
id="hosters-enabled" <button
onChange={() => Hosters.setAll(hostersEnabled)} class:active={hostersEnabled}
/> on:click={async () => {
await Hosters.enableAll();
hostersEnabled = true;
hosters = hosters.map((m) => {
m['disabled'] = false;
return m;
});
}}>On</button
>
<button
class:active={!hostersEnabled}
on:click={async () => {
await Hosters.disableAll();
hostersEnabled = false;
hosters = hosters.map((m) => {
m['disabled'] = true;
return m;
});
}}>Off</button
>
</div>
<table class="setting-table">
{#each hosters as hoster}
<tr>
<td class="setting-name">
<p>{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);
hoster.active = true;
}}>On</button
>
<button
class:disabled={hoster.disabled}
class:active={!hoster.active}
on:click={async () => {
if (hoster.disabled) return;
await Hosters.disable(hoster);
hoster.active = false;
}}>Off</button
>
</td>
</tr>
{/each}
</table>
</div> </div>
<hr /> <hr />
{#each hosters as hoster, i (hoster.id)} <div>
<label for="hoster-{i}" style="cursor: {hostersEnabled ? 'pointer' : 'default'}" <h3 class="header">Other</h3>
>{hoster.name}</label <table>
<tr>
<td class="setting-name">
<p>ff2mpv</p>
</td>
<td class="buttons">
<button
class:active={ff2mpvEnabled}
on:click={async () => {
await Other.setFf2mpv(true);
ff2mpvEnabled = true;
}}>On</button
>
<button
class:active={!ff2mpvEnabled}
on:click={async () => {
await Other.setFf2mpv(false);
ff2mpvEnabled = false;
}}>Off</button
> >
<div>
<Toggle
bind:checked={hoster.active}
disabled={!hostersEnabled}
id="hoster-{i}"
onChange={async () => {
if (hoster.active) {
await Hosters.enable(hoster);
} else {
await Hosters.disable(hoster);
}
}}
></Toggle>
</div>
{/each}
</div>
</fieldset>
{#if !isMobile}
<fieldset>
<legend>Other</legend>
<div class="setting-container">
<label for="ff2mpv">ff2mpv</label>
<div>
<Toggle
bind:checked={ff2mpvEnabled}
id="ff2mpv"
onChange={async () => {
ff2mpvEnabled = !ff2mpvEnabled;
if (await browser.permissions.request({ permissions: ['nativeMessaging'] })) {
await Other.setFf2mpv(ff2mpvEnabled);
ff2mpvEnabled = !ff2mpvEnabled;
}
}}
></Toggle>
<a <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>
</tr>
</table>
</div> </div>
</div> <hr />
</fieldset> <a id="bug-notice" href="https://github.com/ByteDream/stream-bypass/issues"
{/if} >Something does not work</a
<a id="report-notice" href="https://github.com/ByteDream/stream-bypass/issues"
>Report issues or requests</a
> >
</main> </main>
@ -105,12 +125,7 @@
padding: 0 8px; padding: 0 8px;
} }
fieldset { #bug-notice {
border-radius: 5px;
border-color: gray;
}
#report-notice {
border: none; border: none;
color: white; color: white;
display: block; display: block;
@ -133,33 +148,52 @@
text-align: center; text-align: center;
} }
.setting-container { .setting-table {
display: grid; border-collapse: collapse;
grid-template-columns: auto auto; border-spacing: 0;
grid-column-gap: 5px; }
grid-row-gap: 4px;
align-items: end;
width: 100%;
& > label { .setting-name {
height: 34px; height: 34px;
p {
margin: 0; margin: 0;
user-select: none; cursor: default;
cursor: pointer; }
}
.buttons {
display: flex; display: flex;
align-items: center; flex-direction: row;
} height: 34px;
& > hr { button,
grid-column: 1 / span 2; a {
width: 100%; border: 1px solid #281515;
} background-color: transparent;
} color: white;
cursor: pointer;
.info-questionmark { padding: 5px 8px;
display: inline-block; margin: 0;
transform: translateX(-40%) translateY(-100%);
color: black;
text-decoration: none; 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;
justify-content: center;
gap: 4px;
width: 100%;
margin-bottom: 10px;
}
} }
</style> </style>

View File

@ -1,16 +1,14 @@
<!doctype html> <!doctype html>
<html style="overflow-y: hidden" lang="en"> <html style="width: fit-content; height: 500px; 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="overflow-y: scroll"> <body style="width: fit-content; height: 500px; overflow-y: scroll">
<script type="module"> <script type="module">
import { mount } from 'svelte';
import Popup from '~/entries/popup/Popup.svelte'; import Popup from '~/entries/popup/Popup.svelte';
mount(Popup, { new Popup({
target: document.body target: document.body
}); });
</script> </script>

View File

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

View File

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

View File

@ -1,28 +1,15 @@
import { Hosters, Redirect, TmpHost } from './settings'; import { unpack } from './utils';
import { lastPathSegment } from './util/extract'; import { Hosters } from './settings';
import { unpack } from './util/userspace';
export interface Match { export interface Match {
name: string; name: string;
id: string; id: string;
domains: string[]; domains: string[];
replace?: boolean; replace?: boolean;
regex: RegExp[]; regex: RegExp;
notice?: string; notice?: string;
match( match(match: RegExpMatchArray): Promise<string>;
match: RegExpMatchArray
): Promise<
string | { [MatchMediaType.Hls]: string } | { [MatchMediaType.Native]: string } | null
>;
// allow other properties that may be implemented by the objects that use this interface declaration
[other: string]: any;
}
export enum MatchMediaType {
Hls = 'hls',
Native = 'native'
} }
export const Doodstream: Match = { export const Doodstream: Match = {
@ -36,22 +23,19 @@ 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', '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 function (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]}`, {
headers: { headers: {
Range: 'bytes=0-' Range: 'bytes=0-'
@ -67,32 +51,24 @@ export const Doodstream: Match = {
export const DropLoad: Match = { export const DropLoad: Match = {
name: 'Dropload', name: 'Dropload',
id: 'dropload', id: 'dropload',
domains: ['dropload.io'], 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 function (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',
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 function (match: RegExpMatchArray) {
if (window.location.host.startsWith('filemoon')) {
await TmpHost.set(new URL(match[0]).host, Filemoon);
return null;
}
await TmpHost.delete();
match: async (match: RegExpMatchArray) => {
const unpacked = await unpack(match[0]); const unpacked = await unpack(match[0]);
return unpacked.match(/(?<=file:")\S*(?=")/)![0]; return unpacked.match(/(?<=file:").*(?=")/)[0];
} }
}; };
@ -100,9 +76,9 @@ export const GoodStream: Match = {
name: 'Goodstream', name: 'Goodstream',
id: 'goodstream', id: 'goodstream',
domains: ['goodstream.uno'], domains: ['goodstream.uno'],
regex: [/(?<=file:\s+").*(?=")/g], regex: /(?<=file:\s+").*(?=")/g,
match: async function (match: RegExpMatchArray) { match: async (match: RegExpMatchArray) => {
return match[0]; return match[0];
} }
}; };
@ -111,82 +87,23 @@ export const Kwik: Match = {
name: 'Kwik', name: 'Kwik',
id: 'kwik', id: 'kwik',
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 function (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 LoadX: Match = {
name: 'LoadX',
id: 'loadx',
domains: ['loadx.ws'],
regex: [/./gm],
match: async () => {
const hash = encodeURIComponent(lastPathSegment(window.location.href));
const response = await fetch(
`https://${window.location.host}/player/index.php?data=${hash}&do=getVideo`,
{
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}
);
const responseJson = await response.json();
const videoSource: string = responseJson['videoSource'];
// extension of extracted url is '.txt', so we have to manually specify that it's a hls
return { [MatchMediaType.Hls]: videoSource.replace('\\/', '/') };
}
};
export const Luluvdo: Match = {
name: 'Luluvdo',
id: 'luluvdo',
domains: ['luluvdo.com'],
regex: [/./gm],
match: async () => {
const requestBody = new FormData();
requestBody.set('op', 'embed');
requestBody.set('file_code', lastPathSegment(window.location.href));
const response = await fetch(`https://${window.location.host}/dl`, {
method: 'POST',
body: requestBody,
referrer: window.location.href
});
let unpacked;
const responseText = await response.text();
const evalMatch = responseText.match(/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms)!;
// sometimes is packed, sometimes it's not. looks like someone forgets to obfuscate the code when pushing to
// production
if (evalMatch) {
unpacked = await unpack(evalMatch[0]);
return unpacked.match(/(?<=file:").*(?=")/)![0];
} else {
unpacked = responseText;
}
return unpacked.match(/(?<=file:").*(?=")/)![0];
} }
}; };
export const Mixdrop: Match = { export const Mixdrop: Match = {
name: 'Mixdrop', name: 'Mixdrop',
id: 'mixdrop', id: 'mixdrop',
domains: ['mixdrop.bz', 'mixdrop.ch', 'mixdrop.co', 'mixdrop.gl', 'mixdrop.my', 'mixdrop.to'], 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 function (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}`;
} }
}; };
@ -196,11 +113,11 @@ export const Mp4Upload: Match = {
id: 'mp4upload', id: 'mp4upload',
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 function (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];
} }
}; };
@ -208,7 +125,7 @@ export const Newgrounds: Match = {
name: 'Newgrounds', name: 'Newgrounds',
id: 'newgrounds', id: 'newgrounds',
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];
@ -222,28 +139,13 @@ export const Newgrounds: Match = {
} }
}; };
export const StreamA2z: Match = {
name: 'Stream2Az',
id: 'stream2az',
domains: ['streama2z.com', 'streama2z.xyz'],
regex: [/https?:\/\/\S*m3u8.+(?=['"])/gm],
match: async function (match: RegExpMatchArray) {
if (StreamA2z.domains.indexOf(window.location.hostname) !== -1) {
await Redirect.set(StreamA2z);
return null;
}
return match[0];
}
};
export const Streamtape: Match = { export const Streamtape: Match = {
name: 'Streamtape', name: 'Streamtape',
id: 'streamtape', id: 'streamtape',
domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'], domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'],
regex: [/id=.*(?=')/gm], regex: /id=.*(?=')/gm,
match: async function (match: RegExpMatchArray) { match: async (match: RegExpMatchArray) => {
let i = 0; let i = 0;
while (i < match.length) { while (i < match.length) {
if (match[++i - 1] == match[i]) { if (match[++i - 1] == match[i]) {
@ -260,9 +162,9 @@ export const Streamzz: Match = {
name: 'Streamzz', name: 'Streamzz',
id: 'streamzz', id: 'streamzz',
domains: ['streamzz.to', 'streamz.ws'], domains: ['streamzz.to', 'streamz.ws'],
regex: [/(?<=\|)\w{2,}/gm], regex: /(?<=\|)\w{2,}/gm,
match: async function (match: RegExpMatchArray) { match: async (match: RegExpMatchArray) => {
return `https://get.${location.hostname.split('.')[0]}.tw/getlink-${ return `https://get.${location.hostname.split('.')[0]}.tw/getlink-${
match.sort((a, b) => b.length - a.length)[0] match.sort((a, b) => b.length - a.length)[0]
}.dll`; }.dll`;
@ -272,12 +174,12 @@ export const Streamzz: Match = {
export const SuperVideo: Match = { export const SuperVideo: Match = {
name: 'Supervideo', name: 'Supervideo',
id: 'supervideo', id: 'supervideo',
domains: ['supervideo.cc', '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 function (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];
} }
}; };
@ -285,11 +187,11 @@ export const Upstream: Match = {
name: 'Upstream', name: 'Upstream',
id: 'upstream', id: 'upstream',
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 function (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];
} }
}; };
@ -297,10 +199,10 @@ export const Vidmoly: Match = {
name: 'Vidmoly', name: 'Vidmoly',
id: 'vidmoly', id: 'vidmoly',
domains: ['vidmoly.me', 'vidmoly.to'], domains: ['vidmoly.me', 'vidmoly.to'],
regex: [/(?<=file:").+\.m3u8/gm], regex: /(?<=file:").+\.m3u8/gm,
replace: true,
match: async function (match: RegExpMatchArray) { match: async (match: RegExpMatchArray) => {
alert('a');
return match[0]; return match[0];
} }
}; };
@ -308,11 +210,10 @@ export const Vidmoly: Match = {
export const Vidoza: Match = { export const Vidoza: Match = {
name: 'Vidoza', name: 'Vidoza',
id: 'vidoza', id: 'vidoza',
domains: ['vidoza.net', 'videzz.net'], domains: ['vidoza.net'],
regex: [/(?<=src:\s?").+?(?=")/gm], regex: /(?<=src:\s?").+?(?=")/gm,
replace: true,
match: async function (match: RegExpMatchArray) { match: async (match: RegExpMatchArray) => {
return match[0]; return match[0];
} }
}; };
@ -321,67 +222,10 @@ export const Voe: Match = {
name: 'Voe', name: 'Voe',
id: 'voe', id: 'voe',
domains: ['voe.sx'], domains: ['voe.sx'],
regex: [ regex: /https?:\/\/\S*m3u8.+(?=['"])/gm,
// 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) { match: async (match: RegExpMatchArray) => {
if (window.location.host === 'voe.sx') { return match[0];
const redirectUrl = new URL(match[0]);
await TmpHost.set(redirectUrl.host, Voe);
return null;
} else {
let json = match[0];
json = JSON.parse(json);
let deobfuscated = json[0];
deobfuscated = this.rot13(deobfuscated);
deobfuscated = this.removeSpecialSequences(deobfuscated);
deobfuscated = atob(deobfuscated);
deobfuscated = this.shiftString(deobfuscated);
deobfuscated = deobfuscated.split('').reverse().join('');
deobfuscated = atob(deobfuscated);
const payload = JSON.parse(deobfuscated);
return payload['source'];
}
},
rot13: function (encrypted: string) {
let decrypted = '';
for (let i = 0; i < encrypted.length; i++) {
let char = encrypted.charCodeAt(i);
if (char >= 65 && char <= 90) {
char = ((char - 65 + 13) % 26) + 65;
} else if (char >= 97 && char <= 122) {
char = ((char - 97 + 13) % 26) + 97;
}
decrypted += String.fromCharCode(char);
}
return decrypted;
},
removeSpecialSequences: function (input: string) {
return input
.replaceAll(/@\$/g, '')
.replaceAll(/\^\^/g, '')
.replaceAll(/~@/g, '')
.replaceAll(/%\?/g, '')
.replaceAll(/\*~/g, '')
.replaceAll(/!!/g, '')
.replaceAll(/#&/g, '');
},
shiftString: function (input: string) {
let shifted = '';
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i);
const shiftedChar = char - 3;
shifted += String.fromCharCode(shiftedChar);
}
return shifted;
} }
}; };
@ -389,9 +233,9 @@ export const Vupload: Match = {
name: 'Vupload', name: 'Vupload',
id: 'vupload', id: 'vupload',
domains: ['vupload.com'], domains: ['vupload.com'],
regex: [/(?<=src:\s?").+?(?=")/gm], regex: /(?<=src:\s?").+?(?=")/gm,
match: async function (match: RegExpMatchArray) { match: async (match: RegExpMatchArray) => {
return match[0]; return match[0];
} }
}; };
@ -402,12 +246,9 @@ export const matches = {
[Filemoon.id]: Filemoon, [Filemoon.id]: Filemoon,
[GoodStream.id]: GoodStream, [GoodStream.id]: GoodStream,
[Kwik.id]: Kwik, [Kwik.id]: Kwik,
[LoadX.id]: LoadX,
[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,
@ -432,10 +273,5 @@ 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,12 +1,13 @@
import { matches, type Match } from './match'; import type { Match } from './match';
import { matches } from './match';
export const Hosters = { export const Hosters = {
getDisabled: async () => { getDisabled: async () => {
const disabled = (await storageGet('hosters.disabled', [])) as string[]; const disabled = await storageGet<string[]>('hosters.disabled', []);
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', [])) as string[]; const disabled = await storageGet('hosters.disabled', []);
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);
@ -14,7 +15,7 @@ export const Hosters = {
} }
}, },
enable: async (match: Match) => { enable: async (match: Match) => {
const disabled = (await storageGet('hosters.disabled', [])) as string[]; const disabled = await storageGet('hosters.disabled', []);
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);
@ -24,14 +25,17 @@ export const Hosters = {
getAllDisabled: async () => { getAllDisabled: async () => {
return await storageGet<boolean>('hosters.allDisabled', false); return await storageGet<boolean>('hosters.allDisabled', false);
}, },
setAll: async (enable: boolean) => { disableAll: async () => {
await storageSet('hosters.allDisabled', !enable); await storageSet('hosters.allDisabled', true);
},
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('redirect')) as string] || null; return matches[await storageGet<string>('redirect')] || null;
}, },
set: async (match: Match) => { set: async (match: Match) => {
await storageSet('redirect', match.id); await storageSet('redirect', match.id);
@ -41,44 +45,16 @@ 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', false); return await storageGet('other.ff2mpv', true);
}, },
setFf2mpv: async (enable: boolean) => { setFf2mpv: async (enable: boolean) => {
await storageSet('other.ff2mpv', enable); await storageSet('other.ff2mpv', enable);
} }
}; };
async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefined> { export async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefined> {
let resolve: (value: T | undefined) => void; let resolve: (value: T | undefined) => void;
const promise = new Promise<T | undefined>((r) => (resolve = r)); const promise = new Promise<T | undefined>((r) => (resolve = r));
@ -90,7 +66,7 @@ async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefin
return promise; return promise;
} }
async function storageSet<T>(key: string, value: T) { export async function storageSet<T>(key: string, value: T) {
let resolve: () => void; let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r)); const promise = new Promise<void>((r) => (resolve = r));
@ -102,7 +78,7 @@ async function storageSet<T>(key: string, value: T) {
return promise; return promise;
} }
async function storageDelete(key: string) { export async function storageDelete(key: string) {
let resolve: () => void; let resolve: () => void;
const promise = new Promise<void>((r) => (resolve = r)); const promise = new Promise<void>((r) => (resolve = r));

View File

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

View File

@ -16,10 +16,10 @@ export async function unpack(packed: string): Promise<string> {
${packed} ${packed}
}; };
return packed; return packed;
} }'
`; `;
const res = (await runInPageContext(toExecute)) as string; const res: string = await runInPageContext(toExecute);
return res return res
.replace(/;/g, ';\n') .replace(/;/g, ';\n')
.replace(/{/g, '\n{\n') .replace(/{/g, '\n{\n')
@ -29,12 +29,11 @@ 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 | null> { async function runInPageContext<T>(toExecute: string): Promise<T> {
// 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 { } catch (ignore) {
return null; return null;
} }
@ -45,12 +44,12 @@ async function runInPageContext<T>(toExecute: string): Promise<T | null> {
const scriptElm = document.createElement('script'); const scriptElm = document.createElement('script');
scriptElm.setAttribute('type', 'application/javascript'); scriptElm.setAttribute('type', 'application/javascript');
// inject the script const code = `
scriptElm.textContent = `
( (
async function () { async function () {
const response = { const response = {
id: '${resultMessageId}' id: ${resultMessageId}
}; };
try { try {
@ -64,6 +63,9 @@ async function runInPageContext<T>(toExecute: string): Promise<T | null> {
)(); )();
`; `;
// inject the script
scriptElm.textContent = code;
// run the script // run the script
document.documentElement.appendChild(scriptElm); document.documentElement.appendChild(scriptElm);

View File

@ -5,8 +5,7 @@ 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: [
{ {
@ -23,8 +22,7 @@ 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'], permissions: ['storage', 'nativeMessaging']
optional_permissions: ['nativeMessaging']
}; };
const browserAction = { const browserAction = {
@ -41,7 +39,7 @@ const ManifestV2 = {
scripts: ['src/entries/background/mv2.ts'], scripts: ['src/entries/background/mv2.ts'],
persistent: true persistent: true
}, },
content_scripts: [{ ...sharedManifest.content_scripts![0], matches: ['<all_urls>'] }], content_scripts: [{ ...sharedManifest.content_scripts[0], matches: ['<all_urls>'] }],
browser_action: browserAction, browser_action: browserAction,
permissions: [...sharedManifest.permissions, 'webRequest', 'webRequestBlocking', '<all_urls>'] permissions: [...sharedManifest.permissions, 'webRequest', 'webRequestBlocking', '<all_urls>']
}; };
@ -69,7 +67,7 @@ export function getManifest(
...manifest, ...manifest,
...ManifestV2, ...ManifestV2,
manifest_version: manifestVersion manifest_version: manifestVersion
} as chrome.runtime.ManifestV2; };
} }
if (manifestVersion === 3) { if (manifestVersion === 3) {
@ -80,7 +78,7 @@ export function getManifest(
name: `${manifest.name} Lite`, 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

@ -2,7 +2,7 @@
"extends": "@tsconfig/svelte/tsconfig.json", "extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"target": "es2021", "target": "es2019",
"module": "esnext", "module": "esnext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"resolveJsonModule": true, "resolveJsonModule": true,

View File

@ -1,9 +1,9 @@
import { fileURLToPath } from 'url'; import { defineConfig, loadEnv } from 'vite';
import webExtension from '@samrum/vite-plugin-web-extension';
import { svelte } from '@sveltejs/vite-plugin-svelte'; import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig, loadEnv, type PluginOption } from 'vite'; import webExtension from '@samrum/vite-plugin-web-extension';
import { matches } from './src/lib/match'; 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 }) => {
@ -11,7 +11,7 @@ export default defineConfig(({ mode }) => {
return { return {
plugins: [ plugins: [
svelte() as PluginOption, svelte(),
webExtension({ webExtension({
manifest: getManifest(Number(env.MANIFEST_VERSION)), manifest: getManifest(Number(env.MANIFEST_VERSION)),
additionalInputs: { additionalInputs: {
@ -27,10 +27,12 @@ export default defineConfig(({ mode }) => {
} }
] ]
} }
}) as unknown as PluginOption })
], ],
resolve: { resolve: {
alias: [{ find: '~', replacement: fileURLToPath(new URL('./src', import.meta.url)) }] alias: {
'~': path.resolve(__dirname, './src')
}
} }
}; };
}); });