Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e22e92f89 | ||
|
|
f7cfe41211 | ||
|
|
d7e1cbfb48 | ||
|
|
3541c7d2f0 | ||
|
|
df3c66a24c | ||
|
|
b929f0956d | ||
|
|
4c9825ebd5 | ||
|
|
71593f6762 | ||
|
|
ddcb13013a | ||
|
|
8cf14fb17a | ||
|
|
3e3e2f5e75 | ||
|
|
c5deca1b2c | ||
|
|
c4fea538f0 | ||
|
|
0552df008d | ||
|
|
e437d5436a | ||
|
|
c09cf1188d | ||
|
|
eef560f224 | ||
|
|
9857ba6416 | ||
|
|
c08e071c88 | ||
|
|
dbec92375c | ||
|
|
a1328e8908 | ||
|
|
0455b53992 | ||
|
|
c9f5b594ff | ||
|
|
9639f148f9 | ||
|
|
85f4a3b776 | ||
|
|
30e079d665 | ||
|
|
e499adb4ac | ||
|
|
aa6827b059 | ||
|
|
5a86625f3f | ||
|
|
a44f71f056 | ||
|
|
652d0588fc | ||
|
|
a2a7132d0b | ||
|
|
0696165932 | ||
|
|
8cb9a83b2c | ||
|
|
f200312ad5 | ||
|
|
1cf4940453 | ||
|
|
67dbb9c685 | ||
|
|
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 | ||
|
|
67e03eafaa | ||
|
|
db0ccd9b56 | ||
|
|
fd3eda9c61 | ||
|
|
e522916585 | ||
|
|
b9514284da | ||
|
|
06beaa6ebe | ||
|
|
6a56c37dd2 | ||
|
|
018b381732 | ||
|
|
696ec22471 | ||
|
|
e379113aa4 | ||
|
|
84605ceb30 | ||
|
|
67c492db47 | ||
|
|
ae2f196b06 | ||
|
|
298e9308ce | ||
|
|
3cd4c6b6b7 | ||
|
|
6286acf85c | ||
|
|
94eee79218 | ||
|
|
2f186bda07 | ||
|
|
1c9f95cebc | ||
|
|
c57cd03407 | ||
|
|
64cf565da3 | ||
|
|
bf8a7eb602 | ||
|
|
c643a39e2d | ||
|
|
fcfbc41fdb | ||
|
|
1251f079f5 | ||
|
|
ccfc77167b | ||
|
|
a10066458f | ||
|
|
6a8c705b06 | ||
|
|
76a5bac7fd | ||
|
|
3aca863a4b | ||
|
|
fe14edc0ef | ||
|
|
fffe23638c | ||
|
|
5214228a72 | ||
|
|
7000d5a08b | ||
|
|
a21f799e7d | ||
|
|
1fb85313dd | ||
|
|
6e7410b088 | ||
|
|
0054f5da0e | ||
|
|
48dc192964 | ||
|
|
7c45cacd36 | ||
|
|
00514e4e81 | ||
|
|
4c1d0d0b63 | ||
|
|
a920832945 | ||
|
|
7d8d8b6614 | ||
|
|
b4052834f2 | ||
|
|
9900c33243 | ||
|
|
98709bc934 | ||
|
|
f4cbdd3258 | ||
|
|
fa41a8de8e | ||
|
|
ce8bc855b9 | ||
|
|
03202b2a12 | ||
|
|
9f0e1b59ce | ||
|
|
d928d25e09 | ||
|
|
266771aa13 | ||
|
|
d56672d90f | ||
|
|
a9ea5fe4b2 | ||
|
|
bb3f5384d6 | ||
|
|
6989587161 | ||
|
|
dd9bf71a5c | ||
|
|
6da0050df4 | ||
|
|
1a7c22ec0e | ||
|
|
175862b098 | ||
|
|
fd5a532d0f | ||
|
|
8c43eedb23 | ||
|
|
e027c2e09e | ||
|
|
f9a0656d4d | ||
|
|
382d8b1268 | ||
|
|
5b8639ce6a | ||
|
|
841c824590 | ||
|
|
2055a3ea81 | ||
|
|
0262d1853c | ||
|
|
81da6600e6 | ||
|
|
817f5b82f9 | ||
|
|
9a17fb0d9b | ||
|
|
17f8aab216 | ||
|
|
44d4c9cbcf | ||
|
|
b34531b982 | ||
|
|
e699d3885c | ||
|
|
396038a803 | ||
|
|
bd64d4ed0b | ||
|
|
a207c336b0 | ||
|
|
2460657f2a | ||
|
|
698ed5ac3c | ||
|
|
dc42220f09 | ||
|
|
e146649bbf | ||
|
|
424e34190c | ||
|
|
c5f4f8b246 | ||
|
|
e2b8d884af | ||
|
|
b07d0b4be6 | ||
|
|
2d0441997c | ||
|
|
53e040db46 | ||
|
|
6d1ff3fbea | ||
|
|
4cf76eb62a | ||
|
|
672b920f31 | ||
|
|
1e166b5ecc | ||
|
|
213b996755 | ||
|
|
de0d8d5a41 | ||
|
|
1f34f74e11 | ||
|
|
60a2247b02 | ||
|
|
88814fcc83 | ||
|
|
f908e8249b | ||
|
|
8d57dfbc57 | ||
|
|
3aa84ba709 | ||
|
|
9ecfbd44e9 | ||
|
|
b1bea7687c | ||
|
|
c3148a76a3 | ||
|
|
210aa5c263 | ||
|
|
a55c455fcc | ||
|
|
923787bedb | ||
|
|
8990e25a72 | ||
|
|
9b4bcc6c64 | ||
|
|
2e8cce10b5 | ||
|
|
9d4b52c107 | ||
|
|
806320a0de | ||
|
|
774d3b52d7 | ||
|
|
31787bb3e3 | ||
|
|
e64feb5130 | ||
|
|
aa51b5d95f | ||
|
|
27c03c0b5b | ||
|
|
f48adc3db3 | ||
|
|
feaf843584 | ||
|
|
0b6021cf52 | ||
|
|
8b375cae2c | ||
|
|
c9acb929cc | ||
|
|
7a69ac83e0 | ||
|
|
a9a8609cb8 | ||
|
|
e0d4f8747e | ||
|
|
f8213772c8 | ||
|
|
bce27f2beb | ||
|
|
7bfae612f9 | ||
|
|
106f4ffda3 | ||
|
|
b83d287a42 | ||
|
|
a39418e506 |
@@ -0,0 +1,44 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||
with:
|
||||
version: 11
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 26
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Firefox (mv2)
|
||||
run: pnpm run build:firefox
|
||||
|
||||
- name: Build Chrome (mv3)
|
||||
run: pnpm run build
|
||||
|
||||
- name: Upload Firefox (mv2)
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: stream-bypass-mv2
|
||||
path: .output/firefox-mv2
|
||||
|
||||
- name: Upload Chrome (mv3)
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: stream-bypass-mv3
|
||||
path: .output/chrome-mv3
|
||||
@@ -0,0 +1,32 @@
|
||||
name: lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||
with:
|
||||
version: 11
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 26
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
@@ -0,0 +1,89 @@
|
||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||
with:
|
||||
version: 11
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 26
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
pnpm run zip:firefox
|
||||
pnpm 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ steps.paths.outputs.mv2_name }}
|
||||
path: ${{ steps.paths.outputs.mv2_path }}
|
||||
|
||||
- name: Upload mv3 as artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
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@ac75e1407b33421f1368f08e0e6cc567cc4edd25 # 2.11.5
|
||||
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: |
|
||||
pnpm exec wxt submit \
|
||||
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
|
||||
--chrome-zip .output/*-mv3.zip
|
||||
@@ -1 +1,23 @@
|
||||
build/
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
pnpm-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?
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
.zed
|
||||
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
.output
|
||||
.wxt
|
||||
|
||||
.github
|
||||
@@ -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
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add python3 nodejs npm
|
||||
|
||||
RUN npm install -g typescript sass
|
||||
|
||||
COPY [".", "."]
|
||||
|
||||
CMD ["python3", "build.py", "-b", "-c"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 ByteDream
|
||||
Copyright (c) 2022-NOW ByteDream
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -2,129 +2,193 @@
|
||||
|
||||
A multi-browser addon / extension for multiple streaming providers which redirects directly to the source video.
|
||||
|
||||
This addon replaces the video player from this sides with the native player build-in into the browser or redirects directly to the source video.
|
||||
This has the advantage, that no advertising or popups are shown when trying to interact with the video (playing, skipping, ...) or some sites are showing them even if you do nothing.
|
||||
Additionally this enables you to download the video by right-clicking it and just choose the download option.
|
||||
|
||||
<p align="center">
|
||||
<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">
|
||||
</a>
|
||||
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/">
|
||||
<img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Addon%20Store&style=flat-square" alt="Firefox Addon Store">
|
||||
<img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Users&style=flat-square" alt="Firefox Addon Store">
|
||||
</a>
|
||||
<a 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">
|
||||
<a href="https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb">
|
||||
<img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store">
|
||||
</a>
|
||||
<a href="https://discord.gg/gUWwekeNNg">
|
||||
<img src="https://img.shields.io/discord/915659846836162561?label=discord&style=flat-square" alt="Discord">
|
||||
<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">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Supported streaming providers (for a complete list of all supported websites, see [here](SUPPORTED) or in [show all](#all-supported-websites) below):
|
||||
- [streamtape.com](https://streamtape.com)
|
||||
- [vivo.sx](https://vivo.sx)
|
||||
- [voe.sx](https://voe.sx)
|
||||
<p align="center">
|
||||
<a href="#-introduction">Introduction 📝</a>
|
||||
•
|
||||
<a href="#-installation">Installation 📥</a>
|
||||
•
|
||||
<a href="#-features">Features ✨</a>
|
||||
•
|
||||
<a href="#-supported-websites">Supported Websites 📜</a>
|
||||
•
|
||||
<a href="#%EF%B8%8F-building">Building 🛠️</a>
|
||||
•
|
||||
<a href="#%EF%B8%8F-settings">Settings ⚙️</a>
|
||||
•
|
||||
<a href="#-license">License ⚖</a>
|
||||
</p>
|
||||
|
||||
<details id="all-supported-websites">
|
||||
<summary><b>Show all</b></summary>
|
||||
<ul>
|
||||
<li><a href="https://evoload.io">evoload.io</a></li>
|
||||
<li><a href="https://mcloud.to">mcloud.to</a></li>
|
||||
<li><a href="https://mixdrop.co">mixdrop.co</a></li>
|
||||
<li><a href="https://streamtape.com">streamtape.com</a></li>
|
||||
<li><a href="https://streamzz.to">streamzz.to</a></li>
|
||||
<li><a href="https://thevideome.com">thevideome.com</a></li>
|
||||
<li><a href="https://vidlox.me">vidlox.me</a></li>
|
||||
<li><a href="https://vidstream.pro">vidstream.pro</a></li>
|
||||
<li><a href="https://vidoza.net">vidoza.net</a></li>
|
||||
<li><a href="https://vivo.st">vivo.st</a></li>
|
||||
<li><a href="https://vivo.sx">vivo.sx</a></li>
|
||||
<li><a href="https://voe.sx">voe.sx</a></li>
|
||||
<li><a href="https://vupload.com">vupload.com</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
## 📝 Introduction
|
||||
|
||||
---
|
||||
This addon replaces the video player from this sides with the native player build-in into the browser or redirects directly to the source video.
|
||||
This has the advantage, that no advertising or popups are shown when trying to interact with the video (playing, skipping, ...) or some sites are showing them even if you do nothing.
|
||||
Additionally, this enables you to download the video by right-clicking it and just choose the download option.
|
||||
|
||||
<details id="example">
|
||||
<summary><b>How it's working</b></summary>
|
||||
<summary><b>How it works:</b></summary>
|
||||
<img src="example.gif" alt="">
|
||||
</details>
|
||||
|
||||
The addon was tested on
|
||||
- Firefox (96.0.3)
|
||||
- Ungoogled Chromium (97.0)
|
||||
- Vivaldi (5.0)
|
||||
- Opera (83.0)
|
||||
## 📥 Installation
|
||||
|
||||
## Installing
|
||||
### Official browser stores
|
||||
|
||||
### Firefox
|
||||
The best way to install the extension are the official browser extension stores:
|
||||
|
||||
Install the addon directly from the [firefox addon store](https://addons.mozilla.org/de/firefox/addon/stream-bypass/).
|
||||
- [Firefox Addon Store](https://addons.mozilla.org/de/firefox/addon/stream-bypass/) (Firefox for Android is supported too!)
|
||||
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
|
||||
|
||||
### Chromium / Google Chrome
|
||||
<details>
|
||||
<summary><h3 id="manual-installation1">Manual installation</h3></summary>
|
||||
|
||||
1. Download the zipfile from the [latest release](https://smartrelease.bytedream.org/github/ByteDream/stream-bypass/stream_bypass-{tag}.zip) and unzip it (with [7zip](https://www.7-zip.org/) or something like that).
|
||||
2. Go into your browser and type `chrome://extensions` in the address bar.
|
||||
3. Turn the developer mode in the top right corner on.
|
||||
4. Click Load unpacked.
|
||||
5. Choose the cloned / unzipped directory.
|
||||
- Firefox (mv2)
|
||||
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. with [7zip](https://www.7-zip.org/))
|
||||
- Go into your browser and type `about:debugging#/runtime/this-firefox` in the address bar
|
||||
- Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory
|
||||
- Chromium / Google Chrome (mv3)
|
||||
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
|
||||
- Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. [7zip](https://www.7-zip.org/))
|
||||
- Go into your browser and type `chrome://extensions` in the address bar
|
||||
- Turn on the developer mode by checking the switch in the top right corner
|
||||
- Click `Load unpacked` and choose the unzipped directory
|
||||
|
||||
### Opera
|
||||
</details>
|
||||
|
||||
1. Download the zipfile from the [latest release](https://smartrelease.bytedream.org/github/ByteDream/stream-bypass/stream_bypass-{tag}.zip) and unzip it (with [7zip](https://www.7-zip.org/) or something like that).
|
||||
2. Go into your browser and type `opera://extensions` in the address bar.
|
||||
3. Turn the developer mode in the top right corner on.
|
||||
4. Click Load unpacked.
|
||||
5. Choose the cloned / unzipped directory.
|
||||
## ✨ Features
|
||||
|
||||
## Compiling
|
||||
- ✔: Supported.
|
||||
- ✖: Not supported.
|
||||
|
||||
If you want to use / install the addon from source, you have to compile the `typescript` and `sass` files yourself.
|
||||
- Compile it [manual](#manual).
|
||||
- Compile it using [docker](#docker).
|
||||
| Feature | Firefox (mv2) | Chrome (mv3) | Firefox for Android (mv2) |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------------------- |
|
||||
| Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
|
||||
| Support websites that are accessed via a redirect | ✔ | ✖ | ✔ |
|
||||
| Open video in mpv (with [ff2mpv](https://github.com/bytedream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ✖ |
|
||||
|
||||
### Manual
|
||||
## 📜 Supported websites
|
||||
|
||||
For compiling everything bare bones, you need [typescript](https://www.typescriptlang.org/) and [sass](https://sass-lang.com/) installed.
|
||||
- Compile typescript
|
||||
- ✔: Everything ok.
|
||||
- ⚠: Works with limitations.
|
||||
- ✖: Not supported.
|
||||
|
||||
| Site | Firefox & Firefox for Android (mv2) | Chrome & Chromium based (mv3) |
|
||||
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| [dropload.io](https://dropload.io) | ✔ | ✔ |
|
||||
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
|
||||
| [filemoon.to](https://filemoon.to) | ✔ | ✔ |
|
||||
| [goodstream.uno](https://goodstream.uno) | ✔ | ✔ |
|
||||
| [kwik.cx](https://kwik.cx) | ✔ | ✔ |
|
||||
| [loadx.ws](https://loadx.ws) | ✔ | ✖ (background request always required) |
|
||||
| [luluvdo.com](https://luluvdo.com) | ✔ | ✖ (background request always required) |
|
||||
| [mixdrop.co](https://mixdrop.co) | ✔ ️ | ✔ |
|
||||
| [mp4upload.com](https://mp4upload.com) | ✔ | ✔ |
|
||||
| [newgrounds.com](https://newgrounds.com) | ✔ | ✔ |
|
||||
| [streama2z.com](https://streama2z.com) | ✔ | ✖ (redirect always required) |
|
||||
| [streamtape.com](https://streamtape.com) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) | ⚠ (correct video url can't always be extract, retrying/reloading the page might fix it) |
|
||||
| [streamzz.to](https://streamzz.to) / [streamz.ws](https://streamz.ws) | ✔ | ✔ |
|
||||
| [supervideo.tv](https://supervideo.tv) | ✔ | ✔ |
|
||||
| [upstream.to](https://upstream.to) | ✔ | ✔ |
|
||||
| [vidmoly.to](https://vidmoly.me) | ✔ | ✔ |
|
||||
| [vidoza.net](https://vidoza.net) | ✔ | ✔ |
|
||||
| [voe.sx](https://voe.sx) | ✔ | ✖ (redirect always required) |
|
||||
| [vupload.com](https://vupload.com) | ✔ | ✔ |
|
||||
|
||||
_This table might not be 100% accurate, it isn't actively monitored if the addon works for every website!_
|
||||
|
||||
Some sites put much effort in obfuscating their code / how they receive the video stream so that it simply cost too much time for me to reverse engineer it and find out how to bypass the native video player of the site.
|
||||
|
||||
## 🛠️ Building
|
||||
|
||||
If you want to build the addon from source and not using the way described in [installation](#-installation), follow the instructions.
|
||||
|
||||
Requirements:
|
||||
|
||||
- `pnpm` installed.
|
||||
- A copy of this repository and a shell / console open in the copied directory.
|
||||
|
||||
If the requirements are satisfied, you can continue with the following commands:
|
||||
|
||||
```shell
|
||||
# install all dependencies
|
||||
$ pnpm i
|
||||
|
||||
# build the extension and start it in a new firefox instance
|
||||
$ pnpm run dev:firefox
|
||||
|
||||
# build the extension with optimizations to the .output/firefox-mv2 directory
|
||||
$ pnpm run build:firefox
|
||||
```
|
||||
$ tsc -p src
|
||||
```
|
||||
- Compile sass (replace `<path to sass file>` with every `.sass` file in the `src` directory)
|
||||
```
|
||||
$ sass --no-source-map <path to sass file>
|
||||
```
|
||||
The compiled output will be in the `src` directory.
|
||||
|
||||
If you want to keep it a little cleaner, you additionally need [python3](https://www.python.org).
|
||||
- Compile everything with one line
|
||||
```
|
||||
$ python3 build.py -b -c
|
||||
```
|
||||
The compiled output will remain in a (new created if not existing) `build` directory.
|
||||
|
||||
### Docker
|
||||
|
||||
For this, you need [docker](https://www.docker.com/) to be installed.
|
||||
- Build the docker image
|
||||
```
|
||||
$ docker build -t stream-bypass .
|
||||
```
|
||||
- Compile
|
||||
```
|
||||
$ docker run --rm -v build:/build stream-bypass
|
||||
```
|
||||
The compiled output will remain in a (new created if not existing) `build` directory.
|
||||
You can omit the `:firefox` suffix, then it's built for Chrome.
|
||||
|
||||
##### Install
|
||||
|
||||
If you want to use the addon in Chromium or any browser which is based on it (almost every other, Google Chrome, Opera, ...), follow the steps in [installing](#installing).
|
||||
When using firefox, use the following
|
||||
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:
|
||||
|
||||
1. Type `about:debugging` in the browser's address bar.
|
||||
2. Select 'This Firefox' tab (maybe named different, depending on your language).
|
||||
3. Under `Temporary Extensions`, click `Load Temporary Add-on`.
|
||||
4. Choose any file in the directory where the compiled sources are.
|
||||
|
||||
## License
|
||||
## ⚙️ Settings
|
||||
|
||||
> You reach the settings by pressing the settings icon in the top right corner of the extension popup.
|
||||
|
||||
### Hosts
|
||||
|
||||
You can enable or disable for which hosts the extension should work.
|
||||
|
||||
### Per-site disables
|
||||
|
||||
Herer you can disable specific hosts (or all of them) for the current site only - useful when a site uses multiple hosts and you only want to bypass a few, or when a host misbehaves on a particular site.
|
||||
|
||||
### ff2mpv
|
||||
|
||||
[ff2mpv](https://github.com/woodruffw/ff2mpv) allows you to play streams directly in [mpv](https://mpv.io/) instead of the browser.
|
||||
You can enable or disable this behavior.
|
||||
|
||||
<details>
|
||||
<summary><i>Steps to get it set up</i></summary>
|
||||
|
||||
- In the [Usage](https://github.com/woodruffw/ff2mpv#usage) section of the ff2mpv repository pick the installation instruction for your operating system (Linux/Windows/macOS; you do not need the browser addon).
|
||||
- Scroll down to `Install manually`
|
||||
- Follow instructions for Firefox/Chrome
|
||||
- Edit the `ff2mpv.json` you created:
|
||||
- Firefox: Add `{55dd42e8-3dd9-455a-b4fe-86664881b10c}` to `allowed_extensions`:
|
||||
|
||||
```
|
||||
"allowed_extensions": [
|
||||
"ff2mpv@yossarian.net",
|
||||
"{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
|
||||
]
|
||||
```
|
||||
|
||||
- Chrome/Chromium: - Go To: Settings -> Extensions - Click on `Details` of the Stream Bypass extension and copy the ID - Add `chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/` to `allowed_origins`:
|
||||
```
|
||||
"allowed_origins": [
|
||||
"chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
|
||||
"chrome-extension://ddfpfjomnakfckhmilacnbokdaknamdb/"
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## ⚖ License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
evoload.io
|
||||
mcloud.to
|
||||
mixdrop.co
|
||||
streamtape.com
|
||||
streamzz.to
|
||||
thevideome.com
|
||||
vidlox.me
|
||||
vidstream.pro
|
||||
vidoza.net
|
||||
vivo.st
|
||||
vivo.sx
|
||||
voe.sx
|
||||
vupload.com
|
||||
@@ -1,158 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
def load_matches():
|
||||
matched = []
|
||||
|
||||
indexed = False
|
||||
pattern = re.compile(r"(?<=\[')\S*(?=',)")
|
||||
for line in open('src/match.ts', 'r'):
|
||||
if not indexed:
|
||||
if 'constmatches=[' in line.replace(' ', ''):
|
||||
indexed = True
|
||||
else:
|
||||
match = pattern.findall(line)
|
||||
if match:
|
||||
if not line.strip().startswith('//'):
|
||||
matched.append(match[0])
|
||||
else:
|
||||
break
|
||||
|
||||
return matched
|
||||
|
||||
|
||||
def write_manifest():
|
||||
matches = load_matches()
|
||||
manifest = json.load(open('src/manifest.json', 'r'))
|
||||
|
||||
for content_script in manifest['content_scripts']:
|
||||
content_script['matches'] = [f'*://{match}/*' for match in matches]
|
||||
|
||||
json.dump(manifest, open('src/manifest.json', 'w'), indent=2)
|
||||
|
||||
|
||||
def write_supported():
|
||||
open('SUPPORTED', 'w').writelines([f'{match}\n' for match in load_matches()])
|
||||
|
||||
|
||||
def write_readme():
|
||||
firefox_pattern = re.compile(r'Mozilla Firefox (?P<version>.+)')
|
||||
chromium_pattern = re.compile(r'(?P<version>\d+\.\d+)')
|
||||
tested = {}
|
||||
|
||||
stdout, stderr = subprocess.Popen(['firefox', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if stderr == b'':
|
||||
tested['Firefox'] = re.search(firefox_pattern, stdout.decode('utf-8').replace('\n', '')).group('version')
|
||||
|
||||
for command, name in {'chromium': 'Ungoogled Chromium', 'vivaldi-stable': 'Vivaldi', 'opera': 'Opera'}.items():
|
||||
stdout, stderr = subprocess.Popen([command, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if stderr == b'':
|
||||
tested[name] = re.search(chromium_pattern, stdout.decode('utf-8').replace('\n', '')).group('version')
|
||||
|
||||
# it this the right syntax if i want to read and write to a file? * dreams in python3.10 *
|
||||
with open('README.md', 'r') as read_file:
|
||||
readme = read_file.read()
|
||||
|
||||
# adds all available websites
|
||||
all_providers_regex = r'(?<=<ul>\n)(.+?)(?=</ul>)'
|
||||
domains = filter(lambda domain: domain != '', open('SUPPORTED', 'r').read().split('\n'))
|
||||
all_providers = '\n'.join(f'\t\t<li><a href="https://{supported}">{supported}</a></li>' for supported in domains) + '\n'
|
||||
readme = re.sub(all_providers_regex, all_providers, readme, flags=re.DOTALL)
|
||||
|
||||
# adds all installed browsers to the tested browser section. i'm just to lazy to seek out all browser versions manually
|
||||
tested_browsers_regex = r'(?<=The addon was tested on\n)(.+?)(?=\n*## Installing)'
|
||||
tested_browsers = '\n'.join(f'- {name} ({version})' for name, version in tested.items())
|
||||
readme = re.sub(tested_browsers_regex, tested_browsers, readme, flags=re.DOTALL)
|
||||
|
||||
# rewrite the readme
|
||||
with open('README.md', 'w') as write_file:
|
||||
write_file.write(readme)
|
||||
write_file.close()
|
||||
read_file.close()
|
||||
|
||||
|
||||
def copy_built():
|
||||
if not shutil.which('tsc'):
|
||||
sys.stderr.write('The typescript compiler `tsc` could not be found')
|
||||
sys.exit(1)
|
||||
elif not shutil.which('sass'):
|
||||
sys.stderr.write('The sass compiler `sass` could not be found')
|
||||
sys.exit(1)
|
||||
|
||||
write_manifest()
|
||||
|
||||
subprocess.call(['tsc', '-p', 'src'])
|
||||
|
||||
build_path = Path('build')
|
||||
if not build_path.is_dir():
|
||||
build_path.mkdir()
|
||||
for file in Path('src').rglob('*'):
|
||||
build_file = build_path.joinpath(str(file)[4:])
|
||||
if file.is_dir():
|
||||
if not build_file.exists():
|
||||
build_file.mkdir(parents=True)
|
||||
elif file.suffix == '.sass':
|
||||
css_file = str(file)[:-4] + 'css'
|
||||
subprocess.call(['sass', '--no-source-map', file, css_file])
|
||||
shutil.copy(css_file, str(build_path.joinpath(css_file[4:])))
|
||||
elif file.name == 'tsconfig.json':
|
||||
continue
|
||||
elif file.suffix != '.ts':
|
||||
shutil.copy(str(file), str(build_file))
|
||||
|
||||
ext_path = Path('build', 'ext')
|
||||
if not ext_path.is_dir():
|
||||
ext_path.mkdir()
|
||||
|
||||
# download hls.js (version 1.1.1)
|
||||
with zipfile.ZipFile(io.BytesIO(urllib.request.urlopen('https://github.com/video-dev/hls.js/releases/download/v1.1.1/release.zip').read())) as z:
|
||||
open(ext_path.joinpath('hls.light.min.js'), 'wb').write(z.read('dist/hls.light.min.js'))
|
||||
z.close()
|
||||
|
||||
# download popperjs core (version 2.10.2)
|
||||
open(ext_path.joinpath('popper.min.js'), 'wb').write(urllib.request.urlopen('https://unpkg.com/@popperjs/core@2.10.2/dist/umd/popper.min.js').read())
|
||||
|
||||
# download tippy.js (version 6.3.7)
|
||||
open(ext_path.joinpath('tippy-bundle.umd.min.js'), 'wb').write(urllib.request.urlopen('https://unpkg.com/tippy.js@6.3.7/dist/tippy-bundle.umd.min.js').read())
|
||||
|
||||
|
||||
def clean_build():
|
||||
for file in Path('src').rglob('*'):
|
||||
if file.suffix in ['.js', '.css', '.map']:
|
||||
file.unlink()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-m', '--manifest', action='store_true', help='Builds the manifest.json file for addon information in ./src')
|
||||
parser.add_argument('-r', '--readme', action='store_true', help='Updates the README.md with the currently installed ')
|
||||
parser.add_argument('-s', '--supported', action='store_true', help='Builds the SUPPORTED file with all supported domains in the current directory')
|
||||
parser.add_argument('-b', '--build', action='store_true', help='Creates a ./build folder and builds all typescript / sass files')
|
||||
parser.add_argument('-c', '--clean', action='store_true', help='Cleans the ./src folder from .js, .css and .map files')
|
||||
|
||||
parsed = parser.parse_args()
|
||||
|
||||
if parsed.manifest:
|
||||
write_manifest()
|
||||
if parsed.readme:
|
||||
write_readme()
|
||||
if parsed.supported:
|
||||
write_supported()
|
||||
if parsed.build:
|
||||
copy_built()
|
||||
if parsed.clean:
|
||||
clean_build()
|
||||
|
||||
if not parsed.manifest and not parsed.supported and not parsed.build and not parsed.clean:
|
||||
print('\n'.join(load_matches()))
|
||||
@@ -0,0 +1,33 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import ts from 'typescript-eslint';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('.gitignore', import.meta.url));
|
||||
|
||||
export default defineConfig(
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs[('flat/recommended', 'flat/prettier')],
|
||||
prettier,
|
||||
includeIgnoreFile(gitignorePath),
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-alert': 'error',
|
||||
'no-console': 'error',
|
||||
'no-undef': 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "stream-bypass",
|
||||
"version": "5.0.0",
|
||||
"displayName": "Stream Bypass",
|
||||
"author": "bytedream",
|
||||
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/bytedream/stream-bypass.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/bytedream/stream-bypass/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"zip": "wxt zip",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"postinstall": "wxt prepare",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint . && svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||
"@steeze-ui/heroicons": "^2.4.2",
|
||||
"@steeze-ui/svelte-icon": "^1.6.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"@wxt-dev/module-svelte": "^2.0.5",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.20.0",
|
||||
"hls.js": "^1.6.16",
|
||||
"prettier": "^3.9.4",
|
||||
"prettier-plugin-svelte": "^4.1.1",
|
||||
"sass": "^1.101.0",
|
||||
"svelte": "^5.56.4",
|
||||
"svelte-check": "^4.7.2",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vite": "^8.1.4",
|
||||
"web-ext": "^10.4.0",
|
||||
"wxt": "^0.20.27"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
esbuild: true
|
||||
spawn-sync: false
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="738.248" height="738.248" viewBox="0 0 195.328 195.328"><path d="M108.902 30.525c-53.858 0-97.664 43.806-97.664 97.664 0 53.86 43.806 97.665 97.664 97.665 53.86 0 97.664-43.806 97.664-97.665 0-53.858-43.805-97.664-97.664-97.664zm0 13.434c46.6 0 84.23 37.632 84.23 84.23 0 46.6-37.63 84.23-84.23 84.23-46.599 0-84.232-37.63-84.232-84.23 0-46.598 37.633-84.23 84.232-84.23z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="translate(-11.238 -30.525)"/><path d="M8.986 105.297v96.926l83.94-48.463-7.7-4.446zm10.266 17.781 53.143 30.682-53.143 30.681z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="matrix(1.30853 0 0 1.30853 44.795 -103.534)"/><path d="M345.131 686.085c-122.49-10.29-225.617-86.136-271.115-199.397-9.596-23.888-15.72-47.197-20.05-76.315-2.763-18.58-2.76-64.116.004-82.784 7.5-50.64 23.648-93.74 49.956-133.33 39.979-60.166 96.484-103.56 164.472-126.311 53.98-18.063 110.354-21.415 165.476-9.838 82.83 17.396 153.434 65.363 200.69 136.344 10.702 16.073 25.836 46.006 32.68 64.634 6.703 18.242 13.106 43.275 16.28 63.647 2.098 13.461 2.487 20.483 2.534 45.734.06 31.25-.585 38.592-5.576 63.53-26.78 133.796-136.647 235.614-272.394 252.435-17.246 2.137-47.675 2.935-62.957 1.651zm77.476-197.054c113.33-65.435 206.175-119.33 206.322-119.767.277-.827-411.864-239.171-414.659-239.8-1.374-.31-1.586 31.672-1.586 239.605 0 227.996.097 239.938 1.934 239.45 1.063-.283 94.658-54.052 207.989-119.487z" style="fill:#fff;stroke:#000;stroke-width:5.46616" transform="scale(.26458)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 713 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="738.248" height="738.248" viewBox="0 0 195.328 195.328"><path d="M108.902 30.525c-53.858 0-97.664 43.806-97.664 97.664 0 53.86 43.806 97.665 97.664 97.665 53.86 0 97.664-43.806 97.664-97.665 0-53.858-43.805-97.664-97.664-97.664zm0 13.434c46.6 0 84.23 37.632 84.23 84.23 0 46.6-37.63 84.23-84.23 84.23-46.599 0-84.232-37.63-84.232-84.23 0-46.598 37.633-84.23 84.232-84.23z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="translate(-11.238 -30.525)"/><path d="M8.986 105.297v96.926l83.94-48.463-7.7-4.446zm10.266 17.781 53.143 30.682-53.143 30.681z" style="color:#000;fill:#000;-inkscape-stroke:none" transform="matrix(1.30853 0 0 1.30853 44.795 -103.534)"/><path d="M345.131 686.085c-122.49-10.29-225.617-86.136-271.115-199.397-9.596-23.888-15.72-47.197-20.05-76.315-2.763-18.58-2.76-64.116.004-82.784 7.5-50.64 23.648-93.74 49.956-133.33 39.979-60.166 96.484-103.56 164.472-126.311 53.98-18.063 110.354-21.415 165.476-9.838 82.83 17.396 153.434 65.363 200.69 136.344 10.702 16.073 25.836 46.006 32.68 64.634 6.703 18.242 13.106 43.275 16.28 63.647 2.098 13.461 2.487 20.483 2.534 45.734.06 31.25-.585 38.592-5.576 63.53-26.78 133.796-136.647 235.614-272.394 252.435-17.246 2.137-47.675 2.935-62.957 1.651zm77.476-197.054c113.33-65.435 206.175-119.33 206.322-119.767.277-.827-411.864-239.171-414.659-239.8-1.374-.31-1.586 31.672-1.586 239.605 0 227.996.097 239.938 1.934 239.45 1.063-.283 94.658-54.052 207.989-119.487z" style="fill:gray;stroke:#000;stroke-width:5.46616" transform="scale(.26458)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 792 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { BackgroundMessageData, BackgroundMessageType } from '@/lib/communication';
|
||||
import { getHost, hosts } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
const temporaryReferers: Record<string, string> = {};
|
||||
const allHostDomains = hosts.flatMap((host) => host.domains);
|
||||
|
||||
function domainToMatch(domain: string) {
|
||||
return `*://${domain}/*`;
|
||||
}
|
||||
|
||||
function domainsToMatch(domains: string[]) {
|
||||
return domains.map(domainToMatch);
|
||||
}
|
||||
|
||||
async function reInjectContentScript(domain: string) {
|
||||
const contentScript: Browser.scripting.RegisteredContentScript = {
|
||||
id: 'temporary-hosts',
|
||||
js: ['content-scripts/content.js'],
|
||||
matches: [domainToMatch(domain)],
|
||||
persistAcrossSessions: false,
|
||||
allFrames: true,
|
||||
runAt: 'document_end'
|
||||
};
|
||||
|
||||
const contentScripts = await browser.scripting.getRegisteredContentScripts({ ids: [contentScript.id] });
|
||||
if (contentScripts.length !== 0) {
|
||||
const registeredContentScript = contentScripts[0];
|
||||
|
||||
// do not double register for same domain
|
||||
if (registeredContentScript.matches!.indexOf(contentScript.matches![0]) !== -1) return;
|
||||
|
||||
contentScript.matches!.push(...contentScripts[0].matches!);
|
||||
await browser.scripting.updateContentScripts([contentScript]);
|
||||
} else {
|
||||
await browser.scripting.registerContentScripts([contentScript]);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- listeners --- */
|
||||
async function browserRuntimeOnMessageListener(message: any, sender: Browser.runtime.MessageSender) {
|
||||
const type = message.type as BackgroundMessageType;
|
||||
let data = message.data;
|
||||
|
||||
let response;
|
||||
|
||||
switch (type) {
|
||||
case BackgroundMessageType.Ff2mpv: {
|
||||
data = data as BackgroundMessageData<typeof type>;
|
||||
|
||||
await browser.runtime.sendNativeMessage('ff2mpv', { url: data.url });
|
||||
break;
|
||||
}
|
||||
case BackgroundMessageType.RequestTabUrl: {
|
||||
response = sender.tab?.url ?? null;
|
||||
break;
|
||||
}
|
||||
case BackgroundMessageType.RegisterContentScript: {
|
||||
// requires "dynamic" host permissions, which is only available in mv2
|
||||
if (import.meta.env.MANIFEST_VERSION === 3) break;
|
||||
|
||||
data = data as BackgroundMessageData<typeof type>;
|
||||
await reInjectContentScript(data.domain);
|
||||
|
||||
break;
|
||||
}
|
||||
case BackgroundMessageType.RegisterTemporaryReferer: {
|
||||
// requires "dynamic" host permissions, which is only available in mv2
|
||||
if (import.meta.env.MANIFEST_VERSION === 3) break;
|
||||
|
||||
data = data as BackgroundMessageData<typeof type>;
|
||||
if (data.domain in temporaryReferers) break;
|
||||
temporaryReferers[data.domain] = data.referer;
|
||||
|
||||
// remount before send headers listener which to also listen on the new temporary referer domain
|
||||
registerBrowserWebRequestOnBeforeSendHeadersListener();
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
function registerBrowserRuntimeOnMessageListener() {
|
||||
browser.runtime.onMessage.addListener(browserRuntimeOnMessageListener);
|
||||
}
|
||||
|
||||
function browserWebRequestOnBeforeSendHeadersListener(
|
||||
details: Browser.webRequest.OnBeforeSendHeadersDetails
|
||||
): Browser.webRequest.BlockingResponse | undefined {
|
||||
const referer = temporaryReferers[new URL(details.url).host];
|
||||
if (!referer) return;
|
||||
|
||||
details.requestHeaders!.push({
|
||||
name: 'Referer',
|
||||
value: `https://${referer}/`
|
||||
});
|
||||
|
||||
return { requestHeaders: details.requestHeaders };
|
||||
}
|
||||
function registerBrowserWebRequestOnBeforeSendHeadersListener() {
|
||||
// only available in mv2
|
||||
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
||||
// background crashes when a listener with empty url is registered
|
||||
else if (Object.keys(temporaryReferers).length === 0) return;
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.removeListener(browserWebRequestOnBeforeSendHeadersListener);
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
browserWebRequestOnBeforeSendHeadersListener,
|
||||
{
|
||||
urls: domainsToMatch(Object.keys(temporaryReferers)),
|
||||
types: ['xmlhttprequest']
|
||||
},
|
||||
['blocking', 'requestHeaders']
|
||||
);
|
||||
}
|
||||
|
||||
async function browserWebRequestOnBeforeRedirectListener(details: Browser.webRequest.OnBeforeRedirectDetails) {
|
||||
const domain = new URL(details.url).host;
|
||||
const host = await getHost(domain);
|
||||
if (!host) return;
|
||||
|
||||
const newDomain = new URL(details.redirectUrl).host;
|
||||
|
||||
await Promise.all([
|
||||
HostSettings.addTemporaryHostDomain(host.id, newDomain, { registerContentScript: false }),
|
||||
// must be called manually, a message sent from a background script is only received on non-background pages
|
||||
reInjectContentScript(newDomain)
|
||||
]);
|
||||
|
||||
// add new domain to internal list of supported hosts and remount listener to also listen on the new domain
|
||||
if (allHostDomains.indexOf(newDomain) === -1) allHostDomains.push(newDomain);
|
||||
queueMicrotask(registerBrowserWebRequestOnBeforeRedirectListener);
|
||||
}
|
||||
function registerBrowserWebRequestOnBeforeRedirectListener() {
|
||||
// only available in mv2
|
||||
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
||||
|
||||
browser.webRequest.onBeforeRedirect.removeListener(browserWebRequestOnBeforeRedirectListener);
|
||||
browser.webRequest.onBeforeRedirect.addListener(browserWebRequestOnBeforeRedirectListener, {
|
||||
urls: domainsToMatch(allHostDomains),
|
||||
types: ['main_frame', 'sub_frame']
|
||||
});
|
||||
}
|
||||
|
||||
export default defineBackground(() => {
|
||||
registerBrowserRuntimeOnMessageListener();
|
||||
registerBrowserWebRequestOnBeforeRedirectListener();
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { BackgroundMessageType, sendBackgroundMessage } from '@/lib/communication';
|
||||
import { getHost, HostMatchType, hosts, type HostMatch } from '@/lib/host';
|
||||
import { FF2MPVSettings, PerDomainSettings } from '@/lib/settings';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: [...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`))],
|
||||
allFrames: true,
|
||||
runAt: 'document_end',
|
||||
main
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const host = await getHost(window.location.host);
|
||||
if (!host) return;
|
||||
|
||||
// if current windows is an iframe, check if the parent has any disabled hosts
|
||||
if (window !== window.top) {
|
||||
const tabUrl = await sendBackgroundMessage(BackgroundMessageType.RequestTabUrl, undefined);
|
||||
if (tabUrl) {
|
||||
const domain = new URL(tabUrl).host;
|
||||
|
||||
const perDomainSetting = await PerDomainSettings.get(domain);
|
||||
if (perDomainSetting.allDisabled) return;
|
||||
else if (perDomainSetting.disabledHostIds.indexOf(host.id) !== -1) return;
|
||||
}
|
||||
}
|
||||
|
||||
let re = null;
|
||||
for (const regex of host.regex) {
|
||||
if ((re = document.body.innerHTML.match(regex)) !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!re) {
|
||||
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 != HostMatchType.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}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Stream Bypass</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #131313; overflow: hidden">
|
||||
<script type="module">
|
||||
import { mount } from 'svelte';
|
||||
import Player from './Player.svelte';
|
||||
|
||||
mount(Player, {
|
||||
target: document.body
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,87 @@
|
||||
import Hls from 'hls.js';
|
||||
import {
|
||||
BackgroundMessageType,
|
||||
listenTabMessages,
|
||||
sendBackgroundMessage,
|
||||
sendTabBroadcast,
|
||||
TabMessageType
|
||||
} from '@/lib/communication';
|
||||
import { HostMatchType, hosts } from '@/lib/host';
|
||||
|
||||
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
|
||||
// multiple hosts need to have a correct referer set
|
||||
await sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
|
||||
domain: new URL(url).host,
|
||||
referer: 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 sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
|
||||
domain: new URL(url).host,
|
||||
referer: 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 = () =>
|
||||
sendTabBroadcast(TabMessageType.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 = listenTabMessages((type) => {
|
||||
if (type !== TabMessageType.RequestActiveMatch) return;
|
||||
notifyActiveMatch();
|
||||
});
|
||||
window.onbeforeunload = cancel;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import '@/assets/base.css';
|
||||
|
||||
import { fly } from 'svelte/transition';
|
||||
import Toast from '@/entrypoints/popup/components/Toast.svelte';
|
||||
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
|
||||
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
|
||||
import SiteConfig from '@/entrypoints/popup/pages/site-config/SiteConfig.svelte';
|
||||
import { isMobile } from '@/entrypoints/popup/state.js';
|
||||
|
||||
/* state init */
|
||||
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
||||
|
||||
/* types */
|
||||
type Page = 'main' | 'settings' | 'site-config';
|
||||
|
||||
/* states */
|
||||
let activePage = $state<Page>('main');
|
||||
</script>
|
||||
|
||||
<div class="flex w-78 overflow-hidden" class:w-screen={$isMobile}>
|
||||
<Toast />
|
||||
{#if activePage === 'main'}
|
||||
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-80 flex-1 flex flex-col">
|
||||
<Main
|
||||
onSettingsOpenRequest={() => (activePage = 'settings')}
|
||||
onSiteConfigOpenRequest={() => (activePage = 'site-config')}
|
||||
/>
|
||||
</div>
|
||||
{:else if activePage === 'settings'}
|
||||
<div
|
||||
transition:fly={{ x: 300, duration: 150 }}
|
||||
class="min-w-full w-full h-120 flex-1 flex flex-col"
|
||||
class:h-screen={$isMobile}
|
||||
>
|
||||
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
||||
</div>
|
||||
{:else if activePage === 'site-config'}
|
||||
<div
|
||||
transition:fly={{ x: 300, duration: 150 }}
|
||||
class="min-w-full w-full h-120 flex-1 flex flex-col"
|
||||
class:h-screen={$isMobile}
|
||||
>
|
||||
<SiteConfig onBackClick={() => (activePage = 'main')} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { ArrowLeft } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onclick: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onclick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||
{onclick}
|
||||
>
|
||||
<Icon src={ArrowLeft} size="1.1rem" />
|
||||
</button>
|
||||
@@ -0,0 +1,3 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<div class="w-full border-b border-gray-400"></div>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Host } from '@/lib/host';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
host: Host;
|
||||
enabled?: boolean;
|
||||
badge?: string;
|
||||
onclick?: () => void;
|
||||
actions: Snippet;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { host, enabled, badge, onclick, actions }: Props = $props();
|
||||
|
||||
let rowClass = $derived(
|
||||
`flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${onclick ? 'hover:bg-gray-800/40 cursor-pointer' : ''}`
|
||||
);
|
||||
|
||||
/* functions */
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (!onclick) return;
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onclick();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div
|
||||
class={rowClass}
|
||||
role={onclick ? 'button' : undefined}
|
||||
tabindex={onclick ? 0 : undefined}
|
||||
{onclick}
|
||||
onkeydown={handleKeydown}
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
class:bg-linux-mint-green={enabled === true}
|
||||
class:bg-gray-500={enabled === false}
|
||||
class:bg-transparent={enabled === undefined}
|
||||
></span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-100 truncate">{host.name}</p>
|
||||
{#if badge}
|
||||
<span
|
||||
class="shrink-0 text-[0.65rem] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-linux-mint-green/20 text-linux-mint-green"
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 truncate" title={host.domains.join(', ')}>
|
||||
{host.domains.join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
{@render actions()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { ExclamationTriangle, InformationCircle } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
level: 'warning' | 'info';
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { level, title, description }: Props = $props();
|
||||
|
||||
const icons = {
|
||||
warning: ExclamationTriangle,
|
||||
info: InformationCircle
|
||||
};
|
||||
const containerStyles = {
|
||||
warning: 'bg-yellow-900/30 border-yellow-700/50 text-yellow-200',
|
||||
info: 'bg-blue-900/30 border-blue-700/50 text-blue-200'
|
||||
};
|
||||
const iconStyles = {
|
||||
warning: 'text-yellow-400',
|
||||
info: 'text-blue-400'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mx-2 mt-1 flex items-start gap-2 px-2.5 py-2 border rounded-md text-xs {containerStyles[level]}">
|
||||
<Icon src={icons[level]} size="0.95rem" class={iconStyles[level]} />
|
||||
<div class="leading-snug">
|
||||
{#if title}
|
||||
<p class="font-semibold">{title}</p>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="opacity-80">{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
actionsLeft?: Snippet;
|
||||
actionsRight?: Snippet;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { title, subtitle, actionsLeft, actionsRight }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2 px-3 py-2.5">
|
||||
{#if actionsLeft}
|
||||
<div class="shrink-0 flex items-center gap-2">
|
||||
{@render actionsLeft()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<h1 class="text-lg font-semibold text-gray-100 leading-tight truncate">{title}</h1>
|
||||
{#if subtitle}
|
||||
<p class="text-xs text-gray-400 leading-tight truncate" title={subtitle}>{subtitle}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if actionsRight}
|
||||
<div class="shrink-0 flex items-center gap-2">
|
||||
{@render actionsRight()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { MagnifyingGlass, XMark } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { value = $bindable(''), placeholder = 'Search…' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-2 px-2.5 py-1.5 bg-gray-900/60 border border-gray-700 rounded-md focus-within:border-linux-mint-green transition-colors"
|
||||
>
|
||||
<Icon src={MagnifyingGlass} size="0.95rem" class="text-gray-400 shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
bind:value
|
||||
{placeholder}
|
||||
class="flex-1 bg-transparent text-sm text-gray-100 placeholder:text-gray-500 outline-none min-w-0"
|
||||
/>
|
||||
{#if value}
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 hover:text-gray-100 cursor-pointer shrink-0"
|
||||
onclick={() => (value = '')}
|
||||
>
|
||||
<Icon src={XMark} size="0.95rem" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from '@steeze-ui/heroicons';
|
||||
import { Icon, type IconSource } from '@steeze-ui/svelte-icon';
|
||||
import { untrack, type Snippet } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: IconSource;
|
||||
defaultOpen?: boolean;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { title, description, icon, defaultOpen = false, children }: Props = $props();
|
||||
let open = $state(untrack(() => defaultOpen));
|
||||
</script>
|
||||
|
||||
<div class="bg-gray-800/40 border border-gray-700/60 rounded-lg max-h-full flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 cursor-pointer hover:bg-gray-800/60 transition-colors"
|
||||
onclick={() => (open = !open)}
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
{#if icon}
|
||||
<Icon src={icon} size="1.1rem" class="text-gray-400 shrink-0" />
|
||||
{/if}
|
||||
<div class="min-w-0 text-left">
|
||||
<h3 class="text-sm font-semibold text-gray-100 leading-tight">{title}</h3>
|
||||
{#if description}
|
||||
<p class="text-xs text-gray-400 mt-0.5 leading-tight">{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-gray-400 shrink-0 inline-flex chevron" class:rotated={open}>
|
||||
<Icon src={ChevronDown} size="1rem" />
|
||||
</span>
|
||||
</button>
|
||||
{#if open}
|
||||
<div class="px-3 py-3 flex overflow-hidden" transition:slide={{ duration: 150 }}>
|
||||
<div class="w-full">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chevron {
|
||||
transition: transform 150ms ease;
|
||||
}
|
||||
.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { toast } from '@/entrypoints/popup/state';
|
||||
</script>
|
||||
|
||||
{#if $toast}
|
||||
<div
|
||||
class="absolute top-2 left-1/2 -translate-x-1/2 z-20 w-max max-w-11/12 px-3 py-1.5 bg-gray-700 text-gray-100 text-sm rounded shadow-lg pointer-events-none"
|
||||
transition:fly={{ y: -20, duration: 200 }}
|
||||
>
|
||||
{$toast.message}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
/* types */
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
||||
size?: 'sm' | 'md';
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { checked = $bindable(), onChecked, size = 'md', disabled = false, title }: Props = $props();
|
||||
let internalChecked = $state($state.snapshot(checked));
|
||||
|
||||
/* callbacks */
|
||||
async function onInputChange() {
|
||||
if (disabled) return;
|
||||
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"
|
||||
class:cursor-pointer={!disabled}
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:opacity-50={disabled}
|
||||
{title}
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="peer sr-only"
|
||||
bind:checked={internalChecked}
|
||||
onchange={onInputChange}
|
||||
{disabled}
|
||||
/>
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { mount } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,5 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<p class="text-[1.05rem]">Extension disabled</p>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
import { isMobile, showToast } 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 */
|
||||
async function copyUrl() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(urlOutput);
|
||||
showToast('URL copied');
|
||||
} catch {
|
||||
showToast('Failed to copy');
|
||||
}
|
||||
}
|
||||
</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-1 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>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { AdjustmentsHorizontal, ChevronRight, GlobeAlt } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
domain: string;
|
||||
onConfigureClick: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { domain, onConfigureClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 px-3 py-1.5 text-sm text-gray-300">
|
||||
<span class="flex items-center gap-2 min-w-0">
|
||||
<Icon src={GlobeAlt} size="0.95rem" class="text-gray-400 shrink-0" />
|
||||
<span class="truncate" title={domain}>{domain}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 shrink-0 text-xs text-gray-400 hover:text-gray-200 px-1.5 py-0.5 rounded hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||
onclick={onConfigureClick}
|
||||
>
|
||||
<Icon src={AdjustmentsHorizontal} size="0.85rem" />
|
||||
<span>Configure</span>
|
||||
<Icon src={ChevronRight} size="0.75rem" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { Cog6Tooth } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
|
||||
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>
|
||||
|
||||
<PageHeader title="stream-bypass" subtitle="v{import.meta.env.VERSION}">
|
||||
{#snippet actionsRight()}
|
||||
{#key allHostsDisabled}
|
||||
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
|
||||
{/key}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||
onclick={() => onSettingsClick()}
|
||||
>
|
||||
<Icon src={Cog6Tooth} size="1.1rem" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import '@/assets/base.css';
|
||||
|
||||
import { onDestroy } from 'svelte';
|
||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||
import Notice from '@/entrypoints/popup/components/Notice.svelte';
|
||||
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
|
||||
import CurrentSite from '@/entrypoints/popup/pages/main/CurrentSite.svelte';
|
||||
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
|
||||
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
|
||||
import { listenTabMessages, sendTabMessageToActiveTab, TabMessageType } from '@/lib/communication';
|
||||
import { hosts, type Host } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onSettingsOpenRequest: () => void;
|
||||
onSiteConfigOpenRequest: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onSettingsOpenRequest, onSiteConfigOpenRequest }: Props = $props();
|
||||
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||
let currentDomain = $state<string | null>(null);
|
||||
|
||||
let allHostsDisabled = $state(false);
|
||||
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
|
||||
|
||||
/* lifecycle */
|
||||
const cancel = listenTabMessages((type, data) => {
|
||||
if (type !== TabMessageType.NotifyActiveMatch) return;
|
||||
const match = {
|
||||
host: hosts.find((host) => host.id === data.id)!,
|
||||
url: data.url,
|
||||
domain: data.domain
|
||||
};
|
||||
currentMatch = match;
|
||||
});
|
||||
sendTabMessageToActiveTab(TabMessageType.RequestActiveMatch, undefined);
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
|
||||
const url = tabs[0]?.url;
|
||||
if (!url) return;
|
||||
currentDomain = new URL(url).hostname;
|
||||
});
|
||||
|
||||
onDestroy(cancel);
|
||||
</script>
|
||||
|
||||
<div class="w-full shrink-0">
|
||||
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 grid-rows-[min-content_auto_min-content] gap-2 h-full">
|
||||
<div class="px-2 pt-1.5 shrink-0">
|
||||
{#if currentDomain}
|
||||
<CurrentSite domain={currentDomain} onConfigureClick={onSiteConfigOpenRequest} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if allHostsDisabled}
|
||||
<Notice
|
||||
level="warning"
|
||||
title="Extension disabled"
|
||||
description="Configurations are still saved and take effect the when extension is re-enabled."
|
||||
/>
|
||||
{:else if currentMatch}
|
||||
<div class="flex flex-col justify-between h-full px-2">
|
||||
<Match host={currentMatch.host} />
|
||||
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<Notice level="info" title="No match found" description="No redirectable host found on this site" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="px-2 pb-0.5">
|
||||
<p class="text-[.65rem] text-gray-400 text-center">
|
||||
Suggestions or bugs can be submitted <a
|
||||
class="underline"
|
||||
href="https://github.com/bytedream/stream-bypass/issues">here</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { type Host } from '@/lib/host';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
host: Host;
|
||||
}
|
||||
|
||||
/* states */
|
||||
const { host }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<p class="text-md text-gray-400">Match found:</p>
|
||||
<span class="underline text-green-400 text-xl font-bold select-text">{host.name}</span>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { showToast } from '../../state';
|
||||
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 */
|
||||
async function onEnableChange(enabled: boolean) {
|
||||
if (!enabled) return true;
|
||||
|
||||
const id = setTimeout(() => showToast('Accept the permission request to allow communication with mpv'), 100);
|
||||
const confirmed = await browser.permissions.request({ permissions: ['nativeMessaging'] });
|
||||
clearTimeout(id);
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<p class="text-sm font-semibold text-gray-100">Communication enabled</p>
|
||||
<a
|
||||
class="inline-flex items-center gap-1 text-xs text-gray-400 hover:text-gray-200 transition-colors"
|
||||
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon src={InformationCircle} size="0.85rem" />
|
||||
<span>Learn more</span>
|
||||
</a>
|
||||
</div>
|
||||
{#key enabled}
|
||||
<Toggle
|
||||
bind:checked={() => enabled, (v) => FF2MPVSettings.setEnabled(v)}
|
||||
onChecked={onEnableChange}
|
||||
size="sm"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
|
||||
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onBackClick: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onBackClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<PageHeader title="Settings" subtitle="Manage hosts and integrations">
|
||||
{#snippet actionsLeft()}
|
||||
<BackButton onclick={onBackClick} />
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||
import SearchInput from '@/entrypoints/popup/components/SearchInput.svelte';
|
||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||
import { filterHosts, hosts } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { searchQuery = $bindable('') }: Props = $props();
|
||||
let disabledHostIds = $state<Array<string>>([]);
|
||||
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
|
||||
|
||||
let filtered = $derived(filterHosts(hosts, searchQuery));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 max-h-full">
|
||||
<SearchInput bind:value={searchQuery} placeholder="Filter hosts…" />
|
||||
{#if filtered.length === 0}
|
||||
<p class="text-xs text-gray-400 text-center py-2">No hosts match "{searchQuery}"</p>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y divide-gray-700/40 -mx-1 max-h-full overflow-y-scroll">
|
||||
{#each filtered as host (host.id)}
|
||||
<HostRow {host} enabled={!disabledHostIds.includes(host.id)}>
|
||||
{#snippet actions()}
|
||||
{#key disabledHostIds}
|
||||
<Toggle
|
||||
bind:checked={
|
||||
() => !disabledHostIds.includes(host.id),
|
||||
(v) =>
|
||||
v
|
||||
? HostSettings.removeDisabledHost(host.id)
|
||||
: HostSettings.addDisabledHost(host.id)
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
{/key}
|
||||
{/snippet}
|
||||
</HostRow>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,207 @@
|
||||
<script lang="ts">
|
||||
import { showToast } from '../../state';
|
||||
import { Trash } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||
import { getHostFromId, hosts } from '@/lib/host';
|
||||
import { PerDomainSettings, type PerDomainConfig } from '@/lib/settings';
|
||||
|
||||
/* constants */
|
||||
const ALL_HOSTS = '__all__';
|
||||
|
||||
/* states */
|
||||
let perDomainMap = $state<Record<string, PerDomainConfig>>({});
|
||||
let expanded = $state<string | null>(null);
|
||||
|
||||
let newDomainInput = $state<string>('');
|
||||
let selectedHostId = $state<string>('');
|
||||
|
||||
loadMap();
|
||||
|
||||
/* functions */
|
||||
async function loadMap() {
|
||||
perDomainMap = await PerDomainSettings.getMap();
|
||||
}
|
||||
|
||||
function parseDomain(input: string): string | null {
|
||||
let d = input.trim().toLowerCase();
|
||||
if (!d) return null;
|
||||
d = d.replace(/^https?:\/\//, '');
|
||||
d = d.split('/')[0];
|
||||
d = d.split(':')[0];
|
||||
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/.test(d)) {
|
||||
return null;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/* callbacks */
|
||||
async function onClear(domain: string) {
|
||||
await PerDomainSettings.clear(domain);
|
||||
await loadMap();
|
||||
if (expanded === domain) expanded = null;
|
||||
}
|
||||
|
||||
async function onAllRemove(domain: string) {
|
||||
await PerDomainSettings.setAllDisabled(domain, false);
|
||||
await loadMap();
|
||||
}
|
||||
|
||||
async function onHostRemove(domain: string, hostId: string) {
|
||||
await PerDomainSettings.setHostDisabled(domain, hostId, false);
|
||||
await loadMap();
|
||||
}
|
||||
|
||||
async function onHostAdd(domain: string, hostId: string) {
|
||||
if (!hostId) return;
|
||||
if (hostId === ALL_HOSTS) {
|
||||
await PerDomainSettings.setAllDisabled(domain, true);
|
||||
} else {
|
||||
await PerDomainSettings.setHostDisabled(domain, hostId, true);
|
||||
}
|
||||
await loadMap();
|
||||
}
|
||||
|
||||
async function onDomainAdd(input: string) {
|
||||
const domain = parseDomain(input);
|
||||
if (!domain) {
|
||||
showToast('Invalid domain');
|
||||
return;
|
||||
}
|
||||
await PerDomainSettings.add(domain);
|
||||
await loadMap();
|
||||
expanded = domain;
|
||||
}
|
||||
|
||||
function toggleExpand(domain: string) {
|
||||
expanded = expanded === domain ? null : domain;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1 h-full overflow-y-scroll">
|
||||
<div class="flex items-center gap-2 pb-1.5">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newDomainInput}
|
||||
placeholder="Add domain…"
|
||||
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-2 py-1 text-gray-100 placeholder:text-gray-500 focus:border-linux-mint-green focus:outline-none transition-colors"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') onDomainAdd(newDomainInput).then(() => (newDomainInput = ''));
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
||||
disabled={!newDomainInput.trim()}
|
||||
onclick={() => onDomainAdd(newDomainInput).then(() => (newDomainInput = ''))}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
{#if Object.keys(perDomainMap).length === 0}
|
||||
<p class="text-xs text-gray-400 text-center py-2">
|
||||
No per-site disables configured. Use the popup on a streaming site to configure it.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each Object.entries(perDomainMap) as [domain, settings] (domain)}
|
||||
{@const isOpen = expanded === domain}
|
||||
<div class="border border-gray-700/60 rounded-md overflow-hidden">
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="w-full flex items-center justify-between gap-2 px-2.5 py-1.5 bg-gray-900/40 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||
role="button"
|
||||
tabindex={0}
|
||||
onclick={() => toggleExpand(domain)}
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-100 truncate">{domain}</p>
|
||||
<p class="text-[0.7rem] text-gray-400">
|
||||
{#if settings.allDisabled}
|
||||
All hosts disabled
|
||||
{:else}
|
||||
{settings.disabledHostIds.length}
|
||||
{settings.disabledHostIds.length === 1 ? 'host' : 'hosts'} disabled
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear(domain);
|
||||
}}
|
||||
>
|
||||
<Icon src={Trash} size="0.9rem" />
|
||||
</button>
|
||||
</div>
|
||||
{#if isOpen}
|
||||
{@const availableHosts = hosts.filter((h) => !settings.disabledHostIds.includes(h.id))}
|
||||
<div class="px-2 py-2 border-t border-gray-700/60 flex flex-col gap-2">
|
||||
{#if settings.allDisabled}
|
||||
<div class="flex items-center justify-between gap-2 px-2 py-1.5 bg-gray-900/40 rounded">
|
||||
<p class="text-xs text-gray-300">All hosts disabled</p>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||
onclick={() => onAllRemove(domain)}
|
||||
>
|
||||
<Icon src={Trash} size="0.9rem" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if settings.disabledHostIds.length > 0}
|
||||
<div class="flex flex-col -mx-1 max-h-60 overflow-y-auto">
|
||||
{#each settings.disabledHostIds as hostId (hostId)}
|
||||
{@const host = getHostFromId(hostId)}
|
||||
{#if host}
|
||||
<HostRow {host} enabled={false}>
|
||||
{#snippet actions()}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||
onclick={() => onHostRemove(domain, hostId)}
|
||||
>
|
||||
<Icon src={Trash} size="0.9rem" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</HostRow>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !settings.allDisabled && settings.disabledHostIds.length === 0}
|
||||
<p class="text-xs text-gray-400 text-center py-1">No overrides set</p>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<select
|
||||
bind:value={selectedHostId}
|
||||
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-1.5 py-1 text-gray-100 cursor-pointer focus:border-linux-mint-green focus:outline-none transition-colors"
|
||||
>
|
||||
<option value="" disabled>Add host…</option>
|
||||
{#if !settings.allDisabled}
|
||||
<option value={ALL_HOSTS}>All hosts</option>
|
||||
<option disabled>─────────</option>
|
||||
{/if}
|
||||
{#each availableHosts as host (host.id)}
|
||||
<option value={host.id}>{host.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
||||
disabled={!selectedHostId}
|
||||
onclick={() => onHostAdd(domain, selectedHostId).then(() => (selectedHostId = ''))}
|
||||
>
|
||||
{selectedHostId === ALL_HOSTS ? 'Disable all' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { CommandLine, MapPin, ServerStack } from '@steeze-ui/heroicons';
|
||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||
import Section from '@/entrypoints/popup/components/Section.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 PerSiteDisables from '@/entrypoints/popup/pages/settings/PerSiteDisables.svelte';
|
||||
import { isMobile } from '@/entrypoints/popup/state';
|
||||
import { PerDomainSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onSettingsCloseRequest: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onSettingsCloseRequest }: Props = $props();
|
||||
let searchQuery = $state('');
|
||||
|
||||
onDestroy(PerDomainSettings.cleanEmpty);
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<Header onBackClick={onSettingsCloseRequest} />
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-2 h-full overflow-y-scroll">
|
||||
<div class="max-h-full">
|
||||
<Section
|
||||
title="Hosts"
|
||||
description="Enable or disable support for specific streaming providers"
|
||||
icon={ServerStack}
|
||||
defaultOpen
|
||||
>
|
||||
<HostsTable bind:searchQuery />
|
||||
</Section>
|
||||
</div>
|
||||
<Section title="Per-site disables" description="Disable hosts for specific websites" icon={MapPin}>
|
||||
<PerSiteDisables />
|
||||
</Section>
|
||||
{#if !$isMobile}
|
||||
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
|
||||
<Ff2mpv />
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
|
||||
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
domain: string;
|
||||
onBackClick: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { domain, onBackClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<PageHeader title="Site settings" subtitle={domain}>
|
||||
{#snippet actionsLeft()}
|
||||
<BackButton onclick={onBackClick} />
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||
import { hosts, type Host } from '@/lib/host';
|
||||
import { PerDomainSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
domain: string;
|
||||
matchedHostId: string | null;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { domain, matchedHostId }: Props = $props();
|
||||
|
||||
let perDomain = $state<{ allDisabled: boolean; disabledHostIds: string[] }>({
|
||||
allDisabled: false,
|
||||
disabledHostIds: []
|
||||
});
|
||||
|
||||
// `domain` is updated after this component is active, and thus we must react to this update
|
||||
$effect(() => {
|
||||
PerDomainSettings.get(domain).then((val) => (perDomain = val));
|
||||
});
|
||||
|
||||
let orderedHosts = $derived(getOrderedHosts(matchedHostId));
|
||||
|
||||
/* functions */
|
||||
function getOrderedHosts(matchedId: string | null): Host[] {
|
||||
const matched = matchedId ? hosts.find((h) => h.id === matchedId) : null;
|
||||
const rest = hosts
|
||||
.filter((h) => !matched || h.id !== matched.id)
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return matched ? [matched, ...rest] : rest;
|
||||
}
|
||||
|
||||
/* callbacks */
|
||||
async function onHostToggle(hostId: string, enabled: boolean) {
|
||||
await PerDomainSettings.setHostDisabled(domain, hostId, !enabled);
|
||||
perDomain = await PerDomainSettings.get(domain);
|
||||
}
|
||||
|
||||
async function onAllToggle(enabled: boolean) {
|
||||
await PerDomainSettings.setAllDisabled(domain, !enabled);
|
||||
perDomain = await PerDomainSettings.get(domain);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 px-2 pb-2 h-full overflow-hidden">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto -mx-1 divide-y divide-gray-700/40">
|
||||
{#each orderedHosts as host (host.id)}
|
||||
{@const isInDisabledList = perDomain.disabledHostIds.includes(host.id)}
|
||||
<HostRow
|
||||
{host}
|
||||
enabled={!isInDisabledList && !perDomain.allDisabled}
|
||||
badge={host.id === matchedHostId ? 'Match' : undefined}
|
||||
>
|
||||
{#snippet actions()}
|
||||
{#key perDomain.allDisabled + perDomain.disabledHostIds.join(',')}
|
||||
<Toggle
|
||||
disabled={perDomain.allDisabled}
|
||||
title={perDomain.allDisabled ? 'All hosts are disabled for this site' : undefined}
|
||||
bind:checked={
|
||||
() => !isInDisabledList,
|
||||
(v) => {
|
||||
onHostToggle(host.id, v);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
{/key}
|
||||
{/snippet}
|
||||
</HostRow>
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-3 py-2 mt-1 bg-gray-800/40 border border-gray-700/60 rounded-md shrink-0"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-100">Disable all on this site</p>
|
||||
<p class="text-xs text-gray-400 truncate">Turn off all hosts for {domain}</p>
|
||||
</div>
|
||||
{#key perDomain.allDisabled}
|
||||
<Toggle
|
||||
bind:checked={
|
||||
() => !perDomain.allDisabled,
|
||||
(v) => {
|
||||
onAllToggle(v);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||
import Header from '@/entrypoints/popup/pages/site-config/Header.svelte';
|
||||
import PerSiteHostsList from '@/entrypoints/popup/pages/site-config/PerSiteHostsList.svelte';
|
||||
import { listenTabMessages, sendTabMessageToActiveTab, TabMessageType } from '@/lib/communication';
|
||||
import { hosts, type Host } from '@/lib/host';
|
||||
import { PerDomainSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onBackClick: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onBackClick }: Props = $props();
|
||||
|
||||
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||
let currentDomain = $state<string | null>(null);
|
||||
|
||||
/* lifecycle */
|
||||
const cancel = listenTabMessages((type, data) => {
|
||||
if (type !== TabMessageType.NotifyActiveMatch) return;
|
||||
const match = {
|
||||
host: hosts.find((host) => host.id === data.id)!,
|
||||
url: data.url,
|
||||
domain: data.domain
|
||||
};
|
||||
currentMatch = match;
|
||||
});
|
||||
sendTabMessageToActiveTab(TabMessageType.RequestActiveMatch, undefined);
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
|
||||
const url = tabs[0]?.url;
|
||||
if (!url) return;
|
||||
currentDomain = new URL(url).hostname;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
cancel();
|
||||
PerDomainSettings.cleanEmpty();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<Header domain={currentDomain ?? ''} {onBackClick} />
|
||||
<Divider />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
{#if currentDomain}
|
||||
<PerSiteHostsList domain={currentDomain} matchedHostId={currentMatch?.host.id ?? null} />
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-full text-sm text-gray-400 p-4 text-center">
|
||||
No active website detected.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
/* --- global init --- */
|
||||
export const isMobile = writable(false);
|
||||
|
||||
/* --- toast --- */
|
||||
interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const toast = writable<Toast | null>(null);
|
||||
|
||||
let toastId = 0;
|
||||
|
||||
export function showToast(message: string, duration = 2000) {
|
||||
const id = ++toastId;
|
||||
toast.set({ id, message });
|
||||
setTimeout(() => {
|
||||
toast.update((current) => (current?.id === id ? null : current));
|
||||
}, duration);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,46 +0,0 @@
|
||||
function hasSuffix(content: string, suffix: string) {
|
||||
return content.indexOf(suffix, content.length - suffix.length) !== -1
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
chrome.storage.local.get(['all', 'disabled'], function (result) {
|
||||
let keys = Object.keys(result)
|
||||
if (keys.indexOf('all') !== -1 && !result['all']) {
|
||||
return
|
||||
}
|
||||
// @ts-ignore
|
||||
for (let match of matches) {
|
||||
let domain = match[0] as string
|
||||
if (window.location.href.indexOf(domain) !== -1) {
|
||||
if (keys.indexOf('disabled') === -1 || result['disabled'].indexOf(domain) === -1) {
|
||||
let regex = match[1] as RegExp
|
||||
let matchClass = match[2] as Match
|
||||
let reliability = match[3] as Reliability
|
||||
|
||||
let re
|
||||
if (regex !== null) {
|
||||
if ((re = document.body.innerHTML.match(regex)) === null) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
re = document.body.innerHTML.match(regex)
|
||||
}
|
||||
|
||||
if (matchClass === null) {
|
||||
if (regex === null) {
|
||||
location.assign(document.body.innerHTML)
|
||||
} else {
|
||||
// @ts-ignore
|
||||
location.assign(hasSuffix(re[0], 'm3u8') ? chrome.runtime.getURL(`res/hls.html?domain=${domain}&reliability=${reliability}#${re[0]}`) : re[0])
|
||||
}
|
||||
} else {
|
||||
matchClass.match(re).then(function (path) {
|
||||
// @ts-ignore
|
||||
location.assign(hasSuffix(path, 'm3u8') ? chrome.runtime.getURL(`res/hls.html?domain=${domain}&reliability=${reliability}#${path}`) : path)
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
// --- tab communication --- //
|
||||
export enum TabMessageType {
|
||||
RequestActiveMatch = 10,
|
||||
NotifyActiveMatch
|
||||
}
|
||||
|
||||
export type TabMessageData<T extends TabMessageType> = {
|
||||
[TabMessageType.RequestActiveMatch]: undefined;
|
||||
[TabMessageType.NotifyActiveMatch]: {
|
||||
id: string;
|
||||
url: string;
|
||||
domain: string;
|
||||
};
|
||||
}[T];
|
||||
|
||||
export async function sendTabBroadcast<T extends TabMessageType>(message: T, data: TabMessageData<T>) {
|
||||
await browser.runtime.sendMessage({ type: message, data: data });
|
||||
}
|
||||
|
||||
export async function sendTabMessageToActiveTab<T extends TabMessageType>(message: T, data: TabMessageData<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 listenTabMessages(listener: (type: TabMessageType, data: any) => void): () => void {
|
||||
const callback = (callbackData: { type: TabMessageType; data: any }) => {
|
||||
const { type, data } = callbackData;
|
||||
listener(type, data);
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(callback);
|
||||
return () => browser.runtime.onMessage.removeListener(callback);
|
||||
}
|
||||
|
||||
// --- background script communication --- //
|
||||
export enum BackgroundMessageType {
|
||||
Ff2mpv = 20,
|
||||
RequestTabUrl,
|
||||
RegisterContentScript,
|
||||
RegisterTemporaryReferer
|
||||
}
|
||||
|
||||
export type BackgroundMessageData<T extends BackgroundMessageType> = {
|
||||
[BackgroundMessageType.Ff2mpv]: {
|
||||
url: string;
|
||||
};
|
||||
[BackgroundMessageType.RequestTabUrl]: undefined;
|
||||
[BackgroundMessageType.RegisterContentScript]: {
|
||||
domain: string;
|
||||
};
|
||||
[BackgroundMessageType.RegisterTemporaryReferer]: {
|
||||
domain: string;
|
||||
referer: string;
|
||||
};
|
||||
}[T];
|
||||
|
||||
export type BackgroundMessageReply<T extends BackgroundMessageType> = {
|
||||
[BackgroundMessageType.Ff2mpv]: undefined;
|
||||
[BackgroundMessageType.RequestTabUrl]: string | null;
|
||||
[BackgroundMessageType.RegisterContentScript]: undefined;
|
||||
[BackgroundMessageType.RegisterTemporaryReferer]: undefined;
|
||||
}[T];
|
||||
|
||||
export async function sendBackgroundMessage<T extends BackgroundMessageType>(
|
||||
message: T,
|
||||
data: BackgroundMessageData<T>
|
||||
): Promise<BackgroundMessageReply<T>> {
|
||||
const response = await browser.runtime.sendMessage({ type: message, data: data });
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { HostMatchType, type Host } from '@/lib/host';
|
||||
|
||||
export default {
|
||||
name: 'Doodstream',
|
||||
id: 'doodstream',
|
||||
domains: [
|
||||
'do7go.com',
|
||||
'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.NATIVE,
|
||||
url: `${await response.text()}1234567890${match[2]}${Date.now()}`
|
||||
};
|
||||
}
|
||||
} satisfies Host;
|
||||
@@ -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;
|
||||
@@ -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.id, 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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,105 @@
|
||||
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, PerDomainSettings } 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 type HostId = string;
|
||||
|
||||
export interface Host {
|
||||
name: string;
|
||||
id: HostId;
|
||||
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 function getHostFromId(id: HostId): Host | null {
|
||||
return hosts.find((host) => host.id === id) ?? null;
|
||||
}
|
||||
|
||||
export function filterHosts(list: Host[], query: string): Host[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return list;
|
||||
return list.filter((host) => {
|
||||
if (host.name.toLowerCase().includes(q)) return true;
|
||||
return host.domains.some((domain) => domain.toLowerCase().includes(q));
|
||||
});
|
||||
}
|
||||
|
||||
export async function getHost(domain: string): Promise<Host | null> {
|
||||
const [allHostsDisabled, disabledHosts, perDomainSetting] = await Promise.all([
|
||||
HostSettings.getAllHostsDisabled(),
|
||||
HostSettings.getDisabledHosts(),
|
||||
PerDomainSettings.get(domain)
|
||||
]);
|
||||
|
||||
// extension is disabled via popup
|
||||
if (allHostsDisabled) return null;
|
||||
// domain is disabled via popup
|
||||
else if (perDomainSetting.allDisabled) return null;
|
||||
|
||||
for (const host of hosts) {
|
||||
if (host.domains.includes(domain)) {
|
||||
// host is generally disabled via popup
|
||||
if (disabledHosts.includes(host.id)) return null;
|
||||
// host is disabled for this domain via popup
|
||||
else if (perDomainSetting.disabledHostIds.includes(host.id)) return null;
|
||||
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
return HostSettings.checkTemporaryHostDomain(domain).then(getHostFromId);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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.id, window.location.hostname);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: HostMatchType.HLS,
|
||||
url: match[0]
|
||||
};
|
||||
}
|
||||
} satisfies Host;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { HostMatchType, type Host } from '@/lib/host';
|
||||
|
||||
export default {
|
||||
name: 'Vidmoly',
|
||||
id: 'vidmoly',
|
||||
// vidmoly has multiple domains, but regardless of the domain, the actual videos are loaded via an iframe which
|
||||
// always points to vidmoly.net. the "outer" vidmoly site also has a link to some video, which, would
|
||||
// be preferred to use, as the whole site could be replaced by the native video player instead of just the iframe,
|
||||
// but said link doesn't always point the same video as the iframe
|
||||
domains: ['vidmoly.net', 'vidmoly.biz'],
|
||||
regex: [/file:\s?'(http.*)'/],
|
||||
|
||||
match: async function (match: RegExpMatchArray) {
|
||||
return {
|
||||
type: HostMatchType.HLS,
|
||||
url: match[1]
|
||||
};
|
||||
}
|
||||
} satisfies Host;
|
||||
@@ -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.NATIVE,
|
||||
url: match[0]
|
||||
};
|
||||
}
|
||||
} satisfies Host;
|
||||
@@ -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.id, 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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,121 @@
|
||||
import { storage } from '#imports';
|
||||
import { BackgroundMessageType, sendBackgroundMessage } from './communication';
|
||||
import type { HostId } from '@/lib/host';
|
||||
|
||||
class Setting<T> {
|
||||
private item;
|
||||
|
||||
constructor(key: Parameters<typeof storage.defineItem>[0], fallback: T) {
|
||||
this.item = storage.defineItem(key, { fallback });
|
||||
}
|
||||
|
||||
get = () => this.item.getValue();
|
||||
set = (value: T) => this.item.setValue(value);
|
||||
|
||||
update = (fn: (val: T) => T) => this.get().then(fn).then(this.set);
|
||||
}
|
||||
|
||||
export class HostSettings {
|
||||
/* disabled hosts */
|
||||
private static disabledHosts = new Setting<HostId[]>('local:disabledHosts', []);
|
||||
private static allHostsDisabled = new Setting<boolean>('local:allHostsDisabled', false);
|
||||
|
||||
static addDisabledHost = (hostId: HostId) =>
|
||||
this.disabledHosts.update((val) => {
|
||||
if (!val.includes(hostId)) val.push(hostId);
|
||||
return val;
|
||||
});
|
||||
static removeDisabledHost = (hostId: HostId) =>
|
||||
this.disabledHosts.update((val) => {
|
||||
const index = val.indexOf(hostId);
|
||||
if (index !== -1) val.splice(index, 1);
|
||||
return val;
|
||||
});
|
||||
|
||||
static getDisabledHosts = () => this.disabledHosts.get();
|
||||
static getAllHostsDisabled = () => this.allHostsDisabled.get();
|
||||
static setAllHostsDisabled = (disabled: boolean) => this.allHostsDisabled.set(disabled);
|
||||
|
||||
/* tmp */
|
||||
private static temporaryHostDomain = new Setting<Record<string, string>>('local:temporaryHostDomain', {});
|
||||
|
||||
static addTemporaryHostDomain = async (
|
||||
hostId: HostId,
|
||||
domain: string,
|
||||
options = {
|
||||
// only has an effect with mv2
|
||||
registerContentScript: import.meta.env.MANIFEST_VERSION === 2
|
||||
}
|
||||
) => {
|
||||
const backgroundScriptInject = options.registerContentScript
|
||||
? sendBackgroundMessage(BackgroundMessageType.RegisterContentScript, { domain: domain })
|
||||
: Promise.resolve();
|
||||
const temporaryHostDomainUpdate = this.temporaryHostDomain.update((val) => {
|
||||
val[domain] = hostId;
|
||||
return val;
|
||||
});
|
||||
|
||||
return Promise.all([backgroundScriptInject, temporaryHostDomainUpdate]);
|
||||
};
|
||||
static checkTemporaryHostDomain = (domain: string) => this.temporaryHostDomain.get().then((val) => val[domain]);
|
||||
}
|
||||
|
||||
export class FF2MPVSettings {
|
||||
private static ff2mpvEnabled = new Setting<boolean>('local:ff2mpv', false);
|
||||
|
||||
static getEnabled = () => this.ff2mpvEnabled.get();
|
||||
static setEnabled = (enabled: boolean) => this.ff2mpvEnabled.set(enabled);
|
||||
}
|
||||
|
||||
export interface PerDomainConfig {
|
||||
allDisabled: boolean;
|
||||
disabledHostIds: HostId[];
|
||||
}
|
||||
|
||||
export class PerDomainSettings {
|
||||
private static storage = new Setting<Record<string, PerDomainConfig>>('local:perDomainSettings', {});
|
||||
|
||||
private static ensureDomain = (val: Record<string, PerDomainConfig>, domain: string) => {
|
||||
if (!val[domain]) val[domain] = { allDisabled: false, disabledHostIds: [] };
|
||||
return val[domain];
|
||||
};
|
||||
|
||||
static get = (domain: string): Promise<PerDomainConfig> =>
|
||||
this.storage.get().then((val) => val[domain] ?? { allDisabled: false, disabledHostIds: [] });
|
||||
|
||||
static getMap = () => this.storage.get();
|
||||
|
||||
static setAllDisabled = (domain: string, disabled: boolean) =>
|
||||
this.storage.update((val) => {
|
||||
this.ensureDomain(val, domain).allDisabled = disabled;
|
||||
return val;
|
||||
});
|
||||
|
||||
static setHostDisabled = (domain: string, hostId: HostId, disabled: boolean) =>
|
||||
this.storage.update((val) => {
|
||||
const entry = this.ensureDomain(val, domain);
|
||||
if (!disabled) entry.disabledHostIds = entry.disabledHostIds.filter((id) => id !== hostId);
|
||||
else if (!entry.disabledHostIds.includes(hostId)) entry.disabledHostIds.push(hostId);
|
||||
return val;
|
||||
});
|
||||
|
||||
static add = (domain: string) =>
|
||||
this.storage.update((val) => {
|
||||
this.ensureDomain(val, domain);
|
||||
return val;
|
||||
});
|
||||
|
||||
static clear = (domain: string) =>
|
||||
this.storage.update((val) => {
|
||||
delete val[domain];
|
||||
return val;
|
||||
});
|
||||
|
||||
static cleanEmpty = () =>
|
||||
this.storage.update((val) => {
|
||||
for (const [domain, settings] of Object.entries(val)) {
|
||||
if (!settings.allDisabled && settings.disabledHostIds.length === 0) delete val[domain];
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Stream Bypass",
|
||||
"author": "ByteDream",
|
||||
"description": "A multi-browser addon / extension for multiple streaming providers which redirects directly to the source video.",
|
||||
"version": "1.5.1",
|
||||
"homepage_url": "https://github.com/ByteDream/stream-bypass",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{55dd42e8-3dd9-455a-b4fe-86664881b10c}"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"all_frames": true,
|
||||
"matches": [
|
||||
"*://evoload.io/*",
|
||||
"*://mcloud.to/*",
|
||||
"*://mixdrop.co/*",
|
||||
"*://streamtape.com/*",
|
||||
"*://streamzz.to/*",
|
||||
"*://thevideome.com/*",
|
||||
"*://vidlox.me/*",
|
||||
"*://vidstream.pro/*",
|
||||
"*://vidoza.net/*",
|
||||
"*://vivo.st/*",
|
||||
"*://vivo.sx/*",
|
||||
"*://voe.sx/*",
|
||||
"*://vupload.com/*"
|
||||
],
|
||||
"js": [
|
||||
"match.js",
|
||||
"index.js"
|
||||
],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"browser_action": {
|
||||
"default_icon": "icons/stream-bypass.png",
|
||||
"default_title": "Stream Bypass",
|
||||
"default_popup": "popup/popup.html"
|
||||
},
|
||||
"icons": {
|
||||
"128": "icons/stream-bypass.png"
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
enum Reliability {
|
||||
LOW = 1,
|
||||
NORMAL,
|
||||
HIGH
|
||||
}
|
||||
|
||||
interface Match {
|
||||
match(match: RegExpMatchArray): Promise<string>
|
||||
}
|
||||
|
||||
class Evoload implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
const code = window.location.pathname.split('/').slice(-1)[0]
|
||||
const response = await fetch('https://evoload.io/SecurePlayer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({code: code})
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
return json['stream']['src']
|
||||
}
|
||||
}
|
||||
|
||||
class MCloud implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
const code = window.location.pathname.split('/').slice(-1)[0]
|
||||
const response = await fetch(`https://mcloud.to/info/${code}?skey=${match[0]}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
referrer: `https://mcloud.to/embed/${code}`
|
||||
})
|
||||
const json = await response.json()
|
||||
return json['media']['sources'][0]['file']
|
||||
}
|
||||
}
|
||||
|
||||
class Mixdrop implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
return `https://a-${match[1]}.${match[4]}.${match[5]}/v/${match[2]}.${match[6]}?s=${match[12]}&e=${match[13]}`
|
||||
}
|
||||
}
|
||||
|
||||
class Streamtape implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
return `https://streamtape.com/get_video?${match[0]}`
|
||||
}
|
||||
}
|
||||
|
||||
class TheVideoMe implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
return `https://thevideome.com/${match[5]}.mp4`
|
||||
}
|
||||
}
|
||||
|
||||
class Upstream implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
return `https://${match[47]}.upstreamcdn.co/hls/${match.sort((a, b) => {return b.length - a.length})[0]}/master.m3u8`
|
||||
}
|
||||
}
|
||||
|
||||
class Vidstream implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
const code = window.location.pathname.split('/').slice(-1)[0]
|
||||
const response = await fetch(`https://vidstream.pro/info/${code}?skey=${match[0]}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
referrer: `https://vidstream.pro/embed/${code}`
|
||||
})
|
||||
const json = await response.json()
|
||||
return json['media']['sources'][0]['file']
|
||||
}
|
||||
}
|
||||
|
||||
class Vivo implements Match {
|
||||
async match(match: RegExpMatchArray): Promise<string> {
|
||||
return this.rot47(decodeURIComponent(match[0]))
|
||||
}
|
||||
|
||||
// decrypts a string with the rot47 algorithm (https://en.wikipedia.org/wiki/ROT13#Variants)
|
||||
rot47(encoded: string): string {
|
||||
const s = []
|
||||
for(let i = 0; i < encoded.length; i++) {
|
||||
const j = encoded.charCodeAt(i)
|
||||
if((j >= 33) && (j <= 126)) {
|
||||
s[i] = String.fromCharCode(33+((j+ 14)%94))
|
||||
} else {
|
||||
s[i] = String.fromCharCode(j)
|
||||
}
|
||||
}
|
||||
return s.join('')
|
||||
}
|
||||
}
|
||||
|
||||
// all domains to match. the matches must be structured like this:
|
||||
// [domain, regex match (can be null), class to call after match (can be null), reliability]
|
||||
// => the domain which should be redirected
|
||||
// => the regex gets called if the user visits a site with the given domain and matches the websites document body.
|
||||
// if the regex is null, the complete document body gets handled as one big regex match
|
||||
// => the class to call when the regex was parsed successfully. the class has to implement the `Match` interface.
|
||||
// if the class is null, the user gets redirected to the first regex match element
|
||||
// => the reliability shows how reliable a stream redirect is. for example, vivo.sx works nearly every time whereas
|
||||
// upstream.to works only sometimes because of a security mechanism they're using (CORS) which currently can't be bypassed
|
||||
//
|
||||
// every match HAS to be on an separate line (for automatically manifest generation)
|
||||
const matches = [
|
||||
['evoload.io', null, new Evoload(), Reliability.NORMAL],
|
||||
['mcloud.to', new RegExp(/(?<=')\w+(?=';)/gm), new MCloud(), Reliability.NORMAL],
|
||||
['mixdrop.co', new RegExp(/(?<=\|)\w{2,}/gm), new Mixdrop(), Reliability.HIGH],
|
||||
['streamtape.com', new RegExp(/id=\S*(?=')/gm), new Streamtape(), Reliability.NORMAL],
|
||||
['streamzz.to', new RegExp(/https?:\/\/get.streamz.tw\/getlink-\w+\.dll/gm), null, Reliability.NORMAL],
|
||||
['thevideome.com', new RegExp(/(?<=\|)\w{2,}/gm), new TheVideoMe(), Reliability.NORMAL],
|
||||
//['upstream.to', new RegExp(/(?<=\|)\w{2,}/gm), new Upstream(), Reliability.LOW],
|
||||
['vidlox.me', new RegExp(/(?<=\[")\S+?(?=")/gm), null, Reliability.NORMAL],
|
||||
['vidstream.pro', new RegExp(/(?<=')\w+(?=';)/gm), new Vidstream(), Reliability.LOW],
|
||||
['vidoza.net', new RegExp(/(?<=src:(\s*)?")\S*(?=")/gm), null, Reliability.NORMAL],
|
||||
['vivo.st', new RegExp(/(?<=source:\s')(\S+)(?=')/gm), new Vivo(), Reliability.HIGH],
|
||||
['vivo.sx', new RegExp(/(?<=source:\s')(\S+)(?=')/gm), new Vivo(), Reliability.HIGH],
|
||||
['voe.sx', new RegExp(/https?:\/\/\S*m3u8(?=")/gm), null, Reliability.HIGH],
|
||||
['vupload.com', new RegExp(/(?<=src:\s?").+?(?=")/gm), null, Reliability.NORMAL]
|
||||
]
|
||||
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
<script src="../ext/popper.min.js"></script>
|
||||
<script src="../ext/tippy-bundle.umd.min.js"></script>
|
||||
<script src="../match.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="all">
|
||||
<div class="buttons">
|
||||
<a>On</a>
|
||||
<a>Off</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<table id="sub-container">
|
||||
</table>
|
||||
<a id="error" href="https://github.com/ByteDream/stream-bypass/issues/new">Something does not work as expected</a>
|
||||
</div>
|
||||
<script src="./popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +0,0 @@
|
||||
body
|
||||
background-color: #2b2a33
|
||||
font-weight: bold
|
||||
max-height: 500px
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
|
||||
a, p
|
||||
color: white
|
||||
font-size: 16px
|
||||
margin: 5px 0
|
||||
|
||||
a
|
||||
border: 1px solid #281515
|
||||
cursor: pointer
|
||||
font-weight: normal
|
||||
padding: 5px 8px
|
||||
|
||||
&.active
|
||||
background-color: rgba(255, 65, 65, 0.74)
|
||||
cursor: default
|
||||
|
||||
&.disabled
|
||||
background-color: grey
|
||||
cursor: not-allowed
|
||||
|
||||
&#error
|
||||
border: none
|
||||
display: block
|
||||
font-weight: lighter
|
||||
font-size: 0.8rem
|
||||
text-align: center
|
||||
padding: 10px 0 5px 0
|
||||
|
||||
hr
|
||||
margin: 3px 0
|
||||
|
||||
|
||||
#all
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin: 10px 0
|
||||
|
||||
|
||||
.low-reliability
|
||||
text-decoration: underline
|
||||
text-decoration-color: rgb(255, 0, 0)
|
||||
|
||||
.normal-reliability
|
||||
text-decoration: underline
|
||||
text-decoration-color: yellow
|
||||
|
||||
.high-reliability
|
||||
text-decoration: underline
|
||||
text-decoration-color: #00ff00
|
||||
@@ -1,163 +0,0 @@
|
||||
function enableAll(enable: boolean) {
|
||||
// @ts-ignore
|
||||
chrome.storage.local.set({'all': enable})
|
||||
|
||||
// @ts-ignore
|
||||
for (let button of document.getElementById('sub-container').getElementsByTagName('a')) {
|
||||
enable ? button.classList.remove('disabled') : button.classList.add('disabled')
|
||||
}
|
||||
}
|
||||
|
||||
function enableOne(website: string, enable: boolean) {
|
||||
// @ts-ignore
|
||||
chrome.storage.local.get(['disabled'], function (result) {
|
||||
let disabled: string[] = Object.keys(result).length === 0 ? [] : result['disabled']
|
||||
if (enable && disabled.indexOf(website) !== -1) {
|
||||
disabled.splice(disabled.indexOf(website), 1)
|
||||
} else if (!enable && disabled.indexOf(website) === -1) {
|
||||
disabled.push(website)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
chrome.storage.local.set({'disabled': disabled})
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
chrome.storage.local.get(['all', 'disabled'], function (result) {
|
||||
let allDisabled = result['all'] !== undefined && !result['all']
|
||||
let disabled = new Map()
|
||||
|
||||
if (allDisabled) {
|
||||
// @ts-ignore
|
||||
for (let match of matches) {
|
||||
disabled.set(match[0], false)
|
||||
}
|
||||
} else {
|
||||
if (Object.keys(result).indexOf('disabled') !== -1) {
|
||||
for (let disable of result['disabled']) {
|
||||
disabled.set(disable, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let subContainer = document.getElementById('sub-container')
|
||||
// @ts-ignore
|
||||
for (let match of matches) {
|
||||
let row = document.createElement('tr')
|
||||
|
||||
let name = document.createElement('td')
|
||||
let nameValue = document.createElement('p')
|
||||
nameValue.innerText = match[0]
|
||||
switch (match[3]) {
|
||||
case 1: // low
|
||||
nameValue.classList.add('low-reliability')
|
||||
// @ts-ignore
|
||||
tippy(nameValue, {
|
||||
content: 'Low reliability: Errors may occur often'
|
||||
})
|
||||
break
|
||||
case 2: // normal
|
||||
nameValue.classList.add('normal-reliability')
|
||||
// @ts-ignore
|
||||
tippy(nameValue, {
|
||||
content: 'Normal reliability: Save to use but errors may occur'
|
||||
})
|
||||
break
|
||||
case 3: //high
|
||||
nameValue.classList.add('high-reliability')
|
||||
// @ts-ignore
|
||||
tippy(nameValue, {
|
||||
content: 'High reliability: Errors are very unlikely to happen'
|
||||
})
|
||||
break
|
||||
}
|
||||
let buttons = document.createElement('td')
|
||||
buttons.classList.add('buttons')
|
||||
let on = document.createElement('a')
|
||||
on.innerText = 'On'
|
||||
// @ts-ignore
|
||||
let onTippy = tippy(on, {
|
||||
content: `Enable ${match[0]}`,
|
||||
onMount: () => {
|
||||
if (on.classList.contains('active') || off.classList.contains('disabled')) {
|
||||
onTippy.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
let off = document.createElement('a')
|
||||
off.innerText = 'Off'
|
||||
// @ts-ignore
|
||||
let offTippy = tippy(off, {
|
||||
content: `Disable ${match[0]}`,
|
||||
onMount: () => {
|
||||
if (off.classList.contains('active') || off.classList.contains('disabled')) {
|
||||
offTippy.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
disabled.has(match[0]) ? off.classList.add('active') : on.classList.add('active')
|
||||
if (allDisabled) {
|
||||
on.classList.add('disabled')
|
||||
off.classList.add('disabled')
|
||||
}
|
||||
|
||||
on.onclick = function () {
|
||||
if (!on.classList.contains('disabled')) {
|
||||
enableOne(match[0], true)
|
||||
on.classList.add('active')
|
||||
off.classList.remove('active')
|
||||
}
|
||||
}
|
||||
off.onclick = function () {
|
||||
if (!off.classList.contains('disabled')) {
|
||||
enableOne(match[0], false)
|
||||
on.classList.remove('active')
|
||||
off.classList.add('active')
|
||||
}
|
||||
}
|
||||
|
||||
name.append(nameValue)
|
||||
buttons.append(on, off)
|
||||
row.append(name, buttons)
|
||||
subContainer.append(row)
|
||||
}
|
||||
|
||||
let allButtons = document.getElementById('all').getElementsByTagName('a')
|
||||
let allOn = allButtons[0]
|
||||
allButtons[0].onclick = function () {
|
||||
if (!allButtons[0].classList.contains('disabled')) {
|
||||
enableAll(true)
|
||||
allButtons[0].classList.add('active')
|
||||
allButtons[1].classList.remove('active')
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
let allOnTippy = tippy(allOn, {
|
||||
content: 'Enable all websites',
|
||||
onMount: () => {
|
||||
if (allButtons[0].classList.contains('active')) {
|
||||
allOnTippy.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
allButtons[1].onclick = function () {
|
||||
if (!allButtons[1].classList.contains('disabled')) {
|
||||
enableAll(false)
|
||||
allButtons[0].classList.remove('active')
|
||||
allButtons[1].classList.add('active')
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
let allOffTippy = tippy(allButtons[1], {
|
||||
content: 'Disable all websites',
|
||||
onMount: () => {
|
||||
if (allButtons[1].classList.contains('active')) {
|
||||
allOffTippy.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
allDisabled ? allButtons[1].classList.add('active') : allButtons[0].classList.add('active')
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>m3u8</title>
|
||||
<link rel="stylesheet" href="/res/hls.css">
|
||||
<script src="/ext/hls.light.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video"></video>
|
||||
<p id="message" hidden></p>
|
||||
<script src="/res/hls.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
body
|
||||
background-color: #131313
|
||||
|
||||
html, body, video
|
||||
width: 100%
|
||||
height: 100%
|
||||
margin: 0
|
||||
|
||||
video
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
|
||||
#message
|
||||
color: white
|
||||
text-align: center
|
||||
@@ -1,62 +0,0 @@
|
||||
function showMessage(message: string) {
|
||||
let messageElement = document.getElementById('message') as HTMLParagraphElement
|
||||
messageElement.innerText = message
|
||||
messageElement.hidden = false
|
||||
document.getElementById('video').hidden = true
|
||||
}
|
||||
|
||||
function loadHls() {
|
||||
let url = window.location.hash.substring(1)
|
||||
let video = document.getElementById('video') as HTMLVideoElement;
|
||||
|
||||
video.controls = true
|
||||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = url
|
||||
// @ts-ignore
|
||||
} else if (Hls.isSupported()) {
|
||||
// @ts-ignore
|
||||
let hls = new Hls()
|
||||
hls.loadSource(url)
|
||||
hls.attachMedia(video)
|
||||
|
||||
let searchParams = new URLSearchParams(window.location.search)
|
||||
let rawReliability = parseInt(searchParams.get('reliability'))
|
||||
|
||||
let thirdPartyFallback = setTimeout(() => {
|
||||
let message: string
|
||||
|
||||
switch (rawReliability) {
|
||||
case 1: // low
|
||||
message = `The reliability for this domain is low, errors like this are common.
|
||||
Try to choose another streaming provider (if existent) or deactivate the addon for this domain (${searchParams.get('domain')}) and try again`
|
||||
break
|
||||
case 2: // normal
|
||||
message = `The reliability for this domain is normal, errors like this can occur but are not very common. Try to refresh the page`
|
||||
break
|
||||
case 3: // high
|
||||
message = `The reliability for this domains is high, errors like this are very unlikely to happen.
|
||||
Try to refresh the page and if the error still exists you might want to open a new issue <a href="https://github.com/ByteDream/stream-bypass/issues/new">here</a>.
|
||||
When you're using <a href="https://www.torproject.org/">Tor</a> such errors have a slight chance to occur more often,
|
||||
so if this is the case just try to reload the page and see if you it's working then`
|
||||
break
|
||||
}
|
||||
|
||||
// shows a message if hls could not be loaded
|
||||
showMessage(`Could not load hls video. ${message}`)
|
||||
}, rawReliability * 3000)
|
||||
|
||||
document.title = searchParams.get('domain')
|
||||
|
||||
// @ts-ignore
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
clearTimeout(thirdPartyFallback)
|
||||
document.getElementById('video').hidden = false
|
||||
document.getElementById('message').hidden = true
|
||||
})
|
||||
} else {
|
||||
// shows a message if hls is not supported
|
||||
showMessage(`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>`)
|
||||
}
|
||||
}
|
||||
|
||||
loadHls()
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"removeComments": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"scripthost",
|
||||
"es2015.collection",
|
||||
"es2015.promise"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Adapted from http://matthewfl.com/unPacker.html by matthew@matthewfl.com
|
||||
export async function unpack(packed: string): Promise<string> {
|
||||
const context = `
|
||||
{
|
||||
eval: function (c) {
|
||||
packed = c;
|
||||
},
|
||||
window: {},
|
||||
document: {}
|
||||
}
|
||||
`;
|
||||
const toExecute = `
|
||||
function() {
|
||||
let packed = "";
|
||||
with(${context}) {
|
||||
${packed}
|
||||
};
|
||||
return packed;
|
||||
}
|
||||
`;
|
||||
|
||||
const res = (await runInPageContext(toExecute)) as string;
|
||||
return res
|
||||
.replace(/;/g, ';\n')
|
||||
.replace(/{/g, '\n{\n')
|
||||
.replace(/}/g, '\n}\n')
|
||||
.replace(/\n;\n/g, ';\n')
|
||||
.replace(/\n\\n/g, '\n');
|
||||
}
|
||||
|
||||
// Adapted from: https://github.com/arikw/extension-page-context
|
||||
async function runInPageContext<T>(toExecute: string): Promise<T | null> {
|
||||
// test that we are running with the allow-scripts permission
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
window.sessionStorage;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
// returned value container
|
||||
const resultMessageId = crypto.randomUUID();
|
||||
|
||||
// prepare script container
|
||||
const scriptElm = document.createElement('script');
|
||||
scriptElm.setAttribute('type', 'application/javascript');
|
||||
|
||||
// inject the script
|
||||
scriptElm.textContent = `
|
||||
(
|
||||
async function () {
|
||||
const response = {
|
||||
id: '${resultMessageId}'
|
||||
};
|
||||
|
||||
try {
|
||||
response.result = JSON.stringify(await (${toExecute})() ); // run script
|
||||
} catch(err) {
|
||||
response.error = JSON.stringify(err);
|
||||
}
|
||||
|
||||
window.postMessage(response, '*');
|
||||
}
|
||||
)();
|
||||
`;
|
||||
|
||||
// run the script
|
||||
document.documentElement.appendChild(scriptElm);
|
||||
|
||||
// clean up script element
|
||||
scriptElm.remove();
|
||||
|
||||
let resolve: (value: T) => void;
|
||||
let reject: (value: any) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
function onResult(event: MessageEvent) {
|
||||
if (event.data.id === resultMessageId) {
|
||||
window.removeEventListener('message', onResult);
|
||||
if (event.data.error !== undefined) {
|
||||
return reject(JSON.parse(event.data.error));
|
||||
}
|
||||
return resolve(event.data.result !== undefined ? JSON.parse(event.data.result) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', onResult);
|
||||
|
||||
return await promise;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export function lastPathSegment(path: string): string {
|
||||
while (path.endsWith('/')) {
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
return path.substring(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"useDefineForClassFields": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="svelte" />
|
||||
@@ -0,0 +1,61 @@
|
||||
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 ? ['scripting', '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({})
|
||||
}
|
||||
}
|
||||
});
|
||||