// ==UserScript== // @name Torrent Quick Search // @namespace https://github.com/TMD20/torrent-quick-search // @supportURL https://github.com/TMD20/torrent-quick-search // @version 1.57 // @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.notification // @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/* // @require https://cdn.jsdelivr.net/npm/semaphore@1.1.0/lib/semaphore.min.js // @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() } } 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" indexers = await getIndexers() document.querySelector("#torrent-quicksearch-msgnode").textContent = "Fetching Results From Indexers" 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 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) 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() { content = document.querySelector("#torrent-quicksearch-box") if (content.style.display === "inline-block") { hideDisplay() searchObj.cancel() } else if ((content.style.display === "none" || content.style.display === "")) { 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) => { msg = null controller.signal.addEventListener("abort", () => { reject(AbortError) }); searchprograms = 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"])) 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){ 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) { 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){ 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) { req = await fetch(getSearchURLHydraTor(indexer["ID"]), { "timeout": indexerSearchTimeout }) req2 = await fetch(getSearchURLHydraNZB(indexer["ID"]), { "timeout": indexerSearchTimeout }) 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){ newData = await Promise.allSettled(dataCopy.splice(0, Math.min(dataCopy.length, x)).map(async (e) => //array is final dictkey,queryselector,attribute { 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"] ] out = {} out["Grabs"] = "Hydra Does not Report" for (i in t) { key = t[i][0] node = e.querySelector(t[i][1]) 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() { siteName = standardNames[window.location.host] || window.location.host data = infoParser[siteName] if (data === undefined) { 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(){ 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}%`); content.style.display = "inline-block"; } function getTableHead() { 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 } resultList = document.querySelector("#torrent-quicksearch-resultlist") 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"); rowSplit = 12 contentWidth = 70 boxMinHeight = 5 boxMaxHeight = 100 boxHeight = 40 boxWidth = 70 boxMaxWidth = 150 box.innerHTML = `
None