Dynamic Include Sites Script (Protocol-Independent)

Run on default & user-defined sites using wildcard patterns (ignores protocols), with full management features.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/526770/1541997/Dynamic%20Include%20Sites%20Script%20%28Protocol-Independent%29.js

// ==UserScript==
// @name         Site Filter (Protocol-Independent)
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Manage allowed sites dynamically and reference this in other scripts.
// @author       blvdmd
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const USE_EMOJI_FOR_STATUS = true; // Configurable flag to use emoji for true/false status
    const SHOW_STATUS_ONLY_IF_TRUE = true; // Configurable flag to show status only if any value is true

    // ✅ Wait for `SCRIPT_STORAGE_KEY` to be set
    function waitForScriptStorageKey(maxWait = 1000) {
        return new Promise(resolve => {
            const startTime = Date.now();
            const interval = setInterval(() => {
                if (typeof window.SCRIPT_STORAGE_KEY !== 'undefined') {
                    clearInterval(interval);
                    resolve(window.SCRIPT_STORAGE_KEY);
                } else if (Date.now() - startTime > maxWait) {
                    clearInterval(interval);
                    console.error("🚨 SCRIPT_STORAGE_KEY is not set! Make sure your script sets it **before** @require.");
                    resolve(null);
                }
            }, 50);
        });
    }

    (async function initialize() {
        async function waitForDocumentReady() {
            if (document.readyState === "complete") return;
            return new Promise(resolve => {
                window.addEventListener("load", resolve, { once: true });
            });
        }
        // ✅ Wait for the script storage key
        const key = await waitForScriptStorageKey();
        if (!key) return;

        // ✅ Ensure the document is fully loaded before setting `shouldRunOnThisSite`
        await waitForDocumentReady();

        const STORAGE_KEY = `additionalSites_${key}`;

        function getDefaultList() {
            return typeof window.GET_DEFAULT_LIST === "function" ? window.GET_DEFAULT_LIST() : [];
        }

        function normalizeUrl(url) {
            if (typeof url !== 'string') {
                url = String(url);
            }
            return url.replace(/^https?:\/\//, '');
        }

        let additionalSites = GM_getValue(STORAGE_KEY, []);

        let mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
            if (typeof item === 'string') {
                return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
            }
            return { ...item, pattern: normalizeUrl(item.pattern) };
        });

        GM_registerMenuCommand("➕ Add Current Site to Include List", addCurrentSiteMenu);
        GM_registerMenuCommand("📜 View Included Sites", viewIncludedSites);
        GM_registerMenuCommand("🗑️ Delete Specific Entries", deleteEntries);
        GM_registerMenuCommand("✏️ Edit an Entry", editEntry);
        GM_registerMenuCommand("🚨 Clear All Entries", clearAllEntries);
        GM_registerMenuCommand("📤 Export Site List as JSON", exportAdditionalSites);
        GM_registerMenuCommand("📥 Import Site List from JSON", importAdditionalSites);

        async function shouldRunOnThisSite() {
            const currentFullPath = normalizeUrl(`${window.location.href}`);
            return mergedSites.some(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
        }

        function wildcardToRegex(pattern) {
            return new RegExp("^" + pattern
                    .replace(/[-[\]{}()+^$|#\s]/g, '\\$&')
                    .replace(/\./g, '\\.')
                    .replace(/\?/g, '\\?')
                    .replace(/\*/g, '.*')
                + "$");
        }

        function addCurrentSiteMenu() {
            const currentHost = window.location.hostname;
            const currentPath = window.location.pathname;
            const domainParts = currentHost.split('.');
            const baseDomain = domainParts.length > 2 ? domainParts.slice(-2).join('.') : domainParts.join('.');
            const secondLevelDomain = domainParts.length > 2 ? domainParts.slice(-2, -1)[0] : domainParts[0];

            const options = [
                { name: `Preferred Domain Match (*${secondLevelDomain}.*)`, pattern: `*${secondLevelDomain}.*` },
                { name: `Base Hostname (*.${baseDomain}*)`, pattern: `*.${baseDomain}*` },
                { name: `Base Domain (*.${secondLevelDomain}.*)`, pattern: `*.${secondLevelDomain}.*` },
                { name: `Host Contains (*${secondLevelDomain}*)`, pattern: `*${secondLevelDomain}*` },
                { name: `Exact Path (${currentHost}${currentPath})`, pattern: normalizeUrl(`${window.location.href}`) },
                { name: "Custom Wildcard Pattern", pattern: normalizeUrl(`${window.location.href}`) }
            ];

            const userChoice = prompt(
                "Select an option to add the site:\n" +
                options.map((opt, index) => `${index + 1}. ${opt.name}`).join("\n") +
                "\nEnter a number or cancel."
            );

            if (!userChoice) return;
            const selectedIndex = parseInt(userChoice, 10) - 1;
            if (selectedIndex >= 0 && selectedIndex < options.length) {
                let pattern = normalizeUrl(options[selectedIndex].pattern);
                if (options[selectedIndex].name === "Custom Wildcard Pattern") {
                    pattern = normalizeUrl(prompt("Edit custom wildcard pattern:", pattern));
                    if (!pattern.trim()) return alert("Invalid pattern. Operation canceled.");
                }

                const preProcessingRequired = prompt("Is pre-processing required? (y/n)", "n").toLowerCase() === 'y';
                const postProcessingRequired = prompt("Is post-processing required? (y/n)", "n").toLowerCase() === 'y';

                const entry = { pattern, preProcessingRequired, postProcessingRequired };

                if (!additionalSites.some(item => item.pattern === pattern)) {
                    additionalSites.push(entry);
                    GM_setValue(STORAGE_KEY, additionalSites);
                    mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                        if (typeof item === 'string') {
                            return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                        }
                        return { ...item, pattern: normalizeUrl(item.pattern) };
                    });
                    alert(`✅ Added site with pattern: ${pattern}`);
                } else {
                    alert(`⚠️ Pattern "${pattern}" is already in the list.`);
                }
            }
        }

        function viewIncludedSites() {
            const siteList = additionalSites.map(item => {
                const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired);
                return `${item.pattern}${status ? ` (${status})` : ''}`;
            }).join("\n");
            alert(`🔍 Included Sites:\n${siteList || "No sites added yet."}`);
        }

        function deleteEntries() {
            if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to delete.");
            const userChoice = prompt("Select entries to delete (comma-separated numbers):\n" +
                additionalSites.map((item, index) => `${index + 1}. ${item.pattern}`).join("\n"));
            if (!userChoice) return;
            const indicesToRemove = userChoice.split(',').map(num => parseInt(num.trim(), 10) - 1);
            additionalSites = additionalSites.filter((_, index) => !indicesToRemove.includes(index));
            GM_setValue(STORAGE_KEY, additionalSites);
            mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                if (typeof item === 'string') {
                    return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                }
                return { ...item, pattern: normalizeUrl(item.pattern) };
            });
            alert("✅ Selected entries have been deleted.");
        }

        function editEntry() {
            if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to edit.");
            const userChoice = prompt("Select an entry to edit:\n" +
                additionalSites.map((item, index) => {
                    const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired);
                    return `${index + 1}. ${item.pattern}${status ? ` (${status})` : ''}`;
                }).join("\n"));
            if (!userChoice) return;
            const selectedIndex = parseInt(userChoice, 10) - 1;
            if (selectedIndex < 0 || selectedIndex >= additionalSites.length) return alert("❌ Invalid selection.");
            const entry = additionalSites[selectedIndex];
            const newPattern = normalizeUrl(prompt("Edit the pattern:", entry.pattern));
            if (!newPattern || !newPattern.trim()) return;

            const preProcessingRequired = prompt("Is pre-processing required? (y/n)", entry.preProcessingRequired ? "y" : "n").toLowerCase() === 'y';
            const postProcessingRequired = prompt("Is post-processing required? (y/n)", entry.postProcessingRequired ? "y" : "n").toLowerCase() === 'y';

            entry.pattern = newPattern.trim();
            entry.preProcessingRequired = preProcessingRequired;
            entry.postProcessingRequired = postProcessingRequired;
            GM_setValue(STORAGE_KEY, additionalSites);
            mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                if (typeof item === 'string') {
                    return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                }
                return { ...item, pattern: normalizeUrl(item.pattern) };
            });
            alert("✅ Entry updated.");
        }

        function clearAllEntries() {
            if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to clear.");
            if (confirm(`🚨 You have ${additionalSites.length} entries. Clear all?`)) {
                additionalSites = [];
                GM_setValue(STORAGE_KEY, additionalSites);
                mergedSites = [...getDefaultList()].map(item => {
                    if (typeof item === 'string') {
                        return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                    }
                    return { ...item, pattern: normalizeUrl(item.pattern) };
                });
                alert("✅ All user-defined entries cleared.");
            }
        }

        // function exportAdditionalSites() {
        //     GM_download("data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(additionalSites, null, 2)), "additionalSites_backup.json");
        //     alert("📤 Additional sites exported as JSON.");
        // }

        function exportAdditionalSites() {
            const data = JSON.stringify(additionalSites, null, 2);
            const blob = new Blob([data], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'additionalSites_backup.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            alert("📤 Additional sites exported as JSON.");
        }

        // function importAdditionalSites() {
        //     const input = document.createElement("input");
        //     input.type = "file";
        //     input.accept = ".json";
        //     input.onchange = event => {
        //         const reader = new FileReader();
        //         reader.onload = e => {
        //             additionalSites = JSON.parse(e.target.result);
        //             GM_setValue(STORAGE_KEY, additionalSites);
        //             mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
        //                 if (typeof item === 'string') {
        //                     return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
        //                 }
        //                 return { ...item, pattern: normalizeUrl(item.pattern) };
        //             });
        //             alert("📥 Sites imported successfully.");
        //         };
        //         reader.readAsText(event.target.files[0]);
        //     };
        //     input.click();
        // }

        function importAdditionalSites() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json';
    input.style.display = 'none';
    input.onchange = event => {
        const reader = new FileReader();
        reader.onload = e => {
            try {
                const importedData = JSON.parse(e.target.result);
                if (Array.isArray(importedData)) {
                    additionalSites = importedData.map(item => {
                        if (typeof item === 'string') {
                            return normalizeUrl(item);
                        } else if (typeof item === 'object' && item.pattern) {
                            return { ...item, pattern: normalizeUrl(item.pattern) };
                        }
                        throw new Error('Invalid data format');
                    });
                    GM_setValue(STORAGE_KEY, additionalSites);
                    mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                        if (typeof item === 'string') {
                            return normalizeUrl(item);
                        }
                        return { ...item, pattern: normalizeUrl(item.pattern) };
                    });
                    alert('📥 Sites imported successfully.');
                } else {
                    throw new Error('Invalid data format');
                }
            } catch (error) {
                alert('❌ Failed to import sites: ' + error.message);
            }
        };
        reader.readAsText(event.target.files[0]);
    };
    document.body.appendChild(input);
    input.click();
    document.body.removeChild(input);
}

        function formatStatus(preProcessingRequired, postProcessingRequired) {
            if (SHOW_STATUS_ONLY_IF_TRUE && !preProcessingRequired && !postProcessingRequired) {
                return '';
            }
            const preStatus = USE_EMOJI_FOR_STATUS ? (preProcessingRequired ? '✅' : '✖️') : (preProcessingRequired ? 'true' : 'false');
            const postStatus = USE_EMOJI_FOR_STATUS ? (postProcessingRequired ? '✅' : '✖️') : (postProcessingRequired ? 'true' : 'false');
            return `Pre: ${preStatus}, Post: ${postStatus}`;
        }

        window.shouldRunOnThisSite = shouldRunOnThisSite;
        window.isPreProcessingRequired = function() {
            const currentFullPath = normalizeUrl(`${window.location.href}`);
            const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
            return entry ? entry.preProcessingRequired : false;
        };
        window.isPostProcessingRequired = function() {
            const currentFullPath = normalizeUrl(`${window.location.href}`);
            const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
            return entry ? entry.postProcessingRequired : false;
        };
    })();
})();

