Last active
December 19, 2024 09:33
-
-
Save notmarek/4f8fea8ae4e7cc524cba51a3594a128c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name SendToClient | |
// @namespace NotMareks Scripts | |
// @description Painlessly send torrents to your bittorrent client. | |
// @match *://*.gazellegames.net/* | |
// @match *://*.animebytes.tv/* | |
// @match *://*.orpheus.network/* | |
// @match *://*.passthepopcorn.me/* | |
// @match *://*.greatposterwall.com/* | |
// @match *://*.redacted.ch/* | |
// @match *://*.jpopsuki.eu/* | |
// @match *://*.tv-vault.me/* | |
// @match *://*.sugoimusic.me/* | |
// @match *://*.ianon.app/* | |
// @match *://*.alpharatio.cc/* | |
// @match *://*.uhdbits.org/* | |
// @match *://*.morethantv.me/* | |
// @match *://*.empornium.is/* | |
// @match *://*.deepbassnine.com/* | |
// @match *://*.broadcasthe.net/* | |
// @match *://*.secret-cinema.pw/* | |
// @match *://*.blutopia.cc/* | |
// @match *://*.aither.cc/* | |
// @match *://*.desitorrents.tv/* | |
// @match *://*.jptv.club/* | |
// @match *://*.telly.wtf/* | |
// @match *://*.torrentseeds.org/* | |
// @match *://*.torrentleech.org/* | |
// @match *://*.www.torrentleech.org/* | |
// @match *://*.anilist.co/* | |
// @match *://*.karagarga.in/* | |
// @match *://beyond-hd.me/torrents/* | |
// @match *://beyond-hd.me/library/* | |
// @match *://beyond-hd.me/bookmarks/ | |
// @match *://beyond-hd.me/lists/* | |
// @match *://beyond-hd.me/people/* | |
// @match *://beyond-hd.me/ratings/ | |
// @version 2.3.1.2 | |
// @author notmarek | |
// @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/[email protected] | |
// @grant GM.getValue | |
// @grant GM.registerMenuCommand | |
// @grant GM.setValue | |
// @grant GM.unregisterMenuCommand | |
// @grant GM.xmlHttpRequest | |
// @grant GM_addStyle | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
const XFetch = { | |
post: async (url, data, headers = {}) => { | |
return new Promise((resolve, reject) => { | |
GM.xmlHttpRequest({ | |
method: 'POST', | |
url, | |
headers, | |
data, | |
onload: res => { | |
resolve({ | |
json: async () => JSON.parse(res.responseText), | |
text: async () => res.responseText, | |
headers: async () => Object.fromEntries(res.responseHeaders.split('\r\n').map(h => h.split(': '))), | |
raw: res | |
}); | |
} | |
}); | |
}); | |
}, | |
get: async url => { | |
return new Promise((resolve, reject) => { | |
GM.xmlHttpRequest({ | |
method: 'GET', | |
url, | |
headers: { | |
Accept: 'application/json' | |
}, | |
onload: res => { | |
resolve({ | |
json: async () => JSON.parse(res.responseText), | |
text: async () => res.responseText, | |
headers: async () => Object.fromEntries(res.responseHeaders.split('\r\n').map(h => h.split(': '))), | |
raw: res | |
}); | |
} | |
}); | |
}); | |
} | |
}; | |
const addTorrent = async (torrentUrl, clientUrl, username, password, client, path, category) => { | |
let implementations = { | |
qbit: async () => { | |
XFetch.post(`${clientUrl}/api/v2/auth/login`, `username=${username}&password=${password}`, { | |
'content-type': 'application/x-www-form-urlencoded' | |
}); | |
let tor_data = new FormData(); | |
tor_data.append('urls', torrentUrl); | |
if (path) { | |
tor_data.append('savepath', path); | |
} | |
tor_data.append('category', category); | |
XFetch.post(`${clientUrl}/api/v2/torrents/add`, tor_data); | |
}, | |
trans: async (session_id = null) => { | |
let headers = { | |
Authorization: `Basic ${btoa(`${username}:${password}`)}`, | |
'Content-Type': 'application/json' | |
}; | |
if (session_id) headers['X-Transmission-Session-Id'] = session_id; | |
let res = await XFetch.post(`${clientUrl}/transmission/rpc`, JSON.stringify({ | |
arguments: { | |
filename: torrentUrl, | |
'download-dir': path | |
}, | |
method: 'torrent-add' | |
}), headers); | |
if (res.raw.status === 409) { | |
implementations.trans((await res.headers())['X-Transmission-Session-Id']); | |
} | |
}, | |
flood: async () => { | |
// login | |
XFetch.post(`${clientUrl}/api/auth/authenticate`, JSON.stringify({ | |
password, | |
username | |
}), { | |
'content-type': 'application/json' | |
}); | |
XFetch.post(`${clientUrl}/api/torrents/add-urls`, JSON.stringify({ | |
urls: [torrentUrl], | |
destination: path, | |
start: true | |
}), { | |
'content-type': 'application/json' | |
}); | |
}, | |
deluge: async () => { | |
XFetch.post(`${clientUrl}/json`, JSON.stringify({ | |
method: 'auth.login', | |
params: [password], | |
id: 0 | |
}), { | |
'content-type': 'application/json' | |
}); | |
let res = await XFetch.post(`${clientUrl}/json`, JSON.stringify({ | |
method: 'web.download_torrent_from_url', | |
params: [torrentUrl], | |
id: 1 | |
}), { | |
'content-type': 'application/json' | |
}); | |
XFetch.post(`${clientUrl}/json`, JSON.stringify({ | |
method: 'web.add_torrents', | |
params: [[{ | |
path: (await res.json()).result, | |
options: { | |
add_paused: false, | |
download_location: path | |
} | |
}]], | |
id: 2 | |
}), { | |
'content-type': 'application/json' | |
}); | |
}, | |
rutorrent: async () => { | |
// credit to humeur | |
let headers = { | |
Authorization: `Basic ${btoa(`${username}:${password}`)}` | |
}; | |
const response = await fetch(torrentUrl); | |
const data = await response.blob(); | |
let form = new FormData(); | |
form.append('torrent_file[]', data, 'sendtoclient.torrent'); | |
form.append('torrents_start_stopped', 'true'); | |
form.append('dir_edit', path); | |
form.append('label', category); | |
XFetch.post(`${clientUrl}/rutorrent/php/addtorrent.php?json=1`, form, headers); | |
} | |
}; | |
await implementations[client](); | |
}; | |
async function testClient(clientUrl, username, password, client) { | |
let clients = { | |
trans: async () => { | |
let headers = { | |
Authorization: `Basic ${btoa(`${username}:${password}`)}`, | |
'Content-Type': 'application/json', | |
'X-Transmission-Session-Id': null | |
}; | |
let res = await XFetch.post(`${clientUrl}/transmission/rpc`, null, headers); | |
if (res.raw.status !== 401) { | |
return true; | |
} | |
return false; | |
}, | |
qbit: async () => { | |
let res = await XFetch.post(`${clientUrl}/api/v2/auth/login`, `username=${username}&password=${password}`, { | |
'content-type': 'application/x-www-form-urlencoded', | |
cookie: 'SID=' | |
}); | |
if ((await res.text()) === 'Ok.') { | |
return true; | |
} | |
return false; | |
}, | |
deluge: async () => { | |
let res = await XFetch.post(`${clientUrl}/json`, JSON.stringify({ | |
method: 'auth.login', | |
params: [password], | |
id: 0 | |
}), { | |
'content-type': 'application/json' | |
}); | |
try { | |
if ((await res.json()).result) { | |
return true; | |
} | |
} catch (e) { | |
return false; | |
} | |
return false; | |
}, | |
flood: async () => { | |
let res = await XFetch.post(`${clientUrl}/api/auth/authenticate`, JSON.stringify({ | |
password, | |
username | |
}), { | |
'content-type': 'application/json' | |
}); | |
try { | |
if ((await res.json()).success) return true; | |
} catch (e) { | |
return false; | |
} | |
return false; | |
}, | |
rutorrent: async () => { | |
// credit to humeur | |
let headers = { | |
Authorization: `Basic ${btoa(`${username}:${password}`)}`, | |
'Content-Type': 'application/json' | |
}; | |
let res = await XFetch.post(`${clientUrl}/rutorrent/php/addtorrent.php?json=1`, null, headers); | |
if (res.raw.status !== 401) { | |
return true; | |
} | |
return false; | |
// credit to humeur; | |
} | |
}; | |
let result = await clients[client](); | |
return result; | |
} | |
// TODO: new implementation - there should be a class for each client implementating the needed methods | |
const getCategories = async (clientUrl, username, password) => { | |
XFetch.post(`${clientUrl}/api/v2/auth/login`, `username=${username}&password=${password}`, { | |
'content-type': 'application/x-www-form-urlencoded' | |
}); | |
let res = await XFetch.get(`${clientUrl}/api/v2/torrents/categories`); | |
try { | |
return Object.keys(await res.json()); | |
} catch (_unused) { | |
return []; | |
} | |
}; | |
async function detectClient(url) { | |
const res = await XFetch.get(url); | |
const body = await res.text(); | |
const headers = await res.headers(); | |
if (headers.hasOwnProperty('WWW-Authenticate')) { | |
const wwwAuthenticateHeader = headers['WWW-Authenticate']; | |
if (wwwAuthenticateHeader.includes('"Transmission"')) return 'trans'; | |
} | |
if (body.includes('<title>Deluge ')) return 'deluge'; | |
if (body.includes('<title>Flood</title>')) return 'flood'; | |
if (body.includes('<title>qBittorrent ')) return 'qbit'; | |
if (body.includes('ruTorrent ')) return 'rutorrent'; | |
return 'unknown'; | |
} | |
class Profile { | |
constructor(id, name, host, username, password, client, saveLocation, category, linkedTo = []) { | |
this.id = id; | |
this.name = name; | |
this.host = host; | |
this.username = username; | |
this.password = password; | |
this.client = client; | |
this.saveLocation = saveLocation; | |
this.category = category; | |
this.linkedTo = linkedTo; | |
} | |
async linkTo(site, replace = false) { | |
let alreadyLinkedTo = profileManager.profiles.find(p => p.linkedTo.includes(site)); | |
if (alreadyLinkedTo && !replace) { | |
return alreadyLinkedTo.name; | |
} else if (alreadyLinkedTo && replace) { | |
alreadyLinkedTo.unlinkFrom(site); | |
} | |
if (this.linkedTo.includes(site)) return true; | |
this.linkedTo.push(site); | |
profileManager.save(); | |
return true; | |
} | |
async unlinkFrom(site) { | |
this.linkedTo = this.linkedTo.filter(s => s !== site); | |
profileManager.save(); | |
} | |
async getCategories() { | |
if (this.client != 'qbit') return []; | |
let res = await getCategories(this.host, this.username, this.password); | |
console.log(res); | |
return res; | |
} | |
async testConnection() { | |
return await testClient(this.host, this.username, this.password, this.client); | |
} | |
async addTorrent(torrent_uri) { | |
return await addTorrent(torrent_uri, this.host, this.username, this.password, this.client, this.saveLocation, this.category); | |
} | |
} | |
const profileManager = { | |
profiles: [], | |
selectedProfile: null, | |
addProfile: function (profile) { | |
this.profiles.push(profile); | |
}, | |
removeProfile: function (id) { | |
this.profiles = this.profiles.find(p => p.id === id); | |
}, | |
getProfile: function (id) { | |
var _this$profiles$find; | |
return (_this$profiles$find = this.profiles.find(p => Number(p.id) === Number(id))) != null ? _this$profiles$find : new Profile(id, 'New Profile', '', '', '', 'none', '', ''); | |
}, | |
getProfiles: function () { | |
if (this.profiles.length === 0) this.load(); | |
return this.profiles; | |
}, | |
setSelectedProfile: function (id) { | |
this.selectedProfile = this.getProfile(id); | |
window.dispatchEvent(new CustomEvent('profileChanged', { | |
detail: this.selectedProfile | |
})); | |
}, | |
setProfile: function (profile) { | |
if (!this.profiles.includes(this.getProfile(profile.id))) { | |
this.profiles.push(profile); | |
} else { | |
this.profiles = this.profiles.map(p => { | |
if (p.id === profile.id) { | |
p = profile; | |
} | |
return p; | |
}); | |
} | |
}, | |
getNextId: function () { | |
if (this.profiles.length === 0) return 0; | |
return Number(this.profiles.sort((a, b) => Number(b.id) > Number(a.id))[0].id) + 1; | |
}, | |
save: function () { | |
GM.setValue('profiles', JSON.stringify(this.profiles)); | |
GM.setValue('selectedProfile', this.selectedProfile.id); | |
}, | |
load: async function () { | |
var _this$getProfile, _Number; | |
const profiles = await GM.getValue('profiles'); | |
if (profiles) { | |
this.profiles = JSON.parse(profiles).map(p => { | |
var _p$category, _p$linkedTo; | |
return new Profile(p.id, p.name, p.host, p.username, p.password, p.client, p.saveLocation, (_p$category = p.category) != null ? _p$category : '', (_p$linkedTo = p.linkedTo) != null ? _p$linkedTo : []); | |
}); | |
} | |
for (const profile of this.profiles) { | |
for (const site of profile.linkedTo) { | |
if (location.href.includes(site)) { | |
this.selectedProfile = profile; | |
return; | |
} | |
} | |
} | |
this.selectedProfile = (_this$getProfile = this.getProfile((_Number = Number(await GM.getValue('selectedProfile'))) != null ? _Number : 0)) != null ? _this$getProfile : new Profile(0, 'New Profile', '', '', '', 'none', '', ''); | |
} | |
}; | |
var styles = {"title":"style-module_title__Hei5S","desc":"style-module_desc__LACEI","settings":"style-module_settings__N-vGX","wrapper":"style-module_wrapper__qBEFA","select_input":"style-module_select_input__b12Je"}; | |
var stylesheet=".style-module_title__Hei5S{font-size:20px;font-weight:700;line-height:24px;margin-bottom:10px;text-align:center}.style-module_desc__LACEI{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.style-module_settings__N-vGX{grid-row-gap:1rem;grid-column-gap:1rem;display:grid;grid-template-columns:1fr 1fr}:host{align-items:center;backdrop-filter:blur(5px);display:flex;height:100%;justify-content:center;left:0;position:fixed;top:0;width:100%;z-index:99999999999}.style-module_wrapper__qBEFA{border-radius:10px;padding:20px}.style-module_select_input__b12Je{background-color:#fff;border:1px solid grey;height:20px;position:relative}.style-module_select_input__b12Je>select{border:none;bottom:0;left:0;margin:0;position:absolute;top:0;width:100%}.style-module_select_input__b12Je>input{border:none;left:0;padding:1px;position:absolute;top:0;width:calc(100% - 20px)}"; | |
const ButtonTypes = { | |
simple: 0, | |
extended: 1 | |
}; | |
const globalSettingsManager = { | |
settings: { | |
button_type: ButtonTypes.simple | |
}, | |
get button_type() { | |
return this.settings.button_type; | |
}, | |
set button_type(val) { | |
this.settings.button_type = val; | |
this.save(); | |
}, | |
async load() { | |
let settings = await GM.getValue('settings'); | |
if (settings) { | |
this.settings = JSON.parse(settings); | |
} | |
}, | |
async save() { | |
await GM.setValue('settings', JSON.stringify(this.settings)); | |
} | |
}; | |
const clientSelectorOnChange = (e, shadow) => { | |
if (shadow.querySelector('#host').value === '' && e.target.value !== 'unknown') shadow.querySelector('#host').value = e.target.value === 'flood' ? document.location.href.replace(/\/overview|login\/$/, '') : document.location.href.replace(/\/$/, ''); | |
shadow.querySelector('#category').hidden = e.target.value !== 'qbit'; | |
shadow.querySelector("label[for='category']").hidden = e.target.value !== 'qbit'; | |
if (e.target.value === 'qbit') { | |
shadow.querySelector('#category>select').onload(); | |
} | |
shadow.querySelector("label[for='username']").hidden = e.target.value === 'deluge'; | |
shadow.querySelector('#username').hidden = e.target.value === 'deluge'; | |
}; | |
function ClientSelector({ | |
shadow | |
}) { | |
return VM.h(VM.Fragment, null, VM.h("label", { | |
for: "client" | |
}, "Client:"), VM.h("select", { | |
id: "client", | |
name: "client", | |
onchange: e => clientSelectorOnChange(e, shadow) | |
}, VM.h("option", { | |
value: "none", | |
default: true | |
}, "None"), VM.h("option", { | |
value: "deluge" | |
}, "Deluge"), VM.h("option", { | |
value: "flood" | |
}, "Flood"), VM.h("option", { | |
value: "qbit" | |
}, "qBittorrent"), VM.h("option", { | |
value: "trans" | |
}, "Transmission"), VM.h("option", { | |
value: "rutorrent" | |
}, "ruTorrent"), VM.h("option", { | |
value: "unknown", | |
hidden: true | |
}, "Not supported by auto detect"))); | |
} | |
const profileOnSave = (e, shadow) => { | |
let profile = profileManager.getProfile(shadow.querySelector('#profile').value); | |
profile.host = shadow.querySelector('#host').value; | |
profile.username = shadow.querySelector('#username').value; | |
profile.password = shadow.querySelector('#password').value; | |
profile.client = shadow.querySelector('#client').value; | |
profile.saveLocation = shadow.querySelector('#saveLocation').value; | |
profile.name = shadow.querySelector('#profilename').value; | |
profile.category = shadow.querySelector('#category>input').value; | |
profileManager.setSelectedProfile(profile.id); | |
profileManager.setProfile(profile); | |
profileManager.save(); | |
shadow.querySelector('#profile').innerHTML = null; | |
shadow.querySelector('#profile').appendChild(VM.m(VM.h(VM.Fragment, null, profileManager.getProfiles().map(p => { | |
return VM.h("option", { | |
selected: p.id === profileManager.selectedProfile.id, | |
value: p.id | |
}, p.name); | |
}), VM.h("option", { | |
value: profileManager.getNextId() | |
}, "New profile")))); | |
}; | |
const addSiteToProfile = async (hostname, shadow) => { | |
let result = await profileManager.selectedProfile.linkTo(hostname); | |
if (result !== true && confirm(`This site is already linked to "${result}". Do you want to replace it?`)) profileManager.selectedProfile.linkTo(hostname, true); | |
profileSelectHandler({ | |
target: shadow.querySelector('#profile') | |
}, shadow); | |
}; | |
function profileSelectHandler(e, shadow) { | |
const profile = profileManager.getProfile(e.target.value); | |
profileManager.setSelectedProfile(profile.id); | |
shadow.querySelector('#host').value = profile.host; | |
shadow.querySelector('#username').value = profile.username; | |
shadow.querySelector('#password').value = profile.password; | |
shadow.querySelector('#client').value = profile.client; | |
shadow.querySelector('#saveLocation').value = profile.saveLocation; | |
shadow.querySelector('#profilename').value = profile.name; | |
shadow.querySelector('#linkToSite').innerHTML = null; | |
shadow.querySelector('#linkToSite').appendChild(VM.m(VM.h(VM.Fragment, null, profileManager.selectedProfile.linkedTo.map(site => VM.h("option", { | |
value: site | |
}, site)), profileManager.selectedProfile.linkedTo.includes(location.hostname) ? null : VM.h("option", { | |
value: location.hostname | |
}, "Link to this site.")))); | |
shadow.querySelector('select#client').onchange({ | |
target: shadow.querySelector('select#client') | |
}); | |
} | |
function ProfileSelector({ | |
shadow | |
}) { | |
return VM.h(VM.Fragment, null, VM.h("label", { | |
for: "profile" | |
}, "Profile:"), VM.h("select", { | |
id: "profile", | |
name: "profile", | |
onchange: e => profileSelectHandler(e, shadow) | |
}, profileManager.getProfiles().map(p => { | |
return VM.h("option", { | |
selected: p.id === profileManager.selectedProfile.id, | |
value: p.id | |
}, p.name); | |
}), VM.h("option", { | |
value: profileManager.getNextId() | |
}, "New profile"))); | |
} | |
async function loadCategories(shadow) { | |
let options = await profileManager.selectedProfile.getCategories().then(e => e.map(cat => VM.h("option", { | |
value: cat, | |
selected: profileManager.selectedProfile.category === cat | |
}, cat))); | |
options.push(VM.h("option", { | |
value: "", | |
default: true, | |
selected: profileManager.selectedProfile.category === '' | |
}, "Default")); | |
shadow.querySelector('#category>input').value = profileManager.selectedProfile.category; | |
shadow.querySelector('select[name="category"]').innerHTML = null; | |
shadow.querySelector('select[name="category"]').appendChild(VM.m(VM.h(VM.Fragment, null, options))); | |
} | |
function CategorySelector({ | |
shadow, | |
hidden | |
}) { | |
return VM.h(VM.Fragment, null, VM.h("label", { | |
for: "category", | |
hidden: hidden | |
}, "Category:"), VM.h("div", { | |
id: "category", | |
hidden: hidden, | |
className: styles.select_input | |
}, VM.h("select", { | |
name: "category", | |
onload: () => loadCategories(shadow), | |
onchange: e => shadow.querySelector('#category>input').value = e.target.value | |
}), VM.h("input", { | |
type: "text", | |
name: "category" | |
}))); | |
} | |
function LinkToSite({ | |
shadow | |
}) { | |
return VM.h(VM.Fragment, null, VM.h("label", { | |
for: "linkToSite" | |
}, "Linked to:"), VM.h("select", { | |
onchange: async e => { | |
if (profileManager.selectedProfile.linkedTo.includes(e.target.value)) confirm('Do you want to unlink this site?') && profileManager.selectedProfile.unlinkFrom(e.target.value);else await addSiteToProfile(e.target.value, shadow); | |
}, | |
id: "linkToSite", | |
name: "linkToSite" | |
}, profileManager.selectedProfile.linkedTo.map(site => VM.h("option", { | |
value: site | |
}, site)), profileManager.selectedProfile.linkedTo.includes(location.hostname) ? null : VM.h("option", { | |
value: location.hostname | |
}, "Link to this site."))); | |
} | |
function SettingsElement({ | |
panel | |
}) { | |
const shadow = panel.root; | |
return VM.h(VM.Fragment, null, VM.h("div", { | |
className: styles.title | |
}, "SendToClient"), VM.h("div", null, VM.h("div", { | |
className: styles.settings | |
}, VM.h("label", { | |
for: "btn-type", | |
title: "Toggles whatever you want to choose a profile while sending a torrent" | |
}, "Advanced button:"), VM.h("input", { | |
name: "btn-type", | |
type: "checkbox", | |
title: "Change will be applied after a page reload", | |
onchange: e => globalSettingsManager.button_type = Number(e.target.checked), | |
checked: globalSettingsManager.button_type ? true : false | |
})), VM.h("form", { | |
className: styles.settings, | |
onsubmit: async e => { | |
e.preventDefault(); | |
profileOnSave(e, shadow); | |
return false; | |
} | |
}, VM.h(ProfileSelector, { | |
shadow: shadow | |
}), VM.h(LinkToSite, { | |
shadow: shadow | |
}), VM.h(ClientSelector, { | |
shadow: shadow | |
}), VM.h("label", { | |
for: "profilename" | |
}, "Profile name:"), VM.h("input", { | |
type: "text", | |
id: "profilename", | |
name: "profilename" | |
}), VM.h("label", { | |
for: "host" | |
}, "Host:"), VM.h("input", { | |
type: "text", | |
id: "host", | |
name: "host" | |
}), VM.h("label", { | |
for: "username" | |
}, "Username:"), VM.h("input", { | |
type: "text", | |
id: "username", | |
name: "username" | |
}), VM.h("label", { | |
for: "password" | |
}, "Password:"), VM.h("input", { | |
type: "password", | |
id: "password", | |
name: "password" | |
}), VM.h(CategorySelector, { | |
hidden: profileManager.selectedProfile.client !== 'qbit', | |
shadow: shadow | |
}), VM.h("label", { | |
for: "saveLocation" | |
}, "Save location:"), VM.h("input", { | |
type: "text", | |
id: "saveLocation", | |
name: "saveLocation" | |
}), VM.h("button", { | |
onclick: async e => { | |
e.preventDefault(); | |
shadow.querySelector('select#client').value = await detectClient(shadow.querySelector('#host').value); | |
shadow.querySelector('select#client').onchange({ | |
target: shadow.querySelector('select#client') | |
}); | |
return false; | |
} | |
}, "Detect client"), VM.h("button", { | |
onclick: async e => { | |
e.preventDefault(); | |
shadow.querySelector('#res').innerText = (await testClient(shadow.querySelector('#host').value, shadow.querySelector('#username').value, shadow.querySelector('#password').value, shadow.querySelector('select#client').value)) ? 'Client seems to be working' : "Client doesn't seem to be working"; | |
return false; | |
} | |
}, "Test client"), VM.h("input", { | |
type: "submit", | |
value: "Save" | |
}), VM.h("button", { | |
onclick: e => panel.hide() | |
}, "Close")), VM.h("p", { | |
id: "res", | |
style: "text-align: center;" | |
}))); | |
} | |
const Settings = () => { | |
const panel = VM.getPanel({ | |
theme: 'dark', | |
shadow: true, | |
style: stylesheet | |
}); | |
// give the panel access to itself :) | |
panel.setContent(VM.h(SettingsElement, { | |
panel: panel | |
})); | |
panel.setMovable(false); | |
panel.wrapper.children[0].classList.add(styles.wrapper); | |
let original_show = panel.show; | |
panel.show = () => { | |
original_show.apply(panel); | |
document.body.style.overflow = 'hidden'; | |
}; | |
let original_hide = panel.hide; | |
panel.hide = () => { | |
original_hide.apply(panel); | |
document.body.style.overflow = 'auto'; | |
}; | |
panel.show(); | |
profileSelectHandler({ | |
target: { | |
value: profileManager.selectedProfile.id | |
} | |
}, panel.root); | |
}; | |
function ExtendeSTCProfile({ | |
panel, | |
profile, | |
torrentUrl | |
}) { | |
return VM.h("button", { | |
style: "display: block; padding: 5px; margin: 5px; cursor: pointer;", | |
onclick: e => { | |
profile.addTorrent(torrentUrl); | |
return panel.hide(); | |
} | |
}, profile.name); | |
} | |
function ExtendedSTCElement({ | |
panel, | |
torrentUrl | |
}) { | |
let profiles = []; | |
for (let profile of profileManager.profiles) { | |
profiles.push(VM.h(ExtendeSTCProfile, { | |
panel: panel, | |
profile: profile, | |
torrentUrl: torrentUrl | |
})); | |
} | |
return VM.h("div", { | |
style: "display: flex; flex-direction: column; align-items: center; justify-content:center;" | |
}, "Choose which profile to send to", profiles, VM.h("button", { | |
style: "display: block; padding: 5px; margin: 5px; background-color: #fe0000; cursor: pointer;", | |
onclick: () => panel.hide() | |
}, "Cancel")); | |
} | |
const ExtendedSTC = torrentUrl => { | |
const panel = VM.getPanel({ | |
theme: 'dark', | |
shadow: true, | |
style: stylesheet | |
}); | |
// give the panel access to itself :) | |
panel.setContent(VM.h(ExtendedSTCElement, { | |
panel: panel, | |
torrentUrl: torrentUrl | |
})); | |
panel.setMovable(false); | |
panel.wrapper.children[0].classList.add(styles.wrapper); | |
let original_show = panel.show; | |
panel.show = () => { | |
original_show.apply(panel); | |
document.body.style.overflow = 'hidden'; | |
}; | |
let original_hide = panel.hide; | |
panel.hide = () => { | |
original_hide.apply(panel); | |
document.body.style.overflow = 'auto'; | |
}; | |
panel.show(); | |
}; | |
const XSTBTN = ({ | |
torrentUrl, | |
freeleech | |
}) => { | |
return VM.h("a", { | |
title: "Add to client - extended!", | |
href: "#", | |
className: "sendtoclient", | |
onclick: async e => { | |
if (freeleech) if (!confirm('After sending to client a feeleech token will be consumed!')) return; | |
ExtendedSTC(torrentUrl); | |
} | |
}, "X", freeleech ? "F" : "", "ST"); | |
}; | |
const STBTN = ({ | |
torrentUrl | |
}) => { | |
return globalSettingsManager.button_type ? VM.h(XSTBTN, { | |
freeleech: false, | |
torrentUrl: torrentUrl | |
}) : VM.h("a", { | |
title: `Add to ${profileManager.selectedProfile.name}.`, | |
href: "#", | |
className: "sendtoclient", | |
onclick: async e => { | |
e.preventDefault(); | |
await profileManager.selectedProfile.addTorrent(torrentUrl); | |
e.target.innerText = 'Added!'; | |
e.target.onclick = null; | |
} | |
}, "ST"); | |
}; | |
const FSTBTN = ({ | |
torrentUrl | |
}) => { | |
return globalSettingsManager.button_type ? VM.h(XSTBTN, { | |
freeleech: true, | |
torrentUrl: torrentUrl | |
}) : VM.h("a", { | |
href: "#", | |
title: `Freeleechize and add to ${profileManager.selectedProfile.name}.`, | |
className: "sendtoclient", | |
onclick: async e => { | |
e.preventDefault(); | |
if (!confirm('Are you sure you want to use a freeleech token here?')) return; | |
await profileManager.selectedProfile.addTorrent(torrentUrl); | |
e.target.innerText = 'Added!'; | |
e.target.onclick = null; | |
} | |
}, "FST"); | |
}; | |
const handlers = [{ | |
name: 'Gazelle', | |
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"], | |
run: async () => { | |
for (const a of Array.from(document.querySelectorAll('a')).filter(a => a.innerText === 'DL' || a.title == 'Download Torrent')) { | |
let parent = a.parentElement; | |
let torrentUrl = a.href; | |
let buttons = Array.from(parent.childNodes).filter(e => e.nodeName !== '#text'); | |
let fl = Array.from(parent.querySelectorAll('a')).find(a => a.innerText === 'FL'); | |
let fst = fl ? VM.h(VM.Fragment, null, "\xA0|\xA0", VM.h(FSTBTN, { | |
torrentUrl: fl.href | |
})) : null; | |
parent.innerHTML = null; | |
parent.appendChild(VM.m(VM.h(VM.Fragment, null, "[\xA0", buttons.map(e => VM.h(VM.Fragment, null, e, " | ")), VM.h(STBTN, { | |
torrentUrl: torrentUrl | |
}), fst, "\xA0]"))); | |
} | |
window.addEventListener('profileChanged', () => { | |
document.querySelectorAll('a.sendtoclient').forEach(e => { | |
if (e.title.includes('Freeleechize')) { | |
e.title = `Freeleechize and add to ${profileManager.selectedProfile.name}.`; | |
} else { | |
e.title = `Add to ${profileManager.selectedProfile.name}.`; | |
} | |
}); | |
}); | |
} | |
}, { | |
name: 'BLU UNIT3D', | |
matches: ["blutopia.cc","aither.cc"], | |
run: async () => { | |
let rid = await fetch(Array.from(document.querySelectorAll('ul>li>a')).find(e => e.innerText === 'My Profile').href + '/rsskey/edit').then(e => e.text()).then(e => e.replaceAll(/\s/g, '').match(/name="current_rsskey"readonlytype="text"value="(.*?)">/)[1]); | |
handlers.find(h => h.name === 'UNIT3D').run(rid); | |
} | |
}, { | |
name: 'F3NIX', | |
matches: ["beyond-hd.me"], | |
run: async (rid = null) => { | |
if (!rid) { | |
rid = await fetch(location.origin + '/settings/change_rid').then(e => e.text()).then(e => e.match(/class="beta-form-main" name="null" value="(.*?)" disabled>/)[1]); | |
} | |
const appendButton = () => { | |
Array.from(document.querySelectorAll('a[title="Download Torrent"]')).forEach(a => { | |
let parent = a.parentElement; | |
let torrentUrl = `${a.href.replace('/download/', '/torrent/download/')}.${rid}`; | |
parent.appendChild(VM.m(VM.h(VM.Fragment, null, ' ', VM.h(STBTN, { | |
torrentUrl: torrentUrl | |
})))); | |
}); | |
}; | |
appendButton(); | |
let oldPushState = unsafeWindow.history.pushState; | |
unsafeWindow.history.pushState = function () { | |
console.log('[SendToClient] Detected a soft navigation to ${unsafeWindow.location.href}'); | |
appendButton(); | |
return oldPushState.apply(this, arguments); | |
}; | |
} | |
}, { | |
name: 'UNIT3D', | |
matches: ["desitorrents.tv","jptv.club","telly.wtf","torrentseeds.org"], | |
run: async (rid = null) => { | |
if (!rid) { | |
rid = await fetch(Array.from(document.querySelectorAll('ul>li>a')).find(e => e.innerText.includes('My Profile')).href + '/settings/security').then(e => e.text()).then(e => e.match(/ current_rid">(.*?)</)[1]); | |
} | |
const appendButton = () => { | |
Array.from(document.querySelectorAll('a[title="Download"]')).concat(Array.from(document.querySelectorAll('button[title="Download"], button[data-original-title="Download"]')).map(e => e.parentElement)).forEach(a => { | |
let parent = a.parentElement; | |
let torrentUrl = a.href.replace('/torrents/', '/torrent/') + `.${rid}`; | |
parent.appendChild(VM.m(VM.h(STBTN, { | |
torrentUrl: torrentUrl | |
}))); | |
}); | |
}; | |
appendButton(); | |
console.log('[SendToClient] Bypassing CSP so we can listen for soft navigations.'); | |
document.addEventListener('popstate', () => { | |
console.log('[SendToClient] Detected a soft navigation to ' + unsafeWindow.location.href); | |
appendButton(); | |
}); | |
// listen for a CSP violation so that we can grab the nonces | |
document.addEventListener('securitypolicyviolation', e => { | |
const nonce = e.originalPolicy.match(/nonce-(.*?)'/)[1]; | |
let actualScript = VM.m(VM.h("script", { | |
nonce: nonce | |
}, `console.log('[SendToClient] Adding a navigation listener.'); | |
(() => { | |
let oldPushState = history.pushState; | |
history.pushState = function pushState() { | |
let ret = oldPushState.apply(this, arguments); | |
document.dispatchEvent(new Event('popstate')); | |
return ret; | |
}; | |
})();`)); | |
document.head.appendChild(actualScript).remove(); | |
}); | |
// trigger a CSP violation | |
document.head.appendChild(VM.m(VM.h("script", { | |
nonce: "nonce-123" | |
}, "window.csp = \"csp :(\";"))).remove(); | |
} | |
}, { | |
name: 'Karagarga', | |
matches: ["karagarga.in"], | |
run: async () => { | |
if (unsafeWindow.location.href.includes('details.php')) { | |
let dl_btn = document.querySelector('a.index'); | |
let torrent_uri = dl_btn.href; | |
return dl_btn.insertAdjacentElement('afterend', VM.m(VM.h("span", null, "\xA0 ", VM.h(STBTN, { | |
torrentUrl: torrent_uri | |
})))); | |
} | |
document.querySelectorAll("img[alt='Download']").forEach(e => { | |
let parent = e.parentElement; | |
let torrent_uri = e.parentElement.href; | |
let container = parent.parentElement; | |
let st = VM.m(VM.h(STBTN, { | |
torrentUrl: torrent_uri | |
})); | |
container.appendChild(st); | |
}); | |
} | |
}, { | |
name: 'TorrentLeech', | |
matches: ["torrentleech.org","www.torrentleech.org"], | |
run: async () => { | |
const username = document.querySelector('span.link').getAttribute('onclick').match('/profile/(.*?)/view')[1]; | |
let rid = await fetch(`/profile/${username}/edit`).then(e => e.text()).then(e => e.replaceAll(/\s/g, '').match(/rss.torrentleech.org\/(.*?)\</)[1]); | |
document.head.appendChild(VM.m(VM.h("style", null, `td.td-quick-download { display: flex; }`))); | |
for (const a of document.querySelectorAll('a.download')) { | |
let torrent_uri = a.href.match(/\/download\/(\d*?)\/(.*?)$/); | |
torrent_uri = `https://torrentleech.org/rss/download/${torrent_uri[1]}/${rid}/${torrent_uri[2]}`; | |
a.parentElement.appendChild(VM.m(VM.h(STBTN, { | |
torrentUrl: torrent_uri | |
}))); | |
} | |
} | |
}, { | |
name: 'AnilistBytes', | |
matches: ["anilist.co"], | |
run: async () => { | |
unsafeWindow._addTo = async torrentUrl => profileManager.selectedProfile.addTorrent(torrentUrl); | |
} | |
}]; | |
const createButtons = async () => { | |
for (const handler of handlers) { | |
const regex = handler.matches.join('|'); | |
if (unsafeWindow.location.href.match(regex)) { | |
handler.run(); | |
console.log(`%c[SendToClient] Using engine {${handler.name}}`, 'color: #42adf5; font-weight: bold; font-size: 1.5em;'); | |
return handler.name; | |
} | |
} | |
}; | |
GM.registerMenuCommand('Settings', () => { | |
Settings(); | |
}); | |
const profileQuickSwitcher = () => { | |
let id = GM.registerMenuCommand(`Selected Profile: ${profileManager.selectedProfile.name}`, () => {}); | |
window.addEventListener('profileChanged', () => { | |
GM.unregisterMenuCommand(id); | |
profileQuickSwitcher(); | |
window.removeEventListener('profileChanged', () => {}); | |
}); | |
}; | |
globalSettingsManager.load().then(() => profileManager.load().then(() => { | |
profileQuickSwitcher(); | |
createButtons(); | |
})); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
With 2.3.0 / Tampermonkey / Firefox, I can't open the settings menu.