// ==UserScript== // @name Torrent Quick Search // @namespace https://github.com/TMD20/torrent-quick-search // @supportURL https://github.com/TMD20/torrent-quick-search // @version 1.58 // @description Toggle for Searching Torrents via Search aggegrator // @icon https://cdn2.iconfinder.com/data/icons/flat-icons-19/512/Eye.png // @author tmd // @noframes // @run-at document-end // @require https://openuserjs.org/src/libs/sizzle/GM_config.min.js // @grant GM.getValue // @grant GM.setValue // @grant GM.xmlHttpRequest // @grant GM.registerMenuCommand // @grant GM_config // @grant GM.notification // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_notification // @require https://cdn.jsdelivr.net/npm/semaphore@1.1.0/lib/semaphore.min.js // @require https://cdn.jsdelivr.net/npm/semaphore@1.1.0/lib/semaphore.min.js // @match https://animebytes.tv/requests.php?action=viewrequest&id=* // @match https://animebytes.tv/series.php?id=* // @match https://animebytes.tv/torrents.php?id=* // @match https://blutopia.xyz/requests/* // @match https://blutopia.xyz/torrents/* // @match https://beyond-hd.me/requests/* // @match https://beyond-hd.me/torrents/* // @match https://beyond-hd.me/library/title/* // @match https://imdb.com/title/* // @match https://www.imdb.com/title/* // @match https://www.themoviedb.org/movie/* // @match https://www.themoviedb.org/tv/* // @license MIT // @downloadURL none // ==/UserScript== ` General Functions Functions that don't fit in any other catergory `; function recreateController() { controller = new AbortController(); } function semaphoreLeave() { if (sem && sem.current > 0) { sem.leave(); } } let searchObj = { ready: true, search() { if (controller.signal.aborted) { return Promise.reject(AbortError); } return new Promise(async (resolve, reject) => { controller.signal.addEventListener("abort", () => { reject(AbortError); }); document.querySelector("#torrent-quicksearch-msgnode").textContent = "Loading"; let indexers = await getIndexers(); document.querySelector("#torrent-quicksearch-msgnode").textContent = "Fetching Results From Indexers"; let imdb = await setIMDBNode(); setTitleNode(); //reset count let count = []; let length = indexers.length; let data = []; let x = Number.MAX_VALUE; while (indexers.length) { // x at a time let newData = await Promise.allSettled( indexers .splice(0, Math.min(indexers.length, x)) .map((e) => searchIndexer(e, imdb, length, count)) ); data = [...data, ...newData]; } console.log(data); let errorMsgs = data .filter((e) => e["status"] == "rejected") .map((e) => e["reason"].message); errorMsgs = [...new Set(errorMsgs)]; if (errorMsgs.length > 0) { reject(errorMsgs.join("\n")); } resolve(); }); }, cancel() { controller.abort(); }, async setup() { this.searchPromise = new Promise((resolve, reject) => { this.timeout = setTimeout(async () => { try { resolve(await this.search()); } catch (e) { reject(e); } }, 1000); }); }, async doSearch() { showDisplay(); recreateController(); await this.setup(); setTimeout(() => { resetResultList(); resetSearchDOM(); getTableHead(); }, 0); setTimeout(async () => { //reset sem = semaphore(10); try { await this.searchPromise; this.finalize(); } catch (error) { if (error.message.match(/aborted!/i) === null) { GM.notification(error.message, program, searchIcon); } console.log(error); } }, 100); }, finalize() { if ( Array.from(document.querySelectorAll(".torrent-quicksearch-resultitem")) .length == 0 ) { this.nomatchID = setTimeout( () => (document.querySelector( "#torrent-quicksearch-resultlist" ).textContent = "No Matches"), 1000 ); } this.finalmsgID = setTimeout( () => (document.querySelector("#torrent-quicksearch-msgnode").textContent = "Finished"), 1000 ); this.removemsgnodeID = setTimeout(() => { (document.querySelector("#torrent-quicksearch-msgnode").style.display = "none"), 3000; document.querySelector("#torrent-quicksearch-msgnode").textContent = ""; }); }, async toggleSearch() { let content = document.querySelector("#torrent-quicksearch-box"); if (content.style.display === "inline-block") { hideDisplay(); searchObj.cancel(); } else if ( content.style.display === "none" || content.style.display === "" ) { let customSearch = false; await this.doSearch(); } }, }; function searchIndexer(indexerObj, imdb, total, count) { if (controller.signal.aborted) { return Promise.reject(AbortError); } return new Promise(async (resolve, reject) => { let msg = null; controller.signal.addEventListener("abort", () => { reject(AbortError); }); let searchprogram = GM_config.get("searchprogram"); let data = null; if (searchprogram == "Prowlarr") { data = await searchProwlarrIndexer(indexerObj, controller); } else if (searchprogram == "Jackett") { data = await searchJackettIndexer(indexerObj); } else if (searchprogram == "NZBHydra2") { data = await searchHydra2Indexer(indexerObj); } msg = `Results fetched fom ${indexerObj["Name"]}:${ count.length + 1 }/${total} Indexers completed`; data = data.filter((e) => imdbFilter(e, imdbCleanup(imdb))); data.forEach((e) => { if (e["ImdbId"] == 0 || e["ImdbId"] == null) { e["ImdbId"] = imdbParserFail; } }); data = data.filter((e) => currSiteFilter(e["InfoUrl"])); addResultsTable(data); count.push(indexerObj["ID"]); document.querySelector("#torrent-quicksearch-msgnode").textContent = msg; console.log(msg); resolve(data); }); } async function searchProwlarrIndexer(indexer) { console.log(getSearchURLProwlarr(indexer["ID"])); let req = await fetch(getSearchURLProwlarr(indexer["ID"]), { timeout: indexerSearchTimeout, }); let data = JSON.parse(req.responseText) || []; let dataCopy = [...data]; let promiseArray = []; let x = Number.MAX_VALUE; while (dataCopy.length) { let newData = await Promise.allSettled( dataCopy.splice(0, Math.min(dataCopy.length, x)).map(async (e) => { return { Title: e["title"], Indexer: e["indexer"], Grabs: e["grabs"], PublishDate: e["publishDate"], Size: e["size"], Leechers: e["leechers"], Seeders: e["seeders"], InfoUrl: e["infoUrl"], DownloadUrl: e["downloadUrl"], ImdbId: e["imdbId"], Cost: e["indexerFlags"].includes("freeleech") == "100% Freeleech" ? "100% Freeleech" : "Cost Unknown With Prowlarr", Protocol: e["protocol"], }; }) ); promiseArray = [...promiseArray, ...newData]; } return promiseArray.map((e) => e["value"]).filter((e) => e != null); } async function searchJackettIndexer(indexer) { let req = await fetch(getSearchURLJackett(indexer["ID"]), { timeout: indexerSearchTimeout, }); let data = JSON.parse(req.responseText)["Results"] || []; let dataCopy = [...data]; let promiseArray = []; let x = Number.MAX_VALUE; while (dataCopy.length) { let newData = await Promise.allSettled( dataCopy.splice(0, Math.min(dataCopy.length, x)).map(async (e) => { return { Title: e["Title"], Indexer: e["Tracker"], Grabs: e["Grabs"], PublishDate: e["PublishDate"], Size: e["Size"], Leechers: e["Peers"], Seeders: e["Seeders"], InfoUrl: e["Details"], DownloadUrl: e["Link"], ImdbId: e["Imdb"], Cost: `${(1 - e["DownloadVolumeFactor"]) * 100}% Freeleech`, Protocol: "torrent", }; }) ); promiseArray = [...promiseArray, ...newData]; } return promiseArray.map((e) => e["value"]).filter((e) => e != null); } async function searchHydra2Indexer(indexer) { let req = await fetch(getSearchURLHydraTor(indexer["ID"]), { timeout: indexerSearchTimeout, }); let req2 = await fetch(getSearchURLHydraNZB(indexer["ID"]), { timeout: indexerSearchTimeout, }); let parser = new DOMParser(); let data = [ ...Array.from( parser .parseFromString(req.responseText, "text/xml") .querySelectorAll("channel>item") ), ...Array.from( parser .parseFromString(req2.responseText, "text/xml") .querySelectorAll("channel>item") ), ]; let dataCopy = [...data]; let promiseArray = []; let x = Number.MAX_VALUE; while (dataCopy.length) { let newData = await Promise.allSettled( dataCopy.splice(0, Math.min(dataCopy.length, x)).map(async (e) => //array is final dictkey,queryselector,attribute { let t = [ ["Title", "title", "textContent"], ["Indexer", "[name=hydraIndexerName]", "null"], ["Leechers", "[name=peers]", "null"], ["Seeders", "[name=seeders]", "null"], ["Cost", "[name=downloadvolumefactor]", "null"], ["PublishDate", "pubDate", "textContent"], ["Size", "size", "textContent"], ["InfoUrl", "comments", "textContent"], ["DownloadUrl", "link", "textContent"], ["ImdbId", "[name=imdb]", "null"], ]; let out = {}; out["Grabs"] = "Hydra Does not Report"; for (let i in t) { let key = t[i][0]; let node = e.querySelector(t[i][1]); let textContent = t[i][2] == "textContent"; if (!node) { continue; } if (textContent) { out[key] = node.textContent; } else if (key == "cost") { out[key] = `${(1 - node.getAttribute("value")) * 100}% Freeleech`; } else { out[key] = node.getAttribute("value"); } } out["Protocol"] = data[0].querySelector("enclosure").getAttribute("type") == "application/x-bittorrent" ? "torrent" : "usenet"; return out; } ) ); promiseArray = [...promiseArray, ...newData]; } return promiseArray.map((e) => e["value"]).filter((e) => e != null); } function fetch( url, { method = "GET", data = null, headers = {}, timeout = 90000, semaphore = true, } = {} ) { async function semforeFetch() { return new Promise((resolve, reject) => { sem.take(async () => { controller.signal.addEventListener("abort", () => { reject(AbortError); }); setTimeout(() => reject(AbortError), timeout); GM.xmlHttpRequest({ method: method, url: url, data: data, headers: headers, onload: (response) => { semaphoreLeave(); resolve(response); }, onerror: (response) => { semaphoreLeave(); reject(response.responseText); }, }); }); }); } async function normalFetch() { return new Promise((resolve, reject) => { controller.signal.addEventListener("abort", () => { reject(AbortError); }); setTimeout(() => reject(AbortError), timeout); GM.xmlHttpRequest({ method: method, url: url, data: data, headers: headers, onload: (response) => { resolve(response); }, onerror: (response) => { reject(response.responseText); }, }); }); } if (semaphore) { return semforeFetch(); } else { return normalFetch(); } } function getParser() { let siteName = standardNames[window.location.host] || window.location.host; let data = infoParser[siteName]; if (data === undefined) { let msg = "Could not get Parser"; GM.notification(msg, program, searchIcon); throw new Error(msg); } return data; } function verifyConfig() { if ( GM_config.get("searchapi", "null") == "null" || GM_config.get("searchurl", "null") == "null" ) { return false; } if ( GM_config.get("searchapi", "null") == "" || GM_config.get("searchurl", "null") == "" ) { return false; } return true; } ` DOM Manipulators These Functions are used to manipulate the DOM `; function setTitleNode() { if (customSearch == false) { document.querySelector("#torrent-quicksearch-customsearch").value = getTitle(); } } async function setIMDBNode() { let imdb = null; //Get Old IMDB if ( document.querySelector("#torrent-quicksearch-imdbinfo").textContent != imdbParserFail && document.querySelector("#torrent-quicksearch-imdbinfo").textContent .length != 0 && document.querySelector("#torrent-quicksearch-imdbinfo").textContent != "None" ) { imdb = document.querySelector("#torrent-quicksearch-imdbinfo").textContent; } //Else get New IMDB else { imdb = await getIMDB(); document.querySelector("#torrent-quicksearch-imdbinfo").textContent = imdb || imdbParserFail; } return imdb; } function resetSearchDOM() { document.querySelector("#torrent-quicksearch-imdbinfo").textContent = "None"; document.querySelector("#torrent-quicksearch-msgnode").textContent = "Waiting"; } function hideDisplay() { document .querySelector("#torrent-quicksearch-overlay") .style.setProperty("--icon-size", `${iconSmall}%`); document.querySelector("#torrent-quicksearch-customsearch").value = ""; document.querySelector("#torrent-quicksearch-box").style.display = "none"; } function showDisplay() { document.querySelector("#torrent-quicksearch-msgnode").textContent = ""; document.querySelector("#torrent-quicksearch-msgnode").style.display = "block"; document .querySelector("#torrent-quicksearch-overlay") .style.setProperty("--icon-size", `${iconLarge}%`); document.querySelector("#torrent-quicksearch-box").style.display = "inline-block"; } function getTableHead() { let node = document.querySelector("#torrent-quicksearch-resultheader"); node.innerHTML = ` Links Clients Title Indexer Grabs Seeders Leechers DLCost Date Size IMDB `; Array.from(node.children).forEach((e, i) => { e.style.gridColumnStart = i + 1; e.style.fontSize = `${GM_config.get("fontsize", 12)}px`; }); } function addResultsTable(data) { if (data.length == 0) { return; } let resultList = document.querySelector("#torrent-quicksearch-resultlist"); let tempFrag = new DocumentFragment(); data.forEach((e, i) => { let node = document.createElement("span"); node.setAttribute("class", "torrent-quicksearch-resultitem"); node.innerHTML = ` Download

