// ==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
${
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 = `