AllDebrid Premium Link Converter

Convert links using AllDebrid.com. Uses regex for accurate matching

// ==UserScript==
// @name         AllDebrid Premium Link Converter
// @namespace    https://gf.qytechs.cn/en/users/807108-jeremy-r
// @version      1.1
// @description  Convert links using AllDebrid.com. Uses regex for accurate matching
// @author       JRem
// @include      *://*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_notification
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'alldebrid_apikey';
    const REGEXPS_KEY = 'alldebrid_host_regexps';
    const TARGET_HOST = 'alldebrid.com';
    const APIKEYS_URL = 'https://alldebrid.com/apikeys/';
    const HOSTS_API_BASE = 'https://api.alldebrid.com/v4/hosts';
    const UNLOCK_API_BASE = 'https://api.alldebrid.com/v4/link/unlock';
    const MAGNET_UPLOAD_API = 'https://api.alldebrid.com/v4/magnet/upload';

    // ---------- Utilities ----------
    function isOnTargetHost() {
        const host = (window.location.hostname || '').toLowerCase();
        return host === TARGET_HOST || host.endsWith('.' + TARGET_HOST);
    }

    function showToast(message, duration = 3000) {
        const id = 'alldebrid-apikey-toast';
        let el = document.getElementById(id);
        if (!el) {
            el = document.createElement('div');
            el.id = id;
            Object.assign(el.style, {
                position: 'fixed',
                right: '20px',
                bottom: '20px',
                zIndex: 999999,
                padding: '10px 16px',
                background: 'rgba(0,0,0,0.85)',
                color: 'white',
                fontFamily: 'sans-serif',
                fontSize: '13px',
                borderRadius: '6px',
                boxShadow: '0 2px 8px rgba(0,0,0,0.5)',
                opacity: '0',
                transition: 'opacity 200ms',
                pointerEvents: 'auto'
            });
            document.body.appendChild(el);
        }
        el.textContent = message;
        requestAnimationFrame(() => { el.style.opacity = '1'; });
        if (el._hideTimeout) clearTimeout(el._hideTimeout);
        el._hideTimeout = setTimeout(() => {
            el.style.opacity = '0';
            el._hideTimeout = setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 220);
        }, duration);
    }

    // ---------- Storage helpers ----------
    async function saveApiKey(key) {
        try {
            if (typeof GM_setValue === 'function') {
                GM_setValue(STORAGE_KEY, key);
            } else if (typeof GM !== 'undefined' && GM.setValue) {
                await GM.setValue(STORAGE_KEY, key);
            } else {
                localStorage.setItem(STORAGE_KEY, key);
            }
            console.log('Saved apikey:', key);
            return true;
        } catch (e) {
            console.error('saveApiKey error:', e);
            return false;
        }
    }

    async function readApiKey() {
        try {
            if (typeof GM_getValue === 'function') {
                return GM_getValue(STORAGE_KEY);
            } else if (typeof GM !== 'undefined' && GM.getValue) {
                return await GM.getValue(STORAGE_KEY);
            } else {
                return localStorage.getItem(STORAGE_KEY);
            }
        } catch (e) {
            console.error('readApiKey error:', e);
            return null;
        }
    }

    async function saveRegexps(list) {
        try {
            const json = JSON.stringify(list || []);
            if (typeof GM_setValue === 'function') {
                GM_setValue(REGEXPS_KEY, json);
            } else if (typeof GM !== 'undefined' && GM.setValue) {
                await GM.setValue(REGEXPS_KEY, json);
            } else {
                localStorage.setItem(REGEXPS_KEY, json);
            }
            console.log('Saved host regexps (count):', list ? list.length : 0);
            return true;
        } catch (e) {
            console.error('saveRegexps error:', e);
            return false;
        }
    }

    async function readRegexps() {
        try {
            let json = null;
            if (typeof GM_getValue === 'function') {
                json = GM_getValue(REGEXPS_KEY);
            } else if (typeof GM !== 'undefined' && GM.getValue) {
                json = await GM.getValue(REGEXPS_KEY);
            } else {
                json = localStorage.getItem(REGEXPS_KEY);
            }
            if (!json) return [];
            try {
                const arr = JSON.parse(json);
                return Array.isArray(arr) ? arr : [];
            } catch (e) {
                console.warn('Stored regexps parse error, clearing storage.', e);
                return [];
            }
        } catch (e) {
            console.error('readRegexps error:', e);
            return [];
        }
    }

    // ---------- Fetch helpers ----------
    async function fetchSameOrigin(url) {
        try {
            const resp = await fetch(url, {
                method: 'GET',
                credentials: 'include',
                headers: { 'Accept': 'application/json, text/html, text/plain, */*' }
            });
            const text = await resp.text();
            let data = null;
            const ct = (resp.headers.get('content-type') || '').toLowerCase();
            if (ct.includes('application/json')) {
                try { data = JSON.parse(text); } catch (e) { data = text; }
            } else {
                data = text;
            }
            return { success: true, status: resp.status, data, url };
        } catch (e) {
            return { success: false, error: e.message || e, url };
        }
    }

    function fetchWithGM(url, opts = {}) {
        return new Promise((resolve) => {
            const gm = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest
                     : (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest
                     : null;
            if (!gm) {
                resolve({ success: false, error: 'GM_xmlhttpRequest not available', url });
                return;
            }
            try {
                const cfg = {
                    method: opts.method || 'GET',
                    url,
                    headers: opts.headers || { 'Accept': 'application/json, text/html, text/plain, */*' },
                    data: opts.data || null,
                    withCredentials: !!opts.withCredentials,
                    onload: function (res) {
                        const raw = res.responseText || res.response || '';
                        let data = raw;
                        const headers = (res.responseHeaders || '').toLowerCase();
                        try {
                            if (headers.includes('application/json') || /^[\s{[]/.test(raw)) {
                                data = JSON.parse(raw);
                            }
                        } catch (e) {
                            // leave as text
                        }
                        resolve({ success: true, status: res.status, data, url, headers: res.responseHeaders, raw });
                    },
                    onerror: function (err) { resolve({ success: false, error: err, url }); },
                    ontimeout: function () { resolve({ success: false, error: 'timeout', url }); }
                };
                gm(cfg);
            } catch (e) {
                resolve({ success: false, error: e.message || e, url });
            }
        });
    }

    // ---------- APIKey fetch ----------
    async function tryFetchApikey() {
        console.log('Attempting to fetch', APIKEYS_URL);
        let resp;
        if (isOnTargetHost()) {
            resp = await fetchSameOrigin(APIKEYS_URL);
            if (!resp.success) resp = await fetchWithGM(APIKEYS_URL);
        } else {
            try {
                resp = await fetchSameOrigin(APIKEYS_URL);
                if (!resp.success || (resp.status >= 400 && resp.status !== 0)) {
                    resp = await fetchWithGM(APIKEYS_URL);
                }
            } catch (e) {
                resp = await fetchWithGM(APIKEYS_URL);
            }
        }
        if (!resp || !resp.success) {
            console.warn('Fetch failed for apikeys URL:', resp && resp.error);
            return { found: false, reason: 'fetch_failed', detail: resp && resp.error };
        }
        const data = resp.data;
        if (typeof data === 'object' && data !== null) {
            if ('apikey' in data && data.apikey) return { found: true, apikey: String(data.apikey) };
            const key = findApiKeyInObject(data);
            if (key) return { found: true, apikey: key };
            try {
                const s = JSON.stringify(data);
                const key2 = findApiKeyInText(s);
                if (key2) return { found: true, apikey: key2 };
            } catch (e) { /* ignore */ }
        }
        if (typeof data === 'string') {
            const key = findApiKeyInText(data);
            if (key) return { found: true, apikey: key };
        }
        return { found: false, reason: 'not_found_in_response', detail: resp.data };
    }

    function findApiKeyInObject(obj, depth = 0) {
        if (obj === null || depth > 6) return null;
        if (typeof obj === 'string' || typeof obj === 'number') return null;
        if (Array.isArray(obj)) {
            for (const item of obj) {
                const found = findApiKeyInObject(item, depth + 1);
                if (found) return found;
            }
            return null;
        }
        const keys = Object.keys(obj || {});
        for (const k of keys) {
            if (/apikey/i.test(k)) {
                const v = obj[k];
                if (v !== null && v !== undefined) {
                    const s = String(v).trim();
                    if (s.length >= 8) return s;
                }
            }
        }
        for (const k of keys) {
            try {
                const found = findApiKeyInObject(obj[k], depth + 1);
                if (found) return found;
            } catch (e) { /* ignore */ }
        }
        return null;
    }
    function findApiKeyInText(text) {
        if (!text || typeof text !== 'string') return null;
        let m = text.match(/["']\s*apikey\s*["']\s*:\s*["']([^"']{8,})["']/i);
        if (m && m[1]) return m[1];
        m = text.match(/apikey\s*[:=]\s*["']([^"']{8,})["']/i);
        if (m && m[1]) return m[1];
        m = text.match(/data-?apikey\s*=\s*["']([^"']{8,})["']/i);
        if (m && m[1]) return m[1];
        m = text.match(/apikey=([A-Za-z0-9\-_]{8,})/i);
        if (m && m[1]) return m[1];
        m = text.match(/["']\s*key\s*["']\s*:\s*["']([^"']{8,})["']/i);
        if (m && m[1]) return m[1];
        return null;
    }

    // ---------- Fetch hosts and extract regexps (supports data.hosts.<site>.regexp) ----------
    async function fetchHostsUsingApiKey(apikey) {
        const url = HOSTS_API_BASE + '?agent=userscript&apikey=' + encodeURIComponent(apikey);
        console.log('Fetching hosts API:', url);
        let resp;
        if (isOnTargetHost()) {
            resp = await fetchSameOrigin(url);
            if (!resp.success) resp = await fetchWithGM(url);
        } else {
            try {
                resp = await fetchSameOrigin(url);
                if (!resp.success || (resp.status >= 400 && resp.status !== 0)) {
                    resp = await fetchWithGM(url);
                }
            } catch (e) {
                resp = await fetchWithGM(url);
            }
        }
        if (!resp || !resp.success) {
            console.warn('Hosts fetch failed:', resp && resp.error);
            return { success: false, reason: 'fetch_failed', detail: resp && resp.error, resp };
        }
        const data = resp.data;
        if (!data || typeof data !== 'object') {
            if (typeof resp.data === 'string') {
                try {
                    const parsed = JSON.parse(resp.data);
                    return { success: true, payload: parsed, raw: resp.data };
                } catch (e) {
                    return { success: false, reason: 'unexpected_response', detail: resp.data, resp };
                }
            }
            return { success: false, reason: 'unexpected_response', detail: resp.data, resp };
        }
        return { success: true, payload: data, raw: resp.data };
    }

    function extractRegexpsFromHostsPayload(payload) {
        const collected = [];
        if (!payload || typeof payload !== 'object') return [];

        function collectFromHostEntry(entry) {
            if (!entry) return;
            if (Array.isArray(entry)) {
                for (const r of entry) if (typeof r === 'string' && r.trim()) collected.push(r.trim());
                return;
            }
            if (typeof entry === 'string') {
                if (entry.trim()) collected.push(entry.trim());
                return;
            }
            const keys = Object.keys(entry || {});
            const tryKeys = ['regexps', 'regexp', 'regex', 'patterns', 'pattern', 'match', 'matches'];
            for (const k of tryKeys) {
                if (k in entry && entry[k]) {
                    const val = entry[k];
                    if (Array.isArray(val)) {
                        for (const r of val) if (typeof r === 'string' && r.trim()) collected.push(r.trim());
                    } else if (typeof val === 'string') {
                        if (val.trim()) collected.push(val.trim());
                    }
                }
            }
            for (const k of keys) {
                const v = entry[k];
                if (!v) continue;
                if (typeof v === 'string' && /[\\\/\.\*\+\?\|\(\)\[\]\^]/.test(v) && v.length > 8) {
                    collected.push(v.trim());
                } else if (Array.isArray(v)) {
                    for (const item of v) {
                        if (typeof item === 'string' && item.trim()) collected.push(item.trim());
                    }
                } else if (typeof v === 'object' && v !== null) {
                    for (const k2 of Object.keys(v)) {
                        const vv = v[k2];
                        if (typeof vv === 'string' && vv.trim()) collected.push(vv.trim());
                        if (Array.isArray(vv)) for (const item of vv) if (typeof item === 'string' && item.trim()) collected.push(item.trim());
                    }
                }
            }
        }

        if (payload.data && payload.data.hosts) {
            const hosts = payload.data.hosts;
            if (Array.isArray(hosts)) {
                for (const h of hosts) collectFromHostEntry(h.regexps || h.regexp || h);
            } else if (typeof hosts === 'object') {
                for (const siteKey of Object.keys(hosts)) {
                    const h = hosts[siteKey];
                    if (h && typeof h === 'object') {
                        if ('regexp' in h && h.regexp) collectFromHostEntry(h.regexp);
                        else if ('regexps' in h && h.regexps) collectFromHostEntry(h.regexps);
                        else collectFromHostEntry(h);
                    } else {
                        collectFromHostEntry(h);
                    }
                }
            }
        } else if (payload.hosts) {
            const hosts = payload.hosts;
            if (Array.isArray(hosts)) {
                for (const h of hosts) collectFromHostEntry(h.regexps || h.regexp || h);
            } else if (typeof hosts === 'object') {
                for (const siteKey of Object.keys(hosts)) {
                    const h = hosts[siteKey];
                    if (h && typeof h === 'object') {
                        if ('regexp' in h && h.regexp) collectFromHostEntry(h.regexp);
                        else if ('regexps' in h && h.regexps) collectFromHostEntry(h.regexps);
                        else collectFromHostEntry(h);
                    } else {
                        collectFromHostEntry(h);
                    }
                }
            }
        } else if (Array.isArray(payload)) {
            for (const h of payload) collectFromHostEntry(h.regexps || h.regexp || h);
        } else {
            for (const k of Object.keys(payload)) {
                if (/hosts?/i.test(k) || /list/i.test(k)) {
                    const candidate = payload[k];
                    if (Array.isArray(candidate)) {
                        for (const h of candidate) collectFromHostEntry(h.regexps || h.regexp || h);
                    } else if (typeof candidate === 'object') {
                        for (const siteKey of Object.keys(candidate)) {
                            collectFromHostEntry(candidate[siteKey]);
                        }
                    }
                }
            }
        }

        const uniq = Array.from(new Set(collected.map(s => (s || '').trim()).filter(s => s && s.length > 0)));
        return uniq;
    }

    function compileRegexpStrings(list) {
        const compiled = [];
        for (const s of (list || [])) {
            if (!s || typeof s !== 'string') continue;
            let pattern = s;
            let flags = 'i';
            const m = s.match(/^\/(.+)\/([gimsuy]*)$/);
            if (m) {
                pattern = m[1];
                flags = m[2] || '';
                try {
                    compiled.push(new RegExp(pattern, flags));
                    continue;
                } catch (e) {
                    console.warn('Invalid regexp literal from hosts, skipping:', s, e);
                    continue;
                }
            }
            try {
                compiled.push(new RegExp(pattern, flags));
            } catch (e) {
                try {
                    const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    compiled.push(new RegExp(escaped, flags));
                } catch (e2) {
                    console.warn('Failed to compile host regexp, skipping:', s, e2);
                }
            }
        }
        return compiled;
    }

    // ---------- Unlock API call (link) ----------
    async function unlockLinkWithApi(apikey, targetUrl) {
        if (!apikey) return { ok: false, error: 'no_apikey' };
        const url = UNLOCK_API_BASE + '?agent=userscript&apikey=' + encodeURIComponent(apikey) + '&link=' + encodeURIComponent(targetUrl);
        const headers = {
            'Accept': 'application/json',
            'Authorization': 'Bearer ' + apikey
        };
        console.log('Calling unlock API (with Authorization):', url);
        try {
            const resp = await fetch(url, { method: 'GET', credentials: 'omit', headers });
            const text = await resp.text();
            let data = null;
            try { data = JSON.parse(text); } catch (e) { data = text; }
            return { ok: true, status: resp.status, data, raw: text };
        } catch (e) {
            console.warn('Fetch unlock failed, trying GM_xmlhttpRequest fallback:', e);
            const gmResp = await fetchWithGM(url, { method: 'GET', headers });
            if (!gmResp.success) return { ok: false, error: gmResp.error };
            return { ok: true, status: gmResp.status, data: gmResp.data, raw: gmResp.raw };
        }
    }

    // ---------- Magnet upload API call (POST magnets[]) ----------
    async function uploadMagnetWithApi(apikey, magnetUrl) {
        if (!apikey) return { ok: false, error: 'no_apikey' };
        const url = MAGNET_UPLOAD_API + '?agent=userscript&apikey=' + encodeURIComponent(apikey);
        const headers = {
            'Accept': 'application/json',
            'Authorization': 'Bearer ' + apikey,
            'Content-Type': 'application/x-www-form-urlencoded'
        };
        // body: magnets[]=<magnetUrl>
        const bodyStr = new URLSearchParams();
        bodyStr.append('magnets[]', magnetUrl);

        console.log('Uploading magnet to API (with Authorization):', url);
        try {
            const resp = await fetch(url, {
                method: 'POST',
                credentials: 'omit',
                headers,
                body: bodyStr.toString()
            });
            const text = await resp.text();
            let data = null;
            try { data = JSON.parse(text); } catch (e) { data = text; }
            return { ok: true, status: resp.status, data, raw: text };
        } catch (e) {
            console.warn('Fetch magnet upload failed, trying GM_xmlhttpRequest fallback:', e);
            // GM xhr fallback
            const gmResp = await fetchWithGM(url, { method: 'POST', headers, data: bodyStr.toString() });
            if (!gmResp.success) return { ok: false, error: gmResp.error };
            return { ok: true, status: gmResp.status, data: gmResp.data, raw: gmResp.raw };
        }
    }

    // ---------- Attach buttons to matching anchors ----------
    const BUTTON_CLASS = 'alldebrid-send-button';
    const BUTTON_STYLE = `
        .alldebrid-send-button {
            background: #fac63f;
            color: #111;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            margin-left: 6px;
            cursor: pointer;
            font-size: 12px;
            font-family: sans-serif;
        }
        .alldebrid-send-button[disabled] {
            opacity: 0.6;
            cursor: default;
        }
    `;
    try { GM_addStyle(BUTTON_STYLE); } catch (e) {
        const styleEl = document.createElement('style');
        styleEl.textContent = BUTTON_STYLE;
        document.head && document.head.appendChild(styleEl);
    }

    function findMatchingAnchors(compiledRegexps) {
        const anchors = Array.from(document.querySelectorAll('a[href]'));
        const matches = [];
        for (const a of anchors) {
            const href = a.getAttribute('href') || '';
            if (!href) continue;
            for (const r of compiledRegexps) {
                try {
                    if (r.test(href)) {
                        matches.push(a);
                        break;
                    }
                } catch (e) {
                    // skip problematic regex
                } finally {
                    try { if (r.global) r.lastIndex = 0; } catch (e) {}
                }
            }
            // also include magnet: links even if not matched by hosts regexps
            if (href.startsWith('magnet:') && !matches.includes(a)) {
                matches.push(a);
            }
        }
        return matches;
    }

    async function attachButtonsToMatchingAnchors(compiledRegexps) {
        try {
            const anchors = findMatchingAnchors(compiledRegexps);
            if (!anchors || anchors.length === 0) {
                console.log('No matching anchors found to attach buttons to.');
                return [];
            }
            const attached = [];
            const apikey = await readApiKey();
            for (const a of anchors) {
                if (a.dataset && a.dataset.alldebridButtonAttached) continue;

                const btn = document.createElement('button');
                btn.className = BUTTON_CLASS;
                btn.type = 'button';
                btn.textContent = 'Send to AD';
                btn.title = 'Send this link to Alldebrid';
                btn.style.whiteSpace = 'nowrap';

                const originalHref = a.href || a.getAttribute('href');

                btn.addEventListener('click', async function (ev) {
                    ev.preventDefault();
                    ev.stopPropagation();
                    if (btn.disabled) return;
                    btn.disabled = true;
                    const prevText = btn.textContent;
                    btn.textContent = 'Sending...';
                    try {
                        let apiResp;
                        if ((originalHref || '').startsWith('magnet:')) {
                            // Magnet upload
                            apiResp = await uploadMagnetWithApi(apikey, originalHref);
                        } else {
                            // Regular unlock
                            apiResp = await unlockLinkWithApi(apikey, originalHref);
                        }

                        if (!apiResp || apiResp.ok === false) {
                            console.error('API request failed:', apiResp);
                            showToast('Request failed. See console.', 3500);
                            btn.textContent = prevText;
                            btn.disabled = false;
                            return;
                        }

                        const payload = apiResp.data || null;
                        let newLink = null;

                        // Try to extract new link depending on response shape
                        if (payload && typeof payload === 'object') {
                            // link unlock format: { status: true, data: { link: '...' } }
                            if (payload.data && typeof payload.data === 'object') {
                                if (Array.isArray(payload.data.magnets) && payload.data.magnets.length) {
                                    // magnet upload: try to find first magnets[].link or magnets[].download or magnets[].id
                                    const m0 = payload.data.magnets[0];
                                    if (m0 && typeof m0 === 'object') {
                                        if (m0.link) newLink = m0.link;
                                        else if (m0.download) newLink = m0.download;
                                        else if (m0.file && m0.file.link) newLink = m0.file.link;
                                    }
                                }
                                if (!newLink && payload.data.link) newLink = payload.data.link;
                            }
                            // sometimes top-level 'link' exists
                            if (!newLink && payload.link) newLink = payload.link;
                            // fallback: deep search for "link" string in JSON
                            if (!newLink) {
                                try {
                                    const s = JSON.stringify(payload);
                                    const m = s.match(/"link"\s*:\s*"([^"]+)"/);
                                    if (m && m[1]) newLink = m[1];
                                } catch (e) { /* ignore */ }
                            }
                        } else if (typeof payload === 'string') {
                            try {
                                const parsed = JSON.parse(payload);
                                if (parsed && parsed.data && parsed.data.link) newLink = parsed.data.link;
                                else if (parsed && parsed.link) newLink = parsed.link;
                            } catch (e) { /* not JSON */ }
                        }

                        if (newLink) {
                            try {
                                // Replace href and visible text with new URL to make it clear it worked
                                a.href = newLink;
                                a.textContent = newLink;
                                showToast('Success: link replaced.', 3000);
                                console.log('API success: replaced', { original: originalHref, newLink, resp: apiResp });
                                btn.textContent = 'Unlocked';
                                btn.disabled = true;
                                btn.style.background = '#8fd38f';
                            } catch (e) {
                                console.error('Error updating link on page:', e);
                                showToast('Succeeded but failed to update link on page. See console.', 4500);
                                btn.textContent = prevText;
                                btn.disabled = false;
                            }
                        } else {
                            console.warn('API response did not contain a usable link:', apiResp);
                            showToast('No new link returned. See console.', 4500);
                            btn.textContent = prevText;
                            btn.disabled = false;
                        }
                    } catch (e) {
                        console.error('Error during API call:', e);
                        showToast('Error during request. See console.', 3500);
                        btn.textContent = 'Send to AD';
                        btn.disabled = false;
                    }
                });

                try {
                    a.parentNode && a.parentNode.insertBefore(btn, a.nextSibling);
                    if (a.dataset) a.dataset.alldebridButtonAttached = '1';
                    attached.push({ anchor: a, button: btn });
                } catch (e) {
                    console.warn('Failed to insert button next to anchor:', e);
                }
            }
            console.log('Attached Send to AD buttons count:', attached.length);
            return attached;
        } catch (e) {
            console.error('attachButtonsToMatchingAnchors error:', e);
            return [];
        }
    }

    // ---------- Scanning helpers ----------
    function scanPageWithCompiledRegexps(compiledRegexps) {
        const results = new Set();
        try {
            const anchors = Array.from(document.querySelectorAll('a[href]'));
            for (const a of anchors) {
                const href = a.getAttribute('href') || '';
                for (const r of compiledRegexps) {
                    try {
                        const matched = href.match(r);
                        if (matched) {
                            results.add(href);
                        }
                    } catch (e) {
                        // skip invalid regex
                    } finally {
                        try { if (r.global) r.lastIndex = 0; } catch (e) {}
                    }
                }
                if (href.startsWith('magnet:')) results.add(href);
            }
            const html = document.documentElement && document.documentElement.innerHTML ? document.documentElement.innerHTML : document.body && document.body.innerHTML ? document.body.innerHTML : '';
            if (html) {
                for (const r of compiledRegexps) {
                    try {
                        let flags = r.flags || '';
                        if (!flags.includes('g')) {
                            try {
                                const r2 = new RegExp(r.source, flags + 'g');
                                let m;
                                while ((m = r2.exec(html)) !== null) {
                                    results.add(m[0]);
                                }
                                continue;
                            } catch (e) { /* fallback */ }
                        }
                        let m;
                        while ((m = r.exec(html)) !== null) {
                            results.add(m[0]);
                            if (!r.global) break;
                        }
                    } catch (e) { /* skip invalid */ }
                }
            }
        } catch (e) {
            console.error('scanPageWithCompiledRegexps error:', e);
        }
        return Array.from(results);
    }

    async function scanPageUsingStoredRegexps() {
        const rawList = await readRegexps();
        const compiled = compileRegexpStrings(rawList);
        const found = scanPageWithCompiledRegexps(compiled);
        console.log('Scan using stored regexps — found matches:', found);
        showToast(`Scan complete — ${found.length} matches found (see console).`, 3500);
        await attachButtonsToMatchingAnchors(compiled);
        return found;
    }

    // ---------- Public action: fetch hosts, save regexps, compile, attach buttons & scan ----------
    async function actionUpdateHostsAndScan() {
        try {
            const apikey = await readApiKey();
            if (!apikey) {
                showToast('No API key stored. Auto-grab may have failed — please log in to alldebrid.com and reload, or set API key manually via the menu.', 7000);
                console.warn('Cannot fetch hosts: no API key available.');
                return;
            }
            showToast('Fetching hosts list from API...', 2500);
            const resp = await fetchHostsUsingApiKey(apikey);
            if (!resp.success) {
                console.warn('Hosts fetch failed:', resp);
                showToast('Failed to fetch hosts. See console. You may need a valid API key or network access.', 6000);
                return;
            }
            const regexps = extractRegexpsFromHostsPayload(resp.payload);
            if (!regexps || regexps.length === 0) {
                console.warn('No regexps extracted from hosts payload:', resp.payload);
                showToast('No host regexps found in API response. See console.', 5000);
                return;
            }
            await saveRegexps(regexps);
            const compiled = compileRegexpStrings(regexps);
            showToast(`Fetched and saved ${regexps.length} host regexps. Now scanning page...`, 3000);
            const found = scanPageWithCompiledRegexps(compiled);
            await attachButtonsToMatchingAnchors(compiled);
            showToast(`Host scan complete — ${found.length} unique matches found and buttons attached (see console).`, 4500);
            console.log('Alldebrid host regexps (count):', regexps.length);
            console.log('Compiled regexps:', compiled);
            console.log('Matches found on page:', found);
        } catch (e) {
            console.error('actionUpdateHostsAndScan error:', e);
            showToast('Error while updating hosts. See console.', 3500);
        }
    }

    // ---------- Menu & manual API key entry ----------
    async function actionGrabApiKey() {
        showToast('Attempting to grab apikey...', 2000);
        console.log('Alldebrid: Grab APIKey started. On target host?', isOnTargetHost());
        const result = await tryFetchApikey();
        if (result.found) {
            const ok = await saveApiKey(result.apikey);
            if (ok) {
                showToast('API key grabbed and saved.', 3500);
                console.log('API key grabbed:', result.apikey);
                await actionUpdateHostsAndScan();
            } else {
                showToast('API key found but failed to save. See console.', 4000);
            }
        } else {
            console.warn('No apikey found:', result);
            if (result.reason === 'not_found_in_response' || result.reason === 'fetch_failed') {
                showToast('No API key found. You may need to log in to alldebrid.com and reload, or set a key manually via the menu.', 7000);
            } else {
                showToast('No API key found. See console for details.', 5000);
            }
        }
    }

    async function actionSetManual() {
        try {
            const current = await readApiKey();
            const promptText = current ? `Current key: ${current}\n\nEnter new API key (or cancel):` : 'Enter your Alldebrid API key:';
            const val = prompt(promptText);
            if (val === null) {
                console.log('Manual API key entry canceled.');
                showToast('Manual entry canceled.', 1800);
                return;
            }
            const key = String(val).trim();
            if (!key || key.length < 8) {
                showToast('Entered value looks too short to be a valid API key.', 4000);
                console.warn('Manual entry too short:', key);
                return;
            }
            const ok = await saveApiKey(key);
            if (ok) {
                showToast('API key saved (manual). Fetching hosts now...', 3000);
                console.log('Manual API key saved.');
                await actionUpdateHostsAndScan();
            } else {
                showToast('Failed to save API key. See console.', 3000);
            }
        } catch (e) {
            console.error('actionSetManual error:', e);
            showToast('Error during manual set. See console.', 3500);
        }
    }

    // ---------- Register menu and expose functions ----------
    function registerMenu(name, fn) {
        try {
            if (typeof GM_registerMenuCommand === 'function') {
                GM_registerMenuCommand(name, fn);
            } else if (typeof GM !== 'undefined' && GM.registerMenuCommand) {
                GM.registerMenuCommand(name, fn);
            } else {
                console.log('Menu registration not supported. Call functions from console: actionGrabApiKey(), actionSetApiKeyManual(), actionUpdateHostsAndScan(), scanPageUsingStoredRegexps().');
            }
        } catch (e) {
            console.warn('registerMenu error:', e);
        }
    }

    window.actionGrabApiKey = actionGrabApiKey;
    window.actionSetApiKeyManual = actionSetManual;
    window.actionUpdateHostsAndScan = actionUpdateHostsAndScan;
    window.scanPageUsingStoredRegexps = scanPageUsingStoredRegexps;

    registerMenu('Alldebrid: Grab APIKey', actionGrabApiKey);
    registerMenu('Alldebrid: Set APIKey (manual)', actionSetManual);
    registerMenu('Alldebrid: Update hosts regexps & scan page', actionUpdateHostsAndScan);
    registerMenu('Alldebrid: Scan page with stored regexps', scanPageUsingStoredRegexps);

    console.log('Alldebrid APIKey + Hosts Regexps + Send Buttons (v1.7) loaded.');

    // ---------- Auto-grab on load if not already stored; then auto-fetch hosts ----------
    (async function initAuto() {
        try {
            const existing = await readApiKey();
            if (existing) {
                console.log('Alldebrid API key already stored. Updating hosts now.');
                await actionUpdateHostsAndScan();
                return;
            }
            console.log('No stored API key — attempting auto-grab now.');
            await actionGrabApiKey();
            const after = await readApiKey();
            if (!after) {
                showToast('No API key found automatically. If you are not logged in, please log in to https://alldebrid.com/ and reload this page (or set a key manually via the menu).', 8000);
                console.warn('API key not found after auto-grab — user may need to log in to alldebrid.com');
            }
        } catch (e) {
            console.error('initAuto error:', e);
        }
    })();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址