Magnet Links & Torrent Search Filter

Adds magnet links to BT4G, Limetorrents, BT1207, filtering of search results by minimum and maximum size (BT4G only), automatic reload in case of server errors every 5 minutes

// ==UserScript==
// @name            Magnet Links & Torrent Search Filter
// @description     Adds magnet links to BT4G, Limetorrents, BT1207, filtering of search results by minimum and maximum size (BT4G only), automatic reload in case of server errors every 5 minutes
// @version         20251002
// @author          mykarean
// @match           *://bt4gprx.com/*
// @match           *://*.limetorrents.fun/search/all/*
// @match           *://bt1207so.top/search*
// @match           *://bt1207un.top/search*
// @run-at          document-idle
// @grant           GM_xmlhttpRequest
// @grant           GM_addStyle
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           GM_registerMenuCommand
// @compatible      chrome
// @license         GPL3
// @noframes
// @icon           data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAMFBMVEUAAAD/////PwA/v/8KIDUifaxN2//Cw8L5ywC8hwBuQwA+HQDSLAcPBAJvb2////9VWbprAAAAEHRSTlP///////////////////8A4CNdGQAAAAlwSFlzAAALEwAACxMBAJqcGAAABr5pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDYuMC1jMDA1IDc5LjE2NDU5MCwgMjAyMC8xMi8wOS0xMTo1Nzo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIyLjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyNC0wNi0yMVQwODoyMTo0NCswMjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDYtMjFUMTI6MDA6MzQrMDI6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDYtMjFUMTI6MDA6MzQrMDI6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIyIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODZhZDM2M2ItOWY4Ny0xNzQ3LWE2MDQtOTBmODg4MzA3MmU5IiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6ODVkNmE1ODktZmZhZC1kYTRmLWE1NTktOTc5ODAxNmRkMDJjIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6YjAwNjgyYTQtMGQ2NS01YjRlLTg3MTMtMGVmYzNlN2U4ODI0Ij4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMDA2ODJhNC0wZDY1LTViNGUtODcxMy0wZWZjM2U3ZTg4MjQiIHN0RXZ0OndoZW49IjIwMjQtMDYtMjFUMDg6MjE6NDQrMDI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMi4xIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6OTNhYTg2MjktN2RhNy0wOTQ4LWI0Y2UtODQ1YjM2ODA4ZmQzIiBzdEV2dDp3aGVuPSIyMDI0LTA2LTIxVDA4OjUzOjAzKzAyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjIuMSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjg2YWQzNjNiLTlmODctMTc0Ny1hNjA0LTkwZjg4ODMwNzJlOSIgc3RFdnQ6d2hlbj0iMjAyNC0wNi0yMVQxMjowMDozNCswMjowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIyLjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PocnnoQAAAGsSURBVFjD7ZdJcsQgDEV/wNACQXz/22YRNzOSU92VRSpaUv4PDWAknFtDY8JXuliG4K58h4CmziwzoO0eg+wFNOdDlAOBoo+kpAJK7igo2YSsjxQB5JrSiYBJz23aiZhDyHNJZkDdNNbPIhFRiYJzjFMUmPznUOMmIiKKBRd4ysMMQN3y24FSh0CEHaCv/FV7Jgqx+sOhr2kL6DLP4AsQMxDo6XZkLI41lhXk3AS0WBwBEGx9kioBip6JIRJGwGe19jI2y1kE5I9ij+tYAQAedf2YABABLAGgAy6HJQBEAGQA3g5AMk9L7bLzxRQPtgD7tBGA1wD4B/wD/grg5cv0FwDn5o9k8g3AKQGaJ+mw1TABOkKsgJQXDninPG1sFoQmABWQTUvgDOBo9NYfC8C5SYIxJiVnbau3q+e9I7DpzTfywYFNhzK4YDu93bQ45y4LI+DYNFn7IDpAV4J9n9iehQ4wlXDd6g6nSdEvmu0+igLwa/2u3S+3MhX5sR4YtgMHXwjrvffWHTcGjhGROaWUnHNuL9eHrqxNj7g7dN4c+1TCb4y+7xi+fzL+fwG9j0ZciTgBKAAAAABJRU5ErkJggg==
// @namespace https://greasyfork.org/users/1367334
// ==/UserScript==