Details
Arr Clients imdbID sent from entry if null then page
${ e["Title"] } ${ e["Indexer"] } ${ e["Grabs"] || "No Data" } ${ e["Seeders"] || "No Data" } ${ e["Leechers"] || "No Data" } ${ e["Cost"] } ${new Date( e["PublishDate"] ).toLocaleString("en-CA")} ${( parseInt(e["Size"]) / 1073741824 ).toFixed(2)} GB ${ e["ImdbId"] }`; let selNode = node.querySelector("select"); JSON.parse(GM_config.getValue("downloadClients", "[]")).forEach((e) => { let optnode = document.createElement("option"); optnode.setAttribute("id", e.clientID); optnode.setAttribute("value", e.clientID); optnode.textContent = e.clientName; selNode.appendChild(optnode); }); node.querySelector("form").addEventListener("submit", clientFactory(e)); tempFrag.append(node); }); resultList.appendChild(tempFrag); } function resetResultList() { document.querySelector("#torrent-quicksearch-resultheader").textContent = ""; document.querySelector("#torrent-quicksearch-resultlist").textContent = ""; } function createMainDOM() { const box = document.createElement("div"); box.setAttribute("id", "torrent-quicksearch-overlay"); let rowSplit = 12; let contentWidth = 70; let boxMinHeight = 5; let boxMaxHeight = 100; let boxHeight = 40; let boxWidth = 70; let boxMaxWidth = 150; box.innerHTML = `
None