Sleazy Fork is available in English.

Search on RDLP for F95Zone

Adds a search button to F95Zone threads for RDLP torrents

// ==UserScript==
// @name         Search on RDLP for F95Zone
// @namespace    Violentmonkey Scripts
// @match        https://f95zone.to/threads/*
// @grant        none
// @version      1.2
// @author       UglyOldLazyBastard
// @license      WTFPL http://www.wtfpl.net/faq/
// @description  Adds a search button to F95Zone threads for RDLP torrents
// @run-at       document-ready
// ==/UserScript==

(function () {
    "use strict";

    const CONFIG = {
        selectors: {
            title: "h1.p-title-value", // CSS selector to find the main title element on F95Zone
            buttonGroup: ".buttonGroup", // CSS selector to find where the new button should be added
            textNodes: Node.TEXT_NODE, // Node type constant for extracting raw text
        },
        classes: {
            button: "button--link button rpdl-button", // CSS classes for the main button element
            buttonText: "button-text", // CSS class for the button's inner text element
        },
        urls: {
            baseSearch: "https://dl.rpdl.net/torrents", // Base URL for the RDLP search page
        },
        regex: {
            bracketContent: /\[.*\]/, // Regex pattern to find and remove content inside [square brackets]
            specialChars: /[^\w\s]/g, // Regex pattern to find special characters (for cleaning text)
        },
    };

    class TextProcessor {
        /**
         * Remove special characters while preserving letters and spaces
         * @param {string} str - Input string to process
         * @returns {string} - Processed string without special characters
         */
        static removeSpecialCharacters(str) {
            if (typeof str !== "string") {
                throw new TypeError("Input must be a string");
            }

            // Remove characters that are the same in upper and lower case (special chars)
            // and preserve only alphanumeric characters and spaces
            return str
                .split("")
                .filter((char) => {
                    const lower = char.toLowerCase();
                    const upper = char.toUpperCase();
                    return lower !== upper || char.trim() === "";
                })
                .join("");
        }

        /**
         * Extract text content from title element
         * @param {Element} titleElement - The title element to extract text from
         * @returns {string} - Cleaned text content
         */
        static extractTitleText(titleElement) {
            if (!titleElement) {
                throw new Error("Title element is required");
            }

            const textNodes = Array.from(titleElement.childNodes)
                .filter((node) => node.nodeType === CONFIG.selectors.textNodes)
                .map((node) => node.textContent.trim());

            return textNodes.join(" ").trim();
        }

        /**
         * Clean and process the search text
         * @param {string} rawText - Raw text to process
         * @returns {string} - Processed search text
         */
        static processSearchText(rawText) {
            if (typeof rawText !== "string") {
                throw new TypeError("Raw text must be a string");
            }

            // Remove content in brackets and clean special characters
            let cleaned = rawText
                .replace(CONFIG.regex.bracketContent, "")
                .trim();
            cleaned = TextProcessor.removeSpecialCharacters(cleaned);

            return cleaned.trim();
        }
    }

    class RDLPButtonCreator {
        /**
         * Create the RDLP search button
         * @param {string} searchText - Text to use in the search query
         * @returns {HTMLAnchorElement} - Created button element
         */
        static createButton(searchText) {
            if (typeof searchText !== "string") {
                throw new TypeError("Search text must be a string");
            }

            const button = document.createElement("a");
            button.href = `${
                CONFIG.urls.baseSearch
            }?search=${encodeURIComponent(searchText)}`;
            button.className = CONFIG.classes.button;
            button.target = "_blank"; // Open in new tab
            button.rel = "noopener noreferrer"; // Security & privacy tags for new tab
            button.title = `Search "${searchText}" on RDLP`;

            const buttonText = document.createElement("span");
            buttonText.className = CONFIG.classes.buttonText;
            buttonText.textContent = "Search on RDLP";

            button.appendChild(buttonText);
            return button;
        }

        /**
         * Add the RDLP search button to the page
         * @param {string} searchText - Text to use in the search query
         */
        static addButton(searchText) {
            const buttonGroup = document.querySelector(
                CONFIG.selectors.buttonGroup
            );
            if (!buttonGroup) {
                console.warn("Button group not found, RDLP button not added");
                return;
            }

            const existingButton = buttonGroup.querySelector(
                `a[href*="${CONFIG.urls.baseSearch}"]`
            );
            if (existingButton) {
                console.info("RDLP button already exists, skipping creation");
                return;
            }

            const newButton = RDLPButtonCreator.createButton(searchText);
            buttonGroup.appendChild(newButton);
        }
    }

    class F95ZoneRDLPIntegration {
        /**
         * Main method to run the script
         */
        static run() {
            try {
                const titleElement = document.querySelector(
                    CONFIG.selectors.title
                );
                if (!titleElement) {
                    console.warn("Title element not found");
                    return;
                }

                const rawText = TextProcessor.extractTitleText(titleElement);
                if (!rawText) {
                    console.warn("No text content found in title element");
                    return;
                }

                const searchText = TextProcessor.processSearchText(rawText);
                if (!searchText) {
                    console.warn("No valid search text found after processing");
                    return;
                }

                RDLPButtonCreator.addButton(searchText);
            } catch (error) {
                console.error("Error in F95Zone RDLP integration:", error);
            }
        }

        /**
         * Initialize the script with proper timing
         */
        static initialize() {
            // Check if DOM is already ready
            if (document.readyState === "loading") {
                document.addEventListener("DOMContentLoaded", () => {
                    F95ZoneRDLPIntegration.run();
                });
            } else {
                // DOM is already ready
                F95ZoneRDLPIntegration.run();
            }
        }
    }

    // Initialize the script
    F95ZoneRDLPIntegration.initialize();
})();