// ==UserScript==
// @name Torrent Quick Search
// @namespace https://github.com/TMD20/torrent-quick-search
// @supportURL https://github.com/TMD20/torrent-quick-search
// @version 1.41
// @description Toggle for Searching Torrents via Search aggegrator
// @icon https://cdn2.iconfinder.com/data/icons/flat-icons-19/512/Eye.png
// @author tmd
// @noframes
// @inject-into page
// @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/*
// @license MIT
// @downloadURL none
// ==/UserScript==
`
General Functions
Functions that don't fit in any other catergory
`
function recreateController()
{
controller = new AbortController();
}
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()
imdb=await setIMDBNode()
setTitleNode()
//reset count
let count = []
let length = indexers.length
let data = []
x = 5
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"))
}
addNumbers()
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(async()=>{
resetResultList()
resetSearchDOM()
getTableHead()
try{
await this.searchPromise
this.finalize()
}
catch (error)
{
if (error.message.match(/aborted!/i) === null)
{
GM.notification(error.message, program, searchIcon)
}
console.log(error)
}
},0)
},
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)
},
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"] = "Not Provided"
}
})
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)
{
req = await fetch(getSearchURLProwlarr(indexer["id"]),
{
"timeout": indexerSearchTimeout
})
data = JSON.parse(req.responseText)
promiseArray = await Promise.allSettled(data.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["details"],
"DownloadUrl": e["downloadUrl"],
"ImdbId": e["imdbId"],
"TvdbId": await tmdbTVDBConvertor(e["imdbId"]),
"Cost": e["indexerFlags"].includes("freeleech") == "100% Freeleech" ? "100% Freeleech" : "Cost Unknown With Prowlarr",
"Protocol": e["protocol"],
}
}))
return promiseArray.map((e) => e["value"]).filter((e) => e != null)
}
async function searchJackettIndexer(indexer)
{
req = await fetch(getSearchURLJackett(indexer["id"]),
{
"timeout": indexerSearchTimeout
})
data = JSON.parse(req.responseText)["Results"]
promiseArray = await Promise.allSettled(data.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"],
"TvdbId": await tmdbTVDBConvertor(e["Imdb"]),
"Cost": `${(1-e["DownloadVolumeFactor"])*100}% Freeleech`,
"Protocol": "torrent",
}
}))
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();
data = [...Array.from(parser.parseFromString(req.responseText, "text/xml").querySelectorAll("channel>item")), ...Array.from(parser.parseFromString(req2.responseText, "text/xml").querySelectorAll("channel>item"))]
//array is final dictkey,queryselector,attribute
promiseArray = await Promise.allSettled(
data.map(async (e) =>
{
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["TvdbId"] = await tmdbTVDBConvertor(out["ImdbId"])
out["Protocol"] = data[0].querySelector("enclosure").getAttribute("type") == "application/x-bittorrent" ? "torrent" : "usenet"
return out
})
)
return promiseArray.map((e) => e["value"]).filter((e) => e != null)
}
function fetch(url,
{
method = "GET",
data = null,
headers = {},
timeout = 300000
} = {})
{
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)
},
})
}
)
return req
}
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
document.querySelector("#torrent-quicksearch-msgnode").textContent = "Fetching Results From Indexers"
//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 = ""
}
function hideDisplay()
{
document.querySelector("#torrent-quicksearch-overlay").style.setProperty('--icon-size', `${iconSmall}%`);
document.querySelector("#torrent-quicksearch-customsearch").value = ""
content.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
Arr
Number
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) =>
{
node = document.createElement("span");
node.setAttribute("class", "torrent-quicksearch-resultitem")
node.innerHTML = `
Download
Details
Send to Sonarr
Send to Radarr
?
${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']}`
processSonarrNode(node, e)
processRadarrNode(node, e)
tempFrag.append(node)
})
resultList.appendChild(tempFrag)
}
function resetResultList()
{
document.querySelector("#torrent-quicksearch-resultheader").textContent = ""
document.querySelector("#torrent-quicksearch-resultlist").textContent = ""
}
function addNumbers()
{
Array.from(document.querySelectorAll(".torrent-quicksearch-resultitem")).forEach((e, i) =>
{
node = Array.from(e.children).filter((e) => e["textContent"] == "?")[0]
node.textContent = `${i+1}`
}
)
}
async function processSonarrNode(node, data)
{
node = Array.from(node.querySelectorAll("span>span>a")).filter((e) => e["textContent"] == "Send to Sonarr")[0]
if (GM_config.get('sonarrurl') === "null" ||
GM_config.get('sonarrurl') === "" ||
GM_config.get('sonarrapi') === "null" ||
GM_config.get('sonarrapi') === "")
{
node.remove()
return
}
nodeEvent = await sonarrFactory(data)
node.addEventListener("click", nodeEvent)
node.style.cursor = "pointer"
}
async function processRadarrNode(node, data)
{
node = Array.from(node.querySelectorAll("span>span>a")).filter((e) => e["textContent"] == "Send to Radarr")[0]
if (GM_config.get('radarrurl') === "null" ||
GM_config.get('radarrurl') === "" ||
GM_config.get('radarrapi') === "null" ||
GM_config.get('radarrapi') === ""
)
{
node.remove()
return
}
nodeEvent = await radarrFactory(data)
node.addEventListener("click", nodeEvent)
node.style.cursor = "pointer"
}
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 = `