//To use this in another script use @require

// // @run-at       document-end
// // ==/UserScript==

// window.SCRIPT_STORAGE_KEY = "magnetLinkHashChecker"; // UNIQUE STORAGE KEY


// window.GET_DEFAULT_LIST = function() {
//     return [
//         { pattern: "*1337x.*", preProcessingRequired: false, postProcessingRequired: false },
//         { pattern: "*yts.*", preProcessingRequired: true, postProcessingRequired: true },
//         { pattern: "*torrentgalaxy.*", preProcessingRequired: false, postProcessingRequired: true },
//         { pattern: "*bitsearch.*", preProcessingRequired: false, postProcessingRequired: false },
//         { pattern: "*thepiratebay.*", preProcessingRequired: false, postProcessingRequired: false },
//         { pattern: "*ext.*", preProcessingRequired: false, postProcessingRequired: false }
//     ];
// };


// (async function () {
//     'use strict';

//     // ✅ Wait until `shouldRunOnThisSite` is available
//     while (typeof shouldRunOnThisSite === 'undefined') {
//         await new Promise(resolve => setTimeout(resolve, 50));
//     }

//     if (!(await shouldRunOnThisSite())) return;
//     //alert("running");

//     console.log("Pre-Customization enabled for this site: " + isPreProcessingRequired() );
//     console.log("Post-Customization enabled for this site: " + isPostProcessingRequired() );

//     const OFFCLOUD_CACHE_API_URL = 'https://offcloud.com/api/cache';