// ==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/ui@0.7
// @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();
}));
})();