"use strict";

// ---------------------------------------------------------
// Config/Requirements
// ---------------------------------------------------------

const limetorrentsURL = "www.limetorrents.fun";
const bt4gURL = "bt4gprx.com";

const currentPath = window.location.href;
const hostname = location.hostname;
const magnetImage = GM_info.script.icon;

let itemsFoundElement;
if (hostname === bt4gURL) {
    itemsFoundElement = document.querySelector("body > main > p");
} else if (hostname === limetorrentsURL) {
    itemsFoundElement = document.querySelector("#content > h2");
}

// ---------------------------------------------------------
// Layout
// ---------------------------------------------------------

function addCss() {
    GM_addStyle(`
        .magnet-link-img {
            cursor: pointer;
            margin: 0px 5px;
            vertical-align: sub;
            height: 20px;
            transition: filter 0.2s ease;
        }
        a.magnet-link {
            display: inline-block !important;
            vertical-align: middle;
            margin-right: 10px;
            border-radius: 6px;
            padding: 1px 10px 3px 0;
            background: #000000;
            outline: 2px solid #ffffff00;
            font-family: system-ui,-apple-system,sans-serif;
            font-size: 16px;
            font-weight: 400;
            color: white;
            text-decoration: none;
            user-select: none;
            transition: outline .15s ease-in-out;
        }
        a.magnet-link:hover {
            outline: 2px solid #ff3f00;
        }
    `);
    if (hostname === bt4gURL && location.pathname === "/search") {
        GM_addStyle(`
            a.magnet-link {
                vertical-align: top;
            }
        `);
    }

    if (hostname === bt4gURL) {
        GM_addStyle(`
            .lead {
                display: inline-block;
            }
            /* removing the annoying hover effect on search results */
            .result-item:hover,
            .list-group-item:hover {
                transform: none;
            }
        `);
    }
}

// ---------------------------------------------------------
// Helper
// ---------------------------------------------------------

/**
 * @param {String} tag Elements HTML Tag
 * @param {String|RegExp} regex Regular expression or string for text search
 * @param {Number} index Item Index
 * @returns {Object|null} Node or null if not found
 */
function getElementByText(tag, regex, item = 0) {
    if (typeof regex === "string") {
        regex = new RegExp(regex);
    }

    const elements = document.getElementsByTagName(tag);
    let count = 0;

    for (let i = 0; i < elements.length; i++) {
        if (regex.test(elements[i].textContent)) {
            if (count === item) {
                return elements[i];
            }
            count++;
        }
    }

    return null;
}

function requestGM_XHR(details) {
    return new Promise((resolve, reject) => {
        details.onload = function (response) {
            resolve(response);
        };
        details.onerror = function (response) {
            reject(response);
        };
        details.ontimeout = function () {
            reject(new Error("Request timed out"));
        };
        GM_xmlhttpRequest(details);
    });
}

// ---------------------------------------------------------
// Configuration menu
// ---------------------------------------------------------

// Register menu command with current state
function addMenuCommand() {
    const currentState = GM_getValue("includeDN", true);
    const menuText = `Add Name to Magnet Links ${currentState ? "✔️" : "❌"}`;

    GM_registerMenuCommand(menuText, toggleIncludeDN);
}

