// ==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 // @downloadURL https://update.greasyfork.icu/scripts/468376/SendToClient.user.js // @updateURL https://update.greasyfork.icu/scripts/468376/SendToClient.meta.js // ==/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('Deluge ')) return 'deluge'; if (body.includes('<title>Flood')) return 'flood'; if (body.includes('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(); })); })();