Debrid Download Helper (Multi-Provider)

Download files with a single click without visiting the debrid providers' website.

// ==UserScript==
// @name         Debrid Download Helper (Multi-Provider)
// @namespace    http://tampermonkey.net/
// @version      1.13
// @description  Download files with a single click without visiting the debrid providers' website.
// @author       Superflyin
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @connect      api.real-debrid.com
// @connect      api.alldebrid.com
// @connect      www.premiumize.me
// @connect      api.linksnappy.com
// @connect      api.torbox.io
// ==/UserScript==

(function($) {
    'use strict';
    console.log('Debrid Download Helper: Script started! (v26.5)');

    // IMPORTANT: Ensure the script only runs in the top-level window (not in iframes)
    if (window.top !== window.self) {
        console.log('Debrid Download Helper: Script detected running in an iframe. Exiting.');
        return; // Exit if we are in an iframe
    }

    // --- Provider configuration ---
    const providers = {
        "real-debrid": {
            name: "Real-Debrid",
            icon: "https://i.ibb.co/6RW9YRnw/realdebrid-116047.webp", // Updated icon URL
            apiUrl: "https://api.real-debrid.com/rest/1.0/unrestrict/link",
            apiMagnetUrl: "https://api.real-debrid.com/rest/1.0/torrents/addMagnet",
            method: "POST",
            headers: apiKey => ({
                'Authorization': `Bearer ${apiKey}`,
                'Content-Type': 'application/x-www-form-urlencoded'
            }),
            data: url => `link=${encodeURIComponent(url)}`,
            magnetData: magnetUrl => `magnet=${encodeURIComponent(magnetUrl)}`,
            parseResponse: resp => { // For direct links
                try { return JSON.parse(resp).download || null; } catch { return null; }
            },
            parseMagnetResponse: resp => { // For magnet links
                try {
                    const data = JSON.parse(resp);
                    return data && data.id ? true : false; // Return true on successful ID, then user goes to torrents page
                } catch (e) {
                    return false;
                }
            }
        },
        "alldebrid": {
            name: "AllDebrid",
            icon: "https://addons.mozilla.org/user-media/addon_icons/662/662954-64.png?modified=1590413336",
            apiUrl: "https://api.alldebrid.com/v4/link/unlock",
            apiMagnetUrl: "https://api.alldebrid.com/v4/magnet/upload",
            method: "GET", // AllDebrid link unlock uses GET
            headers: () => ({}), // No custom headers for AllDebrid GET
            buildUrl: (apiKey, url) => `https://api.alldebrid.com/v4/link/unlock?agent=userscript&apikey=${apiKey}&link=${encodeURIComponent(url)}`,
            parseResponse: resp => {
                try { let j = JSON.parse(resp); return (j.data && j.data.link) || null; } catch { return null; }
            }
        },
        "premiumize": {
            name: "Premiumize",
            icon: "https://www.premiumize.me/favicon.ico",
            apiUrl: "https://www.premiumize.me/api/unrestrict/link",
            method: "GET",
            headers: () => ({}),
            buildUrl: (apiKey, url) => `https://www.premiumize.me/api/unrestrict/link?link=${encodeURIComponent(url)}&auth=${apiKey}`,
            parseResponse: resp => {
                try { let j = JSON.parse(resp); return (j.status === "success" && j.download) || null; } catch { return null; }
            }
        },
        "linksnappy": {
            name: "LinkSnappy",
            icon: "https://lh3.googleusercontent.com/pCVuXSx1pjsSR5Kzh98zJmRKiB1e1b_p7uKJ7s-5YQ8lpnO7SHtYksHcGpuvoxX4j5ZyVaF31URB3sbHMteygmyF=s120",
            apiUrl: "https://api.linksnappy.com/file/unlock",
            method: "POST",
            headers: apiKey => ({
                'Authorization': `Bearer ${apiKey}`,
                'Content-Type': 'application/x-www-form-urlencoded'
            }),
            data: url => `url=${encodeURIComponent(url)}`,
            parseResponse: resp => {
                try { let j = JSON.parse(resp); return (j.result && j.result.url) || null; } catch { return null; }
            }
        },
        "torbox": {
            name: "TorBox",
            icon: "https://downloads.intercomcdn.com/i/o/570645/98aded70c10f217bd63035f7/80582a6c13665edcd8ddb65505b883fb.png", // Updated TorBox icon link
            apiUrl: "https://api.torbox.io/v1/unrestrict",
            method: "POST",
            headers: apiKey => ({
                'Authorization': `Bearer ${apiKey}`,
                'Content-Type': 'application/x-www-form-urlencoded'
            }),
            data: url => `link=${encodeURIComponent(url)}`,
            parseResponse: resp => {
                try { return JSON.parse(resp).download || null; } catch { return null; }
            }
        }
    };

    // --- Supported hosts and file link patterns (for direct links only) ---
    const supportedPatterns = [
        { host: "1fichier.com",     pattern: /^\/\?[a-zA-Z0-9]{20,}\/?$/ },
        { host: "dailyuploads.net", pattern: /^\/[a-zA-Z0-9]{12,}$/ },
        { host: "ddownload.com",    pattern: /^\/[a-zA-Z0-9]{10,}/ },
        { host: "dropbox.com",      pattern: /^\/s\/[a-zA-Z0-9]+\/.+/ },
        { host: "filefactory.com",  pattern: /^\/file\/([a-zA-Z0-9]+)/ },
        { host: "filenext.com",     pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "filespace.com",    pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "gigapeta.com",     pattern: /^\/file\/[a-zA-Z0-9]+/ },
        { host: "hitfile.net",      pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "katfile.com",      pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "katfile.cloud",    pattern: /^\/[a-zA-Z0-9]+/ },
        { host: "mediafire.com",    pattern: /^\/file\/[a-zA-Z0-9_]+\/.+/ },
        { host: "mega.nz",          pattern: /^\/file\/[a-zA-Z0-9!#]+/ },
        { host: "nitroflare.com",   pattern: /^\/view\/([A-Za-z0-9]+)/ },
        { host: "prefiles.com",     pattern: /^\/f\/[a-z0-9]+\/?/ },
        { host: "rapidgator.net",   pattern: /^\/file\/[a-fA-F0-9]{32}/ },
        { host: "rg.to",            pattern: /^\/file\/[a-fA-F0-9]{32}/ },
        { host: "turbobit.net",     pattern: /([a-z0-9]{15,})\.html$/ },
        { host: "uploady.io",       pattern: /^\/[A-Za-z0-9]{12,}/ },
        { host: "userscloud.com",   pattern: /^\/[a-z0-9]{12,}$/ },
        { host: "usersdrive.com",   pattern: /^\/file\/[a-z0-9]+/ },
        { host: "userupload.net",   pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "vidoza.net",       pattern: /^\/[a-z0-9]{12,}/ },
        { host: "vipfile.com",      pattern: /^\/d\/[a-z0-9]+\/?/ },
        { host: "worldbytez.com",   pattern: /^\/[a-z0-9]+\.html$/ },
        { host: "wupfile.com",      pattern: /^\/file\/[a-z0-9]+/ },
        { host: "4shared.com",      pattern: /^\/file\/.+/ },
        { host: "alfafile.net",     pattern: /^\/file\/.+/ },
        { host: "apkadmin.com",     pattern: /^\/file\/.+/ },
        { host: "clicknupload.cc",  pattern: /^\/file\/.+/ },
        { host: "cloudvideo.tv",    pattern: /^\/[a-zA-Z0-9]{12,}$/ },
        { host: "drop.download",    pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "dropgalaxy.com",   pattern: /^\/file\/.+/ },
        { host: "exload.pro",       pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "fastbit.cc",       pattern: /^\/file\/.+/ },
        { host: "fikper.com",       pattern: /^\/file\/.+/ },
        { host: "file.al",          pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "filedot.xyz",      pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "filerio.in",       pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "filestore.to",     pattern: /^\/file\/.+/ },
        { host: "filextras.com",    pattern: /^\/file\/.+/ },
        { host: "filezip.cc",       pattern: /^\/file\/.+/ },
        { host: "flashbit.cc",      pattern: /^\/file\/.+/ },
        { host: "hexupload.net",    pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "hot4share.com",    pattern: /^\/file\/.+/ },
        { host: "indishare.org",    pattern: /^\/file\/.+/ },
        { host: "isra.cloud",       pattern: /^\/file\/.+/ },
        { host: "mexashare.com",    pattern: /^\/file\/.+/ },
        { host: "mixdrop.ag",       pattern: /^\/f\/.+/ },
        { host: "modsbase.com",     pattern: /^\/download\/.+/ },
        { host: "mp4upload.com",    pattern: /^\/embed-([a-zA-Z0-9]+)\.html/ },
        { host: "send.cm",          pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "sendit.cloud",     pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "sendspace.com",    pattern: /^\/file\/.+/ },
        { host: "sharemods.com",    pattern: /^\/download\/.+/ },
        { host: "simfileshare.net", pattern: /^\/download\/.+/ },
        { host: "terabytez.net",    pattern: /^\/file\/.+/ },
        { host: "upload42.com",     pattern: /^\/file\/.+/ },
        { host: "uploadbank.com",   pattern: /^\/file\/.+/ },
        { host: "uploadbox.com",    pattern: /^\/file\/.+/ },
        { host: "uploadboy.com",   pattern: /^\/file\/.+/ },
        { host: "uploadev.org",     pattern: /^\/[a-zA-Z0-9]{12,}/ },
        { host: "uploadrar.com",    pattern: /^\/file\/.+/ },
        { host: "voe.sx",           pattern: /^\/e\/.+/ },
        { host: "wayupload.com",    pattern: /^\/file\/.+/ },
        { host: "wipfiles.net",     pattern: /^\/file\/.+/ }
    ];

    // Function to check if a URL is a magnet link OR a supported direct file link
    function isSupportedLink(url) {
        if (url.startsWith('magnet:?')) {
            return true;
        }
        try {
            const parsedUrl = new URL(url);
            const host = parsedUrl.hostname.replace(/^www\./, '');
            const fullPathForMatch = parsedUrl.pathname + parsedUrl.search;
            const isMatch = supportedPatterns.some(h => {
                const hostMatch = h.host === host;
                const patternMatch = h.pattern.test(fullPathForMatch);
                return hostMatch && patternMatch;
            });
            return isMatch;
        } catch (e) {
            console.log(`Debrid Download Helper: Skipping invalid URL: ${url}`);
            return false; // Invalid URL
        }
    }

    // Function to check if the current page's URL matches a supported file hoster page (for top bar display)
    function isFileHosterPage(url) {
        try {
            const parsedUrl = new URL(url);
            const host = parsedUrl.hostname.replace(/^www\./, '');
            const fullPathForMatch = parsedUrl.pathname + parsedUrl.search;
            const isMatch = supportedPatterns.some(h => h.host === host && h.pattern.test(fullPathForMatch));
            console.log(`Debrid Download Helper (v${GM_info.script.version}): isFileHosterPage(${url}) -> ${isMatch}. Host: ${host}, Full path for check: ${fullPathForMatch}`);
            return isMatch;
        } catch (e) {
            console.error(`Debrid Download Helper (v${GM_info.script.version}): Error in isFileHosterPage for URL ${url}`, e);
            return false;
        }
    }

    // --- Settings ---
    let currentProvider = GM_getValue('currentProvider', 'real-debrid');
    let apiKeys = GM_getValue('debridApiKeys', {});
    let apiKey = apiKeys[currentProvider] || '';

    // --- Provider selection menu ---
    function showProviderMenu(event) {
        $('#debrid-provider-menu').remove(); // Remove any existing menu

        const $menu = $('<div id="debrid-provider-menu"></div>').css({
            position: 'fixed',
            top: (event.clientY || window.innerHeight / 2) + 'px',
            left: (event.clientX || window.innerWidth / 2) + 'px',
            background: '#222',
            border: '1px solid #555',
            padding: '8px 12px',
            borderRadius: '6px',
            zIndex: 999999, // Ensure it's on top
            display: 'flex',
            gap: '12px',
            boxShadow: '0 0 15px rgba(0,0,0,0.8)',
            color: '#eee',
            fontFamily: 'Arial, sans-serif',
            fontSize: '14px',
            userSelect: 'none'
        });

        Object.entries(providers).forEach(([key, provider]) => {
            const isSelected = (key === currentProvider);
            const $btn = $('<button></button>').css({
                background: isSelected ? '#4CAF50' : 'transparent',
                border: isSelected ? '2px solid #4CAF50' : '1px solid #555',
                cursor: 'pointer',
                padding: '6px 10px',
                borderRadius: '5px',
                display: 'flex',
                alignItems: 'center',
                gap: '8px',
                color: isSelected ? '#fff' : '#ccc',
                fontWeight: isSelected ? 'bold' : 'normal',
                transition: 'background-color 0.3s, border-color 0.3s'
            }).attr('title', provider.name);

            $btn.append($(`<img src="${provider.icon}" width="28" height="28" alt="${provider.name} icon">`).css({ flexShrink: 0 }));
            $btn.append($('<span></span>').text(provider.name));

            $btn.on('click', () => {
                currentProvider = key;
                GM_setValue('currentProvider', currentProvider);
                apiKey = apiKeys[currentProvider] || '';
                $menu.remove();
                console.log(`Debrid Download Helper: Provider switched to ${currentProvider}. Reloading page.`);
                location.reload(); // Auto page refresh implemented here
            });
            $menu.append($btn);
        });
        $('body').append($menu);

        // Close menu on outside click
        $(document).on('click.debridMenu', (e) => {
            if (!$(e.target).closest('#debrid-provider-menu').length) {
                $menu.remove();
                $(document).off('click.debridMenu'); // Deregister handler
            }
        });
    }

    // --- API Key Modal ---
    function showApiKeyModal(providerName, currentKey, callback) {
        $('#debrid-apikey-modal-overlay').remove(); // Remove existing if any

        const $overlay = $('<div id="debrid-apikey-modal-overlay"></div>').css({
            position: 'fixed',
            top: 0,
            left: 0,
            width: '100%',
            height: '100%',
            background: 'rgba(0, 0, 0, 0.7)',
            zIndex: 999998, // Below provider menu, above everything else
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
        });

        const $modal = $('<div id="debrid-apikey-modal"></div>').css({
            background: '#333',
            border: '1px solid #666',
            borderRadius: '8px',
            padding: '25px',
            boxShadow: '0 0 20px rgba(0,0,0,0.9)',
            color: '#eee',
            fontFamily: 'Arial, sans-serif',
            fontSize: '15px',
            textAlign: 'center',
            maxWidth: '400px',
            width: '90%',
            boxSizing: 'border-box'
        });

        $modal.html(`
            <h3 style="margin-top: 0; color: #4CAF50;">${providerName} API Key Required</h3>
            <p style="margin-bottom: 20px;">Please enter your API key for ${providerName}.</p>
            <input type="text" id="debrid-api-key-input" value="${currentKey || ''}" placeholder="Enter API Key here" style="
                width: calc(100% - 20px);
                padding: 10px;
                margin-bottom: 15px;
                border: 1px solid #555;
                border-radius: 4px;
                background: #444;
                color: #eee;
                font-size: 1em;
            ">
            <button id="debrid-api-key-save" style="
                background: #4CAF50;
                color: white;
                border: none;
                padding: 10px 20px;
                border-radius: 5px;
                cursor: pointer;
                font-size: 1em;
                margin-right: 10px;
                transition: background-color 0.2s;
            ">Save</button>
            <button id="debrid-api-key-cancel" style="
                background: #777;
                color: white;
                border: none;
                padding: 10px 20px;
                border-radius: 5px;
                cursor: pointer;
                font-size: 1em;
                transition: background-color 0.2s;
            ">Cancel</button>
        `);

        $('body').append($overlay.append($modal));
        $('#debrid-api-key-input').focus();

        $('#debrid-api-key-save').on('click', () => {
            const newKey = $('#debrid-api-key-input').val().trim();
            apiKeys[currentProvider] = newKey;
            GM_setValue('debridApiKeys', apiKeys);
            apiKey = newKey;
            $overlay.remove();
            if (callback) callback(true); // Indicate key was saved
        }).hover(function() { $(this).css('background-color', '#5cb85c'); }, function() { $(this).css('background-color', '#4CAF50'); });

        $('#debrid-api-key-cancel').on('click', () => {
            $overlay.remove();
            if (callback) callback(false); // Indicate user cancelled
        }).hover(function() { $(this).css('background-color', '#888'); }, function() { $(this).css('background-color', '#777'); });

        // Allow closing with Escape key
        $(document).on('keydown.debridApiModal', function(e) {
            if (e.key === "Escape") {
                $overlay.remove();
                $(document).off('keydown.debridApiModal');
                if (callback) callback(false);
            }
        });
    }


    // --- Tampermonkey menu commands ---
    GM_registerMenuCommand("Switch Debrid Provider", () => {
        showProviderMenu({ clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 });
    });
    GM_registerMenuCommand("Set Debrid API Key (current provider)", () => {
        showApiKeyModal(providers[currentProvider].name, apiKeys[currentProvider] || '');
    });


    // Create a debrid download button (icon only, transparent background, sized, placed after hosting link)
    function createDebridButton(url, $link) {
        const provider = providers[currentProvider];
        // Base width/height for all icons
        const baseIconWidth = 18;
        const baseIconHeight = 18;
        let imgHtml = `<img src="${provider.icon}" width="${baseIconWidth}" height="${baseIconHeight}" alt="${provider.name}">`;
        let inlineStyle = "";

        // Apply specific styles
        if (currentProvider === "real-debrid") {
            inlineStyle += "opacity: 0.8 !important;"; // 20% transparent (80% opacity)
        } else if (currentProvider === "alldebrid") {
            // Make AllDebrid icon a bit bigger (e.g., 20x20px)
            inlineStyle += `width: 20px !important; height: 20px !important;`;
        }

        // Apply inline style if any
        if (inlineStyle) {
            imgHtml = `<img src="${provider.icon}" width="${baseIconWidth}" height="${baseIconHeight}" alt="${provider.name}" style="${inlineStyle}">`;
        }


        const $btn = $('<button>')
            .addClass('debrid-btn')
            .attr('title', `Download with ${provider.name}`)
            .html(imgHtml) // Use the constructed imgHtml
            .on('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                // If Alt key is pressed, show provider menu instead of downloading
                if (e.altKey) {
                    showProviderMenu(e);
                    return false;
                }
                handleDebridDownload(url, $(this));
            });

        $link.after($btn);
        $link.data('debrid-btn-added', true); // Mark link as processed
        return $btn;
    }

    // Add debrid buttons next to supported links and magnet links
    function addIconsToLinks(context = document) {
        let buttonsAddedCount = 0;
        // This line is the fix: It checks if the context itself is a link,
        // otherwise it finds links within the context. This makes it compatible
        // with scripts like Linkify Plus Plus.
        const $links = $(context).is('a[href]') ? $(context) : $(context).find('a[href]');

        $links.each(function() {
            const $link = $(this);
            const href = $link.attr('href');

            // Skip if already processed, no href, or internal fragment link
            if ($link.data('debrid-btn-added') || !href || href.startsWith('#')) {
                return;
            }

            // Resolve relative URLs
            let fullUrl;
            try {
                fullUrl = new URL(href, window.location.href).href;
            } catch (e) {
                console.log(`Debrid Download Helper: Skipping invalid URL: ${href}`);
                return; // Invalid URL
            }

            if (isSupportedLink(fullUrl)) {
                createDebridButton(fullUrl, $link);
                buttonsAddedCount++;
            }
        });
    }

    // --- Add top download bar on supported file pages ---
    function addDownloadBar() {
        console.log(`Debrid Download Helper: Running addDownloadBar`);
        // Remove existing bar if any, to prevent duplicates
        $('#debrid-download-bar').remove();

        const $bar = $(`
            <div id="debrid-download-bar">
                <span>Download this file with ${providers[currentProvider].name}:</span>
                <button class="debrid-bar-action-button" title="Download current page with ${providers[currentProvider].name}">
                    <img src="${providers[currentProvider].icon}" alt="${providers[currentProvider].name}" width="22" height="22">
                </button>
            </div>
        `);

        $bar.find('.debrid-bar-action-button').on('click', function(e) {
            e.preventDefault();
            handleDebridDownload(location.href, $(this));
        });
        $('body').prepend($bar);
        console.log(`Debrid Download Helper: Bar appended to body.`);
    }

    // --- Handle the display logic based on the current URL ---
    function refreshDisplayBasedOnUrl() {
        console.log(`Debrid Download Helper: Running refreshDisplayBasedOnUrl for URL:`, location.href);
        const currentPageIsFileHoster = isFileHosterPage(location.href);

        // Remove all previous indicators to ensure clean state before re-rendering
        $('#debrid-download-bar').remove();
        $('.debrid-btn').remove();
        // Clear the data attribute from ALL links that might have been processed
        // This is crucial for re-processing elements on DOM changes or provider switch
        $('a[data-debrid-btn-added="true"]').each(function() {
            $(this).removeData('debrid-btn-added');
        });


        if (currentPageIsFileHoster) {
            // On a file hoster page, only show the top bar
            addDownloadBar();
            // Disconnect MutationObserver as it's not needed for static file pages
            observer.disconnect();
            console.log(`Debrid Download Helper: Current page is file hoster. Displaying download bar.`);
        } else {
            // On other pages, show icons next to links and observe for new content
            addIconsToLinks();
            // Use MutationObserver to add icons to dynamically loaded content
            observer.observe(document.body, { childList: true, subtree: true });
            console.log(`Debrid Download Helper: Current page is NOT file hoster. Displaying link icons.`);
        }
    }

    // --- Main Download Handler for direct links and magnets ---
    function handleDebridDownload(url, $button) {
        // --- URL CLEANING LOGIC START ---
        // Check if the provided URL needs to be cleaned up before sending to the API
        let finalUrl = url;
        try {
            const parsedUrl = new URL(url);
            const host = parsedUrl.hostname.replace(/^www\./, '');
            const fullPathForMatch = parsedUrl.pathname + parsedUrl.search;

            const matchingPatternDef = supportedPatterns.find(h => h.host === host && h.pattern.test(fullPathForMatch));

            if (matchingPatternDef) {
                const match = fullPathForMatch.match(matchingPatternDef.pattern);
                // If the pattern has a capture group (like the corrected Katfile one), it will be in match[1]
                if (match && match[1]) {
                    // Reconstruct the URL using only the essential captured part (the file ID)
                    finalUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}/${match[1]}`;
                    console.log(`Debrid Download Helper: Reconstructed clean URL for API: ${finalUrl}`);
                }
            }
        } catch (e) {
            console.error("Debrid Download Helper: Error during URL reconstruction, proceeding with original URL.", e);
        }
        // --- URL CLEANING LOGIC END ---


        // Check for API key *before* attempting download
        if (!apiKey) {
            showApiKeyModal(providers[currentProvider].name, '', (keySaved) => {
                if (keySaved && apiKey) { // If key was saved successfully, try download again
                    handleDebridDownload(finalUrl, $button); // Use the potentially cleaned finalUrl
                } else {
                    $button.prop('disabled', false); // Re-enable button if cancelled or not saved
                    $button.html($button.data('original-content')); // Restore original icon
                }
            });
            return; // Stop current download attempt
        }

        const provider = providers[currentProvider];
        $button.prop('disabled', true);
        $button.data('original-content', $button.html());
        $button.html('<span class="debrid-loader"></span>'); // This will be the spinner

        function showCheckmarkPermanent() {
            $button.html('<span class="debrid-checkmark">✔️</span>'); // Checkmark emoji
            $button.prop('disabled', true); // Keep button disabled after successful download
        }

        const isMagnet = finalUrl.startsWith('magnet:?');

        if (isMagnet) {
            // --- Magnet Link Handling ---
            if (!provider.apiMagnetUrl) {
                alert(`Magnet support is not implemented for ${provider.name} in this script version.`);
                $button.prop('disabled', false);
                $button.html($button.data('original-content'));
                return;
            }

            let magnetReqUrl, magnetReqMethod, magnetReqHeaders, magnetReqData;

            if (currentProvider === "real-debrid") {
                magnetReqUrl = provider.apiMagnetUrl;
                magnetReqMethod = provider.method; // POST
                magnetReqHeaders = provider.headers(apiKey);
                magnetReqData = provider.magnetData(finalUrl);
            } else if (currentProvider === "alldebrid") {
                magnetReqUrl = `https://api.alldebrid.com/v4/magnet/upload?agent=userscript&apikey=${apiKey}&magnet=${encodeURIComponent(finalUrl)}`;
                magnetReqMethod = "GET";
                magnetReqHeaders = {};
                magnetReqData = null;
            } else {
                alert(`Magnet support is not implemented for ${provider.name} in this script version.`);
                $button.prop('disabled', false);
                $button.html($button.data('original-content'));
                return;
            }

            GM_xmlhttpRequest({
                method: magnetReqMethod,
                url: magnetReqUrl,
                headers: magnetReqHeaders,
                data: magnetReqData,
                onload: function(response) {
                    console.log(`Debrid Download Helper: API Response for magnet link (${currentProvider}):`, response.responseText);
                    try {
                        const data = JSON.parse(response.responseText);
                        let success = false;
                        if (currentProvider === "real-debrid") {
                            if (provider.parseMagnetResponse(response.responseText)) {
                                console.log("Debrid Download Helper: Real-Debrid magnet added successfully.");
                                window.open("https://real-debrid.com/torrents", "_blank");
                                success = true;
                            } else {
                                alert("Failed to add magnet link to Real-Debrid. Check your API key and magnet link.");
                                console.error(`Debrid Download Helper: Real-Debrid magnet add failed. Response:`, response.responseText);
                            }
                        } else if (currentProvider === "alldebrid") {
                            if (data.status === "success" && data.data.magnets && data.data.magnets.length > 0) {
                                const magnet = data.data.magnets[0];
                                console.log("Debrid Download Helper: AllDebrid magnet data:", magnet);
                                if (magnet.ready && magnet.links && magnet.links.length > 0) {
                                    const downloadUrl = magnet.links[0];
                                    const a = document.createElement('a');
                                    a.href = downloadUrl;
                                    a.download = '';
                                    a.style.display = 'none';
                                    document.body.appendChild(a);
                                    a.click();
                                    setTimeout(() => document.body.removeChild(a), 1000);
                                    success = true;
                                } else if (magnet.id) {
                                    window.open(`https://alldebrid.com/magnets/?id=${magnet.id}`, "_blank");
                                    success = true;
                                } else {
                                    alert("Magnet added to AllDebrid, but not ready yet. Check your magnets page manually.");
                                }
                            } else {
                                alert("Failed to add magnet link to AllDebrid. Check your API key and magnet link.");
                                console.error(`Debrid Download Helper: AllDebrid magnet add failed. Response:`, response.responseText);
                            }
                        }
                        if (success) {
                            showCheckmarkPermanent();
                        } else {
                            $button.prop('disabled', false);
                            $button.html($button.data('original-content'));
                        }
                    } catch (e) {
                        alert(`Error parsing response from ${provider.name} for magnet link: ` + e.message);
                        $button.prop('disabled', false);
                        $button.html($button.data('original-content'));
                    }
                },
                onerror: function(response) {
                    alert(`Network error contacting ${provider.name} for magnet link.`);
                    $button.prop('disabled', false);
                    $button.html($button.data('original-content'));
                }
            });
        } else {
            // --- Direct Link Handling ---
            let reqUrl, reqOpts;
            if (provider.method === "GET") {
                reqUrl = provider.buildUrl(apiKey, finalUrl);
                reqOpts = { method: "GET", url: reqUrl, headers: provider.headers(apiKey) };
            } else { // POST method
                reqUrl = provider.apiUrl;
                reqOpts = { method: provider.method, url: reqUrl, headers: provider.headers(apiKey), data: provider.data(finalUrl) };
            }

            GM_xmlhttpRequest({
                method: reqOpts.method,
                url: reqOpts.url,
                headers: reqOpts.headers,
                data: reqOpts.data,
                onload: function(response) {
                    console.log(`Debrid Download Helper: API Response for direct link (${currentProvider}):`, response.responseText);
                    try {
                        const downloadUrl = provider.parseResponse(response.responseText);
                        console.log(`Debrid Download Helper: Parsed Download URL:`, downloadUrl);
                        if (downloadUrl) {
                            const a = document.createElement('a');
                            a.href = downloadUrl;
                            a.download = '';
                            a.style.display = 'none';
                            document.body.appendChild(a);
                            a.click();
                            setTimeout(() => document.body.removeChild(a), 1000);
                            showCheckmarkPermanent();
                        } else {
                            alert('Failed to get direct download link. Check your API key and link, or the file might be offline.');
                            console.error(`Debrid Download Helper: Failed to parse download URL from response:`, response.responseText);
                            $button.prop('disabled', false);
                            $button.html($button.data('original-content'));
                        }
                    } catch (e) {
                        alert('Error parsing response from debrid provider for direct link.');
                        console.error('Debrid Download Helper: Error during response parsing:', e, 'Response:', response.responseText);
                        $button.prop('disabled', false);
                        $button.html($button.data('original-content'));
                    }
                },
                onerror: function(response) {
                    alert('Network error contacting debrid provider for direct link.');
                    console.error('Debrid Download Helper: Network error for direct link:', response);
                    $button.prop('disabled', false);
                    $button.html($button.data('original-content'));
                }
            });
        }
    }

    // --- MutationObserver for dynamic content (only active on non-file-hoster pages) ---
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) { // Element node
                        addIconsToLinks(node);
                    }
                });
            }
        });
    });

    // --- Init ---
    $(document).ready(function() {
        console.log(`Debrid Download Helper (v${GM_info.script.version}): Document ready. Initializing script.`);
        refreshDisplayBasedOnUrl(); // Initial check and display

        GM_addStyle(`
            /* General button/icon styles */
            .debrid-btn {
                background: transparent !important;
                border: none !important;
                padding: 0 !important;
                margin: 0 0 0 5px !important;
                cursor: pointer !important;
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
                width: 22px !important;
                height: 22px !important;
                vertical-align: middle !important;
                box-sizing: border-box !important;
            }
            .debrid-btn img {
                display: block !important;
                width: 18px !important;
                height: 18px !important;
                opacity: 1 !important; /* Base opacity, overridden by inline style for RD and Alldebrid */
                transition: opacity 0.2s !important;
                border-radius: 3px !important;
            }
            .debrid-btn:hover img {
                opacity: 1 !important; /* Remain fully visible on hover */
            }

            /* Spinner and Checkmark styles */
            .debrid-loader {
                display: inline-block;
                width: 18px;
                height: 18px;
                border: 2px solid #f3f3f3; /* Light grey */
                border-top: 2px solid #4CAF50; /* Green */
                border-radius: 50%;
                animation: spin 1s linear infinite;
            }
            .debrid-checkmark {
                font-size: 16.2px; /* 10% smaller than 18px */
                line-height: 1; /* Ensure vertical alignment */
                color: #4CAF50; /* Green color for checkmark */
                opacity: 0.8; /* 20% transparent */
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }

            /* Provider menu styles */
            #debrid-provider-menu button:hover {
                opacity: 1 !important;
            }

            /* Download bar styles */
            #debrid-download-bar {
                position: fixed;
                top: 0;
                left: 50%;
                transform: translateX(-50%);
                background: #282c34;
                color: white;
                padding: 8px 15px;
                border-radius: 0 0 8px 8px;
                box-shadow: 0 4px 8px rgba(0,0,0,0.3);
                z-index: 100000;
                display: flex;
                align-items: center;
                gap: 10px;
                font-family: Arial, sans-serif;
                font-size: 14px;
            }
            #debrid-download-bar .debrid-bar-action-button {
                background: transparent;
                border: none;
                padding: 0;
                cursor: pointer;
            }
            #debrid-download-bar .debrid-bar-action-button img {
                width: 22px;
                height: 22px;
                vertical-align: middle;
                opacity: 1 !important; /* MADE FULLY OPAQUE AS REQUESTED */
                border-radius: 3px;
                /* Removed transition and hover opacity rule for simplicity since it's always 1 now */
            }

            /* New API Key Modal Styles */
            #debrid-apikey-modal-overlay {
                /* Styles defined inline in showApiKeyModal for dynamic positioning */
            }
            #debrid-apikey-modal {
                /* Styles defined inline in showApiKeyModal for dynamic positioning */
            }
            #debrid-apikey-modal button:hover {
                filter: brightness(1.1); /* Slight brightness change on hover */
            }
        `);
    });
})(jQuery);