Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9a0197f7f | |||
| d8bb70890a | |||
| 45900b2e86 | |||
| 3058847f31 | |||
| 082f30722c | |||
| 39a6fffd50 | |||
| 6109cd5543 | |||
| de38196a77 | |||
| d2b6086155 | |||
| 66c638425d | |||
| b2da03f9a1 | |||
| 76a4217aab | |||
| cac464cc65 | |||
| 7bd8f01a1b | |||
| b7c5e020b6 | |||
| fdd1ca350e | |||
| 607326e6d6 | |||
| 47f6de2de8 | |||
| d159fb0aa4 | |||
| 290733a85a | |||
| 84eb32ab61 | |||
| 927a4e3ae3 | |||
| 5926177920 | |||
| 9a29316641 | |||
| c300125f64 | |||
| 59f2ffec57 | |||
| 6dff691c25 | |||
| c4085f9ac8 | |||
| 422702b16f | |||
| c3dad3681c | |||
| 6eb3259ec5 | |||
| 68e445a738 | |||
| e864bc6100 | |||
| 3ac73ac3b4 | |||
| 6aaf960c28 | |||
| 8a13dea681 | |||
| 8d575241fe | |||
| 50f400b8b9 | |||
| f6fcfd354a | |||
| c081127d35 | |||
| 416fceba88 | |||
| a9cf03c176 | |||
| 8852318483 | |||
| 7067aaf4a0 | |||
| bf723e2ed6 | |||
| 9c80362a1c | |||
| ae738ff32f | |||
| 16d4f2956a | |||
|
|
f6c6102436 | ||
|
|
f1525817b1 | ||
| 1e1d8cdfa6 | |||
| 47eed2d12b |
40
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build Firefox (mv2)
|
||||||
|
run: npm run build:firefox
|
||||||
|
|
||||||
|
- name: Build Chrome (mv3)
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload Firefox (mv2)
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: stream-bypass-mv2
|
||||||
|
path: .output/firefox-mv2
|
||||||
|
|
||||||
|
- name: Upload Chrome (mv3)
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: stream-bypass-mv3
|
||||||
|
path: .output/chrome-mv3
|
||||||
56
.github/workflows/ci.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: ci
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install nodejs
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- manifest_version: 2
|
|
||||||
- manifest_version: 3
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install nodejs
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
MANIFEST_VERSION: ${{ matrix.manifest_version }}
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: stream-bypass-mv${{ matrix.manifest_version }}
|
|
||||||
path: ./dist
|
|
||||||
if-no-files-found: error
|
|
||||||
28
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
tags-ignore:
|
||||||
|
- '*'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
||||||
85
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
name: publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: Dry run
|
||||||
|
default: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Zip extensions
|
||||||
|
run: |
|
||||||
|
npm run zip:firefox
|
||||||
|
npm run zip
|
||||||
|
|
||||||
|
- name: Resolve zip extension paths
|
||||||
|
id: paths
|
||||||
|
run: |
|
||||||
|
MV2_PATH=$(find .output -type f -name "*-mv2.zip")
|
||||||
|
MV2_NAME=$(basename $MV2_PATH)
|
||||||
|
MV3_PATH=$(find .output -type f -name "*-mv3.zip")
|
||||||
|
MV3_NAME=$(basename $MV3_PATH)
|
||||||
|
echo "mv2_path=$MV2_PATH" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mv2_name=$MV2_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mv3_path=$MV3_PATH" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mv3_name=$MV3_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Upload mv2 as artifact
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: ${{ steps.paths.outputs.mv2_name }}
|
||||||
|
path: ${{ steps.paths.outputs.mv2_path }}
|
||||||
|
|
||||||
|
- name: Upload mv3 as artifact
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: ${{ steps.paths.outputs.mv3_name }}
|
||||||
|
path: ${{ steps.paths.outputs.mv3_path }}
|
||||||
|
|
||||||
|
- name: Upload to latest release
|
||||||
|
if: github.event.inputs.dry_run != 'true'
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: .output/*mv*.zip
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
overwrite: true
|
||||||
|
file_glob: true
|
||||||
|
|
||||||
|
- name: Submit to stores
|
||||||
|
env:
|
||||||
|
DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||||
|
FIREFOX_EXTENSION_ID: ${{ secrets.FIREFOX_EXTENSION_ID }}
|
||||||
|
FIREFOX_JWT_ISSUER: ${{ secrets.FIREFOX_JWT_ISSUER }}
|
||||||
|
FIREFOX_JWT_SECRET: ${{ secrets.FIREFOX_JWT_SECRET }}
|
||||||
|
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||||
|
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
||||||
|
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||||
|
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
npx wxt submit \
|
||||||
|
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
|
||||||
|
--chrome-zip .output/*-mv3.zip
|
||||||
31
.gitignore
vendored
@@ -1,4 +1,27 @@
|
|||||||
.idea/
|
# Logs
|
||||||
dist/
|
logs
|
||||||
release/
|
*.log
|
||||||
node_modules/
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.output
|
||||||
|
stats.html
|
||||||
|
stats-*.json
|
||||||
|
.wxt
|
||||||
|
web-ext.config.ts
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.zed
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
.idea
|
||||||
/dist
|
.vscode
|
||||||
.env
|
.zed
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
.output
|
||||||
|
.wxt
|
||||||
|
|
||||||
|
.github
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
|
||||||
18
.prettierrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
module.exports = {
|
||||||
|
useTabs: true,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'none',
|
||||||
|
printWidth: 120,
|
||||||
|
tabWidth: 4,
|
||||||
|
plugins: ['prettier-plugin-svelte', '@ianvs/prettier-plugin-sort-imports'],
|
||||||
|
/* prettier-plugin-svelte */
|
||||||
|
overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }],
|
||||||
|
/* @ianvs/prettier-plugin-sort-imports */
|
||||||
|
importOrder: ['^~/(.*)$', '^./(.*)$', ''],
|
||||||
|
importOrderParserPlugins: ['typescript'],
|
||||||
|
importOrderTypeScriptVersion: '5.0.0',
|
||||||
|
importOrderCaseSensitive: false
|
||||||
|
};
|
||||||
98
README.md
@@ -4,7 +4,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/bytedream/stream-bypass/releases/latest">
|
<a href="https://github.com/bytedream/stream-bypass/releases/latest">
|
||||||
<img src="https://img.shields.io/github/v/release/ByteDream/stream-bypass?label=Version&style=flat-square" alt="Version">
|
<img src="https://img.shields.io/github/v/release/bytedream/stream-bypass?label=Version&style=flat-square" alt="Version">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/">
|
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/">
|
||||||
<img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Users&style=flat-square" alt="Firefox Addon Store">
|
<img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Users&style=flat-square" alt="Firefox Addon Store">
|
||||||
@@ -13,7 +13,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
|
|||||||
<img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store">
|
<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>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ This has the advantage, that no advertising or popups are shown when trying to i
|
|||||||
Additionally, this enables you to download the video by right-clicking it and just choose the download option.
|
Additionally, this enables you to download the video by right-clicking it and just choose the download option.
|
||||||
|
|
||||||
<details id="example">
|
<details id="example">
|
||||||
<summary><b>How it's working:</b></summary>
|
<summary><b>How it works:</b></summary>
|
||||||
<img src="example.gif" alt="">
|
<img src="example.gif" alt="">
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -53,55 +53,60 @@ 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/) (Firefox for Android is supported too!)
|
||||||
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
|
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
|
||||||
|
|
||||||
### Manual installation
|
<details>
|
||||||
|
<summary><h3 id="manual-installation1">Manual installation</h3></summary>
|
||||||
|
|
||||||
- Firefox
|
- Firefox (mv2)
|
||||||
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that)
|
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. with [7zip](https://www.7-zip.org/))
|
||||||
- Go into your browser and type `about:debugging#/runtime/this-firefox` in the address bar
|
- Go into your browser and type `about:debugging#/runtime/this-firefox` in the address bar
|
||||||
- Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory
|
- Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory
|
||||||
- Chromium / Google Chrome
|
- Chromium / Google Chrome (mv3)
|
||||||
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
|
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
|
||||||
- Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that)
|
- Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. [7zip](https://www.7-zip.org/))
|
||||||
- Go into your browser and type `chrome://extensions` in the address bar
|
- Go into your browser and type `chrome://extensions` in the address bar
|
||||||
- Turn on the developer mode by checking the switch in the top right corner
|
- Turn on the developer mode by checking the switch in the top right corner
|
||||||
- Click `Load unpacked` and choose the unzipped directory
|
- Click `Load unpacked` and choose the unzipped directory
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
| Feature | Firefox | Chrome | Firefox for Android |
|
- ✔: Supported.
|
||||||
| --------------------------------------------------------------------------------------------------------------------------------- | ------- | ------ | ------------------- |
|
- ✖: Not supported.
|
||||||
| Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
|
|
||||||
| Support websites that are accessed via a redirect | ✔ | ❌ | ✔ |
|
|
||||||
| Open video in mpv (with [ff2mpv](https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ❌ |
|
|
||||||
|
|
||||||
- ✔️: Supported.
|
| Feature | Firefox (mv2) | Chrome (mv3) | Firefox for Android (mv2) |
|
||||||
- ❌: Not supported.
|
| --------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------------------- |
|
||||||
|
| Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
|
||||||
|
| Support websites that are accessed via a redirect | ✔ | ✖ | ✔ |
|
||||||
|
| Open video in mpv (with [ff2mpv](https://github.com/bytedream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ✖ |
|
||||||
|
|
||||||
## 📜 Supported websites
|
## 📜 Supported websites
|
||||||
|
|
||||||
| Site | Firefox & Firefox for Android | Chrome & Chromium based |
|
- ✔: Everything ok.
|
||||||
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
- ⚠: Works with limitations.
|
||||||
|
- ✖: Not supported.
|
||||||
|
|
||||||
|
| Site | Firefox & Firefox for Android (mv2) | Chrome & Chromium based (mv2) |
|
||||||
|
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||||
| [dropload.io](https://dropload.io) | ✔ | ✔ |
|
| [dropload.io](https://dropload.io) | ✔ | ✔ |
|
||||||
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔️ | ⚠ (redirect probably required) |
|
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
|
||||||
| [filemoon.to](https://filemoon.to) | ✔ | ✔ |
|
| [filemoon.to](https://filemoon.to) | ✔ | ✔ |
|
||||||
| [goodstream.uno](https://goodstream.uno) | ✔ | ✔ |
|
| [goodstream.uno](https://goodstream.uno) | ✔ | ✔ |
|
||||||
|
| [kwik.cx](https://kwik.cx) | ✔ | ✔ |
|
||||||
|
| [loadx.ws](https://loadx.ws) | ✔ | ✖ (background request always required) |
|
||||||
|
| [luluvdo.com](https://luluvdo.com) | ✔ | ✖ (background request always required) |
|
||||||
| [mixdrop.co](https://mixdrop.co) | ✔ ️ | ✔ |
|
| [mixdrop.co](https://mixdrop.co) | ✔ ️ | ✔ |
|
||||||
| [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
|
| [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
|
||||||
| [newgrounds.com](https://newgrounds.com) | ✔ | ✔ |
|
| [newgrounds.com](https://newgrounds.com) | ✔ | ✔ |
|
||||||
| [streama2z.com](https://streama2z.com) | ✔ | ❌ (redirect always required) |
|
| [streama2z.com](https://streama2z.com) | ✔ | ✖ (redirect always required) |
|
||||||
| [streamtape.com](https://streamtape.com) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) |
|
| [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) | ✔ | ✔ |
|
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | ✔ |
|
||||||
| [supervideo.tv](https://supervideo.tv) | ✔ | ✔ |
|
| [supervideo.tv](https://supervideo.tv) | ✔ | ✔ |
|
||||||
| [upstream.to](https://upstream.to) | ✔ | ✔ |
|
| [upstream.to](https://upstream.to) | ✔ | ✔ |
|
||||||
| [vidmoly.to](https://vidmoly.me) | ✔ | ✔ |
|
| [vidmoly.to](https://vidmoly.me) | ✔ | ✔ |
|
||||||
| [vidoza.net](https://vidoza.net) | ⚠ (doesn't always work the first time, retrying/reloading the page one or two times fixes it) | ⚠ (doesn't always work the first time, retrying/reloading the page one or two times fixes it) |
|
| [vidoza.net](https://vidoza.net) | ✔ | ✔ |
|
||||||
| [voe.sx](https://voe.sx) | ✔ | ❌ (redirect always required) |
|
| [voe.sx](https://voe.sx) | ✔ | ✖ (redirect always required) |
|
||||||
| [vupload.com](https://vupload.com) | ✔ | ✔ |
|
| [vupload.com](https://vupload.com) | ✔ | ✔ |
|
||||||
| [kwik.cx](https://kwik.cx) | ✔ | ✔ |
|
|
||||||
|
|
||||||
- ✔️: Everything ok.
|
|
||||||
- ⚠: Works with limitations.
|
|
||||||
- ❌: Not supported.
|
|
||||||
|
|
||||||
_This table might not be 100% accurate, it isn't actively monitored if the addon works for every website!_
|
_This table might not be 100% accurate, it isn't actively monitored if the addon works for every website!_
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ Some sites put much effort in obfuscating their code / how they receive the vide
|
|||||||
|
|
||||||
## 🛠️ Building
|
## 🛠️ Building
|
||||||
|
|
||||||
If you want to build the addon from source and not using the [installation](#installation) way, follow the instructions.
|
If you want to build the addon from source and not using the way described in [installation](#-installation), follow the instructions.
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
@@ -120,18 +125,20 @@ If the requirements are satisfied, you can continue with the following commands:
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
# install all dependencies
|
# install all dependencies
|
||||||
$ npm install
|
$ npm i
|
||||||
|
|
||||||
# build the extension source to the dist/ directory
|
# build the extension and start it in a new firefox instance
|
||||||
$ npm run build
|
$ npm run dev:firefox
|
||||||
|
|
||||||
# same as build + more optimizations and browser specific settings at release/
|
# build the extension with optimizations to the .output/firefox-mv2 directory
|
||||||
$ npm run release:firefox # or "release:chrome" to create a release for chromium based browsers
|
$ npm run build:firefox
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can omit the `:firefox` suffix, then it's built for Chrome.
|
||||||
|
|
||||||
##### Install
|
##### Install
|
||||||
|
|
||||||
If you want to use the addon in Chromium or any browser which is based on it, follow the steps in [installation](#-installation).
|
If you want to use the addon in Chromium or any browser which is based on it, follow the steps in the [manual installation](#-installation).
|
||||||
When using firefox, use the following:
|
When using firefox, use the following:
|
||||||
|
|
||||||
1. Type `about:debugging` in the browser's address bar.
|
1. Type `about:debugging` in the browser's address bar.
|
||||||
@@ -141,27 +148,34 @@ When using firefox, use the following:
|
|||||||
|
|
||||||
## ⚙️ Settings
|
## ⚙️ Settings
|
||||||
|
|
||||||
### <ins>ff2mpv: use mpv to directly play streams</ins>
|
> You reach the settings by pressing the tree dots (⋮) in the top right corner of the extension popup.
|
||||||
|
|
||||||
ff2mpv is located at this repository: https://github.com/woodruffw/ff2mpv
|
### Hosts
|
||||||
|
|
||||||
Steps to get it set up:
|
You can enable or disabled for which hosts the extension should redirect.
|
||||||
|
|
||||||
|
### ff2mpv
|
||||||
|
|
||||||
|
[ff2mpv](https://github.com/woodruffw/ff2mpv) allows you to play streams directly in [mpv](https://mpv.io/) instead of the browser.
|
||||||
|
You can enable or disable this behavior.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><i>Steps to get it set up</i></summary>
|
||||||
|
|
||||||
- In the [Usage](https://github.com/woodruffw/ff2mpv#usage) section of the ff2mpv repository pick the installation instruction for your operating system (Linux/Windows/macOS; you do not need the browser addon).
|
- In the [Usage](https://github.com/woodruffw/ff2mpv#usage) section of the ff2mpv repository pick the installation instruction for your operating system (Linux/Windows/macOS; you do not need the browser addon).
|
||||||
- Scroll down to `Install manually`
|
- Scroll down to `Install manually`
|
||||||
- Follow instructions for Firefox/Chrome
|
- Follow instructions for Firefox/Chrome
|
||||||
- Edit the `ff2mpv.json` you created:
|
- Edit the `ff2mpv.json` you created:
|
||||||
- Firefox: Add `{55dd42e8-3dd9-455a-b4fe-86664881b10c}` to `allowed_extensions` ->
|
- Firefox: Add `{55dd42e8-3dd9-455a-b4fe-86664881b10c}` to `allowed_extensions`:
|
||||||
|
|
||||||
```
|
```
|
||||||
"allowed_extensions": [
|
"allowed_extensions": [
|
||||||
"ff2mpv@yossarian.net",
|
"ff2mpv@yossarian.net",
|
||||||
"{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
|
"{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
- Chrome/Chromium:
|
|
||||||
- Go To: Settings -> Extensions
|
- Chrome/Chromium: - Go To: Settings -> Extensions - Click on `Details` of the Stream Bypass extension and copy the ID - Add `chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins`:
|
||||||
- Click on `Details` of the Stream Bypass extension and copy the ID
|
|
||||||
- Add `chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins` ->
|
|
||||||
```
|
```
|
||||||
"allowed_origins": [
|
"allowed_origins": [
|
||||||
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
|
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
|
||||||
@@ -169,6 +183,8 @@ Steps to get it set up:
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## ⚖ License
|
## ⚖ License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details.
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details.
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import prettier from 'eslint-config-prettier';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { includeIgnoreFile } from '@eslint/compat';
|
||||||
import js from '@eslint/js';
|
import js from '@eslint/js';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
import svelte from 'eslint-plugin-svelte';
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import ts from 'typescript-eslint';
|
import ts from 'typescript-eslint';
|
||||||
|
|
||||||
export default ts.config(
|
const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
...ts.configs.recommended,
|
...ts.configs.recommended,
|
||||||
...svelte.configs['flat/recommended'],
|
...svelte.configs[('flat/recommended', 'flat/prettier')],
|
||||||
prettier,
|
prettier,
|
||||||
...svelte.configs['flat/prettier'],
|
includeIgnoreFile(gitignorePath),
|
||||||
{
|
{
|
||||||
files: ['**/*.svelte'],
|
files: ['**/*.svelte'],
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: ts.parser
|
parser: ts.parser
|
||||||
@@ -23,18 +27,5 @@ export default ts.config(
|
|||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
'no-undef': 'off'
|
'no-undef': 'off'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
'.DS_Store',
|
|
||||||
'node_modules',
|
|
||||||
'dist',
|
|
||||||
'release',
|
|
||||||
'.idea',
|
|
||||||
'.env',
|
|
||||||
'.env.*',
|
|
||||||
'!.env.example',
|
|
||||||
'package-lock.json'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
6210
package-lock.json
generated
74
package.json
@@ -1,21 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "stream-bypass",
|
"name": "stream-bypass",
|
||||||
"version": "3.1.1",
|
"version": "4.0.0",
|
||||||
"displayName": "Stream Bypass",
|
"displayName": "Stream Bypass",
|
||||||
"author": "bytedream",
|
"author": "bytedream",
|
||||||
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
|
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"watch": "vite build --watch --mode development --minify false",
|
|
||||||
"dev": "vite",
|
|
||||||
"serve:firefox": "web-ext run --start-url \"about:debugging#/runtime/this-firefox\" --source-dir ./dist/",
|
|
||||||
"serve:chrome": "web-ext run -t chromium --start-url \"https://example.com\" --source-dir ./dist/",
|
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
||||||
"lint": "prettier --check --plugin prettier-plugin-svelte . && eslint .",
|
|
||||||
"format": "prettier --write --plugin prettier-plugin-svelte .",
|
|
||||||
"release:firefox": "MANIFEST_VERSION=2 vite build --outDir release/firefox",
|
|
||||||
"release:chrome": "MANIFEST_VERSION=3 vite build --outDir release/chrome"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -24,27 +12,43 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bytedream/stream-bypass/issues"
|
"url": "https://github.com/bytedream/stream-bypass/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"type": "module",
|
||||||
"@samrum/vite-plugin-web-extension": "^5.1.1",
|
"scripts": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
"dev": "wxt",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"dev:firefox": "wxt -b firefox",
|
||||||
"@types/chrome": "^0.0.287",
|
"build": "wxt build",
|
||||||
"@types/firefox-webext-browser": "^120.0.4",
|
"build:firefox": "wxt build -b firefox",
|
||||||
"eslint": "^9.17.0",
|
"zip": "wxt zip",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"zip:firefox": "wxt zip -b firefox",
|
||||||
"eslint-plugin-svelte": "^2.46.1",
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
"hls.js": "^1.5.17",
|
"postinstall": "wxt prepare",
|
||||||
"prettier": "^3.4.2",
|
"format": "prettier --write .",
|
||||||
"prettier-plugin-svelte": "^3.3.2",
|
"lint": "prettier --check . && eslint ."
|
||||||
"sass": "^1.83.0",
|
|
||||||
"svelte": "^5.14.0",
|
|
||||||
"svelte-check": "^4.1.1",
|
|
||||||
"svelte-preprocess": "^6.0.3",
|
|
||||||
"tslib": "^2.8.1",
|
|
||||||
"typescript": "^5.7.2",
|
|
||||||
"typescript-eslint": "^8.18.0",
|
|
||||||
"vite": "^6.0.3",
|
|
||||||
"web-ext": "^8.3.0"
|
|
||||||
},
|
},
|
||||||
"type": "module"
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.4.1",
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
||||||
|
"@steeze-ui/heroicons": "^2.4.2",
|
||||||
|
"@steeze-ui/svelte-icon": "^1.6.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
|
"@tsconfig/svelte": "^5.0.5",
|
||||||
|
"@wxt-dev/module-svelte": "^2.0.4",
|
||||||
|
"eslint": "^9.38.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-svelte": "^3.12.5",
|
||||||
|
"hls.js": "^1.6.13",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
|
"sass": "^1.93.2",
|
||||||
|
"svelte": "^5.43.2",
|
||||||
|
"svelte-check": "^4.3.3",
|
||||||
|
"tailwindcss": "^4.1.16",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.46.2",
|
||||||
|
"vite": "^7.1.12",
|
||||||
|
"web-ext": "^9.1.0",
|
||||||
|
"wxt": "^0.20.11"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 713 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 792 B After Width: | Height: | Size: 792 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
21
src/assets/base.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-linux-mint-green: #35a854;
|
||||||
|
|
||||||
|
--default-font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
background-color: #030712;
|
||||||
|
color: var(--color-gray-300);
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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'] }
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import './shared';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
chrome.runtime.onMessage.addListener(async (message) => {
|
|
||||||
if (message.action == 'ff2mpv') {
|
|
||||||
await chrome.runtime.sendNativeMessage('ff2mpv', { url: message.url });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import type { Match } from '~/lib/match';
|
|
||||||
import { getMatch } from '~/lib/match';
|
|
||||||
import { Other, Redirect } from '~/lib/settings';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let match: Match | null;
|
|
||||||
let redirect = false;
|
|
||||||
if ((match = await getMatch(window.location.host)) === null) {
|
|
||||||
if ((match = await Redirect.get()) === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
redirect = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// some sites have a javascript based redirect, e.g. example.com redirects to example.org by changing
|
|
||||||
// window.location.href instead of a 3XX http redirect. an empty body is a sign that such a javascript redirect
|
|
||||||
// occurred
|
|
||||||
if (document.body == null) {
|
|
||||||
await Redirect.set(match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let re = null;
|
|
||||||
for (const regex of match.regex) {
|
|
||||||
if ((re = document.body.innerHTML.match(regex)) !== null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (re === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (redirect) {
|
|
||||||
await Redirect.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
let url: string | null;
|
|
||||||
try {
|
|
||||||
url = await match.match(re);
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application
|
|
||||||
if (await Other.getFf2mpv()) {
|
|
||||||
await chrome.runtime.sendMessage({ action: 'ff2mpv', url: url });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match.replace && !url.includes('.m3u8')) {
|
|
||||||
// this destroys all intervals that may spawn popups or events
|
|
||||||
let intervalId = window.setInterval(() => {}, 0);
|
|
||||||
while (intervalId--) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
let timeoutId = window.setTimeout(() => {}, 0);
|
|
||||||
while (timeoutId--) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear completed document
|
|
||||||
document.documentElement.innerHTML = '';
|
|
||||||
|
|
||||||
document.body.style.backgroundColor = '#131313';
|
|
||||||
|
|
||||||
// video player
|
|
||||||
const player = document.createElement('video');
|
|
||||||
player.style.width = '100%';
|
|
||||||
player.style.height = '100%';
|
|
||||||
player.controls = true;
|
|
||||||
player.src = url;
|
|
||||||
|
|
||||||
// add video player to document body
|
|
||||||
document.body.style.margin = '0';
|
|
||||||
document.body.append(player);
|
|
||||||
} else {
|
|
||||||
window.location.assign(
|
|
||||||
chrome.runtime.getURL(
|
|
||||||
`src/entries/player/player.html?id=${match.id}&url=${encodeURIComponent(url)}&domain=${
|
|
||||||
window.location.hostname
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { play } from '~/entries/player/player';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
let errorMessage: string | null = $state(null);
|
|
||||||
|
|
||||||
let videoElem: HTMLVideoElement;
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
await play(videoElem);
|
|
||||||
videoElem.controls = true;
|
|
||||||
} catch (e) {
|
|
||||||
errorMessage = e as string;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_media_has_caption -->
|
|
||||||
<video id="video" bind:this={videoElem}></video>
|
|
||||||
{#if errorMessage}
|
|
||||||
<div id="message-container">
|
|
||||||
<p>
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
||||||
{@html errorMessage}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- eslint-disable -->
|
|
||||||
<style lang="scss" global>
|
|
||||||
body {
|
|
||||||
background-color: #131313;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#message-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
& a,
|
|
||||||
& a:visited {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { matches } from '~/lib/match';
|
|
||||||
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);
|
|
||||||
|
|
||||||
videoElem.src = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function playHls(url: string, domain: string, videoElem: HTMLVideoElement) {
|
|
||||||
if (videoElem.canPlayType('application/vnd.apple.mpegurl')) {
|
|
||||||
videoElem.src = url;
|
|
||||||
} else if (Hls.isSupported()) {
|
|
||||||
const hls = new Hls({
|
|
||||||
enableWorker: false,
|
|
||||||
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
|
|
||||||
await UrlReferer.set(new URL(url).hostname, domain);
|
|
||||||
xhr.open('GET', url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
hls.loadSource(url);
|
|
||||||
hls.attachMedia(videoElem);
|
|
||||||
} else {
|
|
||||||
throw 'Failed to play m3u8 video (hls is not supported). Try again or create a new issue <a href="https://github.com/ByteDream/stream-bypass/issues/new">here</a>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function play(videoElem: HTMLVideoElement) {
|
|
||||||
const urlQuery = new URLSearchParams(window.location.search);
|
|
||||||
const id = urlQuery.get('id') as string;
|
|
||||||
const url = decodeURIComponent(urlQuery.get('url') as string);
|
|
||||||
const domain = urlQuery.get('domain') as string;
|
|
||||||
|
|
||||||
const match = matches[id];
|
|
||||||
if (match === undefined) {
|
|
||||||
throw `Invalid id: ${id}. Please report this <a href="https://github.com/ByteDream/stream-bypass/issues">here</a>`;
|
|
||||||
}
|
|
||||||
document.title = `Stream Bypass (${domain})`;
|
|
||||||
|
|
||||||
if (new URL(url).pathname.endsWith('.m3u8')) {
|
|
||||||
await playHls(url, domain, videoElem);
|
|
||||||
} else {
|
|
||||||
await playNative(url, domain, videoElem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { type Match, matches } from '~/lib/match';
|
|
||||||
import { Hosters, Other } from '~/lib/settings';
|
|
||||||
import Toggle from './toggle.svelte';
|
|
||||||
|
|
||||||
let hostersEnabled: boolean;
|
|
||||||
let hosters: (Match & { active: boolean; disabled: boolean })[] = [];
|
|
||||||
(async () => {
|
|
||||||
hostersEnabled = !(await Hosters.getAllDisabled());
|
|
||||||
|
|
||||||
const disabled = await Hosters.getDisabled();
|
|
||||||
hosters = Object.values(matches).map((m: any) => {
|
|
||||||
m['active'] = disabled.findIndex((p) => p.id == m.id) == -1;
|
|
||||||
return m;
|
|
||||||
}) as typeof hosters;
|
|
||||||
})();
|
|
||||||
|
|
||||||
let isMobile: boolean;
|
|
||||||
(async () => {
|
|
||||||
isMobile = (await browser.runtime.getPlatformInfo()).os === 'android';
|
|
||||||
})();
|
|
||||||
|
|
||||||
let ff2mpvEnabled: boolean;
|
|
||||||
(async () => {
|
|
||||||
ff2mpvEnabled = (await Other.getFf2mpv()) as boolean;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<main
|
|
||||||
style={isMobile
|
|
||||||
? 'height: 100vh; display: flex; flex-direction: column; align-items: center'
|
|
||||||
: 'height: 500px'}
|
|
||||||
>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Hoster</legend>
|
|
||||||
<div class="setting-container" style={isMobile ? 'grid-column-gap: 5rem' : ''}>
|
|
||||||
<label for="hosters-enabled">Enabled</label>
|
|
||||||
<div>
|
|
||||||
<Toggle
|
|
||||||
bind:checked={hostersEnabled}
|
|
||||||
id="hosters-enabled"
|
|
||||||
onChange={() => Hosters.setAll(hostersEnabled)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
{#each hosters as hoster, i}
|
|
||||||
<label for="hoster-{i}" style="cursor: {hostersEnabled ? 'pointer' : 'default'}"
|
|
||||||
>{hoster.name}</label
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Toggle
|
|
||||||
bind:checked={hoster.active}
|
|
||||||
disabled={!hostersEnabled}
|
|
||||||
id="hoster-{i}"
|
|
||||||
onChange={async () => {
|
|
||||||
if (hoster.active) {
|
|
||||||
await Hosters.enable(hoster);
|
|
||||||
} else {
|
|
||||||
await Hosters.disable(hoster);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></Toggle>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{#if !isMobile}
|
|
||||||
<fieldset>
|
|
||||||
<legend>Other</legend>
|
|
||||||
<div class="setting-container">
|
|
||||||
<label for="ff2mpv">ff2mpv</label>
|
|
||||||
<div>
|
|
||||||
<Toggle
|
|
||||||
bind:checked={ff2mpvEnabled}
|
|
||||||
id="ff2mpv"
|
|
||||||
onChange={async () => {
|
|
||||||
ff2mpvEnabled = !ff2mpvEnabled;
|
|
||||||
if (await browser.permissions.request({ permissions: ['nativeMessaging'] })) {
|
|
||||||
await Other.setFf2mpv(ff2mpvEnabled);
|
|
||||||
ff2mpvEnabled = !ff2mpvEnabled;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></Toggle>
|
|
||||||
<a
|
|
||||||
class="info-questionmark"
|
|
||||||
href="https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams"
|
|
||||||
>?</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{/if}
|
|
||||||
<a id="report-notice" href="https://github.com/ByteDream/stream-bypass/issues"
|
|
||||||
>Report issues or requests</a
|
|
||||||
>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- eslint-disable -->
|
|
||||||
<style lang="scss" global>
|
|
||||||
body {
|
|
||||||
background-color: #2b2a33;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
border-radius: 5px;
|
|
||||||
border-color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
#report-notice {
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
display: block;
|
|
||||||
font-weight: lighter;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 5px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto;
|
|
||||||
grid-column-gap: 5px;
|
|
||||||
grid-row-gap: 4px;
|
|
||||||
align-items: end;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
& > label {
|
|
||||||
height: 34px;
|
|
||||||
margin: 0;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > hr {
|
|
||||||
grid-column: 1 / span 2;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-questionmark {
|
|
||||||
display: inline-block;
|
|
||||||
transform: translateX(-40%) translateY(-100%);
|
|
||||||
color: black;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html style="overflow-y: hidden" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Stream Bypass</title>
|
|
||||||
</head>
|
|
||||||
<body style="overflow-y: scroll">
|
|
||||||
<script type="module">
|
|
||||||
import Popup from '~/entries/popup/Popup.svelte';
|
|
||||||
import { mount } from 'svelte';
|
|
||||||
|
|
||||||
mount(Popup, {
|
|
||||||
target: document.body
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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>
|
|
||||||
30
src/entrypoints/background/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { UrlReferer } from '@/lib/settings';
|
||||||
|
|
||||||
|
export default defineBackground(() => {
|
||||||
|
browser.runtime.onMessage.addListener(async (message) => {
|
||||||
|
if (message.action == 'ff2mpv') {
|
||||||
|
await browser.runtime.sendNativeMessage('ff2mpv', { url: message.url });
|
||||||
|
}
|
||||||
|
|
||||||
|
// the following listener is only available in mv2
|
||||||
|
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
||||||
|
|
||||||
|
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
async (details) => {
|
||||||
|
const referer = await UrlReferer.get(new URL(details.url).hostname);
|
||||||
|
if (!referer) return;
|
||||||
|
|
||||||
|
details.requestHeaders!.push({
|
||||||
|
name: 'Referer',
|
||||||
|
value: `https://${referer}/`
|
||||||
|
});
|
||||||
|
|
||||||
|
return { requestHeaders: details.requestHeaders };
|
||||||
|
},
|
||||||
|
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] },
|
||||||
|
['blocking', 'requestHeaders']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
78
src/entrypoints/content/index.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { getHost, hosts, type Host, type HostMatch } from '@/lib/host';
|
||||||
|
import { FF2MPVSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
export default defineContentScript({
|
||||||
|
matches: [
|
||||||
|
...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`)),
|
||||||
|
// only mv2 allows to match all urls
|
||||||
|
...(import.meta.env.MANIFEST_VERSION === 2 ? ['<all_urls>'] : [])
|
||||||
|
],
|
||||||
|
allFrames: true,
|
||||||
|
runAt: 'document_end',
|
||||||
|
main
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let host: Host | null;
|
||||||
|
if ((host = await getHost(window.location.host)) === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let re = null;
|
||||||
|
for (const regex of host.regex) {
|
||||||
|
if ((re = document.body.innerHTML.match(regex)) !== null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (re === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hostMatch: HostMatch | null;
|
||||||
|
try {
|
||||||
|
hostMatch = await host.match(re);
|
||||||
|
} catch {
|
||||||
|
hostMatch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hostMatch || !hostMatch.url) return;
|
||||||
|
|
||||||
|
// send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application
|
||||||
|
if (await FF2MPVSettings.getEnabled()) {
|
||||||
|
await browser.runtime.sendMessage({ action: 'ff2mpv', url: hostMatch.url });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.replace && hostMatch.type != 'hls') {
|
||||||
|
// this destroys all intervals that may spawn popups or events
|
||||||
|
let intervalId = window.setInterval(() => {}, 0);
|
||||||
|
while (intervalId--) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
let timeoutId = window.setTimeout(() => {}, 0);
|
||||||
|
while (timeoutId--) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear completed document
|
||||||
|
document.documentElement.innerHTML = '';
|
||||||
|
|
||||||
|
document.body.style.backgroundColor = '#131313';
|
||||||
|
|
||||||
|
// video player
|
||||||
|
const player = document.createElement('video');
|
||||||
|
player.style.width = '100%';
|
||||||
|
player.style.height = '100%';
|
||||||
|
player.controls = true;
|
||||||
|
player.src = hostMatch.url;
|
||||||
|
|
||||||
|
// add video player to document body
|
||||||
|
document.body.style.margin = '0';
|
||||||
|
document.body.append(player);
|
||||||
|
} else {
|
||||||
|
window.location.assign(
|
||||||
|
browser.runtime.getURL(
|
||||||
|
`/player.html?id=${host.id}&url=${encodeURIComponent(hostMatch.url)}&domain=${window.location.hostname}&type=${hostMatch.type}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/entrypoints/player/Player.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { play } from './player';
|
||||||
|
|
||||||
|
let errorMessage: string | null = $state(null);
|
||||||
|
|
||||||
|
let videoElem: HTMLVideoElement;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await play(videoElem);
|
||||||
|
videoElem.controls = true;
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage = e as string;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video class="absolute top-0 left-0 w-full h-full m-0" bind:this={videoElem}></video>
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="h-full flex items-center justify-center text-center">
|
||||||
|
<p>
|
||||||
|
{errorMessage} <a class="underline" href="https://github.com/bytedream/stream-bypass/issues">here</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -3,11 +3,20 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Stream Bypass</title>
|
<title>Stream Bypass</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="background-color: #131313; overflow: hidden">
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import Player from '~/entries/player/Player.svelte';
|
|
||||||
import { mount } from 'svelte';
|
import { mount } from 'svelte';
|
||||||
|
import Player from './Player.svelte';
|
||||||
|
|
||||||
mount(Player, {
|
mount(Player, {
|
||||||
target: document.body
|
target: document.body
|
||||||
75
src/entrypoints/player/player.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import Hls from 'hls.js';
|
||||||
|
import { listenMessages, MessageType, sendMessage } from '@/lib/communication';
|
||||||
|
import { HostMatchType, hosts } from '@/lib/host';
|
||||||
|
import { UrlReferer } from '@/lib/settings';
|
||||||
|
|
||||||
|
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
|
||||||
|
// multiple hosts need to have a correct referer set
|
||||||
|
await UrlReferer.addTemporary(new URL(url).hostname, domain);
|
||||||
|
|
||||||
|
videoElem.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function playHls(url: string, domain: string, videoElem: HTMLVideoElement) {
|
||||||
|
if (videoElem.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
|
videoElem.src = url;
|
||||||
|
} else if (Hls.isSupported()) {
|
||||||
|
const hls = new Hls({
|
||||||
|
enableWorker: false,
|
||||||
|
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
|
||||||
|
// multiple hosts need to have a correct referer set
|
||||||
|
await UrlReferer.addTemporary(new URL(url).hostname, domain);
|
||||||
|
xhr.open('GET', url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hls.loadSource(url);
|
||||||
|
hls.attachMedia(videoElem);
|
||||||
|
} else {
|
||||||
|
throw 'Failed to play m3u8 video (hls is not supported). Try again or create a new issue';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function play(videoElem: HTMLVideoElement) {
|
||||||
|
const urlQuery = new URLSearchParams(window.location.search);
|
||||||
|
const id = urlQuery.get('id') as string;
|
||||||
|
const url = decodeURIComponent(urlQuery.get('url') as string);
|
||||||
|
const domain = urlQuery.get('domain') as string;
|
||||||
|
const type = urlQuery.get('type') as HostMatchType;
|
||||||
|
|
||||||
|
const host = hosts.find((host) => host.id === id);
|
||||||
|
if (!host) {
|
||||||
|
throw `Invalid id: ${id}. Please report this`;
|
||||||
|
}
|
||||||
|
document.title = `Stream Bypass (${domain})`;
|
||||||
|
|
||||||
|
initCommunication(id, url, domain);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case HostMatchType.NATIVE:
|
||||||
|
await playNative(url, domain, videoElem);
|
||||||
|
break;
|
||||||
|
case HostMatchType.HLS:
|
||||||
|
await playHls(url, domain, videoElem);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCommunication(id: string, url: string, domain: string) {
|
||||||
|
const notifyActiveMatch = () =>
|
||||||
|
sendMessage(MessageType.NotifyActiveMatch, {
|
||||||
|
id: id,
|
||||||
|
url: url,
|
||||||
|
domain: domain
|
||||||
|
});
|
||||||
|
|
||||||
|
// if an extension popup is open, it will be notified that a match/player is now active
|
||||||
|
notifyActiveMatch();
|
||||||
|
|
||||||
|
// if an extension popup is opened, the listener will recognize it's active match request and send the match/player
|
||||||
|
// data
|
||||||
|
const cancel = listenMessages((type) => {
|
||||||
|
if (type !== MessageType.RequestActiveMatch) return;
|
||||||
|
notifyActiveMatch();
|
||||||
|
});
|
||||||
|
window.onbeforeunload = cancel;
|
||||||
|
}
|
||||||
33
src/entrypoints/popup/App.svelte
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import '@/assets/base.css';
|
||||||
|
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
|
||||||
|
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
|
||||||
|
import { isMobile } from '@/entrypoints/popup/state.js';
|
||||||
|
|
||||||
|
/* state init */
|
||||||
|
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
type Page = 'main' | 'settings';
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let activePage = $state<Page>('main');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex w-[350px] overflow-hidden" class:w-screen={$isMobile}>
|
||||||
|
{#if activePage === 'main'}
|
||||||
|
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-[300px] flex-1 flex flex-col">
|
||||||
|
<Main onSettingsOpenRequest={() => (activePage = 'settings')} />
|
||||||
|
</div>
|
||||||
|
{:else if activePage === 'settings'}
|
||||||
|
<div
|
||||||
|
transition:fly={{ x: 300, duration: 150 }}
|
||||||
|
class="min-w-full w-full h-[300px] flex-1 flex flex-col"
|
||||||
|
class:h-screen={$isMobile}
|
||||||
|
>
|
||||||
|
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
1
src/entrypoints/popup/components/Divider.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div class="w-full border-b-[1px] border-gray-400"></div>
|
||||||
57
src/entrypoints/popup/components/Toggle.svelte
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
checked: boolean;
|
||||||
|
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { checked = $bindable(), onChecked, size = 'md' }: Props = $props();
|
||||||
|
let internalChecked = $state($state.snapshot(checked));
|
||||||
|
|
||||||
|
/* callbacks */
|
||||||
|
async function onInputChange() {
|
||||||
|
internalChecked = !internalChecked;
|
||||||
|
let approved = false;
|
||||||
|
|
||||||
|
if (!onChecked) {
|
||||||
|
approved = true;
|
||||||
|
} else {
|
||||||
|
const ret = onChecked(!internalChecked);
|
||||||
|
|
||||||
|
if (typeof ret === 'boolean') {
|
||||||
|
approved = ret;
|
||||||
|
} else if (typeof ret === 'object' && 'then' in ret && typeof ret.then === 'function') {
|
||||||
|
const promiseRet = await ret;
|
||||||
|
if (typeof promiseRet === 'undefined') approved = true;
|
||||||
|
else approved = promiseRet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approved) {
|
||||||
|
internalChecked = !internalChecked;
|
||||||
|
checked = internalChecked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label class="flex items-center cursor-pointer">
|
||||||
|
<div class="relative">
|
||||||
|
<input type="checkbox" class="peer sr-only" bind:checked={internalChecked} onchange={onInputChange} />
|
||||||
|
<div
|
||||||
|
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
|
||||||
|
class:w-8={size === 'sm'}
|
||||||
|
class:h-4={size === 'sm'}
|
||||||
|
class:w-10={size === 'md'}
|
||||||
|
class:h-5={size === 'md'}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute flex items-center justify-center transition bg-white rounded-full dot left-0 top-0 peer-checked:translate-x-full"
|
||||||
|
class:w-4={size === 'sm'}
|
||||||
|
class:h-4={size === 'sm'}
|
||||||
|
class:w-5={size === 'md'}
|
||||||
|
class:h-5={size === 'md'}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
13
src/entrypoints/popup/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Stream Bypass</title>
|
||||||
|
<meta name="manifest.type" content="browser_action" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" class="contents h-full"></div>
|
||||||
|
<script type="module" src="./main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
src/entrypoints/popup/main.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
|
import App from './App.svelte';
|
||||||
|
|
||||||
|
const app = mount(App, {
|
||||||
|
target: document.getElementById('app')!
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
3
src/entrypoints/popup/pages/main/AllDisabled.svelte
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="h-full flex items-center justify-center">
|
||||||
|
<p class="text-[1.05rem]">Extension disabled</p>
|
||||||
|
</div>
|
||||||
69
src/entrypoints/popup/pages/main/CopyMatch.svelte
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
import { isMobile } from '@/entrypoints/popup/state';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
url: string;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UrlType = 'url' | 'curl';
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { url, domain }: Props = $props();
|
||||||
|
|
||||||
|
let urlOutputType: UrlType = $state('url');
|
||||||
|
let urlOutput = $derived(getUrl(urlOutputType));
|
||||||
|
|
||||||
|
/* functions */
|
||||||
|
function getUrl(type: UrlType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'url':
|
||||||
|
return url;
|
||||||
|
case 'curl':
|
||||||
|
return `curl -H "Referer: https://${domain}/" "${encodeURI(url)}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* callbacks */
|
||||||
|
function copyUrl() {
|
||||||
|
navigator.clipboard.writeText(urlOutput);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex gap-2 items-center pb-1">
|
||||||
|
<p class="mt-0.5 text-sm">Show video as</p>
|
||||||
|
<select
|
||||||
|
class="w-fit text-xs border text-slate-200 border-gray-500 rounded cursor-pointer pt-1 pb-0.5 pl-1"
|
||||||
|
bind:value={urlOutputType}
|
||||||
|
>
|
||||||
|
<option value="url">URL</option>
|
||||||
|
<option value="curl">cURL</option>
|
||||||
|
</select>
|
||||||
|
{#if urlOutputType === 'url'}
|
||||||
|
<div class="relative group h-4 flex justify-center items-center">
|
||||||
|
<button class="text-sm peer"><Icon src={InformationCircle} size="1rem" /></button>
|
||||||
|
<span
|
||||||
|
class="z-10 absolute w-58 bottom-5/4 p-1 bg-gray-800 text-xs invisible opacity-0 group-hover:visible group-hover:opacity-100 peer-focus:visible peer-focus:opacity-100 transition-[opacity]"
|
||||||
|
>You may have to send the referer header <code class="select-text">Referer: {domain}</code> when accessing
|
||||||
|
the url</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<div class="relative group w-full" tabindex={$isMobile ? 0 : undefined}>
|
||||||
|
<pre
|
||||||
|
class="w-full h-20 overflow-y-scroll text-[0.8rem] wrap-anywhere text-wrap select-text rounded bg-gray-900 py-[0.25rem] px-1.5">{urlOutput}</pre>
|
||||||
|
<div
|
||||||
|
class="absolute top-2 right-2 transition-opacity duration-100 opacity-0 group-hover:opacity-100 group-focus:opacity-100 h-full"
|
||||||
|
>
|
||||||
|
<button class="cursor-pointer" title="Copy to clipboard" onclick={copyUrl}>
|
||||||
|
<Icon src={Clipboard} size="1rem" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
46
src/entrypoints/popup/pages/main/Header.svelte
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
allHostsDisabled: boolean;
|
||||||
|
onSettingsClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { allHostsDisabled = $bindable(), onSettingsClick }: Props = $props();
|
||||||
|
|
||||||
|
/* effects */
|
||||||
|
$effect(() => {
|
||||||
|
HostSettings.setAllHostsDisabled(allHostsDisabled);
|
||||||
|
browser.browserAction.setIcon({
|
||||||
|
path: allHostsDisabled
|
||||||
|
? {
|
||||||
|
16: 'icon/stream-bypass_disabled@16px.png',
|
||||||
|
32: 'icon/stream-bypass_disabled@32px.png',
|
||||||
|
48: 'icon/stream-bypass_disabled@48px.png',
|
||||||
|
128: 'icon/stream-bypass_disabled@128px.png'
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
16: 'icon/stream-bypass@16px.png',
|
||||||
|
32: 'icon/stream-bypass@32px.png',
|
||||||
|
48: 'icon/stream-bypass@48px.png',
|
||||||
|
128: 'icon/stream-bypass@128px.png'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center p-2">
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<h1>stream-bypass</h1>
|
||||||
|
<span class="text-xs text-gray-400">v{import.meta.env.VERSION}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{#key allHostsDisabled}
|
||||||
|
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
|
||||||
|
{/key}
|
||||||
|
<button class="font-bold cursor-pointer" onclick={() => onSettingsClick()}>⋮</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
65
src/entrypoints/popup/pages/main/Main.svelte
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import '@/assets/base.css';
|
||||||
|
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
|
import AllDisabled from '@/entrypoints/popup/pages/main/AllDisabled.svelte';
|
||||||
|
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
|
||||||
|
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
|
||||||
|
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
|
||||||
|
import NoMatch from '@/entrypoints/popup/pages/main/NoMatch.svelte';
|
||||||
|
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
|
||||||
|
import { hosts, type Host } from '@/lib/host';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
onSettingsOpenRequest: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { onSettingsOpenRequest }: Props = $props();
|
||||||
|
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||||
|
|
||||||
|
let allHostsDisabled = $state(false);
|
||||||
|
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
|
||||||
|
|
||||||
|
/* lifecycle */
|
||||||
|
const cancel = listenMessages((type, data) => {
|
||||||
|
if (type !== MessageType.NotifyActiveMatch) return;
|
||||||
|
currentMatch = {
|
||||||
|
host: hosts.find((host) => host.id === data.id)!,
|
||||||
|
url: data.url,
|
||||||
|
domain: data.domain
|
||||||
|
};
|
||||||
|
});
|
||||||
|
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
|
||||||
|
|
||||||
|
onDestroy(cancel);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
<div class="px-2 h-full">
|
||||||
|
{#if allHostsDisabled}
|
||||||
|
<AllDisabled />
|
||||||
|
{:else if !currentMatch}
|
||||||
|
<NoMatch />
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col justify-between h-full pb-2">
|
||||||
|
<Match host={currentMatch.host} domain={currentMatch.domain} />
|
||||||
|
<div class="divider border-dashed"></div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
@apply select-none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
src/entrypoints/popup/pages/main/Match.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
host: Host;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
const { host, domain }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center w-full h-full">
|
||||||
|
<p class="text-lg">Match found:</p>
|
||||||
|
<div class="[&>*]:select-text">
|
||||||
|
<span class="underline text-green-400 text-2xl font-bold">{host.name}</span>
|
||||||
|
<span class="text-xs text-slate-300">({domain})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
13
src/entrypoints/popup/pages/main/NoMatch.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="relative h-full">
|
||||||
|
<div class="h-full flex items-center justify-center">
|
||||||
|
<p class="text-[1.05rem]">No supported video found on this site</p>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0.5">
|
||||||
|
<p class="text-xs text-gray-400">
|
||||||
|
Suggestions or bugs can be submitted <a
|
||||||
|
class="underline"
|
||||||
|
href="https://github.com/bytedream/stream-bypass/issues">here</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
35
src/entrypoints/popup/pages/settings/Ff2mpv.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { InformationCircle } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
|
import { FF2MPVSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let enabled = $state(false);
|
||||||
|
FF2MPVSettings.getEnabled().then((val) => (enabled = val));
|
||||||
|
|
||||||
|
/* callbacks */
|
||||||
|
function onEnableChange(enabled: boolean) {
|
||||||
|
if (!enabled) return true;
|
||||||
|
|
||||||
|
return browser.permissions.request({ permissions: ['nativeMessaging'] });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="relative mr-3">
|
||||||
|
<span>Communication enabled</span>
|
||||||
|
<a
|
||||||
|
class="absolute -top-1 -right-4 text-sm"
|
||||||
|
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv-use-mpv-to-directly-play-streams"
|
||||||
|
target="_blank"><Icon src={InformationCircle} size="1rem" /></a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{#key enabled}
|
||||||
|
<Toggle
|
||||||
|
bind:checked={() => enabled, (v) => FF2MPVSettings.setEnabled(v)}
|
||||||
|
onChecked={onEnableChange}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
16
src/entrypoints/popup/pages/settings/Header.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
onBackClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { onBackClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center p-2">
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<button class="cursor-pointer" onclick={() => onBackClick()}>←</button>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
41
src/entrypoints/popup/pages/settings/HostsTable.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
|
import { hosts } from '@/lib/host';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let disabledHostIds = $state<Array<string>>([]);
|
||||||
|
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[35%_43%_22%] gap-y-0.75">
|
||||||
|
<p class="font-bold">Host</p>
|
||||||
|
<p class="font-bold">Domains</p>
|
||||||
|
<p class="font-bold">Enabled</p>
|
||||||
|
{#each hosts as host (host.id)}
|
||||||
|
{@const domainList = host.domains.join(', ')}
|
||||||
|
<p>{host.name}</p>
|
||||||
|
<div>
|
||||||
|
<label for={host.id}>
|
||||||
|
<input id={host.id} type="checkbox" class="peer hidden" checked />
|
||||||
|
<p
|
||||||
|
title={domainList}
|
||||||
|
class="cursor-pointer overflow-hidden peer-checked:text-ellipsis peer-checked:text-nowrap"
|
||||||
|
>
|
||||||
|
{domainList}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[.2rem]">
|
||||||
|
{#key disabledHostIds}
|
||||||
|
<Toggle
|
||||||
|
bind:checked={
|
||||||
|
() => !disabledHostIds.includes(host.id),
|
||||||
|
(v) => (v ? HostSettings.removeDisabledHost(host) : HostSettings.addDisabledHost(host))
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
></Toggle>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
60
src/entrypoints/popup/pages/settings/Settings.svelte
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
|
import Ff2mpv from '@/entrypoints/popup/pages/settings/Ff2mpv.svelte';
|
||||||
|
import Header from '@/entrypoints/popup/pages/settings/Header.svelte';
|
||||||
|
import HostsTable from '@/entrypoints/popup/pages/settings/HostsTable.svelte';
|
||||||
|
import { isMobile } from '@/entrypoints/popup/state';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
onSettingsCloseRequest: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { onSettingsCloseRequest }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<Header onBackClick={onSettingsCloseRequest} />
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-y-1 pt-1 h-full mx-2 my-1 overflow-y-scroll">
|
||||||
|
<details class="details" open>
|
||||||
|
<summary>Hosts</summary>
|
||||||
|
<HostsTable />
|
||||||
|
</details>
|
||||||
|
{#if !$isMobile}
|
||||||
|
<details class="details">
|
||||||
|
<summary>ff2mpv</summary>
|
||||||
|
<Ff2mpv />
|
||||||
|
</details>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
@apply select-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
/* using normal css instead of tailwind in the following blocks.
|
||||||
|
for some reason tailwind fails to resolve many references */
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid var(--color-gray-600);
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] > summary {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
3
src/entrypoints/popup/state.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const isMobile = writable(false);
|
||||||
36
src/lib/communication.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export enum MessageType {
|
||||||
|
RequestActiveMatch,
|
||||||
|
NotifyActiveMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageData<T extends MessageType> = {
|
||||||
|
[MessageType.RequestActiveMatch]: undefined;
|
||||||
|
[MessageType.NotifyActiveMatch]: {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
}[T];
|
||||||
|
|
||||||
|
export async function sendMessage<T extends MessageType>(message: T, data: MessageData<T>) {
|
||||||
|
await browser.runtime.sendMessage({ type: message, data: data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMessageToActiveTab<T extends MessageType>(message: T, data: MessageData<T>) {
|
||||||
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
|
||||||
|
await browser.tabs.sendMessage(tabs[0].id!, { type: message, data: data }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenMessages(listener: (type: MessageType, data: any) => void): () => void {
|
||||||
|
const callback = (callbackData: { type: MessageType; data: any }) => {
|
||||||
|
const { type, data } = callbackData;
|
||||||
|
listener(type, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
browser.runtime.onMessage.addListener(callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
browser.runtime.onMessage.removeListener(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
41
src/lib/host/doodstream.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Doodstream',
|
||||||
|
id: 'doodstream',
|
||||||
|
domains: [
|
||||||
|
'doodstream.com',
|
||||||
|
'dood.pm',
|
||||||
|
'dood.ws',
|
||||||
|
'dood.wf',
|
||||||
|
'dood.cx',
|
||||||
|
'dood.sh',
|
||||||
|
'dood.watch',
|
||||||
|
'dood.work',
|
||||||
|
'dood.to',
|
||||||
|
'dood.so',
|
||||||
|
'dood.la',
|
||||||
|
'dood.li',
|
||||||
|
'dood.re',
|
||||||
|
'dood.yt',
|
||||||
|
'doods.pro',
|
||||||
|
'ds2play.com',
|
||||||
|
'dooood.com',
|
||||||
|
'd000d.com'
|
||||||
|
],
|
||||||
|
replace: true,
|
||||||
|
regex: [/(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
const response = await fetch(`https://${window.location.host}${match[1]}`, {
|
||||||
|
headers: {
|
||||||
|
Range: 'bytes=0-'
|
||||||
|
},
|
||||||
|
referrer: `https://${window.location.host}/e/${window.location.pathname.split('/').slice(-1)[0]}`
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: `${await response.text()}1234567890${match[2]}${Date.now()}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
17
src/lib/host/dropload.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Dropload',
|
||||||
|
id: 'dropload',
|
||||||
|
domains: ['dropload.io'],
|
||||||
|
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
const unpacked = await unpack(match[0]);
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: unpacked.match(/(?<=file:").*(?=")/)![0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
24
src/lib/host/filemoon.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Filemoon',
|
||||||
|
id: 'filemoon',
|
||||||
|
domains: ['filemoon.sx', 'filemoon.to', 'filemoon.in'],
|
||||||
|
regex: [/(?<=<iframe\s*src=")\S*(?=")/s, /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
||||||
|
replace: true,
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
if (window.location.host.startsWith('filemoon')) {
|
||||||
|
await HostSettings.addTemporaryHostDomain(this, new URL(match[0]).host);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpacked = await unpack(match[0]);
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: unpacked.match(/(?<=file:")\S*(?=")/)![0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
15
src/lib/host/goodstream.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Goodstream',
|
||||||
|
id: 'goodstream',
|
||||||
|
domains: ['goodstream.uno'],
|
||||||
|
regex: [/(?<=file:\s+").*(?=")/g],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: match[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
78
src/lib/host/index.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import Doodstream from './doodstream';
|
||||||
|
import Dropload from './dropload';
|
||||||
|
import Filemoon from './filemoon';
|
||||||
|
import Goodstream from './goodstream';
|
||||||
|
import Kwik from './kwik';
|
||||||
|
import Loadx from './loadx';
|
||||||
|
import Luluvdo from './luluvdo';
|
||||||
|
import Mixdrop from './mixdrop';
|
||||||
|
import Mp4Upload from './mp4upload';
|
||||||
|
import Newgrounds from './newgrounds';
|
||||||
|
import StreamA2z from './streama2z';
|
||||||
|
import Streamtape from './streamtape';
|
||||||
|
import Streamzz from './streamzz';
|
||||||
|
import SuperVideo from './supervideo';
|
||||||
|
import Upstream from './upstream';
|
||||||
|
import Vidmoly from './vidmoly';
|
||||||
|
import Vidoza from './vidoza';
|
||||||
|
import Voe from './voe';
|
||||||
|
import Vupload from './vupload';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
export enum HostMatchType {
|
||||||
|
NATIVE = 'native',
|
||||||
|
HLS = 'hls'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HostMatch {
|
||||||
|
type: HostMatchType;
|
||||||
|
/** If null, it's interpreted that a url should be present but isn't, probably because the website broke */
|
||||||
|
url: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Host {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
domains: string[];
|
||||||
|
replace?: boolean;
|
||||||
|
regex: RegExp[];
|
||||||
|
notice?: string;
|
||||||
|
|
||||||
|
match(match: RegExpMatchArray): Promise<HostMatch | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hosts = [
|
||||||
|
Doodstream,
|
||||||
|
Dropload,
|
||||||
|
Filemoon,
|
||||||
|
Goodstream,
|
||||||
|
Kwik,
|
||||||
|
Loadx,
|
||||||
|
Luluvdo,
|
||||||
|
Mixdrop,
|
||||||
|
Mp4Upload,
|
||||||
|
Newgrounds,
|
||||||
|
StreamA2z,
|
||||||
|
Streamtape,
|
||||||
|
Streamzz,
|
||||||
|
SuperVideo,
|
||||||
|
Upstream,
|
||||||
|
Vidmoly,
|
||||||
|
Vidoza,
|
||||||
|
Voe,
|
||||||
|
Vupload
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function getHost(domain: string) {
|
||||||
|
if (await HostSettings.getAllHostsDisabled()) return null;
|
||||||
|
|
||||||
|
const disabledIds = await HostSettings.getDisabledHosts();
|
||||||
|
for (const host of hosts) {
|
||||||
|
if (host.domains.includes(domain)) {
|
||||||
|
if (!disabledIds.includes(host.id)) return host;
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HostSettings.checkTemporaryHostDomain(domain);
|
||||||
|
}
|
||||||
17
src/lib/host/kwik.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Kwik',
|
||||||
|
id: 'kwik',
|
||||||
|
domains: ['kwik.cx'],
|
||||||
|
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
const unpacked = await unpack(match[0]);
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: unpacked.match(/(?<=source=').*(?=')/)![0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
27
src/lib/host/loadx.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { lastPathSegment } from '@/utils/extract';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoadX',
|
||||||
|
id: 'loadx',
|
||||||
|
domains: ['loadx.ws'],
|
||||||
|
regex: [/./gm],
|
||||||
|
|
||||||
|
match: async () => {
|
||||||
|
const hash = encodeURIComponent(lastPathSegment(window.location.href));
|
||||||
|
const response = await fetch(`https://${window.location.host}/player/index.php?data=${hash}&do=getVideo`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseJson = await response.json();
|
||||||
|
const videoSource: string = responseJson['videoSource'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: videoSource.replace('\\/', '/')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
32
src/lib/host/luluvdo.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
import { lastPathSegment } from '@/utils/extract';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Luluvdo',
|
||||||
|
id: 'luluvdo',
|
||||||
|
domains: ['luluvdo.com'],
|
||||||
|
regex: [/./gm],
|
||||||
|
|
||||||
|
match: async () => {
|
||||||
|
const requestBody = new FormData();
|
||||||
|
requestBody.set('op', 'embed');
|
||||||
|
requestBody.set('file_code', lastPathSegment(window.location.href));
|
||||||
|
const response = await fetch(`https://${window.location.host}/dl`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: requestBody,
|
||||||
|
referrer: window.location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
const evalMatch = responseText.match(/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms)!;
|
||||||
|
// sometimes is packed, sometimes it's not. looks like someone forgets to obfuscate the code when pushing to
|
||||||
|
// production
|
||||||
|
const unpacked = evalMatch ? await unpack(evalMatch[0]) : responseText;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: unpacked.match(/(?<=file:").*(?=")/)![0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
18
src/lib/host/mixdrop.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Mixdrop',
|
||||||
|
id: 'mixdrop',
|
||||||
|
domains: ['mixdrop.bz', 'mixdrop.ch', 'mixdrop.co', 'mixdrop.gl', 'mixdrop.my', 'mixdrop.to'],
|
||||||
|
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
const unpacked = await unpack(match[0]);
|
||||||
|
const url = unpacked.match(/(?<=MDCore.wurl=").*(?=")/)![0];
|
||||||
|
return {
|
||||||
|
type: HostMatchType.NATIVE,
|
||||||
|
url: `https:${url}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
18
src/lib/host/mp4upload.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Mp4Upload',
|
||||||
|
id: 'mp4upload',
|
||||||
|
domains: ['mp4upload.com'],
|
||||||
|
replace: true,
|
||||||
|
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
const unpacked = await unpack(match[0]);
|
||||||
|
return {
|
||||||
|
type: HostMatchType.NATIVE,
|
||||||
|
url: unpacked.match(/(?<=player.src\(").*(?=")/)![0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
22
src/lib/host/newgrounds.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Newgrounds',
|
||||||
|
id: 'newgrounds',
|
||||||
|
domains: ['newgrounds.com'],
|
||||||
|
regex: [/.*/gm],
|
||||||
|
|
||||||
|
match: async () => {
|
||||||
|
const id = window.location.pathname.split('/').slice(-1)[0];
|
||||||
|
const response = await fetch(`https://www.newgrounds.com/portal/video/${id}`, {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: decodeURI(json['sources'][Object.keys(json['sources'])[0]][0]['src'])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
20
src/lib/host/streama2z.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Stream2Az',
|
||||||
|
id: 'stream2az',
|
||||||
|
domains: ['streama2z.com', 'streama2z.xyz'],
|
||||||
|
regex: [/https?:\/\/\S*m3u8.+(?=['"])/gm],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
if (this.domains.indexOf(window.location.hostname) !== -1) {
|
||||||
|
await HostSettings.addTemporaryHostDomain(this, window.location.hostname);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: match[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
26
src/lib/host/streamtape.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Streamtape',
|
||||||
|
id: 'streamtape',
|
||||||
|
domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'],
|
||||||
|
regex: [/id=.*(?=')/gm],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
let i = 0;
|
||||||
|
while (i < match.length) {
|
||||||
|
if (match[++i - 1] == match[i]) {
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: `https://streamtape.com/get_video?${match[i]}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
// use the old method as fallback
|
||||||
|
url: `https://streamtape.com/get_video?${match.reverse()[0]}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
17
src/lib/host/streamzz.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Streamzz',
|
||||||
|
id: 'streamzz',
|
||||||
|
domains: ['streamzz.to', 'streamz.ws'],
|
||||||
|
regex: [/(?<=\|)\w{2,}/gm],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: `https://get.${location.hostname.split('.')[0]}.tw/getlink-${
|
||||||
|
match.sort((a, b) => b.length - a.length)[0]
|
||||||
|
}.dll`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
17
src/lib/host/supervideo.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Supervideo',
|
||||||
|
id: 'supervideo',
|
||||||
|
domains: ['supervideo.cc', 'supervideo.tv'],
|
||||||
|
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
const unpacked = await unpack(match[0]);
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: unpacked.match(/(?<=file:").*(?=")/)![0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
17
src/lib/host/upstream.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { unpack } from '@/utils/content';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Upstream',
|
||||||
|
id: 'upstream',
|
||||||
|
domains: ['upstream.to'],
|
||||||
|
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
const unpacked = await unpack(match[0]);
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: unpacked.match(/(?<=file:").*(?=")/)![0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
15
src/lib/host/vidmoly.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Vidmoly',
|
||||||
|
id: 'vidmoly',
|
||||||
|
domains: ['vidmoly.me', 'vidmoly.net', 'vidmoly.to'],
|
||||||
|
regex: [/(?<=file:").+\.m3u8.*(?=")/gm],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: match[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
16
src/lib/host/vidoza.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Vidoza',
|
||||||
|
id: 'vidoza',
|
||||||
|
domains: ['vidoza.net', 'videzz.net'],
|
||||||
|
regex: [/(?<=src:\s?").+?(?=")/gm],
|
||||||
|
replace: true,
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: match[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
74
src/lib/host/voe.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
function rot13(encrypted: string) {
|
||||||
|
let decrypted = '';
|
||||||
|
for (let i = 0; i < encrypted.length; i++) {
|
||||||
|
let char = encrypted.charCodeAt(i);
|
||||||
|
if (char >= 65 && char <= 90) {
|
||||||
|
char = ((char - 65 + 13) % 26) + 65;
|
||||||
|
} else if (char >= 97 && char <= 122) {
|
||||||
|
char = ((char - 97 + 13) % 26) + 97;
|
||||||
|
}
|
||||||
|
decrypted += String.fromCharCode(char);
|
||||||
|
}
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSpecialSequences(input: string) {
|
||||||
|
return input
|
||||||
|
.replaceAll(/@\$/g, '')
|
||||||
|
.replaceAll(/\^\^/g, '')
|
||||||
|
.replaceAll(/~@/g, '')
|
||||||
|
.replaceAll(/%\?/g, '')
|
||||||
|
.replaceAll(/\*~/g, '')
|
||||||
|
.replaceAll(/!!/g, '')
|
||||||
|
.replaceAll(/#&/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftString(input: string) {
|
||||||
|
let shifted = '';
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const char = input.charCodeAt(i);
|
||||||
|
const shiftedChar = char - 3;
|
||||||
|
shifted += String.fromCharCode(shiftedChar);
|
||||||
|
}
|
||||||
|
return shifted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Voe',
|
||||||
|
id: 'voe',
|
||||||
|
domains: ['voe.sx'],
|
||||||
|
regex: [
|
||||||
|
// voe.sx
|
||||||
|
/(?<=window\.location\.href\s=\s')\S*(?=')/gm,
|
||||||
|
// whatever site voe.sx redirects to
|
||||||
|
/(?<=<script type="application\/json">).*(?=<\/script>)/m
|
||||||
|
],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
if (window.location.host === 'voe.sx') {
|
||||||
|
await HostSettings.addTemporaryHostDomain(this, new URL(match[0]).host);
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
let json = match[0];
|
||||||
|
json = JSON.parse(json);
|
||||||
|
|
||||||
|
let deobfuscated = json[0];
|
||||||
|
deobfuscated = rot13(deobfuscated);
|
||||||
|
deobfuscated = removeSpecialSequences(deobfuscated);
|
||||||
|
deobfuscated = atob(deobfuscated);
|
||||||
|
deobfuscated = shiftString(deobfuscated);
|
||||||
|
deobfuscated = deobfuscated.split('').reverse().join('');
|
||||||
|
deobfuscated = atob(deobfuscated);
|
||||||
|
|
||||||
|
const payload = JSON.parse(deobfuscated);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: payload['source']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
15
src/lib/host/vupload.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { HostMatchType, type Host } from '@/lib/host';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Vupload',
|
||||||
|
id: 'vupload',
|
||||||
|
domains: ['vupload.com'],
|
||||||
|
regex: [/(?<=src:\s?").+?(?=")/gm],
|
||||||
|
|
||||||
|
match: async function (match: RegExpMatchArray) {
|
||||||
|
return {
|
||||||
|
type: HostMatchType.HLS,
|
||||||
|
url: match[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} satisfies Host;
|
||||||
309
src/lib/match.ts
@@ -1,309 +0,0 @@
|
|||||||
import { unpack } from './utils';
|
|
||||||
import { Hosters, Redirect, TmpHost } from './settings';
|
|
||||||
|
|
||||||
export interface Match {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
domains: string[];
|
|
||||||
replace?: boolean;
|
|
||||||
regex: RegExp[];
|
|
||||||
notice?: string;
|
|
||||||
|
|
||||||
match(match: RegExpMatchArray): Promise<string | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Doodstream: Match = {
|
|
||||||
name: 'Doodstream',
|
|
||||||
id: 'doodstream',
|
|
||||||
domains: [
|
|
||||||
'doodstream.com',
|
|
||||||
'dood.pm',
|
|
||||||
'dood.ws',
|
|
||||||
'dood.wf',
|
|
||||||
'dood.cx',
|
|
||||||
'dood.sh',
|
|
||||||
'dood.watch',
|
|
||||||
'dood.to',
|
|
||||||
'dood.so',
|
|
||||||
'dood.la',
|
|
||||||
'dood.li',
|
|
||||||
'dood.re',
|
|
||||||
'dood.yt',
|
|
||||||
'doods.pro',
|
|
||||||
'ds2play.com',
|
|
||||||
'dooood.com',
|
|
||||||
'd000d.com'
|
|
||||||
],
|
|
||||||
replace: true,
|
|
||||||
regex: [/(\/pass_md5\/.*?)'.*(\?token=.*?expiry=)/s],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
const response = await fetch(`https://${window.location.host}${match[1]}`, {
|
|
||||||
headers: {
|
|
||||||
Range: 'bytes=0-'
|
|
||||||
},
|
|
||||||
referrer: `https://${window.location.host}/e/${
|
|
||||||
window.location.pathname.split('/').slice(-1)[0]
|
|
||||||
}`
|
|
||||||
});
|
|
||||||
return `${await response.text()}1234567890${match[2]}${Date.now()}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DropLoad: Match = {
|
|
||||||
name: 'Dropload',
|
|
||||||
id: 'dropload',
|
|
||||||
domains: ['dropload.ui'],
|
|
||||||
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
const unpacked = await unpack(match[0]);
|
|
||||||
return unpacked.match(/(?<=file:").*(?=")/)![0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Filemoon: Match = {
|
|
||||||
name: 'Filemoon',
|
|
||||||
id: 'filemoon',
|
|
||||||
domains: ['filemoon.sx', 'filemoon.to', 'filemoon.in'],
|
|
||||||
regex: [/(?<=<iframe\s*src=")\S*(?=")/s, /eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
|
||||||
replace: true,
|
|
||||||
|
|
||||||
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]);
|
|
||||||
return unpacked.match(/(?<=file:")\S*(?=")/)![0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GoodStream: Match = {
|
|
||||||
name: 'Goodstream',
|
|
||||||
id: 'goodstream',
|
|
||||||
domains: ['goodstream.uno'],
|
|
||||||
regex: [/(?<=file:\s+").*(?=")/g],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Kwik: Match = {
|
|
||||||
name: 'Kwik',
|
|
||||||
id: 'kwik',
|
|
||||||
domains: ['kwik.cx'],
|
|
||||||
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
const unpacked = await unpack(match[0]);
|
|
||||||
return unpacked.match(/(?<=source=').*(?=')/)![0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mixdrop: Match = {
|
|
||||||
name: 'Mixdrop',
|
|
||||||
id: 'mixdrop',
|
|
||||||
domains: ['mixdrop.co', 'mixdrop.to', 'mixdrop.ch', 'mixdrop.bz', 'mixdrop.gl'],
|
|
||||||
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
const unpacked = await unpack(match[0]);
|
|
||||||
const url = unpacked.match(/(?<=MDCore.wurl=").*(?=")/)![0];
|
|
||||||
return `https:${url}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mp4Upload: Match = {
|
|
||||||
name: 'Mp4Upload',
|
|
||||||
id: 'mp4upload',
|
|
||||||
domains: ['mp4upload.com'],
|
|
||||||
replace: true,
|
|
||||||
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
const unpacked = await unpack(match[0]);
|
|
||||||
return unpacked.match(/(?<=player.src\(").*(?=")/)![0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Newgrounds: Match = {
|
|
||||||
name: 'Newgrounds',
|
|
||||||
id: 'newgrounds',
|
|
||||||
domains: ['newgrounds.com'],
|
|
||||||
regex: [/.*/gm],
|
|
||||||
|
|
||||||
match: async () => {
|
|
||||||
const id = window.location.pathname.split('/').slice(-1)[0];
|
|
||||||
const response = await fetch(`https://www.newgrounds.com/portal/video/${id}`, {
|
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const json = await response.json();
|
|
||||||
return decodeURI(json['sources'][Object.keys(json['sources'])[0]][0]['src']);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StreamA2z: Match = {
|
|
||||||
name: 'Stream2Az',
|
|
||||||
id: 'stream2az',
|
|
||||||
domains: ['streama2z.com', 'streama2z.xyz'],
|
|
||||||
regex: [/https?:\/\/\S*m3u8.+(?=['"])/gm],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
if (StreamA2z.domains.indexOf(window.location.hostname) !== -1) {
|
|
||||||
await Redirect.set(StreamA2z);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Streamtape: Match = {
|
|
||||||
name: 'Streamtape',
|
|
||||||
id: 'streamtape',
|
|
||||||
domains: ['streamtape.com', 'streamtape.net', 'shavetape.cash'],
|
|
||||||
regex: [/id=.*(?=')/gm],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
let i = 0;
|
|
||||||
while (i < match.length) {
|
|
||||||
if (match[++i - 1] == match[i]) {
|
|
||||||
return `https://streamtape.com/get_video?${match[i]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// use the old method as fallback
|
|
||||||
return `https://streamtape.com/get_video?${match.reverse()[0]}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Streamzz: Match = {
|
|
||||||
name: 'Streamzz',
|
|
||||||
id: 'streamzz',
|
|
||||||
domains: ['streamzz.to', 'streamz.ws'],
|
|
||||||
regex: [/(?<=\|)\w{2,}/gm],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
return `https://get.${location.hostname.split('.')[0]}.tw/getlink-${
|
|
||||||
match.sort((a, b) => b.length - a.length)[0]
|
|
||||||
}.dll`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SuperVideo: Match = {
|
|
||||||
name: 'Supervideo',
|
|
||||||
id: 'supervideo',
|
|
||||||
domains: ['supervideo.tv'],
|
|
||||||
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
const unpacked = await unpack(match[0]);
|
|
||||||
return unpacked.match(/(?<=file:").*(?=")/)![0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Upstream: Match = {
|
|
||||||
name: 'Upstream',
|
|
||||||
id: 'upstream',
|
|
||||||
domains: ['upstream.to'],
|
|
||||||
regex: [/eval\(function\(p,a,c,k,e,d\).*?(?=<\/script>)/gms],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
const unpacked = await unpack(match[0]);
|
|
||||||
return unpacked.match(/(?<=file:").*(?=")/)![0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Vidmoly: Match = {
|
|
||||||
name: 'Vidmoly',
|
|
||||||
id: 'vidmoly',
|
|
||||||
domains: ['vidmoly.me', 'vidmoly.to'],
|
|
||||||
regex: [/(?<=file:").+\.m3u8/gm],
|
|
||||||
replace: true,
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Vidoza: Match = {
|
|
||||||
name: 'Vidoza',
|
|
||||||
id: 'vidoza',
|
|
||||||
domains: ['vidoza.net', 'videzz.net'],
|
|
||||||
regex: [/(?<=src:\s?").+?(?=")/gm],
|
|
||||||
replace: true,
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Voe: Match = {
|
|
||||||
name: 'Voe',
|
|
||||||
id: 'voe',
|
|
||||||
domains: ['voe.sx'],
|
|
||||||
regex: [/(?<='hls':\s*')\S*(?=')/gm],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
return atob(match[0]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Vupload: Match = {
|
|
||||||
name: 'Vupload',
|
|
||||||
id: 'vupload',
|
|
||||||
domains: ['vupload.com'],
|
|
||||||
regex: [/(?<=src:\s?").+?(?=")/gm],
|
|
||||||
|
|
||||||
match: async (match: RegExpMatchArray) => {
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const matches = {
|
|
||||||
[Doodstream.id]: Doodstream,
|
|
||||||
[DropLoad.id]: DropLoad,
|
|
||||||
[Filemoon.id]: Filemoon,
|
|
||||||
[GoodStream.id]: GoodStream,
|
|
||||||
[Kwik.id]: Kwik,
|
|
||||||
[Mixdrop.id]: Mixdrop,
|
|
||||||
[Mp4Upload.id]: Mp4Upload,
|
|
||||||
[Newgrounds.id]: Newgrounds,
|
|
||||||
[StreamA2z.id]: StreamA2z,
|
|
||||||
[Streamtape.id]: Streamtape,
|
|
||||||
[Streamzz.id]: Streamzz,
|
|
||||||
[SuperVideo.id]: SuperVideo,
|
|
||||||
[Upstream.id]: Upstream,
|
|
||||||
[Vidmoly.id]: Vidmoly,
|
|
||||||
[Vidoza.id]: Vidoza,
|
|
||||||
[Voe.id]: Voe,
|
|
||||||
[Vupload.id]: Vupload
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getMatch(domain: string): Promise<Match | null> {
|
|
||||||
if (await Hosters.getAllDisabled()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const match of Object.values(matches)) {
|
|
||||||
if (
|
|
||||||
match.domains.indexOf(domain) !== -1 &&
|
|
||||||
!(await Hosters.getDisabled().then((d) => d.find((p) => p.id == match.id)))
|
|
||||||
) {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tmpHost = await TmpHost.get();
|
|
||||||
if (tmpHost && tmpHost[0] === domain) {
|
|
||||||
return tmpHost[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,113 +1,88 @@
|
|||||||
import type { Match } from './match';
|
import { storage } from '#imports';
|
||||||
import { matches } from './match';
|
import { hosts, type Host } from '@/lib/host';
|
||||||
|
|
||||||
export const Hosters = {
|
export class HostSettings {
|
||||||
getDisabled: async () => {
|
/* disabled hosts */
|
||||||
const disabled = (await storageGet('hosters.disabled', [])) as string[];
|
private static disabledHosts = storage.defineItem<string[]>('local:disabledHosts', { fallback: [] });
|
||||||
return disabled.map((id) => matches[id]).filter((m) => m !== undefined);
|
private static allHostsDisabled = storage.defineItem<boolean>('local:allHostsDisabled', { fallback: false });
|
||||||
},
|
|
||||||
disable: async (match: Match) => {
|
static async addDisabledHost(host: Host) {
|
||||||
const disabled = (await storageGet('hosters.disabled', [])) as string[];
|
const ids = await this.disabledHosts.getValue();
|
||||||
const index = disabled.indexOf(match.id);
|
|
||||||
|
const index = ids.indexOf(host.id);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
disabled.push(match.id);
|
ids.push(host.id);
|
||||||
await storageSet('hosters.disabled', disabled);
|
await this.disabledHosts.setValue(ids);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
enable: async (match: Match) => {
|
static async removeDisabledHost(host: Host) {
|
||||||
const disabled = (await storageGet('hosters.disabled', [])) as string[];
|
const ids = await this.disabledHosts.getValue();
|
||||||
const index = disabled.indexOf(match.id);
|
|
||||||
|
const index = ids.indexOf(host.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
disabled.splice(index, 1);
|
ids.splice(index, 1);
|
||||||
await storageSet('hosters.disabled', disabled);
|
await this.disabledHosts.setValue(ids);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
getAllDisabled: async () => {
|
|
||||||
return await storageGet<boolean>('hosters.allDisabled', false);
|
|
||||||
},
|
|
||||||
setAll: async (enable: boolean) => {
|
|
||||||
await storageSet('hosters.allDisabled', !enable);
|
|
||||||
}
|
}
|
||||||
};
|
static async getDisabledHosts() {
|
||||||
|
return await this.disabledHosts.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
export const Redirect = {
|
static async getAllHostsDisabled() {
|
||||||
get: async (): Promise<Match | null> => {
|
return await this.allHostsDisabled.getValue();
|
||||||
return matches[(await storageGet('redirect')) as string] || null;
|
|
||||||
},
|
|
||||||
set: async (match: Match) => {
|
|
||||||
await storageSet('redirect', match.id);
|
|
||||||
},
|
|
||||||
delete: async () => {
|
|
||||||
await storageDelete('redirect');
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const TmpHost = {
|
static async setAllHostsDisabled(disabled: boolean) {
|
||||||
get: async (): Promise<[string, Match] | null> => {
|
await this.allHostsDisabled.setValue(disabled);
|
||||||
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 = {
|
/* tmp */
|
||||||
get: async (url: string): Promise<string | null> => {
|
private static temporaryHostDomain = storage.defineItem<Record<string, string>>('local:temporaryHostDomain', {
|
||||||
return (await storageGet(`urlReferer.${url}`)) || null;
|
fallback: {}
|
||||||
},
|
|
||||||
set: async (url: string, referer: string) => {
|
|
||||||
await storageSet(`urlReferer.${url}`, referer);
|
|
||||||
},
|
|
||||||
delete: async (url: string) => {
|
|
||||||
await storageDelete(`urlReferer.${url}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Other = {
|
|
||||||
getFf2mpv: async () => {
|
|
||||||
return await storageGet('other.ff2mpv', false);
|
|
||||||
},
|
|
||||||
setFf2mpv: async (enable: boolean) => {
|
|
||||||
await storageSet('other.ff2mpv', enable);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function storageGet<T>(key: string, defaultValue?: T): Promise<T | undefined> {
|
|
||||||
let resolve: (value: T | undefined) => void;
|
|
||||||
const promise = new Promise<T | undefined>((r) => (resolve = r));
|
|
||||||
|
|
||||||
chrome.storage.local.get(key, (entry) => {
|
|
||||||
const value = entry[key];
|
|
||||||
resolve(value === undefined ? defaultValue : value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
static async addTemporaryHostDomain(host: Host, domain: string) {
|
||||||
|
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
|
||||||
|
|
||||||
|
temporaryHostDomains[domain] = host.id;
|
||||||
|
await this.temporaryHostDomain.setValue(temporaryHostDomains);
|
||||||
|
console.log(await this.temporaryHostDomain.getValue());
|
||||||
|
}
|
||||||
|
static async checkTemporaryHostDomain(domain: string) {
|
||||||
|
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
|
||||||
|
|
||||||
|
const hostId = temporaryHostDomains[domain];
|
||||||
|
return hostId ? (hosts.find((host) => host.id === hostId) ?? null) : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storageSet<T>(key: string, value: T) {
|
export class FF2MPVSettings {
|
||||||
let resolve: () => void;
|
private static ff2mpvEnabled = storage.defineItem<boolean>('local:ff2mpv', { fallback: false });
|
||||||
const promise = new Promise<void>((r) => (resolve = r));
|
|
||||||
|
|
||||||
const obj = {
|
static async getEnabled() {
|
||||||
[key]: value
|
return await this.ff2mpvEnabled.getValue();
|
||||||
};
|
}
|
||||||
chrome.storage.local.set(obj, () => resolve());
|
static async setEnabled(enabled: boolean) {
|
||||||
|
console.log('set', enabled);
|
||||||
return promise;
|
await this.ff2mpvEnabled.setValue(enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storageDelete(key: string) {
|
export class UrlReferer {
|
||||||
let resolve: () => void;
|
private static temporaryUrlReferer = storage.defineItem<Record<string, string>>('local:temporaryUrlReferer', {
|
||||||
const promise = new Promise<void>((r) => (resolve = r));
|
fallback: {}
|
||||||
|
});
|
||||||
|
|
||||||
chrome.storage.local.remove(key, () => resolve());
|
static async addTemporary(hostname: string, referer: string) {
|
||||||
|
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
|
||||||
|
|
||||||
return promise;
|
tmpUrlReferer[hostname] = referer;
|
||||||
|
await this.temporaryUrlReferer.setValue(tmpUrlReferer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get(hostname: string) {
|
||||||
|
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
|
||||||
|
|
||||||
|
return tmpUrlReferer[hostname] ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
import pkg from '../package.json';
|
|
||||||
import { matches } from './lib/match';
|
|
||||||
|
|
||||||
const sharedManifest: Partial<chrome.runtime.ManifestBase> = {
|
|
||||||
browser_specific_settings: {
|
|
||||||
gecko: {
|
|
||||||
id: '{55dd42e8-3dd9-455a-b4fe-86664881b10c}'
|
|
||||||
},
|
|
||||||
gecko_android: {}
|
|
||||||
},
|
|
||||||
content_scripts: [
|
|
||||||
{
|
|
||||||
all_frames: true,
|
|
||||||
matches: Object.values(matches).flatMap((m) => m.domains.map((d) => `*://${d}/*`)),
|
|
||||||
js: ['src/entries/contentScript/main.ts'],
|
|
||||||
run_at: 'document_end'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
icons: {
|
|
||||||
16: 'icons/stream-bypass@16px.png',
|
|
||||||
32: 'icons/stream-bypass@32px.png',
|
|
||||||
48: 'icons/stream-bypass@48px.png',
|
|
||||||
96: 'icons/stream-bypass@96px.png',
|
|
||||||
128: 'icons/stream-bypass@128px.png'
|
|
||||||
},
|
|
||||||
permissions: ['storage'],
|
|
||||||
optional_permissions: ['nativeMessaging']
|
|
||||||
};
|
|
||||||
|
|
||||||
const browserAction = {
|
|
||||||
default_icon: {
|
|
||||||
16: 'icons/stream-bypass@16px.png',
|
|
||||||
32: 'icons/stream-bypass@32px.png'
|
|
||||||
},
|
|
||||||
default_popup: 'src/entries/popup/index.html'
|
|
||||||
};
|
|
||||||
|
|
||||||
const ManifestV2 = {
|
|
||||||
...sharedManifest,
|
|
||||||
background: {
|
|
||||||
scripts: ['src/entries/background/mv2.ts'],
|
|
||||||
persistent: true
|
|
||||||
},
|
|
||||||
content_scripts: [{ ...sharedManifest.content_scripts![0], matches: ['<all_urls>'] }],
|
|
||||||
browser_action: browserAction,
|
|
||||||
permissions: [...sharedManifest.permissions, 'webRequest', 'webRequestBlocking', '<all_urls>']
|
|
||||||
};
|
|
||||||
|
|
||||||
const ManifestV3 = {
|
|
||||||
...sharedManifest,
|
|
||||||
action: browserAction,
|
|
||||||
background: {
|
|
||||||
service_worker: 'src/entries/background/mv3.ts'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getManifest(
|
|
||||||
manifestVersion: number
|
|
||||||
): chrome.runtime.ManifestV2 | chrome.runtime.ManifestV3 {
|
|
||||||
const manifest = {
|
|
||||||
author: pkg.author,
|
|
||||||
description: pkg.description,
|
|
||||||
name: pkg.displayName ?? pkg.name,
|
|
||||||
version: pkg.version
|
|
||||||
};
|
|
||||||
|
|
||||||
if (manifestVersion === 2) {
|
|
||||||
return {
|
|
||||||
...manifest,
|
|
||||||
...ManifestV2,
|
|
||||||
manifest_version: manifestVersion
|
|
||||||
} as chrome.runtime.ManifestV2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manifestVersion === 3) {
|
|
||||||
return {
|
|
||||||
...manifest,
|
|
||||||
// just like all the adblockers which are unable to fully work under MV3, we need access to every website
|
|
||||||
// the user enters in order to work correctly, which is forbidden when using MV3
|
|
||||||
name: `${manifest.name} Lite`,
|
|
||||||
...ManifestV3,
|
|
||||||
manifest_version: manifestVersion
|
|
||||||
} as chrome.runtime.ManifestV3;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Missing manifest definition for manifestVersion ${manifestVersion}`);
|
|
||||||
}
|
|
||||||
6
src/utils/extract.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function lastPathSegment(path: string): string {
|
||||||
|
while (path.endsWith('/')) {
|
||||||
|
path = path.slice(0, -1);
|
||||||
|
}
|
||||||
|
return path.substring(path.lastIndexOf('/') + 1);
|
||||||
|
}
|
||||||
3
src/vite-env.d.ts
vendored
@@ -1,3 +0,0 @@
|
|||||||
/// <reference types="svelte" />
|
|
||||||
/// <reference types="vite/client" />
|
|
||||||
/// <reference types="@samrum/vite-plugin-web-extension/client" />
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import sveltePreprocess from 'svelte-preprocess';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
// Consult https://github.com/sveltejs/svelte-preprocess
|
|
||||||
// for more information about preprocessors
|
|
||||||
preprocess: sveltePreprocess()
|
|
||||||
};
|
|
||||||
@@ -1,18 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "./.wxt/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowSyntheticDefaultImports": true,
|
"useDefineForClassFields": true
|
||||||
"target": "es2019",
|
}
|
||||||
"module": "esnext",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"removeComments": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"~/*": ["src/*"]
|
|
||||||
},
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { defineConfig, loadEnv } from 'vite';
|
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
||||||
import webExtension from '@samrum/vite-plugin-web-extension';
|
|
||||||
import path from 'path';
|
|
||||||
import { getManifest } from './src/manifest';
|
|
||||||
import { matches } from './src/lib/match';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig(({ mode }) => {
|
|
||||||
const env = loadEnv(mode, process.cwd(), '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
plugins: [
|
|
||||||
svelte(),
|
|
||||||
webExtension({
|
|
||||||
manifest: getManifest(Number(env.MANIFEST_VERSION)),
|
|
||||||
additionalInputs: {
|
|
||||||
html: [
|
|
||||||
{
|
|
||||||
fileName: 'src/entries/player/player.html',
|
|
||||||
webAccessible: {
|
|
||||||
matches:
|
|
||||||
Number(env.MANIFEST_VERSION) === 3
|
|
||||||
? Object.values(matches).flatMap((m) => m.domains.map((d) => `*://${d}/*`))
|
|
||||||
: ['<all_urls>']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'~': path.resolve(__dirname, './src')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
1
wxt-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="svelte" />
|
||||||
58
wxt.config.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { defineConfig } from 'wxt';
|
||||||
|
import { version } from './package.json';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
srcDir: 'src',
|
||||||
|
modules: ['@wxt-dev/module-svelte'],
|
||||||
|
manifest: ({ browser, manifestVersion }) => ({
|
||||||
|
name: manifestVersion === 2 ? 'Stream Bypass' : 'Stream Bypass Lite',
|
||||||
|
icons: {
|
||||||
|
16: 'icon/stream-bypass@16px.png',
|
||||||
|
32: 'icon/stream-bypass@32px.png',
|
||||||
|
48: 'icon/stream-bypass@48px.png',
|
||||||
|
128: 'icon/stream-bypass@128px.png'
|
||||||
|
},
|
||||||
|
browser_specific_settings:
|
||||||
|
browser === 'firefox'
|
||||||
|
? {
|
||||||
|
gecko: {
|
||||||
|
id: '{55dd42e8-3dd9-455a-b4fe-86664881b10c}',
|
||||||
|
data_collection_permissions: {
|
||||||
|
required: ['none']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gecko_android: {}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
permissions: ['storage', ...(manifestVersion === 2 ? ['webRequest', 'webRequestBlocking', '<all_urls>'] : [])],
|
||||||
|
optional_permissions: ['nativeMessaging'],
|
||||||
|
web_accessible_resources: [
|
||||||
|
{
|
||||||
|
resources: ['player.html'],
|
||||||
|
// TODO: Replace this with all hosts domains if target manifest version is 3.
|
||||||
|
// This isn't working atm because importing '@/lib/host' fails. Ahhhh I love the whole fucking JS/TS
|
||||||
|
// environment. Maybe I'm also overlooking something and the fix is easy, but that itsn't working out of the
|
||||||
|
// box is once again terrible DX
|
||||||
|
matches: ['<all_urls>']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
zip: {
|
||||||
|
artifactTemplate: '{{name}}-{{version}}-{{manifestVersion}}.zip'
|
||||||
|
},
|
||||||
|
|
||||||
|
vite: () => ({
|
||||||
|
define: {
|
||||||
|
'import.meta.env.VERSION': JSON.stringify(version)
|
||||||
|
},
|
||||||
|
plugins: [tailwindcss()]
|
||||||
|
}),
|
||||||
|
|
||||||
|
svelte: {
|
||||||
|
vite: {
|
||||||
|
preprocess: vitePreprocess({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||