├── .browserslistrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .husky
└── pre-push
├── .npmrc
├── LICENSE
├── README.md
├── babel.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── rollup.conf.js
├── sites.json
└── src
├── XFetch.js
├── clientUtils.js
├── globalSettingsManager.js
├── index.js
├── meta.js
├── profileManager.js
├── settings.js
├── style.module.css
├── trackerUtils.js
└── types
├── css.d.ts
└── index.d.ts
/.browserslistrc:
--------------------------------------------------------------------------------
1 | Chrome >= 55
2 | Firefox >= 53
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | quote_type = single
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /*
2 | !/src
3 | !/test
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: [
4 | require.resolve('@gera2ld/plaid/eslint'),
5 | 'plugin:prettier/recommended',
6 | ],
7 | settings: {
8 | 'import/resolver': {
9 | 'babel-module': {},
10 | },
11 | react: {
12 | pragma: 'VM',
13 | },
14 | },
15 | globals: {
16 | VM: true,
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | /.idea
4 | /dist
5 | /.nyc_output
6 | /coverage
7 | /types
8 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | # #!/bin/sh
2 | # . "$(dirname "$0")/_/husky.sh"
3 |
4 | # npm run lint
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist = true
2 | strict-peer-dependencies = false
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 notmarek
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Install | View Source
4 |
5 | 
6 | 
7 |
8 | This script let's you send torrents straight to your download client with a single button!
9 |
10 |
11 |
12 | ## **Limitations:**
13 |
14 | qBittorrent, Transmission, Deluge, ruTorrent, and Flood (this lets you use anything flood supports) are supported, if you want to see your client added leave a comment.
15 |
16 | ## **Supported sites:**
17 |
18 | **Gazelle:** PTP, GGn, AB, OPS, GPW, RED, JPS, TVV, Sugoi Music, iAnon, AR, UHDB, MTV, EMP, BTN, SC (Some of these are untested an may not work, please report so)
19 |
20 | **UNIT3D:** BLU, Aither, DT, Telly, TS, BHD (F3NIX fork)
21 |
22 | **Others:** TorrentLeech, AnilistBytes script
23 |
24 | ## **Changelog:**
25 | [2.3.1](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/b8ba70c85aadebfbe4536be23b6907e35e7c47c1/SendToClient.user.js) - Fixed a bug that killed the whole extension
26 |
27 | [2.3.0](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/61e0b340385905f03e0895ffdca42e52fea0ea54/SendToClient.user.js) - Extended ST button - let's you pick the profile on each torrent seperatly
28 |
29 | [2.2.5](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/987f099d7cbed678626214d10ae46185130db786/SendToClient.user.js) - BHD support
30 |
31 | [2.2.4](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/98663a9a84f4b05352eaa3a9bb78bf8fdb4fc312/SendToClient.user.js) - ruTorrent support
32 |
33 | [2.2.3](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/5bfd7a3a42a8faa64b9632671c500fc6995486e0/SendToClient.user.js) - KG support, minor code cleanup
34 |
35 | [2.2.2](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/3507cc15889ad03402ec3d113e302267a2ed04f8/SendToClient.user.js) - The script now only injects on sites, where it actually works.
36 |
37 | [2.2.1](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/79dc5d28acee6fc075954c86aee3f721a1e1d93a/SendToClient.user.js) - Added ability to pin profiles to sites, added support for TL
38 |
39 | [2.2.0](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/b9ecedd78f75f5b85c5dbeb7949d5d119cb68c30/SendToClient.user.js) - (qbit) Categories support!
40 |
41 | [2.1.2](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/6fd7299728af356b95782f62bc05d5ef7be3be4b/SendToClient.user.js) - Make FST Smarter - don't show on sites without FL, add need to accept
42 |
43 | [2.1.1](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/5f77d26a1681dfc451086276aeaea8f16099527c/SendToClient.user.js) - Added FST (Freeleech & Send) to Gazelle
44 |
45 | [2.1.0](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/7493fe573fbb08b05986b370479c42f21914023f/SendToClient.user.js) - UNIT3D support! - currently supported: BLU, Aither, DT, JPTV, Telly, TS
46 |
47 | [2.0.2](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/c5af7a9f65470e1d4dcaccf8b011f4b13d306e77/SendToClient.user.js) - Added DB9, SC, BTN
48 |
49 | [2.0.1](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/b64fc7adfc1ff805d47680538cc333196e3ccf9b/SendToClient.user.js) - Add support for AnilistBytes (my other AB specific UserScript)
50 |
51 | [2.0.0](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/83645715e62a99588c12baa6831de0589d3f8a89/SendToClient.user.js) - Huge update, the userscript is now detached from AB and works on most Gazelle based trackers! this change is breaking and you will need to configure the script from scratch. The source code was also moved to gihtub as i now use a more complicated workflow
52 |
53 |
54 | Previous versions (For animebytes only):
55 |
56 | [1.4.3](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/3c28507528cd01797abe5ed7db1c36dfc74ec03e/SendToClient.user.js) - fixed fixed an issue on OPS
57 |
58 | [1.4.2](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/2c8cd162170372618d5bfc64d4ed4f943443b28d/SendToClient.user.js) - fixed an issue on OPS
59 |
60 | [1.4.1](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/5f328456d61002f0ed3f5dc98a80ad23aa92e68a/SendToClient.user.js) - Added support for multiple other trackers, settings still reside here for now
61 |
62 | [1.4](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/67b71f84fdfc0a441f6987d87222e0fe8cd0fdb2/SendToClient.user.js) - Added support for Deluge
63 |
64 | [1.3](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/5ab94412d34b35a09effe1d101b55f9025a77453/SendToClient.user.js) - Added support for Flood
65 |
66 | [1.2](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/13e746d012af9f4231f55c19273a8e83aebb3dcb/SendToClient.user.js) - Added transmission support he script now let's specify the save path
67 |
68 | [1.1](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/7e302e52ba046354a462ab0009a4cfa5e36191a3/SendToClient.user.js) - Hotfix for compatibility with AnilistBytes
69 |
70 | [1.0](https://gist.github.com/notmarek/4f8fea8ae4e7cc524cba51a3594a128c/raw/2d1288fae418dc7f0409fb439f7c62ca3c43d1d8/SendToClient.user.js) - Original release
71 |
72 |
73 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: require.resolve('@gera2ld/plaid/config/babelrc-base'),
3 | presets: [
4 | ],
5 | plugins: [
6 | ['@babel/plugin-transform-react-jsx', {
7 | pragma: 'VM.h', // use 'VM.hm' if you don't need SVG support and don't want to call VM.m
8 | pragmaFrag: 'VM.Fragment',
9 | }],
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "es6",
5 | "moduleResolution": "node",
6 | "declaration": false,
7 | "allowSyntheticDefaultImports": true,
8 | "jsx": "preserve",
9 | "lib": [
10 | "DOM",
11 | "ES6",
12 | "DOM.Iterable",
13 | "ScriptHost",
14 | "ESNext"
15 | ]
16 | },
17 | "include": [
18 | "src/**/*",
19 | "node_modules/@violentmonkey/types"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sendtoclient",
3 | "version": "2.3.1",
4 | "description": "",
5 | "author": "notmarek",
6 | "license": "MIT",
7 | "private": true,
8 | "engines": {
9 | "node": ">=16"
10 | },
11 | "scripts": {
12 | "prepare": "husky install",
13 | "dev": "rollup -wc rollup.conf.js",
14 | "clean": "del-cli dist",
15 | "build:js": "NODE_ENV=production rollup -c rollup.conf.js",
16 | "prebuild": "run-s ci clean",
17 | "build": "cross-env NODE_ENV=production run-s build:js",
18 | "ci": "run-s lint",
19 | "lint": "eslint --ext .js ."
20 | },
21 | "dependencies": {
22 | "@babel/runtime": "^7.18.9",
23 | "@violentmonkey/dom": "^2.1.3",
24 | "@violentmonkey/ui": "^0.7.6"
25 | },
26 | "devDependencies": {
27 | "@babel/plugin-transform-react-jsx": "^7.18.10",
28 | "@gera2ld/plaid": "~2.5.6",
29 | "@gera2ld/plaid-rollup": "~2.5.6",
30 | "@violentmonkey/types": "^0.1.4",
31 | "del-cli": "^5.0.0",
32 | "eslint-config-prettier": "^8.5.0",
33 | "eslint-plugin-prettier": "^4.2.1",
34 | "husky": "^8.0.1",
35 | "prettier": "^2.7.1",
36 | "rollup-plugin-userscript": "^0.1.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/rollup.conf.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { getRollupPlugins } = require('@gera2ld/plaid');
3 |
4 | const userscript = require('rollup-plugin-userscript');
5 | const pkg = require('./package.json');
6 | const env = process.env.NODE_ENV || 'development';
7 |
8 | const DIST = 'dist';
9 | const FILENAME = 'SendToClient';
10 | let sites = require('./sites.json');
11 |
12 | const bundleOptions = {
13 | extend: true,
14 | esModule: false,
15 | };
16 | const postcssOptions = {
17 | ...require('@gera2ld/plaid/config/postcssrc'),
18 | inject: false,
19 | minimize: true,
20 | };
21 |
22 | const coolreplace = () => {
23 | return {
24 | name: 'coolreplace',
25 |
26 | buildStart() {
27 | sites = require('./sites.json');
28 | },
29 |
30 | renderChunk(code, chunk) {
31 | return {
32 | code: code.replaceAll(/'sites\[(.*?)\]'/g, (match, site) =>
33 | JSON.stringify(sites[site])
34 | ),
35 | };
36 | },
37 |
38 | transform(code, id) {
39 | return {
40 | code: code.replaceAll(/'sites\[(.*?)\]'/g, (match, site) =>
41 | JSON.stringify(sites[site])
42 | ),
43 | };
44 | },
45 | };
46 | };
47 | const rollupConfig = [
48 | {
49 | input: {
50 | input: 'src/index.js',
51 | plugins: [
52 | ...getRollupPlugins({
53 | esm: true,
54 | minimize: false,
55 | postcss: postcssOptions,
56 | }),
57 | userscript(path.resolve('src/meta.js'), (meta) =>
58 | meta
59 | .replace('process.env.VERSION', pkg.version)
60 | .replace('process.env.AUTHOR', pkg.author)
61 | .replace(
62 | '// @match STC.sites',
63 | (() => {
64 | let result = '';
65 | for (let type in sites) {
66 | for (let site of sites[type]) {
67 | result += `// @match *://*.${site}/*\n`;
68 | }
69 | }
70 | return result.replace(/\n$/, '');
71 | })()
72 | )
73 | ),
74 | coolreplace(),
75 | ],
76 | },
77 | output: {
78 | format: 'iife',
79 | file: `${DIST}/${FILENAME}.user.js`,
80 | banner:
81 | env === 'development'
82 | ? `GM.registerMenuCommand('Built @ ${new Date()}', ()=>{});`
83 | : '',
84 | ...bundleOptions,
85 | },
86 | },
87 | ];
88 |
89 | rollupConfig.forEach((item) => {
90 | item.output = {
91 | indent: false,
92 | // If set to false, circular dependencies and live bindings for external imports won't work
93 | externalLiveBindings: false,
94 | ...item.output,
95 | };
96 | });
97 |
98 | module.exports = rollupConfig.map(({ input, output }) => ({
99 | ...input,
100 | output,
101 | }));
102 |
--------------------------------------------------------------------------------
/sites.json:
--------------------------------------------------------------------------------
1 | {
2 | "Gazelle": [
3 | "gazellegames.net",
4 | "animebytes.tv",
5 | "orpheus.network",
6 | "passthepopcorn.me",
7 | "greatposterwall.com",
8 | "redacted.ch",
9 | "jpopsuki.eu",
10 | "tv-vault.me",
11 | "sugoimusic.me",
12 | "ianon.app",
13 | "alpharatio.cc",
14 | "uhdbits.org",
15 | "morethantv.me",
16 | "empornium.is",
17 | "deepbassnine.com",
18 | "broadcasthe.net",
19 | "secret-cinema.pw"
20 | ],
21 | "BLU UNIT3D": ["blutopia.cc", "aither.cc"],
22 | "UNIT3D": ["desitorrents.tv", "jptv.club", "telly.wtf", "torrentseeds.org"],
23 | "TorrentLeech": ["torrentleech.org", "www.torrentleech.org"],
24 | "AnilistBytes": ["anilist.co"],
25 | "Karagarga": ["karagarga.in"],
26 | "F3NIX": ["beyond-hd.me"]
27 | }
28 |
--------------------------------------------------------------------------------
/src/XFetch.js:
--------------------------------------------------------------------------------
1 | export const XFetch = {
2 | post: async (url, data, headers = {}) => {
3 | return new Promise((resolve, reject) => {
4 | GM.xmlHttpRequest({
5 | method: 'POST',
6 | url,
7 | headers,
8 | data,
9 | onload: (res) => {
10 | resolve({
11 | json: async () => JSON.parse(res.responseText),
12 | text: async () => res.responseText,
13 | headers: async () =>
14 | Object.fromEntries(
15 | res.responseHeaders.split('\r\n').map((h) => h.split(': '))
16 | ),
17 | raw: res,
18 | });
19 | },
20 | });
21 | });
22 | },
23 | get: async (url) => {
24 | return new Promise((resolve, reject) => {
25 | GM.xmlHttpRequest({
26 | method: 'GET',
27 | url,
28 | headers: {
29 | Accept: 'application/json',
30 | },
31 | onload: (res) => {
32 | resolve({
33 | json: async () => JSON.parse(res.responseText),
34 | text: async () => res.responseText,
35 | headers: async () =>
36 | Object.fromEntries(
37 | res.responseHeaders.split('\r\n').map((h) => h.split(': '))
38 | ),
39 | raw: res,
40 | });
41 | },
42 | });
43 | });
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/src/clientUtils.js:
--------------------------------------------------------------------------------
1 | import { XFetch } from './XFetch';
2 |
3 | export const addTorrent = async (
4 | torrentUrl,
5 | clientUrl,
6 | username,
7 | password,
8 | client,
9 | path,
10 | category
11 | ) => {
12 | let implementations = {
13 | qbit: async () => {
14 | XFetch.post(
15 | `${clientUrl}/api/v2/auth/login`,
16 | `username=${username}&password=${password}`,
17 | { 'content-type': 'application/x-www-form-urlencoded' }
18 | );
19 | let tor_data = new FormData();
20 | tor_data.append('urls', torrentUrl);
21 | if (path) {
22 | tor_data.append('savepath', path);
23 | }
24 | tor_data.append('category', category);
25 | XFetch.post(`${clientUrl}/api/v2/torrents/add`, tor_data);
26 | },
27 | trans: async (session_id = null) => {
28 | let headers = {
29 | Authorization: `Basic ${btoa(`${username}:${password}`)}`,
30 | 'Content-Type': 'application/json',
31 | };
32 | if (session_id) headers['X-Transmission-Session-Id'] = session_id;
33 | let res = await XFetch.post(
34 | `${clientUrl}/transmission/rpc`,
35 | JSON.stringify({
36 | arguments: { filename: torrentUrl, 'download-dir': path },
37 | method: 'torrent-add',
38 | }),
39 | headers
40 | );
41 | if (res.raw.status === 409) {
42 | implementations.trans(
43 | (await res.headers())['X-Transmission-Session-Id']
44 | );
45 | }
46 | },
47 | flood: async () => {
48 | // login
49 | XFetch.post(
50 | `${clientUrl}/api/auth/authenticate`,
51 | JSON.stringify({ password, username }),
52 | { 'content-type': 'application/json' }
53 | );
54 | XFetch.post(
55 | `${clientUrl}/api/torrents/add-urls`,
56 | JSON.stringify({ urls: [torrentUrl], destination: path, start: true }),
57 | { 'content-type': 'application/json' }
58 | );
59 | },
60 | deluge: async () => {
61 | XFetch.post(
62 | `${clientUrl}/json`,
63 | JSON.stringify({
64 | method: 'auth.login',
65 | params: [password],
66 | id: 0,
67 | }),
68 | { 'content-type': 'application/json' }
69 | );
70 | let res = await XFetch.post(
71 | `${clientUrl}/json`,
72 | JSON.stringify({
73 | method: 'web.download_torrent_from_url',
74 | params: [torrentUrl],
75 | id: 1,
76 | }),
77 | { 'content-type': 'application/json' }
78 | );
79 | XFetch.post(
80 | `${clientUrl}/json`,
81 | JSON.stringify({
82 | method: 'web.add_torrents',
83 | params: [
84 | [
85 | {
86 | path: (await res.json()).result,
87 | options: {
88 | add_paused: false,
89 | download_location: path,
90 | },
91 | },
92 | ],
93 | ],
94 | id: 2,
95 | }),
96 | { 'content-type': 'application/json' }
97 | );
98 | },
99 | rutorrent: async () => {
100 | // credit to humeur
101 | let headers = {
102 | Authorization: `Basic ${btoa(`${username}:${password}`)}`,
103 | };
104 | const response = await fetch(torrentUrl);
105 | const data = await response.blob();
106 | let form = new FormData();
107 | form.append('torrent_file[]', data, 'sendtoclient.torrent');
108 |
109 | form.append('torrents_start_stopped', 'true');
110 | form.append('dir_edit', path);
111 | form.append('label', category);
112 | XFetch.post(`${clientUrl}/rutorrent/php/addtorrent.php?json=1`, form, headers);
113 | }
114 | };
115 |
116 | await implementations[client]();
117 | };
118 |
119 | export async function testClient(clientUrl, username, password, client) {
120 | let clients = {
121 | trans: async () => {
122 | let headers = {
123 | Authorization: `Basic ${btoa(`${username}:${password}`)}`,
124 | 'Content-Type': 'application/json',
125 | 'X-Transmission-Session-Id': null,
126 | };
127 | let res = await XFetch.post(
128 | `${clientUrl}/transmission/rpc`,
129 | null,
130 | headers
131 | );
132 | if (res.raw.status !== 401) {
133 | return true;
134 | }
135 | return false;
136 | },
137 | qbit: async () => {
138 | let res = await XFetch.post(
139 | `${clientUrl}/api/v2/auth/login`,
140 | `username=${username}&password=${password}`,
141 | { 'content-type': 'application/x-www-form-urlencoded', cookie: 'SID=' }
142 | );
143 | if ((await res.text()) === 'Ok.') {
144 | return true;
145 | }
146 | return false;
147 | },
148 | deluge: async () => {
149 | let res = await XFetch.post(
150 | `${clientUrl}/json`,
151 | JSON.stringify({
152 | method: 'auth.login',
153 | params: [password],
154 | id: 0,
155 | }),
156 | { 'content-type': 'application/json' }
157 | );
158 | try {
159 | if ((await res.json()).result) {
160 | return true;
161 | }
162 | } catch (e) {
163 | return false;
164 | }
165 | return false;
166 | },
167 | flood: async () => {
168 | let res = await XFetch.post(
169 | `${clientUrl}/api/auth/authenticate`,
170 | JSON.stringify({ password, username }),
171 | { 'content-type': 'application/json' }
172 | );
173 | try {
174 | if ((await res.json()).success) return true;
175 | } catch (e) {
176 | return false;
177 | }
178 | return false;
179 | },
180 | rutorrent: async () => {
181 | // credit to humeur
182 | let headers = {
183 | Authorization: `Basic ${btoa(`${username}:${password}`)}`,
184 | 'Content-Type': 'application/json'
185 | };
186 | let res = await XFetch.post(`${clientUrl}/rutorrent/php/addtorrent.php?json=1`, null, headers);
187 | if (res.raw.status !== 401) {
188 | return true;
189 | }
190 | return false
191 | // credit to humeur;
192 | }
193 | };
194 | let result = await clients[client]();
195 | return result;
196 | }
197 | // TODO: new implementation - there should be a class for each client implementating the needed methods
198 | export const getCategories = async (clientUrl, username, password) => {
199 | XFetch.post(
200 | `${clientUrl}/api/v2/auth/login`,
201 | `username=${username}&password=${password}`,
202 | { 'content-type': 'application/x-www-form-urlencoded' }
203 | );
204 | let res = await XFetch.get(`${clientUrl}/api/v2/torrents/categories`);
205 | try {
206 | return Object.keys(await res.json());
207 | } catch {
208 | return [];
209 | }
210 | };
211 |
212 | export async function detectClient(url) {
213 | const res = await XFetch.get(url);
214 | const body = await res.text();
215 | const headers = await res.headers();
216 | if (headers.hasOwnProperty('WWW-Authenticate')) {
217 | const wwwAuthenticateHeader = headers['WWW-Authenticate'];
218 | if (wwwAuthenticateHeader.includes('"Transmission"')) return 'trans';
219 | }
220 | if (body.includes('Deluge ')) return 'deluge';
221 | if (body.includes('Flood')) return 'flood';
222 | if (body.includes('qBittorrent ')) return 'qbit';
223 | if (body.includes('ruTorrent ')) return 'rutorrent';
224 | return 'unknown';
225 | }
226 |
--------------------------------------------------------------------------------
/src/globalSettingsManager.js:
--------------------------------------------------------------------------------
1 | const ButtonTypes = {
2 | simple: 0,
3 | extended: 1,
4 | }
5 |
6 | export const globalSettingsManager = {
7 | settings: {
8 | button_type: ButtonTypes.simple,
9 | },
10 |
11 | get button_type() {
12 | return this.settings.button_type;
13 | },
14 |
15 | set button_type(val) {
16 | this.settings.button_type = val;
17 | this.save();
18 | },
19 |
20 | async load() {
21 | let settings = await GM.getValue('settings');
22 | if (settings) {
23 | this.settings = JSON.parse(settings);
24 | }
25 | },
26 |
27 |
28 | async save() {
29 | await GM.setValue('settings', JSON.stringify(this.settings));
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { profileManager } from './profileManager';
2 | import { Settings } from './settings';
3 | import { createButtons } from './trackerUtils';
4 | import { globalSettingsManager } from './globalSettingsManager.js';
5 |
6 | GM.registerMenuCommand('Settings', () => {
7 | Settings();
8 | });
9 | const profileQuickSwitcher = () => {
10 | let id = GM.registerMenuCommand(
11 | `Selected Profile: ${profileManager.selectedProfile.name}`,
12 | () => {}
13 | );
14 | window.addEventListener('profileChanged', () => {
15 | GM.unregisterMenuCommand(id);
16 | profileQuickSwitcher();
17 | window.removeEventListener('profileChanged', () => {});
18 | });
19 | };
20 | globalSettingsManager.load().then(()=>
21 | profileManager.load().then(() => {
22 | profileQuickSwitcher();
23 | createButtons();
24 | }));
25 |
26 | document.addEventListener('PTPAddReleasesFromOtherTrackersComplete', () => {
27 | console.log('Adding buttons for added releases');
28 | profileQuickSwitcher();
29 | createButtons();
30 | });
--------------------------------------------------------------------------------
/src/meta.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name SendToClient
3 | // @namespace NotMareks Scripts
4 | // @description Painlessly send torrents to your bittorrent client.
5 | // @match STC.sites
6 | // @grant GM_addStyle
7 | // @grant GM.registerMenuCommand
8 | // @grant GM.unregisterMenuCommand
9 | // @grant GM.getValue
10 | // @grant GM.setValue
11 | // @grant GM.xmlHttpRequest
12 | // @version process.env.VERSION
13 | // @author process.env.AUTHOR
14 | // @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@0.7
15 | // ==/UserScript==
16 |
17 | /**
18 | * @match STC.sites gets replaced with the sites from the sites.json file
19 | */
20 |
--------------------------------------------------------------------------------
/src/profileManager.js:
--------------------------------------------------------------------------------
1 | import { testClient, addTorrent, getCategories } from './clientUtils';
2 |
3 | class Profile {
4 | constructor(
5 | id,
6 | name,
7 | host,
8 | username,
9 | password,
10 | client,
11 | saveLocation,
12 | category,
13 | linkedTo = []
14 | ) {
15 | this.id = id;
16 | this.name = name;
17 | this.host = host;
18 | this.username = username;
19 | this.password = password;
20 | this.client = client;
21 | this.saveLocation = saveLocation;
22 | this.category = category;
23 | this.linkedTo = linkedTo;
24 | }
25 | async linkTo(site, replace = false) {
26 | let alreadyLinkedTo = profileManager.profiles.find((p) =>
27 | p.linkedTo.includes(site)
28 | );
29 | if (alreadyLinkedTo && !replace) {
30 | return alreadyLinkedTo.name;
31 | } else if (alreadyLinkedTo && replace) {
32 | alreadyLinkedTo.unlinkFrom(site);
33 | }
34 | if (this.linkedTo.includes(site)) return true;
35 | this.linkedTo.push(site);
36 | profileManager.save();
37 | return true;
38 | }
39 | async unlinkFrom(site) {
40 | this.linkedTo = this.linkedTo.filter((s) => s !== site);
41 | profileManager.save();
42 | }
43 | async getCategories() {
44 | if (this.client != 'qbit') return [];
45 | let res = await getCategories(this.host, this.username, this.password);
46 | console.log(res);
47 | return res;
48 | }
49 | async testConnection() {
50 | return await testClient(
51 | this.host,
52 | this.username,
53 | this.password,
54 | this.client
55 | );
56 | }
57 |
58 | async addTorrent(torrent_uri) {
59 | return await addTorrent(
60 | torrent_uri,
61 | this.host,
62 | this.username,
63 | this.password,
64 | this.client,
65 | this.saveLocation,
66 | this.category
67 | );
68 | }
69 | }
70 |
71 | export const profileManager = {
72 | profiles: [],
73 | selectedProfile: null,
74 | addProfile: function (profile) {
75 | this.profiles.push(profile);
76 | },
77 | removeProfile: function (id) {
78 | this.profiles = this.profiles.find((p) => p.id === id);
79 | },
80 | getProfile: function (id) {
81 | return (
82 | this.profiles.find((p) => Number(p.id) === Number(id)) ??
83 | new Profile(id, 'New Profile', '', '', '', 'none', '', '')
84 | );
85 | },
86 | getProfiles: function () {
87 | if (this.profiles.length === 0) this.load();
88 | return this.profiles;
89 | },
90 | setSelectedProfile: function (id) {
91 | this.selectedProfile = this.getProfile(id);
92 | window.dispatchEvent(
93 | new CustomEvent('profileChanged', { detail: this.selectedProfile })
94 | );
95 | },
96 | setProfile: function (profile) {
97 | if (!this.profiles.includes(this.getProfile(profile.id))) {
98 | this.profiles.push(profile);
99 | } else {
100 | this.profiles = this.profiles.map((p) => {
101 | if (p.id === profile.id) {
102 | p = profile;
103 | }
104 | return p;
105 | });
106 | }
107 | },
108 | getNextId: function () {
109 | if (this.profiles.length === 0) return 0;
110 | return (
111 | Number(this.profiles.sort((a, b) => Number(b.id) > Number(a.id))[0].id) +
112 | 1
113 | );
114 | },
115 | save: function () {
116 | GM.setValue('profiles', JSON.stringify(this.profiles));
117 | GM.setValue('selectedProfile', this.selectedProfile.id);
118 | },
119 | load: async function () {
120 | const profiles = await GM.getValue('profiles');
121 | if (profiles) {
122 | this.profiles = JSON.parse(profiles).map(
123 | (p) =>
124 | new Profile(
125 | p.id,
126 | p.name,
127 | p.host,
128 | p.username,
129 | p.password,
130 | p.client,
131 | p.saveLocation,
132 | p.category ?? '',
133 | p.linkedTo ?? []
134 | )
135 | );
136 | }
137 | for (const profile of this.profiles) {
138 | for (const site of profile.linkedTo) {
139 | if (location.href.includes(site)) {
140 | this.selectedProfile = profile;
141 | return;
142 | }
143 | }
144 | }
145 | this.selectedProfile =
146 | this.getProfile(Number(await GM.getValue('selectedProfile')) ?? 0) ??
147 | new Profile(0, 'New Profile', '', '', '', 'none', '', '');
148 | },
149 | };
150 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | import styles, { stylesheet } from './style.module.css';
2 | import { testClient, detectClient } from './clientUtils';
3 | import { profileManager } from './profileManager';
4 | import { globalSettingsManager } from './globalSettingsManager.js';
5 | const clientSelectorOnChange = (e, shadow) => {
6 | if (
7 | shadow.querySelector('#host').value === '' &&
8 | e.target.value !== 'unknown'
9 | )
10 | shadow.querySelector('#host').value =
11 | e.target.value === 'flood'
12 | ? document.location.href.replace(/\/overview|login\/$/, '')
13 | : document.location.href.replace(/\/$/, '');
14 | shadow.querySelector('#category').hidden = e.target.value !== 'qbit';
15 | shadow.querySelector("label[for='category']").hidden =
16 | e.target.value !== 'qbit';
17 | if (e.target.value === 'qbit') {
18 | shadow.querySelector('#category>select').onload();
19 | }
20 | shadow.querySelector("label[for='username']").hidden =
21 | e.target.value === 'deluge';
22 | shadow.querySelector('#username').hidden = e.target.value === 'deluge';
23 | };
24 |
25 | function ClientSelector({ shadow }) {
26 | return (
27 | <>
28 |
29 |
30 |
47 | >
48 | );
49 | }
50 |
51 | const profileOnSave = (e, shadow) => {
52 | let profile = profileManager.getProfile(
53 | shadow.querySelector('#profile').value
54 | );
55 | profile.host = shadow.querySelector('#host').value;
56 | profile.username = shadow.querySelector('#username').value;
57 | profile.password = shadow.querySelector('#password').value;
58 | profile.client = shadow.querySelector('#client').value;
59 | profile.saveLocation = shadow.querySelector('#saveLocation').value;
60 | profile.name = shadow.querySelector('#profilename').value;
61 | profile.category = shadow.querySelector('#category>input').value;
62 | profileManager.setSelectedProfile(profile.id);
63 | profileManager.setProfile(profile);
64 | profileManager.save();
65 | shadow.querySelector('#profile').innerHTML = null;
66 | shadow.querySelector('#profile').appendChild(
67 | VM.m(
68 | <>
69 | {profileManager.getProfiles().map((p) => {
70 | return (
71 |
77 | );
78 | })}
79 |
80 | >
81 | )
82 | );
83 | };
84 |
85 | const addSiteToProfile = async (hostname, shadow) => {
86 | let result = await profileManager.selectedProfile.linkTo(hostname);
87 |
88 | if (
89 | result !== true &&
90 | confirm(
91 | `This site is already linked to "${result}". Do you want to replace it?`
92 | )
93 | )
94 | profileManager.selectedProfile.linkTo(hostname, true);
95 | profileSelectHandler({ target: shadow.querySelector('#profile') }, shadow);
96 | };
97 | function profileSelectHandler(e, shadow) {
98 | const profile = profileManager.getProfile(e.target.value);
99 | profileManager.setSelectedProfile(profile.id);
100 | shadow.querySelector('#host').value = profile.host;
101 | shadow.querySelector('#username').value = profile.username;
102 | shadow.querySelector('#password').value = profile.password;
103 | shadow.querySelector('#client').value = profile.client;
104 | shadow.querySelector('#saveLocation').value = profile.saveLocation;
105 | shadow.querySelector('#profilename').value = profile.name;
106 | shadow.querySelector('#linkToSite').innerHTML = null;
107 | shadow.querySelector('#linkToSite').appendChild(
108 | VM.m(
109 | <>
110 | {profileManager.selectedProfile.linkedTo.map((site) => (
111 |
112 | ))}
113 | {profileManager.selectedProfile.linkedTo.includes(
114 | location.hostname
115 | ) ? null : (
116 |
117 | )}
118 | >
119 | )
120 | );
121 | shadow
122 | .querySelector('select#client')
123 | .onchange({ target: shadow.querySelector('select#client') });
124 | }
125 |
126 | function ProfileSelector({ shadow }) {
127 | return (
128 | <>
129 |
130 |
147 | >
148 | );
149 | }
150 |
151 | async function loadCategories(shadow) {
152 | let options = await profileManager.selectedProfile.getCategories().then((e) =>
153 | e.map((cat) => (
154 |
160 | ))
161 | );
162 | options.push(
163 |
170 | );
171 | shadow.querySelector('#category>input').value =
172 | profileManager.selectedProfile.category;
173 | shadow.querySelector('select[name="category"]').innerHTML = null;
174 | shadow
175 | .querySelector('select[name="category"]')
176 | .appendChild(VM.m(<>{options}>));
177 | }
178 | function CategorySelector({ shadow, hidden }) {
179 | return (
180 | <>
181 |
184 |
185 |
192 |
193 |
194 | >
195 | );
196 | }
197 |
198 | function LinkToSite({ shadow }) {
199 | return (
200 | <>
201 |
202 |
221 | >
222 | );
223 | }
224 |
225 | function SettingsElement({ panel }) {
226 | const shadow = panel.root;
227 | return (
228 | <>
229 | SendToClient
230 |
231 |
232 |
233 | globalSettingsManager.button_type = Number(e.target.checked)}
237 | checked={globalSettingsManager.button_type ? true : false} />
238 |
239 |
298 |
299 |
300 | >
301 | );
302 | }
303 |
304 | export const Settings = () => {
305 | const panel = VM.getPanel({
306 | theme: 'dark',
307 | shadow: true,
308 | style: stylesheet,
309 | });
310 | // give the panel access to itself :)
311 | panel.setContent();
312 | panel.setMovable(false);
313 | panel.wrapper.children[0].classList.add(styles.wrapper);
314 | let original_show = panel.show;
315 | panel.show = () => {
316 | original_show.apply(panel);
317 | document.body.style.overflow = 'hidden';
318 | };
319 |
320 | let original_hide = panel.hide;
321 | panel.hide = () => {
322 | original_hide.apply(panel);
323 | document.body.style.overflow = 'auto';
324 | };
325 | panel.show();
326 | profileSelectHandler(
327 | { target: { value: profileManager.selectedProfile.id } },
328 | panel.root
329 | );
330 | };
331 |
--------------------------------------------------------------------------------
/src/style.module.css:
--------------------------------------------------------------------------------
1 | .title {
2 | font-weight: 700;
3 | font-size: 20px;
4 | line-height: 24px;
5 | margin-bottom: 10px;
6 | text-align: center;
7 | }
8 |
9 | .desc {
10 | @apply text-green-600;
11 | }
12 |
13 | .settings {
14 | display: grid;
15 | grid-template-columns: 1fr 1fr;
16 | grid-row-gap: 1rem;
17 | grid-column-gap: 1rem;
18 | }
19 |
20 | :host {
21 | backdrop-filter: blur(5px);
22 | position: fixed;
23 | top: 0;
24 | left: 0;
25 | width: 100%;
26 | height: 100%;
27 | display: flex;
28 | justify-content: center;
29 | align-items: center;
30 | z-index: 99999999999;
31 | }
32 |
33 | .wrapper {
34 | border-radius: 10px;
35 | padding: 20px;
36 | }
37 |
38 | .select_input {
39 | background-color: white;
40 | border: solid grey 1px;
41 | height: 20px;
42 | position: relative;
43 | }
44 |
45 | .select_input > select {
46 | border: none;
47 | bottom: 0px;
48 | left: 0px;
49 | margin: 0;
50 | position: absolute;
51 | top: 0px;
52 | width: 100%;
53 | }
54 |
55 | .select_input > input {
56 | border: none;
57 | left: 0px;
58 | padding: 1px;
59 | position: absolute;
60 | top: 0px;
61 | width: calc(100% - 20px);
62 | }
63 |
--------------------------------------------------------------------------------
/src/trackerUtils.js:
--------------------------------------------------------------------------------
1 | import { profileManager } from './profileManager';
2 | import { globalSettingsManager } from './globalSettingsManager.js';
3 | import styles, { stylesheet } from './style.module.css';
4 | function ExtendeSTCProfile({ panel, profile, torrentUrl }) {
5 | return (
13 | );
14 | };
15 | function ExtendedSTCElement({ panel, torrentUrl }) {
16 | let profiles = [];
17 | for (let profile of profileManager.profiles) {
18 | profiles.push();
19 | }
20 | return (
21 | Choose which profile to send to
22 | {profiles}
23 |
24 |
);
25 | };
26 |
27 | const ExtendedSTC = (torrentUrl) => {
28 | const panel = VM.getPanel({
29 | theme: 'dark',
30 | shadow: true,
31 | style: stylesheet,
32 | });
33 | // give the panel access to itself :)
34 | panel.setContent();
35 | panel.setMovable(false);
36 | panel.wrapper.children[0].classList.add(styles.wrapper);
37 | let original_show = panel.show;
38 | panel.show = () => {
39 | original_show.apply(panel);
40 | document.body.style.overflow = 'hidden';
41 | };
42 |
43 | let original_hide = panel.hide;
44 | panel.hide = () => {
45 | original_hide.apply(panel);
46 | document.body.style.overflow = 'auto';
47 | };
48 | panel.show();
49 | };
50 | const XSTBTN = ({ torrentUrl, freeleech }) => {
51 | return (
52 | {
56 | if (freeleech)
57 | if (!confirm('After sending to client a feeleech token will be consumed!'))
58 | return;
59 |
60 | ExtendedSTC(torrentUrl);
61 | }}
62 | >
63 | X{freeleech ? "F" : ""}ST
64 |
65 | );
66 | }
67 | const STBTN = ({ torrentUrl }) => {
68 | return globalSettingsManager.button_type ? : (
69 | {
74 | e.preventDefault();
75 | await profileManager.selectedProfile.addTorrent(torrentUrl);
76 | e.target.innerText = 'Added!';
77 | e.target.onclick = null;
78 | }}
79 | >
80 | ST
81 |
82 | );
83 | };
84 | const FSTBTN = ({ torrentUrl }) => {
85 | return globalSettingsManager.button_type ? : (
86 | {
91 | e.preventDefault();
92 | if (!confirm('Are you sure you want to use a freeleech token here?'))
93 | return;
94 | await profileManager.selectedProfile.addTorrent(torrentUrl);
95 | e.target.innerText = 'Added!';
96 | e.target.onclick = null;
97 | }}
98 | >
99 | FST
100 |
101 | );
102 | };
103 |
104 | const handlers = [{
105 | name: 'Gazelle',
106 | matches: ["gazellegames.net", "animebytes.tv", "orpheus.network", "passthepopcorn.me", "greatposterwall.com", "redacted.ch", "jpopsuki.eu", "tv-vault.me", "sugoimusic.me", "ianon.app", "alpharatio.cc", "uhdbits.org", "morethantv.me", "empornium.is", "deepbassnine.com", "broadcasthe.net", "secret-cinema.pw"],
107 | run: async () => {
108 | const links = Array.from(document.querySelectorAll('a')).filter(a =>
109 | a.innerText.trim() === 'DL' ||
110 | a.title === 'Download Torrent' ||
111 | a.classList.contains('link_1')
112 | );
113 |
114 | for (const a of links) {
115 | let parent = a.closest('.basic-movie-list__torrent__action');
116 | if (!parent) {
117 | parent = a.parentElement;
118 | }
119 | let torrentUrl = a.href;
120 | let buttons = Array.from(parent.childNodes).filter(e => e.nodeName !== '#text');
121 | let fl = Array.from(parent.querySelectorAll('a')).find(a => a.innerText === 'FL');
122 | let fst = fl ? VM.h(VM.Fragment, null, "\xA0|\xA0", VM.h(FSTBTN, {
123 | torrentUrl: fl.href
124 | })) : null;
125 |
126 | parent.innerHTML = ''; // Use '' instead of null to avoid issues
127 | parent.appendChild(VM.m(VM.h(VM.Fragment, null, "[\xA0", buttons.map(e => VM.h(VM.Fragment, null, e, " | ")), VM.h(STBTN, {
128 | torrentUrl: torrentUrl
129 | }), fst, "\xA0]")));
130 | }
131 |
132 | window.addEventListener('profileChanged', () => {
133 | document.querySelectorAll('a.sendtoclient').forEach(e => {
134 | if (e.title.includes('Freeleechize')) {
135 | e.title = `Freeleechize and add to ${profileManager.selectedProfile.name}.`;
136 | } else {
137 | e.title = `Add to ${profileManager.selectedProfile.name}.`;
138 | }
139 | });
140 | });
141 | }
142 | },
143 | {
144 | name: 'BLU UNIT3D',
145 | matches: 'sites[BLU UNIT3D]',
146 | run: async () => {
147 | let rid = await fetch(
148 | Array.from(document.querySelectorAll('ul>li>a')).find(
149 | (e) => e.innerText === 'My Profile'
150 | ).href + '/rsskey/edit'
151 | )
152 | .then((e) => e.text())
153 | .then(
154 | (e) =>
155 | e
156 | .replaceAll(/\s/g, '')
157 | .match(
158 | /name="current_rsskey"readonlytype="text"value="(.*?)">/
159 | )[1]
160 | );
161 | handlers.find((h) => h.name === 'UNIT3D').run(rid);
162 | },
163 | },
164 | {
165 | name: 'F3NIX',
166 | matches: 'sites[F3NIX]',
167 | run: async (rid = null) => {
168 | if (!rid) {
169 | rid = await fetch(location.origin + '/settings/change_rid')
170 | .then((e) => e.text())
171 | .then(
172 | (e) =>
173 | e.match(
174 | /class="beta-form-main" name="null" value="(.*?)" disabled>/
175 | )[1]
176 | );
177 | }
178 | const appendButton = () => {
179 | Array.from(
180 | document.querySelectorAll('a[title="Download Torrent"]')
181 | ).forEach((a) => {
182 | let parent = a.parentElement;
183 | let torrentUrl = `${a.href.replace(
184 | '/download/',
185 | '/torrent/download/'
186 | )}.${rid}`;
187 | parent.appendChild(
188 | VM.m(
189 | <>
190 | {' '}
191 |
192 | >
193 | )
194 | );
195 | });
196 | };
197 | appendButton();
198 | let oldPushState = unsafeWindow.history.pushState;
199 | unsafeWindow.history.pushState = function () {
200 | console.log(
201 | '[SendToClient] Detected a soft navigation to ${unsafeWindow.location.href}'
202 | );
203 | appendButton();
204 | return oldPushState.apply(this, arguments);
205 | };
206 | },
207 | },
208 | {
209 | name: 'UNIT3D',
210 | matches: 'sites[UNIT3D]',
211 | run: async (rid = null) => {
212 | if (!rid) {
213 | rid = await fetch(
214 | Array.from(document.querySelectorAll('ul>li>a')).find((e) =>
215 | e.innerText.includes('My Profile')
216 | ).href + '/settings/security'
217 | )
218 | .then((e) => e.text())
219 | .then((e) => e.match(/ current_rid">(.*?))[1]);
220 | }
221 | const appendButton = () => {
222 | Array.from(document.querySelectorAll('a[title="Download"]'))
223 | .concat(
224 | Array.from(
225 | document.querySelectorAll(
226 | 'button[title="Download"], button[data-original-title="Download"]'
227 | )
228 | ).map((e) => e.parentElement)
229 | )
230 | .forEach((a) => {
231 | let parent = a.parentElement;
232 | let torrentUrl =
233 | a.href.replace('/torrents/', '/torrent/') + `.${rid}`;
234 | parent.appendChild(VM.m());
235 | });
236 | };
237 | appendButton();
238 | console.log(
239 | '[SendToClient] Bypassing CSP so we can listen for soft navigations.'
240 | );
241 |
242 | document.addEventListener('popstate', () => {
243 | console.log(
244 | '[SendToClient] Detected a soft navigation to ' +
245 | unsafeWindow.location.href
246 | );
247 | appendButton();
248 | });
249 | // listen for a CSP violation so that we can grab the nonces
250 | document.addEventListener('securitypolicyviolation', (e) => {
251 | const nonce = e.originalPolicy.match(/nonce-(.*?)'/)[1];
252 | let actualScript = VM.m(
253 |
264 | );
265 | document.head.appendChild(actualScript).remove();
266 | });
267 | // trigger a CSP violation
268 | document.head
269 | .appendChild(
270 | VM.m()
271 | )
272 | .remove();
273 | },
274 | },
275 | {
276 | name: 'Karagarga',
277 | matches: 'sites[Karagarga]',
278 | run: async () => {
279 | if (unsafeWindow.location.href.includes('details.php')) {
280 | let dl_btn = document.querySelector('a.index');
281 | let torrent_uri = dl_btn.href;
282 | return dl_btn.insertAdjacentElement(
283 | 'afterend',
284 | VM.m(
285 |
286 |
287 |
288 | )
289 | );
290 | }
291 | document.querySelectorAll("img[alt='Download']").forEach((e) => {
292 | let parent = e.parentElement;
293 | let torrent_uri = e.parentElement.href;
294 | let container = parent.parentElement;
295 | let st = VM.m();
296 | container.appendChild(st);
297 | });
298 | },
299 | },
300 | {
301 | name: 'TorrentLeech',
302 | matches: 'sites[TorrentLeech]',
303 | run: async () => {
304 | const username = document
305 | .querySelector('span.link')
306 | .getAttribute('onclick')
307 | .match('/profile/(.*?)/view')[1];
308 | let rid = await fetch(`/profile/${username}/edit`)
309 | .then((e) => e.text())
310 | .then(
311 | (e) =>
312 | e.replaceAll(/\s/g, '').match(/rss.torrentleech.org\/(.*?)\)[1]
313 | );
314 | document.head.appendChild(
315 | VM.m()
316 | );
317 | for (const a of document.querySelectorAll('a.download')) {
318 | let torrent_uri = a.href.match(/\/download\/(\d*?)\/(.*?)$/);
319 | torrent_uri = `https://torrentleech.org/rss/download/${torrent_uri[1]}/${rid}/${torrent_uri[2]}`;
320 | a.parentElement.appendChild(VM.m());
321 | }
322 | },
323 | },
324 | {
325 | name: 'AnilistBytes',
326 | matches: 'sites[AnilistBytes]',
327 | run: async () => {
328 | unsafeWindow._addTo = async (torrentUrl) =>
329 | profileManager.selectedProfile.addTorrent(torrentUrl);
330 | },
331 | },
332 | ];
333 |
334 | export const createButtons = async () => {
335 | document.querySelectorAll('.sendtoclient').forEach(button => button.remove());
336 |
337 | for (const handler of handlers) {
338 | const regex = handler.matches.join('|');
339 | if (unsafeWindow.location.href.match(regex)) {
340 | handler.run();
341 | console.log(`%c[SendToClient] Using engine {${handler.name}}`, 'color: #42adf5; font-weight: bold; font-size: 1.5em;');
342 | return handler.name;
343 | }
344 | }
345 | };
346 |
--------------------------------------------------------------------------------
/src/types/css.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.module.css' {
2 | /**
3 | * Generated CSS for CSS modules
4 | */
5 | export const stylesheet: string;
6 | /**
7 | * Exported classes
8 | */
9 | const classMap: {
10 | [key: string]: string;
11 | };
12 | export default classMap;
13 | }
14 |
15 | declare module '*.css' {
16 | /**
17 | * Generated CSS
18 | */
19 | const css: string;
20 | export default css;
21 | }
22 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as dom from '@violentmonkey/dom';
2 | import * as ui from '@violentmonkey/ui';
3 |
4 | declare global {
5 | const VM: typeof dom & typeof ui;
6 |
7 | namespace JSX {
8 | /**
9 | * JSX.Element can be different based on pragma in babel config:
10 | * - VNode - when jsxFactory is VM.h
11 | * - DomNode - when jsxFactory is VM.hm
12 | */
13 | type Element = import('@gera2ld/jsx-dom').VNode;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------