// Toggle function
function showToast(message) {
    const toast = document.createElement("div");
    toast.textContent = message;
    toast.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: #323232;
        color: white;
        padding: 15px 20px;
        border-radius: 5px;
        box-shadow: 0 2px 5px rgba(0,0,0,0.3);
        z-index: 10000;
        font-family: Arial, sans-serif;
        font-size: 14px;
        opacity: 0;
        transition: opacity 0.3s ease-in-out;
    `;

    document.body.appendChild(toast);

    // Trigger fade-in animation
    setTimeout(() => {
        toast.style.opacity = "1";
    }, 10);

    // Fade out and remove
    setTimeout(() => {
        toast.style.opacity = "0";
        setTimeout(() => toast.remove(), 300);
    }, 1500);
}

function toggleIncludeDN() {
    const currentState = GM_getValue("includeDN", true);
    const newState = !currentState;
    GM_setValue("includeDN", newState);

    showToast(`Add Name to Magnet Links: ${newState ? "ON" : "OFF"}`);

    // Reload page to apply changes
    setTimeout(() => {
        location.reload();
    }, 2000);
}

// ---------------------------------------------------------
// search results handling
// ---------------------------------------------------------

function observeSearchResultsChange() {
    const observer = new MutationObserver(() => {
        observer.disconnect();
        setTimeout(() => {
            // console.debug("Search results changed, processing links...");
            processLinksInSearchResults().then(() => {
                observeSearchResultsChange();
            });
        }, 100);
    });

    observer.observe(document, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ["class"],
    });
}

function observeNewSearchResults() {
    const observer = new MutationObserver((mutations) => {
        let hasNewResults = false;

        for (const mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                const newSearchResultWithSize = Array.from(mutation.addedNodes).some((node) => {
                    if (node.querySelector) {
                        // Returns boolean value indicating whether the element with class "cpill" exists in `node`
                        return !!node.querySelector(".cpill");
                    }
                    return false;
                });

                if (newSearchResultWithSize) {
                    hasNewResults = true;
                    break;
                }
            }
        }

        if (hasNewResults) {
            itemFilterBySize();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        // attributes: false, // Ignore style changes from itemFilterBySize()
        characterData: false, // Ignore text changes from processLinksInSearchResults()
    });
}

async function processLinksInSearchResults() {
    const links = Array.from(getSearchResultLinks());
    const promises = links.map(async (link) => {
        if (hostname === bt4gURL) {
            await processLinksInSearchResultsBt4g(link);
        } else if (hostname === limetorrentsURL) {
            processLinksInSearchResultsLimeTorrents(link);
        }
    });

    await Promise.all(promises);

    if (hostname.includes("bt1207")) {
        for (let link of links) {
            await processLinksInSearchResultsBt1207(link);
            // await new Promise((resolve) => setTimeout(resolve, 100));

            // add magnets on hover
            link.addEventListener("mouseover", async function () {
                const magnetLink = link.getAttribute("data-magnet-added");
                if (magnetLink !== "true") {
                    try {
                        // Try to process the link
                        await processLinksInSearchResultsBt1207(link);

                        // Only set the attribute if no error occurs
                        // link.setAttribute("data-magnet-added", "true");
                    } catch (error) {
                        console.error("Error processing link:", error);
                        // Handle error (e.g., log it, but don't set data-magnet-added)
                    }
                }
            });
        }
    }

    // Add amount of visible magnet links into text
    const amountVisibleMagnets = links.length;
    const magnetLinkAllSpan = document.querySelector(".magnet-link-all-span");
    if (links && typeof links.length === "number" && magnetLinkAllSpan) {
        magnetLinkAllSpan.innerHTML = `Open all <span class="badge bg-primary">${amountVisibleMagnets}</span> loaded magnet links`;
    }

    // Remove spam elements
    setTimeout(() => {
        links.forEach((link) => {
            const title = link.title;
            if (title.includes("Downloader.exe") || title.includes("Downloader.dmg")) {
                link.parentElement.parentElement.remove();
            }
        });
    }, 100);
}

function getSearchResultLinks() {
    if (hostname === bt4gURL) {
        const elements = document.querySelectorAll('a[href*="/magnet/"]:not([href^="magnet:"])');

        // Filter and return only the visible elements (those without 'display: none' in their parent chain)
        return Array.from(elements).filter((element) => {
            let current = element;
            while (current) {
                if (window.getComputedStyle(current).display === "none") {
                    return false;
                }
                current = current.parentElement;
            }
            return true;
        });
    } else if (hostname === limetorrentsURL) {
        return document.querySelectorAll('a[href*="//itorrents.net/torrent/"]');
    } else if (hostname.includes("bt1207")) {
        return document.querySelectorAll("body > div.container-fluid > div:nth-child(6) > div.col-md-6 > ul > li:nth-child(1) > a");
    }
}

async function processLinksInSearchResultsBt4g(link) {
    try {
        // Skip if magnet link exists
        const magnetLink = link.getAttribute("data-magnet-added");
        if (magnetLink === "true") return;

        const details = {
            method: "GET",
            url: link.href,
            timeout: 5000,
        };

        const response = await requestGM_XHR(details);
        const html = response.responseText;

        // Find magnet links
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, "text/html");

        const downloadLink = doc.querySelector('a[href^="//downloadtorrentfile.com/hash/"]');
        if (downloadLink) {
            const hash = extractHashFromUrl(downloadLink.href.split("/").pop().split("?")[0]);
            const torrentName = link?.textContent;
            if (hash) {
                insertMagnetLink(link, hash, torrentName);
                link.setAttribute("data-magnet-added", "true");
            }
        }
    } catch (error) {
        console.error("Error getting magnet link:", error);
    }
}

async function processLinksInSearchResultsBt1207(link) {
    try {
        const details = {
            method: "GET",
            url: link.href,
            timeout: 3000,
            headers: {
                Referer: document.referrer,
                Cookie: document.cookie,
                "User-Agent": navigator.userAgent,
                Referer: window.location.href,
                Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
                "Accept-Language": navigator.language || navigator.userLanguage,
                "Accept-Encoding": "gzip, deflate, br",
                Connection: "keep-alive",
            },
        };

        const response = await requestGM_XHR(details);
        const html = response.responseText;

        // Find magnet links
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, "text/html");

        // Skip if magnet link exists
        const magnetLink = link.getAttribute("data-magnet-added");
        if (magnetLink === "true") return;

        const downloadLink = doc.querySelector("#magnet");
        if (downloadLink) {
            const hash = extractHashFromUrl(downloadLink.href);
            const torrentName = link?.textContent;
            if (hash) {
                insertMagnetLink(link, hash, torrentName);
                link.setAttribute("data-magnet-added", "true");
            }
        }
    } catch (error) {
        console.error("Error getting magnet link:", error);
    }
}

function processLinksInSearchResultsLimeTorrents(link) {
    console.log("link: ", link);
    // Skip if magnet link exists
    const magnetLink = link.getAttribute("data-magnet-added");
    if (magnetLink === "true") return;

    const hash = extractHashFromUrl(link.href.split("/").pop().split("?")[0]);
    const torrentName = link?.nextElementSibling?.textContent;
    if (hash) {
        insertMagnetLink(link, hash, torrentName);
        link.setAttribute("data-magnet-added", "true");
        // Hide unnecessary element
        link.style.display = "none";
    }
}

function insertMagnetLink(link, hash, torrentName = "") {
    // Get configuration value to determine if display name should be included
    const includeDN = GM_getValue("includeDN", true);
    let magnetLink = `magnet:?xt=urn:btih:${hash}`;

    // Add display name parameter if enabled and torrentName is provided
    if (includeDN && torrentName) {
        const encodedName = encodeURIComponent(torrentName);
        magnetLink += `&dn=${encodedName}`;
    }

    const newLink = document.createElement("a");
    newLink.classList.add("magnet-link");
    newLink.href = magnetLink;
    newLink.addEventListener("click", function () {
        imgElement.style.filter = "grayscale(100%) opacity(0.5)";
    });

    const imgElement = document.createElement("img");
    imgElement.src = magnetImage;
    imgElement.classList.add("magnet-link-img");

    // Create a text node after the image
    const textNode = document.createTextNode("Magnet Link");

    // Append image first, then text
    newLink.appendChild(imgElement);
    newLink.appendChild(textNode);

    link.parentNode.insertBefore(newLink, link);
}

function extractHashFromUrl(href) {
    const hashRegex = /(^|\/|&|-|\.|\?|=|:)([a-fA-F0-9]{40})/;
    const matches = href.match(hashRegex);
    return matches ? matches[2] : null;
}

// ---------------------------------------------------------

function addClickAllMagnetLinks() {
    // only needed if document-start
    // const openAllMagnetLinks = document.querySelector(".magnet-link-all-span");
    // if (openAllMagnetLinks) {
    //     return;
    // }

    // no elements found
    if (
        document.querySelector("body > main > p")?.textContent.includes("did not match any documents") ||
        document.querySelector("#content > h2:nth-child(9)")
    )
        return;

    const targetElement = itemsFoundElement?.parentElement?.children[3];
    if (targetElement) {
        const openAllMagnetLinksSpan = document.createElement("span");
        openAllMagnetLinksSpan.innerHTML = "Open all <span class='badge bg-primary'>0</span> loaded magnet links";
        openAllMagnetLinksSpan.classList.add("magnet-link-all-span", "lead");
        openAllMagnetLinksSpan.style.marginLeft = "10px";

        const openAllMagnetLinksImg = document.createElement("img");
        openAllMagnetLinksImg.src = magnetImage;
        openAllMagnetLinksImg.classList.add("magnet-link-img");
        openAllMagnetLinksImg.style.cssText = "cursor:pointer;vertical-align:sub;";

        targetElement.insertAdjacentElement("afterend", openAllMagnetLinksSpan);
        openAllMagnetLinksSpan.insertAdjacentElement("afterend", openAllMagnetLinksImg);

        openAllMagnetLinksImg.addEventListener("click", () => {
            const addedMagnetLinks = document.querySelectorAll("a.magnet-link");
            if (addedMagnetLinks.length > 0) {
                openAllMagnetLinksImg.style.filter = "grayscale(100%) opacity(0.5)";
                addedMagnetLinks.forEach((link, index) => {
                    // ignore hidden elements
                    if (getComputedStyle(link.parentElement.parentElement).display !== "none") {
                        setTimeout(() => {
                            link.click();
                        }, index * 100);
                    }
                });
            } else {
                openAllMagnetLinksSpan.textContent = "No magnet links found";
            }
        });

        // for a fixed position and more space, remove superfluous information
        if (hostname === bt4gURL) {
            itemsFoundElement.innerHTML = itemsFoundElement.innerHTML.replace(/(\ items)\ for\ .*/, "$1");
        } else if (hostname === limetorrentsURL) {
            itemsFoundElement.textContent = "";
        }
    }
}

// ---------------------------------------------------------
// size filter
// ---------------------------------------------------------

function itemFilterBySize() {
    if (hostname !== bt4gURL) return;

    // no elements found
    if (document.querySelector("body > main > p")?.textContent.includes("did not match any documents")) return;

    if (!document.getElementById("item-filter-styles")) {
        GM_addStyle(`
        .filter-container {
            display: inline-flex;
            align-items: center;
        }
        .filter-button {
            color: #212121;
            padding: 3px 7px;
            border: none;
            margin-right: 5px;
            margin-left: 10px;
        }
        .filter-input {
            margin-left: 5px !important;
            /* padding-left: 12px !important; */
            width: 50px !important;
            text-align: center !important;
        }
        .filter-label {
            font-weight: bold;
            margin-left: 5px;
        }
        .hidden-item {
            display: none !important;
        }
        `).setAttribute("id", "item-filter-styles");
    }

    function createFilterControl(id, text) {
        const button = document.createElement("button");
        button.id = id;
        button.className = "filter-button btn";
        button.textContent = text;

        const input = document.createElement("input");
        input.type = "number";
        input.step = "1";
        input.className = "filter-input";
        input.id = `${id}-input`;

        return { button, input };
    }

    function createFilterControls(buttonTarget) {
        const container = document.createElement("span");
        container.className = "filter-container";

        const minFilter = createFilterControl("filter-min-size-button", "Min");
        const maxFilter = createFilterControl("filter-max-size-button", "Max");

        const unitLabel = document.createElement("span");
        unitLabel.textContent = "GB";
        unitLabel.className = "filter-label";

        container.append(minFilter.button, minFilter.input, maxFilter.button, maxFilter.input, unitLabel);

        buttonTarget.parentNode.insertBefore(container, buttonTarget.nextSibling);

        return {
            minButton: minFilter.button,
            maxButton: maxFilter.button,
            minInput: minFilter.input,
            maxInput: maxFilter.input,
        };
    }

    async function setupFilter(button, input, isMinFilter) {
        const filterType = isMinFilter ? "Min" : "Max";
        let isFiltered = await GM.getValue(`is${filterType}Filtered`, false);
        let threshold = await GM.getValue(`${filterType.toLowerCase()}FilterThreshold`, isMinFilter ? 1 : 10);

        button.textContent = isFiltered ? `${filterType} filter on` : `${filterType} filter off`;
        button.style.backgroundColor = isFiltered ? "#b2dfdb" : "#dfb2b2";
        input.value = threshold;

        button.addEventListener("click", async () => {
            isFiltered = !isFiltered;
            await GM.setValue(`is${filterType}Filtered`, isFiltered);

            button.textContent = isFiltered ? `${filterType} filter on` : `${filterType} filter off`;
            button.style.backgroundColor = isFiltered ? "#b2dfdb" : "#dfb2b2";
            await applyItemFilterBySize();
        });

        input.addEventListener("input", async () => {
            threshold = parseFloat(input.value);
            await GM.setValue(`${filterType.toLowerCase()}FilterThreshold`, threshold);
            if (isFiltered) {
                await applyItemFilterBySize();
            }
        });
    }

    async function initializeFilter() {
        const buttonTarget = itemsFoundElement;
        if (!buttonTarget) return;

        const { minButton, maxButton, minInput, maxInput } = createFilterControls(buttonTarget);

        await setupFilter(minButton, minInput, true);
        await setupFilter(maxButton, maxInput, false);
        await applyItemFilterBySize();
    }

    async function applyItemFilterBySize() {
        const minFiltered = await GM.getValue("isMinFiltered", false);
        const maxFiltered = await GM.getValue("isMaxFiltered", false);
        const minThreshold = await GM.getValue("minFilterThreshold", 1);
        const maxThreshold = await GM.getValue("maxFilterThreshold", 10);

        document.querySelectorAll("b.cpill").forEach((element) => {
            const size = parseFloat(element.innerText);
            const parentElement = element.parentElement.parentElement.parentElement;

            if (parentElement && size) {
                const itemBelowGb = !element.className.includes("red-pill");
                const hideMin = minFiltered && (size < minThreshold || itemBelowGb);
                const hideMax = maxFiltered && size > maxThreshold && !itemBelowGb;

                if (hideMin || hideMax) {
                    parentElement.classList.add("hidden-item");
                } else {
                    parentElement.classList.remove("hidden-item");
                }
            }
        });
    }

    // Check if toggle buttons already exist
    const existingMinButton = document.getElementById("filter-min-size-button");
    const existingMaxButton = document.getElementById("filter-max-size-button");
    if (!existingMinButton && !existingMaxButton) {
        initializeFilter();
    } else {
        applyItemFilterBySize();
    }
}

async function main() {
    const ERROR_TITLES = ["Web server is returning an unknown error", "525: SSL handshake failed"];
    if (ERROR_TITLES.some((error) => document.title.includes(error))) {
        const RELOAD_DELAY = 5 * 60 * 1000;
        console.log("Web server error detected. Waiting 5 minutes before reloading...");
        return setTimeout(() => location.reload(), RELOAD_DELAY);
    }

    addCss();

    addMenuCommand();

    // handle search results
    if (/\/search/.test(currentPath)) {
        itemFilterBySize();
        addClickAllMagnetLinks();
        await processLinksInSearchResults();

        observeSearchResultsChange();
        observeNewSearchResults();
    } else if (/\/magnet/.test(currentPath)) {
        // BT4G only: torrent detail page
        const link = document.querySelector('a[href*="/hash/"]:not([href^="magnet:"])');
        const hash = extractHashFromUrl(link?.href || "");
        const torrentName = document.querySelector("body main h3")?.textContent;
        if (hash) {
            insertMagnetLink(link, hash, torrentName);
        }
    }
}

main();