CoverUp

Rehost RED Album & Artist Covers: intelligent quality detection, multi-source artwork picker (Discogs, MusicBrainz, Apple Music, Qobuz, Bandcamp, Deezer, Tidal), smart alt-cover management, and upload to ptpimg/imgbb/catbox/TheSunGod/custom hosts.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==UserScript==
// @name         CoverUp
// @version      7.0.51
// @description  Rehost RED Album & Artist Covers: intelligent quality detection, multi-source artwork picker (Discogs, MusicBrainz, Apple Music, Qobuz, Bandcamp, Deezer, Tidal), smart alt-cover management, and upload to ptpimg/imgbb/catbox/TheSunGod/custom hosts.
// @match        https://redacted.sh/torrents.php*
// @match        https://redacted.sh/artist.php*
// @match        https://ptpimg.me/*.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @connect      api.discogs.com
// @connect      i.discogs.com
// @connect      ptpimg.me
// @connect      redacted.sh
// @connect      api.imgbb.com
// @connect      i.ibb.co
// @connect      catbox.moe
// @connect      files.catbox.moe
// @connect      itunes.apple.com
// @connect      is1-ssl.mzstatic.com
// @connect      is2-ssl.mzstatic.com
// @connect      is3-ssl.mzstatic.com
// @connect      is4-ssl.mzstatic.com
// @connect      is5-ssl.mzstatic.com
// @connect      api.deezer.com
// @connect      cdn-images.dzcdn.net
// @connect      f4.bcbits.com
// @connect      bandcamp.com
// @connect      open.spotify.com
// @connect      coverartarchive.org
// @connect      musicbrainz.org
// @connect      amazon.com
// @connect      m.media-amazon.com
// @connect      api.qobuz.com
// @connect      archive.org
// @connect      open.qobuz.com
// @connect      static.qobuz.com
// @connect      thesungod.xyz
// @connect      cdn.thesungod.xyz
// @connect      *
// @license      MIT
// @namespace    https://greasyfork.org/users/1568924
// ==/UserScript==




(function() {
    'use strict';

    // --- CONFIGURATION ---
    const MAX_DIMENSION   = 3000;
    const JPEG_QUALITY    = 0.95;
    const MIN_RESOLUTION  = 500;
    const REHOST_TRIGGERS = ['imgur.com', 'ptpimg.me'];
    // Domains that are valid permanent rehost destinations — images already here are "done"
    const REHOST_DOMAINS = [
        'ptpimg.me', 'imgbb.com', 'i.imgbb.com', 'ibb.co',
        'catbox.moe', 'files.catbox.moe',
        'ra.thesungod.xyz',
    ];
    // Source domains that produce temporary/expiring URLs — rehost recommended even if image loads fine
    const SOURCE_DOMAINS = [
        'i.discogs.com', 'coverartarchive.org', 'archive.org',
        'musicbrainz.org', 'lastfm.freetls.fastly.net', 'last.fm',
        'static.qobuz.com', 'mzstatic.com', 'is1-ssl.mzstatic.com',
        'i.scdn.co', 'e.snmc.io',
        'f4.bcbits.com', 'bcbits.com',
        'cdn-images.dzcdn.net',
        'm.media-amazon.com',
        'resources.tidal.com',
        'i.scdn.co', 'spotifycdn.com',
    ];
    function isOnRehostDomain(url) {
        try {
            const hostname = new URL(url).hostname.toLowerCase();
            return REHOST_DOMAINS.some(d => hostname === d || hostname.endsWith('.' + d));
        } catch { return false; }
    }
    function isOnSourceDomain(url) {
        try {
            const hostname = new URL(url).hostname.toLowerCase();
            return SOURCE_DOMAINS.some(d => hostname === d || hostname.endsWith('.' + d));
        } catch { return false; }
    }

    const PTPIMG_DEFAULT_KEY = '';
    const IMGBB_DEFAULT_KEY  = '';

    function getImgbbKey()   { return GM_getValue('imgbbAPIKey',    IMGBB_DEFAULT_KEY); }
    function getPtpimgKey()  { return GM_getValue('ptpimgAPIKey',   PTPIMG_DEFAULT_KEY); }
    function getCatboxHash() { return GM_getValue('catboxUserHash', ''); }
    function setImgbbKey(key) { GM_setValue('imgbbAPIKey',    key); }
    function setCatboxHash(h) { GM_setValue('catboxUserHash', h); }

    function getSungodKey()      { return GM_getValue('sungodAPIKey', ''); }
    function setSungodKey(key)   { GM_setValue('sungodAPIKey', key); }

    function getCustomUploadUrl()    { return GM_getValue('customUploadUrl',    ''); }
    function getCustomFileField()    { return GM_getValue('customFileField',    'file'); }
    function getCustomResponsePath() { return GM_getValue('customResponsePath', 'plaintext'); }
    function setCustomUploadUrl(v)    { GM_setValue('customUploadUrl',    v); }
    function setCustomFileField(v)    { GM_setValue('customFileField',    v); }
    function setCustomResponsePath(v) { GM_setValue('customResponsePath', v); }

    function getPreferredFallbackHost() { return GM_getValue('preferredFallbackHost', ''); }
    function setPreferredFallbackHost(host) { GM_setValue('preferredFallbackHost', host); }

    // Persist rehosted URL mappings across page loads (survives redirects)
    function getRehostedUrl(originalUrl) { return GM_getValue('rehosted_' + originalUrl, ''); }
    function setRehostedUrl(originalUrl, newUrl) { GM_setValue('rehosted_' + originalUrl, newUrl); }

    function chooseFallbackHost(callback) {
        const current = getPreferredFallbackHost();
        if (current === 'imgbb' || current === 'catbox' || current === 'sungod') {
            callback(current);
            return;
        }
        if (current === 'custom' && getCustomUploadUrl()) {
            callback('custom');
            return;
        }

        const overlay = document.createElement('div');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.92);z-index:100000;display:flex;align-items:center;justify-content:center;';
        overlay.innerHTML = `
            <div style="background:#1a1a1a;padding:30px;border-radius:12px;border:2px solid #444;max-width:560px;width:92%;color:#fff;font-family:sans-serif;">
                <h2 style="margin-top:0;color:#4CAF50;">Choose fallback image host</h2>
                <p style="color:#ccc;line-height:1.6;margin-bottom:18px;">ptpimg is unavailable or not configured. Which service should this script use for uploads?</p>
                <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;">
                    <button id="choose-imgbb" style="padding:16px;background:#222;border:2px solid #555;border-radius:10px;color:#fff;cursor:pointer;text-align:left;">
                        <div style="font-weight:bold;font-size:15px;margin-bottom:5px;">imgbb</div>
                        <div style="font-size:12px;color:#aaa;line-height:1.5;">Requires a free API key. Good if you already use imgbb.</div>
                    </button>
                    <button id="choose-catbox" style="padding:16px;background:#222;border:2px solid #555;border-radius:10px;color:#fff;cursor:pointer;text-align:left;">
                        <div style="font-weight:bold;font-size:15px;margin-bottom:5px;">catbox</div>
                        <div style="font-size:12px;color:#aaa;line-height:1.5;">No API key required for anonymous uploads. Simpler default.</div>
                    </button>
                    <div id="sungod-section" style="background:#222;border:2px solid #555;border-radius:10px;padding:16px;">
                        <div style="font-weight:bold;font-size:15px;margin-bottom:6px;color:#fff;">TheSunGod ☀️</div>
                        <div style="font-size:12px;color:#aaa;line-height:1.5;margin-bottom:10px;">Uses the TheSunGod API. Supports direct URL rehosting via <code style="color:#aaa;">/api/image/rehost_new</code>.</div>
                        <input id="sungod-api-key" type="text" placeholder="Your TheSunGod API key"
                            value="${getSungodKey()}"
                            style="width:100%;padding:9px 10px;background:#111;border:1px solid #555;color:#fff;border-radius:6px;font-size:13px;box-sizing:border-box;margin-bottom:10px;">
                        <button id="choose-sungod" style="width:100%;padding:10px;background:#f59e0b;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:bold;">Use TheSunGod</button>
                    </div>
                </div>
                <div id="custom-host-section" style="background:#222;border:2px solid #555;border-radius:10px;padding:16px;margin-bottom:16px;">
                    <div style="font-weight:bold;font-size:15px;margin-bottom:6px;color:#fff;">Custom host</div>
                    <div style="font-size:12px;color:#aaa;line-height:1.5;margin-bottom:10px;">
                        Any host that accepts a simple file upload via HTTP POST (e.g. ShareX-compatible services).
                        Just enter the upload URL — the script will detect the response format automatically.<br>
                        <span style="color:#ff9800;">⚠ Not all hosts are guaranteed to work.</span>
                    </div>
                    <input id="custom-host-url" type="text" placeholder="https://example.com/upload"
                        value="${getCustomUploadUrl()}"
                        style="width:100%;padding:9px 10px;background:#111;border:1px solid #555;color:#fff;border-radius:6px;font-size:13px;box-sizing:border-box;margin-bottom:8px;">
                    <div id="custom-advanced-toggle" style="font-size:12px;color:#4CAF50;cursor:pointer;margin-bottom:0;">▶ Advanced options</div>
                    <div id="custom-advanced" style="display:none;margin-top:10px;">
                        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
                            <div>
                                <label style="font-size:11px;color:#aaa;display:block;margin-bottom:4px;">File field name</label>
                                <input id="custom-file-field" type="text" placeholder="file" value="${getCustomFileField()}"
                                    style="width:100%;padding:7px 9px;background:#111;border:1px solid #555;color:#fff;border-radius:5px;font-size:12px;box-sizing:border-box;">
                            </div>
                            <div>
                                <label style="font-size:11px;color:#aaa;display:block;margin-bottom:4px;">Response URL path</label>
                                <input id="custom-response-path" type="text" placeholder="plaintext or data.url" value="${getCustomResponsePath()}"
                                    style="width:100%;padding:7px 9px;background:#111;border:1px solid #555;color:#fff;border-radius:5px;font-size:12px;box-sizing:border-box;">
                            </div>
                        </div>
                        <div style="font-size:11px;color:#666;margin-top:6px;">Leave blank to auto-detect. Use dot notation for JSON paths e.g. <code style="color:#aaa;">data.url</code></div>
                    </div>
                    <button id="choose-custom" style="margin-top:12px;width:100%;padding:10px;background:#2196F3;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:bold;">Use Custom Host</button>
                </div>
                <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">
                    <label style="font-size:13px;color:#aaa;display:flex;align-items:center;gap:8px;cursor:pointer;">
                        <input id="remember-fallback-choice" type="checkbox">
                        Remember my choice
                    </label>
                    <button id="cancel-fallback-choice" style="padding:10px 16px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;">Cancel</button>
                </div>
            </div>`;
        document.body.appendChild(overlay);

        // Ensure remember checkbox is unchecked by default
        overlay.querySelector('#remember-fallback-choice').checked = false;

        // Advanced toggle
        overlay.querySelector('#custom-advanced-toggle').onclick = function() {
            const adv = overlay.querySelector('#custom-advanced');
            const open = adv.style.display !== 'none';
            adv.style.display = open ? 'none' : 'block';
            this.textContent = (open ? '▶' : '▼') + ' Advanced options';
        };

        function finish(host) {
            const remember = overlay.querySelector('#remember-fallback-choice').checked;
            if (remember) setPreferredFallbackHost(host);
            document.body.removeChild(overlay);
            callback(host);
        }

        overlay.querySelector('#choose-imgbb').onclick  = () => finish('imgbb');
        overlay.querySelector('#choose-catbox').onclick = () => finish('catbox');

        overlay.querySelector('#choose-sungod').onclick = () => {
            const key = overlay.querySelector('#sungod-api-key').value.trim();
            if (!key) { alert('Please enter your TheSunGod API key.'); return; }
            setSungodKey(key);
            finish('sungod');
        };

        overlay.querySelector('#choose-custom').onclick = () => {
            const url = overlay.querySelector('#custom-host-url').value.trim();
            if (!url) { alert('Please enter an upload URL.'); return; }
            if (!/^https?:\/\//i.test(url)) { alert('URL must start with http:// or https://'); return; }
            setCustomUploadUrl(url);
            const field = overlay.querySelector('#custom-file-field').value.trim();
            if (field) setCustomFileField(field);
            const path = overlay.querySelector('#custom-response-path').value.trim();
            if (path) setCustomResponsePath(path);
            finish('custom');
        };

        overlay.querySelector('#cancel-fallback-choice').onclick = () => {
            document.body.removeChild(overlay);
            callback('');
        };
    }

    // ============================================================
    // --- IMGBB UPLOAD ---
    // ============================================================

    function uploadToImgbb(blob, callback) {
        const key = getImgbbKey();
        if (!key) {
            const entered = prompt('Enter your imgbb API key (get one at https://api.imgbb.com):');
            if (!entered) { callback(null); return; }
            setImgbbKey(entered.trim());
        }
        const reader = new FileReader();
        reader.onload = function() {
            const base64 = reader.result.split(',')[1];
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://api.imgbb.com/1/upload',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                data: `key=${getImgbbKey()}&image=${encodeURIComponent(base64)}`,
                onload: function(r) {
                    try {
                        const res = JSON.parse(r.responseText);
                        if (res.success) callback(res.data.url);
                        else { console.error('imgbb error:', res); callback(null); }
                    } catch(e) { console.error('imgbb parse error:', e); callback(null); }
                },
                onerror: function() { callback(null); }
            });
        };
        reader.readAsDataURL(blob);
    }

    // ============================================================
    // --- CATBOX UPLOAD ---
    // ============================================================

    function uploadToCatbox(blob, callback) {
        const fd = new FormData();
        fd.append('reqtype', 'fileupload');
        const hash = getCatboxHash();
        if (hash) fd.append('userhash', hash);
        fd.append('fileToUpload', blob, 'cover.jpg');
        GM_xmlhttpRequest({
            method: 'POST',
            url: 'https://catbox.moe/user/api.php',
            data: fd,
            timeout: 30000,
            onload: function(r) {
                const url = r.responseText && r.responseText.trim();
                if (url && url.startsWith('https://')) callback(url);
                else { console.error('Catbox error:', r.responseText); callback(null); }
            },
            onerror:   function() { callback(null); },
            ontimeout: function() { callback(null); }
        });
    }

    // ============================================================
    // --- SUNGOD UPLOAD ---
    // ============================================================

    function uploadToSungod(blobOrUrl, callback) {
        let key = getSungodKey();
        if (!key) {
            const entered = prompt('Enter your TheSunGod API key (get one at https://thesungod.xyz/settings/api):');
            if (!entered) { callback(null); return; }
            setSungodKey(entered.trim());
            key = entered.trim();
        }

        // If we have a raw URL (string), use the faster rehost_new endpoint
        if (typeof blobOrUrl === 'string') {
            const fd = new FormData();
            fd.append('api_key', key);
            fd.append('link', blobOrUrl);
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'https://thesungod.xyz/api/image/rehost_new',
                data: fd,
                timeout: 30000,
                onload: function(r) {
                    try {
                        const res = JSON.parse(r.responseText);
                        if (res.link && /^https?:\/\//i.test(res.link)) { callback(res.link); return; }
                        console.error('[SunGod] rehost_new unexpected response:', r.responseText);
                        callback(null);
                    } catch(e) { console.error('[SunGod] rehost_new parse error:', e); callback(null); }
                },
                onerror:   function() { callback(null); },
                ontimeout: function() { callback(null); }
            });
            return;
        }

        // Blob upload via /api/image/upload
        const fd = new FormData();
        fd.append('api_key', key);
        fd.append('image', blobOrUrl, 'cover.jpg');
        GM_xmlhttpRequest({
            method: 'POST',
            url: 'https://thesungod.xyz/api/image/upload',
            data: fd,
            timeout: 30000,
            onload: function(r) {
                try {
                    const res = JSON.parse(r.responseText);
                    if (res.links && res.links.length > 0) { callback(res.links[0]); return; }
                    console.error('[SunGod] upload unexpected response:', r.responseText);
                    callback(null);
                } catch(e) { console.error('[SunGod] upload parse error:', e); callback(null); }
            },
            onerror:   function() { callback(null); },
            ontimeout: function() { callback(null); }
        });
    }

    // ============================================================
    // --- CUSTOM HOST UPLOAD ---
    // ============================================================

    function uploadToCustomHost(blob, callback) {
        const uploadUrl = getCustomUploadUrl();
        if (!uploadUrl) { callback(null); return; }

        const fileField = getCustomFileField() || 'file';
        const fd = new FormData();
        fd.append(fileField, blob, 'cover.jpg');

        GM_xmlhttpRequest({
            method: 'POST',
            url: uploadUrl,
            data: fd,
            timeout: 30000,
            onload: function(r) {
                try {
                    const body = r.responseText && r.responseText.trim();
                    if (!body) { console.error('Custom host: empty response'); callback(null); return; }

                    // Strategy 1: plain text response that looks like a URL
                    if (/^https?:\/\//i.test(body)) {
                        callback(body);
                        return;
                    }

                    // Strategy 2: stored response path (e.g. 'data.url')
                    const storedPath = getCustomResponsePath();
                    if (storedPath && storedPath !== 'plaintext') {
                        try {
                            const json = JSON.parse(body);
                            const val  = storedPath.split('.').reduce((o, k) => o && o[k], json);
                            if (val && /^https?:\/\//i.test(val)) { callback(val); return; }
                        } catch(e) {}
                    }

                    // Strategy 3: try common JSON paths automatically
                    try {
                        const json  = JSON.parse(body);
                        const paths = ['url','data.url','image.url','files.0.url','file.url','link','data.link','location'];
                        for (const path of paths) {
                            const val = path.split('.').reduce((o, k) => {
                                if (o === null || o === undefined) return undefined;
                                // handle numeric indices
                                return isNaN(k) ? o[k] : o[parseInt(k)];
                            }, json);
                            if (val && /^https?:\/\//i.test(val)) {
                                // Save this path for next time
                                setCustomResponsePath(path);
                                callback(val);
                                return;
                            }
                        }
                        console.error('Custom host: could not find URL in JSON response:', body);
                        callback(null);
                    } catch(e) {
                        console.error('Custom host: unparseable response:', body);
                        callback(null);
                    }
                } catch(e) {
                    console.error('Custom host error:', e);
                    callback(null);
                }
            },
            onerror:   function() { callback(null); },
            ontimeout: function() { callback(null); }
        });
    }

    // ============================================================
    // --- PTPIMG CANARY CHECK ---
    // ============================================================

    function ptpimgIsWorking(apiKey, callback) {
        const canaryBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI6QAAAABJRU5ErkJggg==';
        const binary = atob(canaryBase64);
        const bytes  = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
        const blob = new Blob([bytes], { type: 'image/png' });
        const fd   = new FormData();
        fd.append('file-upload[]', blob, 'canary.png');
        fd.append('api_key', apiKey);
        GM_xmlhttpRequest({
            method: 'POST', url: 'https://ptpimg.me/upload.php', data: fd, timeout: 8000,
            onload: function(r) {
                try {
                    const res = JSON.parse(r.responseText)[0];
                    const url = `https://ptpimg.me/${res.code}.${res.ext}`;
                    GM_xmlhttpRequest({
                        method: 'HEAD', url, timeout: 5000,
                        onload:    function(r2) { callback(r2.status === 200); },
                        onerror:   function()   { callback(false); },
                        ontimeout: function()   { callback(false); }
                    });
                } catch(e) { callback(false); }
            },
            onerror:   function() { callback(false); },
            ontimeout: function() { callback(false); }
        });
    }

    // ============================================================
    // --- UNIFIED UPLOAD WITH FALLBACK: ptpimg → imgbb → catbox ---
    // ============================================================

    function uploadWithFallback(blob, oldUrl, link, callback) {
        const apiKey = getPtpimgKey();

        function uploadToPreferredHost() {
            chooseFallbackHost(function(host) {
                if (!host) {
                    link.textContent = 'Upload cancelled';
                    link.style.color = 'red';
                    return;
                }
                if (host === 'imgbb') {
                    link.textContent = 'Uploading to imgbb...';
                    link.style.color = 'orange';
                    tryImgbb(blob, link, callback);
                } else if (host === 'catbox') {
                    link.textContent = 'Uploading to catbox...';
                    link.style.color = 'orange';
                    tryCatbox(blob, link, callback);
                } else if (host === 'sungod') {
                    link.textContent = 'Uploading to TheSunGod...';
                    link.style.color = 'orange';
                    trySungod(blob, oldUrl, link, callback);
                } else if (host === 'custom') {
                    const hostLabel = getCustomUploadUrl()
                        ? new URL(getCustomUploadUrl()).hostname
                        : 'custom host';
                    link.textContent = `Uploading to ${hostLabel}...`;
                    link.style.color = 'orange';
                    uploadToCustomHost(blob, function(url) {
                        if (url) callback(url);
                        else {
                            link.textContent = 'Custom host failed — trying catbox...';
                            link.style.color = 'orange';
                            tryCatbox(blob, link, callback);
                        }
                    });
                }
            });
        }

        if (apiKey) {
            ptpimgIsWorking(apiKey, function(working) {
                if (working) {
                    const fd = new FormData();
                    fd.append('file-upload[]', blob, 'cover.jpg');
                    fd.append('api_key', apiKey);
                    GM_xmlhttpRequest({
                        method: 'POST', url: 'https://ptpimg.me/upload.php', data: fd,
                        onload: function(r) {
                            try {
                                const res = JSON.parse(r.responseText)[0];
                                callback(`https://ptpimg.me/${res.code}.${res.ext}`);
                            } catch(e) {
                                console.warn('ptpimg upload failed, using preferred fallback host');
                                uploadToPreferredHost();
                            }
                        },
                        onerror: function() { uploadToPreferredHost(); }
                    });
                } else {
                    console.warn('ptpimg CDN down, using preferred fallback host');
                    uploadToPreferredHost();
                }
            });
        } else {
            uploadToPreferredHost();
        }
    }

    function tryImgbb(blob, link, callback) {
        link.textContent = 'Uploading to imgbb...';
        link.style.color = 'orange';
        uploadToImgbb(blob, function(url) {
            if (url) callback(url);
            else {
                console.warn('imgbb failed, trying catbox');
                tryCatbox(blob, link, callback);
            }
        });
    }

    function tryCatbox(blob, link, callback) {
        link.textContent = 'Uploading to catbox...';
        link.style.color = 'orange';
        uploadToCatbox(blob, function(url) {
            if (url) callback(url);
            else { link.textContent = 'Upload failed (all services down)'; link.style.color = 'red'; }
        });
    }

    function trySungod(blob, oldUrl, link, callback) {
        link.textContent = 'Uploading to TheSunGod...';
        link.style.color = 'orange';
        // Prefer rehost_new if we have the original URL (faster, no re-encode)
        // but not if it's the noartwork placeholder
        const isPlaceholder = oldUrl && typeof oldUrl === 'string' && oldUrl.includes('noartwork');
        const target = (!isPlaceholder && oldUrl && typeof oldUrl === 'string' && /^https?:\/\//i.test(oldUrl))
            ? oldUrl : blob;
        uploadToSungod(target, function(url) {
            if (url) callback(url);
            else {
                // If rehost failed and we used a URL, retry with blob
                if (typeof target === 'string' && blob instanceof Blob) {
                    uploadToSungod(blob, function(url2) {
                        if (url2) callback(url2);
                        else {
                            link.textContent = 'TheSunGod failed — trying catbox...';
                            link.style.color = 'orange';
                            tryCatbox(blob, link, callback);
                        }
                    });
                } else {
                    link.textContent = 'TheSunGod failed — trying catbox...';
                    link.style.color = 'orange';
                    tryCatbox(blob, link, callback);
                }
            }
        });
    }

    // ============================================================
    // --- DISCOGS TOKEN MANAGEMENT ---
    // ============================================================

    function getDiscogsToken()      { return GM_getValue('discogsToken', ''); }
    function setDiscogsToken(token) { GM_setValue('discogsToken', token); }

    function showDiscogsSetup(callback) {
        const overlay = document.createElement('div');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.92);z-index:100000;display:flex;align-items:center;justify-content:center;';
        overlay.innerHTML = `
            <div style="background:#1a1a1a;padding:35px;border-radius:12px;border:2px solid #444;max-width:550px;color:#fff;font-family:sans-serif;">
                <h2 style="margin-top:0;color:#4CAF50;">🎵 Discogs Setup Required</h2>
                <p style="color:#ccc;line-height:1.6;margin-bottom:20px;">To search Discogs for artwork, this script needs your API token.</p>
                <div style="background:#252525;padding:20px;border-radius:8px;border-left:4px solid #4CAF50;margin-bottom:20px;">
                    <h3 style="margin:0 0 10px 0;font-size:16px;color:#4CAF50;">How to get your token:</h3>
                    <ol style="margin:0;padding-left:20px;color:#aaa;line-height:1.8;">
                        <li>Click "Open Discogs" below to login</li>
                        <li>Go to Settings → Developers</li>
                        <li>Generate a new token</li>
                        <li>Copy and paste it below</li>
                    </ol>
                </div>
                <input type="text" id="discogs-token-input" placeholder="Paste your Discogs token here..."
                    style="width:100%;padding:12px;background:#222;border:1px solid #555;color:#fff;border-radius:6px;margin-bottom:20px;font-family:monospace;box-sizing:border-box;">
                <div style="display:flex;gap:10px;justify-content:flex-end;">
                    <button id="open-discogs-btn"     style="padding:12px 24px;background:#2196F3;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:bold;">Open Discogs</button>
                    <button id="save-token-btn"       style="padding:12px 24px;background:#4CAF50;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:bold;">Save Token</button>
                    <button id="cancel-discogs-setup" style="padding:12px 24px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;">Cancel</button>
                </div>
            </div>`;
        document.body.appendChild(overlay);
        const input = overlay.querySelector('#discogs-token-input');
        input.focus();
        overlay.querySelector('#open-discogs-btn').onclick     = () => window.open('https://www.discogs.com/settings/developers', '_blank');
        overlay.querySelector('#save-token-btn').onclick       = () => {
            const token = input.value.trim();
            if (token) { setDiscogsToken(token); document.body.removeChild(overlay); callback(true); }
            else alert('Please enter a valid token');
        };
        overlay.querySelector('#cancel-discogs-setup').onclick = () => { document.body.removeChild(overlay); callback(false); };
        input.addEventListener('keypress', e => { if (e.key === 'Enter') overlay.querySelector('#save-token-btn').click(); });
    }

    // ============================================================
    // --- PTPIMG.ME DASHBOARD LOGIC ---
    // ============================================================

    if (window.location.hostname.includes('ptpimg.me')) {
        const detectKeyAndShowModal = () => {
            const pageText = document.body.innerText;
            const apiInput = document.querySelector('input[value*="-"]');
            const hasApiKey = pageText.match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i) ||
                             (apiInput && apiInput.value.length === 36);
            if (hasApiKey && !document.getElementById('rehost-modal-overlay')) {
                const overlay = document.createElement('div');
                overlay.id = 'rehost-modal-overlay';
                overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:center;justify-content:center;';
                overlay.innerHTML = `
                    <div style="background:#fff;color:#333;padding:40px;border-radius:15px;text-align:center;max-width:500px;box-shadow:0 10px 30px rgba(0,0,0,0.5);font-family:sans-serif;">
                        <div style="font-size:50px;margin-bottom:20px;">✅</div>
                        <h2 style="margin:0 0 15px 0;color:#27ae60;">API Key Ready!</h2>
                        <p style="font-size:16px;line-height:1.5;color:#555;">The script has detected your credentials.<br><br>
                            <b>Click the button below</b> to close this tab. If a Tampermonkey window appears, click <b>"Always Allow"</b>.</p>
                        <button id="close-rehost-tab" style="margin-top:25px;padding:15px 30px;background:#27ae60;color:white;border:none;border-radius:8px;font-size:18px;font-weight:bold;cursor:pointer;width:100%;">Close Tab & Continue</button>
                    </div>`;
                document.body.appendChild(overlay);
                document.getElementById('close-rehost-tab').onclick = () => window.close();
            }
        };
        detectKeyAndShowModal();
        setInterval(detectKeyAndShowModal, 1500);
        return;
    }

    // ============================================================
    // --- PARSE ALBUM INFO FROM PAGE ---
    // ============================================================

    function parseAlbumInfo() {
        const info = { artist: '', album: '', year: '' };

        // Read artist and album from structured DOM elements — immune to other
        // userscripts injecting text into the h2's textContent.
        const artistLink = document.querySelector('.header h2 a[href*="artist.php"]');
        if (artistLink) info.artist = artistLink.textContent.trim();

        // Gazelle wraps the album title in <span dir="ltr">
        const albumSpan = document.querySelector('.header h2 span[dir="ltr"]');
        if (albumSpan) {
            info.album = albumSpan.textContent.trim();
        } else {
            // Fallback: strip artist prefix from raw h2 text
            const header = document.querySelector('.header h2');
            if (header) {
                const text  = header.textContent.trim();
                const match = text.match(/^(.+?)\s*[-–—]\s*(.+?)$/);
                if (match) {
                    if (!info.artist) info.artist = match[1].trim();
                    info.album = match[2].trim();
                } else { info.album = text; }
            }
        }

        // Year from the edition/group info block
        if (!info.year) {
            const groupInfo = document.querySelector('.group_info');
            if (groupInfo) {
                const yearMatch = groupInfo.textContent.match(/\b(19|20)\d{2}\b/);
                if (yearMatch) info.year = yearMatch[0];
            }
        }

        // Strip metadata tags like [2001], [Anthology], [Deluxe Edition] from album title
        info.album = info.album
            .replace(/\s*\[[^\]]*\]\s*/g, ' ')
            .replace(/\s{2,}/g, ' ')
            .trim();

        return info;
    }

    // ============================================================
    // --- MULTI-SOURCE IMAGE EXTRACTION ---
    // ============================================================

    function getPageSourceLinks() {
        const links = [];
        const seen  = new Set();
        document.querySelectorAll('a[href]').forEach(a => {
            const href = a.href;
            if (!href || seen.has(href)) return;
            seen.add(href);
            const label = a.textContent.trim() || new URL(href).hostname;

            if      (/discogs\.com\/(?:[^/]+\/)?(?:release|master)\/\d+/i.test(href))
                links.push({ href, source: 'Discogs Direct', label });
            else if (/musicbrainz\.org\/release\/[a-f0-9-]{36}/i.test(href))
                links.push({ href, source: 'MusicBrainz', label });
            else if (/(?:open\.)?qobuz\.com\/(?:[a-z]{2}-[a-z]{2}\/)?album\//i.test(href))
                links.push({ href, source: 'Qobuz', label });
            else if (/music\.apple\.com.*\/album/i.test(href) || /itunes\.apple\.com.*\/album/i.test(href))
                links.push({ href, source: 'Apple Music', label });
            else if (/bandcamp\.com\/(album|music)/i.test(href) || /\.bandcamp\.com/i.test(href))
                links.push({ href, source: 'Bandcamp', label });
            else if (/deezer\.com\/(?:\w+\/)?album\/\d+/i.test(href))
                links.push({ href, source: 'Deezer', label });
            else if (/tidal\.com\/(album|browse\/album)/i.test(href))
                links.push({ href, source: 'Tidal', label });
            else if (/open\.spotify\.com\/album/i.test(href))
                links.push({ href, source: 'Spotify', label });
            else if (/amazon\.(com|co\.uk|de|fr).*\/(dp|gp\/product)/i.test(href))
                links.push({ href, source: 'Amazon', label });
        });
        return links;
    }

    function resolveSourceImage(linkObj, callback) {
        const { href, source } = linkObj;
        if      (source === 'Discogs Direct') resolveDiscogsRelease(href, callback);
        else if (source === 'MusicBrainz')    resolveMusicBrainz(href, callback);
        else if (source === 'Apple Music')    resolveAppleMusic(href, callback);
        else if (source === 'Qobuz')          resolveQobuz(href, callback);
        else if (source === 'Deezer')         resolveDeezer(href, callback);
        else if (source === 'Bandcamp')       resolveBandcamp(href, callback);
        else if (source === 'Tidal')          resolveViaOgImage(href, source, callback);
        else if (source === 'Spotify')        resolveViaOgImage(href, source, callback);
        else if (source === 'Amazon')         resolveViaOgImage(href, source, callback);
        else callback(null);
    }

    // --- Discogs: resolve release or master via API, return all images ---
    function resolveDiscogsRelease(href, callback) {
        const token        = getDiscogsToken();
        const releaseMatch = href.match(/discogs\.com\/(?:[^/]+\/)?release\/(\d+)/i);
        const masterMatch  = href.match(/discogs\.com\/(?:[^/]+\/)?master\/(\d+)/i);

        let resourceUrl = null;
        let isMaster    = false;
        let masterId    = null;

        if (releaseMatch)     { resourceUrl = `https://api.discogs.com/releases/${releaseMatch[1]}`; }
        else if (masterMatch) { masterId = masterMatch[1]; resourceUrl = `https://api.discogs.com/masters/${masterId}`; isMaster = true; }
        else { callback(null); return; }

        const headers = { 'User-Agent': 'CoverUp/6.47' };
        if (token) headers['Authorization'] = `Discogs token=${token}`;

        function releaseSummary(rel) {
            const parts = [];
            if (rel.year || rel.released) parts.push(rel.year || rel.released);
            if (rel.country) parts.push(rel.country);
            const fmt = Array.isArray(rel.format) ? rel.format : [];
            if (fmt.length) parts.push(fmt.join(', '));
            const lbl = Array.isArray(rel.label) ? rel.label : [];
            if (lbl.length) parts.push(lbl.slice(0, 2).join(', '));
            return parts.join(' \u2022 ') || 'Discogs release';
        }

        function emitImages(data, pageHref, prefix) {
            const images = data.images || [];
            if (images.length === 0) {
                if (data.thumb) callback({ imageUrl: data.thumb, displayUrl: pageHref, source: 'Discogs Direct', label: prefix ? `${prefix} \u2014 Thumb` : 'Thumb' });
                return;
            }
            const sorted = [...images.filter(i => i.type === 'primary'), ...images.filter(i => i.type !== 'primary')];
            sorted.forEach((img, idx) => callback({
                imageUrl:   img.uri,
                displayUrl: pageHref,
                source:     'Discogs Direct',
                label:      prefix ? `${prefix} \u2014 ${idx === 0 ? 'Primary' : 'Image ' + (idx + 1)}` : (idx === 0 ? 'Primary' : 'Image ' + (idx + 1))
            }));
        }

        GM_xmlhttpRequest({
            method: 'GET', url: resourceUrl, headers, timeout: 10000,
            onload: function(r) {
                try {
                    const data = JSON.parse(r.responseText);
                    emitImages(data, href, isMaster ? 'Master' : 'Release');
                    if (!isMaster) {
                        callback({
                            source: 'Discogs Releases', type: 'release_list', displayUrl: href,
                            releases: [{
                                id:         data.id,
                                title:      data.title || 'Release',
                                year:       data.year || '',
                                country:    data.country || '',
                                format:     data.formats ? data.formats.map(f => f.name) : [],
                                label:      data.labels  ? data.labels.map(l => l.name)  : [],
                                thumb:      data.thumb || '',
                                coverImage: (data.images || []).find(i => i.type === 'primary')?.uri || data.thumb || '',
                                webUrl:     data.uri || `https://www.discogs.com/release/${data.id}`,
                                summary:    releaseSummary({ year: data.year, country: data.country,
                                    format: data.formats ? data.formats.map(f => f.name) : [],
                                    label:  data.labels  ? data.labels.map(l => l.name)  : [] })
                            }]
                        });
                    }
                } catch(e) { console.warn('[CoverUp] Discogs resolve error:', e); callback(null); }
            },
            onerror: function() { callback(null); },
            ontimeout: function() { callback(null); }
        });

        if (isMaster && masterId) {
            const allVersions = [];
            function emitAllVersions() {
                if (!allVersions.length) return;
                callback({
                    source: 'Discogs Releases', type: 'release_list', displayUrl: href,
                    releases: allVersions.map(rel => ({
                        id:         rel.id,
                        title:      rel.title || 'Release',
                        year:       rel.released || rel.year || '',
                        country:    rel.country || '',
                        format:     Array.isArray(rel.format) ? rel.format : [],
                        label:      Array.isArray(rel.label)  ? rel.label  : [],
                        thumb:      rel.thumb || '',
                        coverImage: rel.thumb || '',
                        webUrl:     `https://www.discogs.com/release/${rel.id}`,
                        summary:    releaseSummary(rel)
                    }))
                });
            }
            function fetchVersionsPage(page) {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.discogs.com/masters/${masterId}/versions?per_page=100&page=${page}&sort=released&sort_order=asc`,
                    headers, timeout: 15000,
                    onload: function(r) {
                        try {
                            const data     = JSON.parse(r.responseText);
                            const versions = Array.isArray(data.versions) ? data.versions : [];
                            versions.forEach(v => allVersions.push(v));
                            const pagination  = data.pagination  || {};
                            const totalPages  = pagination.pages || 1;
                            const currentPage = pagination.page  || page;
                            if (currentPage < totalPages) {
                                setTimeout(() => fetchVersionsPage(currentPage + 1), 250);
                            } else {
                                emitAllVersions();
                            }
                        } catch(e) { console.warn('[CoverUp] Discogs versions page error:', e); }
                    },
                    onerror:   function() { console.warn('[CoverUp] Discogs versions fetch failed page ' + page); },
                    ontimeout: function() { console.warn('[CoverUp] Discogs versions timeout page ' + page); }
                });
            }
            fetchVersionsPage(1);
        }
    }

    // --- MusicBrainz: Cover Art Archive API — all images for a release ---
    function resolveMusicBrainz(href, callback) {
        const mbidMatch = href.match(/musicbrainz\.org\/release\/([a-f0-9-]{36})/i);
        if (!mbidMatch) { callback(null); return; }

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://coverartarchive.org/release/${mbidMatch[1]}`,
            headers: { 'User-Agent': 'RehostREDCovers/6.17 ( https://greasyfork.org/users/1568924 )' },
            timeout: 10000,
            onload: function(r) {
                try {
                    const data   = JSON.parse(r.responseText);
                    const images = data.images || [];
                    if (images.length === 0) { callback(null); return; }
                    const sorted = [
                        ...images.filter(img => img.front),
                        ...images.filter(img => !img.front)
                    ];
                    sorted.forEach((img, i) => {
                        const imageUrl = (img.thumbnails && img.thumbnails['1200'])
                            ? img.thumbnails['1200']
                            : (img.thumbnails && img.thumbnails.large)
                            ? img.thumbnails.large
                            : img.image;
                        const types = img.types && img.types.length
                            ? img.types.join(', ')
                            : (img.front ? 'Front' : 'Image');
                        callback({
                            imageUrl,
                            displayUrl: href,
                            source: 'MusicBrainz',
                            label:  i === 0 ? types : `${types} ${i + 1}`
                        });
                    });
                } catch(e) { console.warn('MusicBrainz CAA error:', e); callback(null); }
            },
            onerror:   function() { callback(null); },
            ontimeout: function() { callback(null); }
        });
    }

    // --- Apple Music: iTunes lookup API at max resolution ---
    function resolveAppleMusic(href, callback) {
        const idMatch = href.match(/\/album\/(?:[^/]+\/)?(\d+)/);
        if (!idMatch) { callback(null); return; }
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://itunes.apple.com/lookup?id=${idMatch[1]}&entity=album`,
            onload: function(r) {
                try {
                    const data   = JSON.parse(r.responseText);
                    const result = data.results && data.results[0];
                    if (result && result.artworkUrl100) {
                        const imageUrl = result.artworkUrl100
                            .replace(/\d+x\d+bb\.jpg$/, '10000x10000bb.jpg')
                            .replace(/\d+x\d+\.jpg$/,   '10000x10000.jpg');
                        callback({ imageUrl, displayUrl: href, source: 'Apple Music' });
                    } else callback(null);
                } catch(e) { console.warn('Apple Music lookup error:', e); callback(null); }
            },
            onerror: function() { callback(null); }
        });
    }

    // --- Qobuz: all regional variants and open.qobuz.com ---
    // Patterns: qobuz.com/gb-en/album/title/ID  |  open.qobuz.com/album/ID
    function resolveQobuz(href, callback) {
        const idMatch = href.match(/\/album\/(?:[^/]+\/)?([0-9a-zA-Z]+)\/?(?:[?#].*)?$/);
        if (!idMatch) { resolveViaOgImage(href, 'Qobuz', callback); return; }

        const albumId = idMatch[1];
        const level1  = albumId.slice(-2)   || '00';  // last 2 chars
        const level2  = albumId.slice(-4,-2) || '00'; // chars before that
        const cdnUrl  = `https://static.qobuz.com/images/covers/${level1}/${level2}/${albumId}_org.jpg`;
        const cdnUrl600 = `https://static.qobuz.com/images/covers/${level1}/${level2}/${albumId}_600.jpg`;

        // Extract search query from URL slug — more reliable than album ID search
        // e.g. /album/animal-style-wynona-bleach/g47k7vpgx1mgp -> "animal style wynona bleach"
        const slugMatch = href.match(/\/album\/([^/]+)\/[^/]+$/);
        const slugQuery = slugMatch ? slugMatch[1].replace(/-/g, ' ') : '';
        const searchQuery = slugQuery || albumId;

        function trySlugSearch() {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://www.qobuz.com/api.json/0.2/album/search?query=${encodeURIComponent(searchQuery)}&limit=5`,
                headers: { 'X-App-Id': '285473059', 'User-Agent': 'Mozilla/5.0' },
                timeout: 8000,
                onload: function(r) {
                    try {
                        const data  = JSON.parse(r.responseText);
                        const items = (data.albums && data.albums.items) || [];
                        const match = items.find(it => String(it.id) === albumId) || items[0];
                        const img   = match && match.image && (match.image.mega || match.image.large);
                        if (img) callback({ imageUrl: img, displayUrl: href, source: 'Qobuz' });
                        else callback(null);
                    } catch(e) { callback(null); }
                },
                onerror: function() { callback(null); }
            });
        }

        // Try CDN URL via Image probe first (fast path for older/established releases)
        // Fall back to slug search if CDN 404s or errors
        const probe = new Image();
        let settled = false;
        const timer = setTimeout(() => {
            if (!settled) { settled = true; trySlugSearch(); }
        }, 4000);
        probe.onload = () => {
            if (settled) return; settled = true; clearTimeout(timer);
            callback({ imageUrl: cdnUrl, displayUrl: href, source: 'Qobuz' });
        };
        probe.onerror = () => {
            if (settled) return; settled = true; clearTimeout(timer);
            trySlugSearch();
        };
        probe.src = cdnUrl;
        // If _org 404s, also try _600
        probe.onerror = () => {
            const probe2 = new Image();
            probe2.onload = () => {
                if (settled) return; settled = true; clearTimeout(timer);
                callback({ imageUrl: cdnUrl600, displayUrl: href, source: 'Qobuz' });
            };
            probe2.onerror = () => {
                if (settled) return; settled = true; clearTimeout(timer);
        trySlugSearch();
            };
            probe2.src = cdnUrl600;
        };
    }

    // --- Deezer: public API returns cover_xl ---
    function resolveDeezer(href, callback) {
        const idMatch = href.match(/\/album\/(\d+)/);
        if (!idMatch) { callback(null); return; }
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://api.deezer.com/album/${idMatch[1]}`,
            onload: function(r) {
                try {
                    const data = JSON.parse(r.responseText);
                    // Standard cover fields (may be empty for some regions)
                    let imageUrl = data.cover_xl || data.cover_big || data.cover_medium || data.cover;
                    // Fallback: construct URL from md5_image which is always present
                    if (!imageUrl && data.md5_image) {
                        imageUrl = `https://cdn-images.dzcdn.net/images/cover/${data.md5_image}/1000x1000-000000-80-0-0.jpg`;
                    }
                    if (imageUrl) callback({ imageUrl, displayUrl: href, source: 'Deezer' });
                    else callback(null);
                } catch(e) { console.warn('Deezer API error:', e); callback(null); }
            },
            onerror: function() { callback(null); }
        });
    }

    // --- Generic: fetch page HTML and extract og:image / twitter:image ---
    // --- Bandcamp: extract art_id from embedded JSON for true original resolution ---
    function resolveBandcamp(href, callback) {
        GM_xmlhttpRequest({
            method: 'GET', url: href, timeout: 10000,
            onload: function(r) {
                try {
                    const parser = new DOMParser();
                    const doc    = parser.parseFromString(r.responseText, 'text/html');

                    // Bandcamp embeds release data in a <script data-tralbum> JSON blob
                    // which contains art_id — use that to construct the full-res URL
                    const tralbum = doc.querySelector('script[data-tralbum]');
                    if (tralbum) {
                        const data   = JSON.parse(tralbum.dataset.tralbum);
                        const artId  = data.art_id || (data.current && data.current.art_id);
                        if (artId) {
                            // _0 suffix = original resolution on Bandcamp's CDN
                            const imageUrl = `https://f4.bcbits.com/img/a${artId}_0.jpg`;
                            callback({ imageUrl, displayUrl: href, source: 'Bandcamp' });
                            return;
                        }
                    }

                    // Fallback: og:image with _7 → _0 bump
                    const og = doc.querySelector('meta[property="og:image"]') ||
                               doc.querySelector('meta[name="twitter:image"]') ||
                               doc.querySelector('meta[property="og:image:secure_url"]');
                    if (og && og.content) {
                        const imageUrl = og.content.replace(/_7\.(jpg|jpeg|png)/i, '_0.$1');
                        callback({ imageUrl, displayUrl: href, source: 'Bandcamp' });
                    } else callback(null);
                } catch(e) { console.warn('Bandcamp parse error:', e); callback(null); }
            },
            onerror:   function() { callback(null); },
            ontimeout: function() { callback(null); }
        });
    }

    // --- Generic: fetch page HTML and extract og:image / twitter:image ---
    function resolveViaOgImage(href, source, callback) {
        GM_xmlhttpRequest({
            method: 'GET', url: href, timeout: 10000,
            onload: function(r) {
                try {
                    const parser = new DOMParser();
                    const doc    = parser.parseFromString(r.responseText, 'text/html');
                    const og     = doc.querySelector('meta[property="og:image"]') ||
                                   doc.querySelector('meta[name="twitter:image"]') ||
                                   doc.querySelector('meta[property="og:image:secure_url"]');
                    if (og && og.content) callback({ imageUrl: og.content, displayUrl: href, source });
                    else callback(null);
                } catch(e) { console.warn(`og:image parse error for ${source}:`, e); callback(null); }
            },
            onerror:   function() { callback(null); },
            ontimeout: function() { callback(null); }
        });
    }

    // ============================================================
    // --- DISCOGS KEYWORD SEARCH ---
    // ============================================================

    // Search Discogs using multiple query strategies, then fetch full release
    // images for any result that has an empty cover_image in the search index.
    function searchDiscogs(query, callback, albumInfo) {
        const token = getDiscogsToken();
        if (!token) { callback([]); return; }

        function stripPunct(s) {
            return (s || '').replace(/[?!&–—]/g, ' ').replace(/\s{2,}/g, ' ').trim();
        }

        function buildStrategies(rawQuery, info) {
            const cleaned = stripPunct(rawQuery);
            const artist = stripPunct(info && info.artist ? info.artist : '');
            const album  = stripPunct(info && info.album ? info.album : '');
            // Structured field queries first (most precise), then freetext fallback
            const C = (artist && album)
                ? 'artist=' + encodeURIComponent(artist) + '&release_title=' + encodeURIComponent(album) + '&type=release&per_page=10'
                : null;
            const D = album
                ? 'release_title=' + encodeURIComponent(album) + '&type=release&per_page=10'
                : null;
            // Freetext fallback — catches cases where field search returns nothing
            const E = (artist && album)
                ? 'q=' + encodeURIComponent(artist + ' ' + album) + '&type=release&per_page=10'
                : null;
            const seen = new Set();
            return [C, D, E].filter(s => s && !seen.has(s) && seen.add(s));
        }

        function fetchReleaseImages(releaseId, cb) {
            GM_xmlhttpRequest({
                method:  'GET',
                url:     'https://api.discogs.com/releases/' + releaseId + '?token=' + token,
                headers: { 'User-Agent': 'REDCoverRehost/6.21' },
                timeout: 15000,
                onload: function(r) {
                    try {
                        const data   = JSON.parse(r.responseText);
                        const images = (data.images || []).filter(function(img) { return img.uri && img.uri.startsWith('http'); });
                        cb(images);
                    } catch(e) { cb([]); }
                },
                onerror: function() { cb([]); },
                ontimeout: function() { cb([]); }
            });
        }

        const strategies = buildStrategies(query, albumInfo || {});
        let strategyIndex = 0;

        function tryNextStrategy() {
            if (strategyIndex >= strategies.length) {
                console.warn('[Rehost] All Discogs strategies exhausted — no results found.');
                callback([]); return;
            }
            const params = strategies[strategyIndex++];
            const fullUrl = 'https://api.discogs.com/database/search?' + params + '&token=' + token;
            console.log('[Rehost] Discogs strategy', strategyIndex, ':', fullUrl.replace(token, 'TOKEN'));
            GM_xmlhttpRequest({
                method: 'GET',
                url: fullUrl,
                headers: { 'User-Agent': 'REDCoverRehost/6.21' },
                timeout: 15000,
                onload: function(r) {
                    console.log('[Rehost] Discogs response status:', r.status, '— body length:', r.responseText.length);
                    try {
                        const parsed = JSON.parse(r.responseText);
                        const results = parsed.results || [];
                        console.log('[Rehost] Discogs results count:', results.length,
                            results.map(function(x){ return x.id + ':' + (x.cover_image ? 'has_cover' : 'no_cover'); }));
                        // Filter results for relevance before accepting
                        const artistLow = (albumInfo && albumInfo.artist || '').toLowerCase();
                        const albumLow  = (albumInfo && albumInfo.album  || '').toLowerCase();
                        const relevant  = results.filter(r => {
                            const t = (r.title || '').toLowerCase();
                            // Discogs title format is "Artist - Album"
                            // Check artist words first (most discriminating)
                            const artistWords = artistLow.split(/\s+/).filter(w => w.length > 2);
                            if (artistWords.length && !artistWords.some(w => t.includes(w))) return false;
                            // If no artist words matched nothing or artist is short/common,
                            // also check album words
                            const albumWords = albumLow.split(/\s+/).filter(w => w.length > 3);
                            if (!artistWords.length && albumWords.length && !albumWords.some(w => t.includes(w))) return false;
                            return true;
                        });
                        console.log('[Rehost] Discogs relevant after filter:', relevant.length, 
                            'from', results.length, 'artistLow:', artistLow, 
                            'titles:', results.slice(0,3).map(r => r.title));
                        if (relevant.length === 0) { tryNextStrategy(); return; }
                        const results_filtered = relevant;
                        // Re-assign for downstream use
                        results.length = 0; results_filtered.forEach(r => results.push(r));
                        if (results.length === 0) { tryNextStrategy(); return; }
                        const withoutCover = results.filter(function(rel) {
                            return !rel.cover_image || rel.cover_image.includes('spacer.gif');
                        });
                        console.log('[Rehost] Results needing release fetch:', withoutCover.length);
                        if (withoutCover.length === 0) { callback(results); return; }
                        const toFetch = withoutCover.slice(0, 5);
                        let fetched = 0;
                        toFetch.forEach(function(rel) {
                            fetchReleaseImages(rel.id, function(images) {
                                console.log('[Rehost] Release', rel.id, 'fetch returned', images.length, 'images');
                                if (images.length > 0) {
                                    const primary = images.find(function(i) { return i.type === 'primary'; }) || images[0];
                                    rel.cover_image = primary.uri;
                                    rel._allImages = images;
                                }
                                fetched++;
                                if (fetched === toFetch.length) {
                                    console.log('[Rehost] All release fetches done — calling back with', results.length, 'results');
                                    callback(results);
                                }
                            });
                        });
                    } catch(e) { console.error('[Rehost] Discogs parse error:', e); tryNextStrategy(); }
                },
                onerror: function(e) { console.error('[Rehost] Discogs XHR error:', e); tryNextStrategy(); },
                ontimeout: function() { console.warn('[Rehost] Discogs XHR timeout'); tryNextStrategy(); }
            });
        }

        tryNextStrategy();
    }

    // ============================================================
    // --- UNIFIED MULTI-SOURCE IMAGE PICKER OVERLAY ---
    // ============================================================

    const SOURCE_BADGE_COLORS = {
        'Discogs Direct': '#333',
        'MusicBrainz':    '#ba478f',
        'Apple Music':    '#fc3c44',
        'Qobuz':          '#0070c9',
        'Deezer':         '#a238ff',
        'Bandcamp':       '#1da0c3',
        'Tidal':          '#222',
        'Spotify':        '#1DB954',
        'Amazon':         '#ff9900',
    };


    // ============================================================
    // --- DEEZER SEARCH ---
    // ============================================================

    function searchDeezer(albumInfo, callback) {
        const artist = (albumInfo.artist || '').trim();
        const album  = (albumInfo.album  || '').trim();
        if (!artist && !album) { callback([]); return; }

        const q = [artist, album].filter(Boolean).join(' ');
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://api.deezer.com/search/album?q=${encodeURIComponent(q)}&limit=10`,
            timeout: 10000,
            onload: function(r) {
                try {
                    const data = JSON.parse(r.responseText);
                    const items = data.data || [];
                    callback(items);
                } catch(e) { callback([]); }
            },
            onerror:   function() { callback([]); },
            ontimeout: function() { callback([]); }
        });
    }

    // ============================================================
    // --- MUSICBRAINZ SEARCH ---
    // ============================================================

    function searchMusicBrainz(albumInfo, callback) {
        const artist = (albumInfo.artist || '').trim();
        const album  = (albumInfo.album  || '').trim();
        if (!artist && !album) { callback([]); return; }

        // MusicBrainz Lucene query: artist + release fields
        let luceneQuery;
        if (artist && album) {
            luceneQuery = `artist:"${artist}" AND release:"${album}"`;
        } else if (album) {
            luceneQuery = `release:"${album}"`;
        } else {
            luceneQuery = `artist:"${artist}"`;
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(luceneQuery)}&fmt=json&limit=8`,
            headers: { 'User-Agent': 'CoverUp/6.37 ( https://greasyfork.org/users/1568924 )' },
            timeout: 12000,
            onload: function(r) {
                try {
                    const data     = JSON.parse(r.responseText);
                    const releases = (data.releases || []).filter(rel => rel.id);
                    if (releases.length === 0) { callback([]); return; }

                    const results = [];
                    let pending   = releases.length;

                    releases.forEach(rel => {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: `https://coverartarchive.org/release/${rel.id}`,
                            headers: { 'User-Agent': 'CoverUp/6.37 ( https://greasyfork.org/users/1568924 )' },
                            timeout: 8000,
                            onload: function(r2) {
                                try {
                                    const caa = JSON.parse(r2.responseText);
                                    if (caa.images && caa.images.length > 0) {
                                        const sorted = [
                                            ...caa.images.filter(i => i.front),
                                            ...caa.images.filter(i => !i.front)
                                        ];
                                        sorted.forEach(img => {
                                            const imageUrl = (img.thumbnails && img.thumbnails['1200'])
                                                || (img.thumbnails && img.thumbnails.large)
                                                || img.image;
                                            results.push({
                                                imageUrl,
                                                title:    rel.title,
                                                artist:   rel['artist-credit'] && rel['artist-credit'][0]
                                                            ? rel['artist-credit'][0].name : '',
                                                date:     rel.date || rel['release-group'] && rel['release-group']['first-release-date'] || '',
                                                label:    img.front ? 'Front' : (img.types && img.types[0] || 'Image'),
                                                mbid:     rel.id
                                            });
                                        });
                                    }
                                } catch(e) {}
                                if (--pending === 0) callback(results);
                            },
                            onerror:   function() { if (--pending === 0) callback(results); },
                            ontimeout: function() { if (--pending === 0) callback(results); }
                        });
                    });
                } catch(e) { callback([]); }
            },
            onerror:   function() { callback([]); },
            ontimeout: function() { callback([]); }
        });
    }

    // ============================================================
    // --- QOBUZ SEARCH ---
    // ============================================================

    function searchQobuz(albumInfo, callback) {
        const artist = (albumInfo.artist || '').trim();
        const album  = (albumInfo.album  || '').trim();
        if (!artist && !album) { callback([]); return; }

        // Qobuz public search — artist+album gives much better results than freetext
        const q = [artist, album].filter(Boolean).join(' ');

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://www.qobuz.com/api.json/0.2/album/search?query=${encodeURIComponent(q)}&limit=10`,
            headers: {
                'X-App-Id': '285473059',
                'User-Agent': 'Mozilla/5.0'
            },
            timeout: 12000,
            onload: function(r) {
                try {
                    const data  = JSON.parse(r.responseText);
                    const items = (data.albums && data.albums.items) || [];
                    // Boost exact-match items to the top
                    const albumLower  = album.toLowerCase();
                    const artistLower = artist.toLowerCase();
                    items.sort((a, b) => {
                        const aMatch = (a.title || '').toLowerCase().includes(albumLower) &&
                                       (a.artist && a.artist.name || '').toLowerCase().includes(artistLower);
                        const bMatch = (b.title || '').toLowerCase().includes(albumLower) &&
                                       (b.artist && b.artist.name || '').toLowerCase().includes(artistLower);
                        return (bMatch ? 1 : 0) - (aMatch ? 1 : 0);
                    });
                    callback(items);
                } catch(e) { callback([]); }
            },
            onerror:   function() { callback([]); },
            ontimeout: function() { callback([]); }
        });
    }


    // ============================================================
    // --- BANDCAMP SEARCH ---
    // ============================================================

    function searchBandcamp(albumInfo, callback) {
        const artist = (albumInfo.artist || '').trim();
        const album  = (albumInfo.album  || '').trim();
        if (!album) { callback([]); return; }

        // Skip artist for VA/unknown releases — "Unknown Artist(s)" / "Various Artists"
        // are RED-specific labels that won't match anything on Bandcamp
        const VA_PATTERNS = /^(unknown artist|various artists?|va|various|multiple artists?)$/i;
        const useArtist = artist && !VA_PATTERNS.test(artist);

        // Also try a shorter query using only the part before a colon in the album title
        // e.g. "The Weevil Series: Pupa" → try "The Weevil Series Pupa" and "Pupa"
        const albumShort = album.replace(/\s*:.*$/, '').trim();

        const q = [useArtist ? artist : '', album].filter(Boolean).join(' ').trim()
               || album;

        function parseResults(html) {
            const parser = new DOMParser();
            const doc    = parser.parseFromString(html, 'text/html');
            const results = [];
            doc.querySelectorAll('.result-items li').forEach(el => {
                // Accept album-type items (filter by itemtype div or class)
                const itemType = el.querySelector('.itemtype');
                const typeText = itemType ? itemType.textContent.trim().toUpperCase() : '';
                if (typeText && typeText !== 'ALBUM') return;

                const thumb     = el.querySelector('img');
                const headingEl = el.querySelector('.heading a');
                const subheadEl = el.querySelector('.subhead');
                if (!thumb || !headingEl) return;

                // Must use getAttribute('src') not .src — DOMParser doesn't
                // load resources so .src returns a wrong origin-prefixed URL
                const src = thumb.getAttribute('data-src') || thumb.getAttribute('src') || '';
                if (!src || !src.includes('bcbits.com')) return; // skip if no valid CDN URL
                const imageUrl = src.replace(/_\d+\.(jpg|jpeg|png)/i, '_0.$1');
                const href   = headingEl.href;
                const title  = headingEl.textContent.trim();
                const artist = (subheadEl ? subheadEl.textContent : '').replace(/^by\s*/i, '').trim();
                if (imageUrl && href) results.push({ imageUrl, href, title, artist });
            });
            return results;
        }

        function doSearch(query, fallbackQuery) {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://bandcamp.com/search?q=${encodeURIComponent(query)}&item_type=a`,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                },
                timeout: 12000,
                onload: function(r) {
                    try {
                        const results = parseResults(r.responseText);
                        if (results.length === 0 && fallbackQuery && fallbackQuery !== query) {
                            // Retry with shorter/simpler query
                            doSearch(fallbackQuery, null);
                        } else {
                            callback(results);
                        }
                    } catch(e) { console.warn('Bandcamp search error:', e); callback([]); }
                },
                onerror:   function() { callback([]); },
                ontimeout: function() { callback([]); }
            });
        }

        // Try full query first; fall back to album title only (short form before colon)
        const fallback = albumShort !== album ? albumShort : (useArtist ? album : null);
        doSearch(q, fallback);
    }

    // ============================================================
    // --- ITUNES SEARCH ---
    // ============================================================

    function searchItunes(albumInfo, callback) {
        const artist = (albumInfo.artist || '').trim();
        const album  = (albumInfo.album  || '').trim();
        if (!artist && !album) { callback([]); return; }

        // iTunes Search API: term = "Artist Album", entity=album, media=music
        const term = [artist, album].filter(Boolean).join(' ');
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://itunes.apple.com/search?term=${encodeURIComponent(term)}&entity=album&media=music&limit=12`,
            timeout: 10000,
            onload: function(r) {
                try {
                    const data = JSON.parse(r.responseText);
                    const items = (data.results || []).filter(a => a.artworkUrl100);
                    // Boost exact artist+album matches to top
                    const aLow = artist.toLowerCase();
                    const bLow = album.toLowerCase();
                    items.sort((x, y) => {
                        const xMatch = (x.collectionName || '').toLowerCase().includes(bLow) &&
                                       (x.artistName     || '').toLowerCase().includes(aLow);
                        const yMatch = (y.collectionName || '').toLowerCase().includes(bLow) &&
                                       (y.artistName     || '').toLowerCase().includes(aLow);
                        return (yMatch ? 1 : 0) - (xMatch ? 1 : 0);
                    });
                    callback(items);
                } catch(e) { callback([]); }
            },
            onerror:   function() { callback([]); },
            ontimeout: function() { callback([]); }
        });
    }

    // ============================================================
    // --- AMAZON SEARCH ---
    // ============================================================

    function searchAmazon(albumInfo, callback) {
        const artist = (albumInfo.artist || '').trim();
        const album  = (albumInfo.album  || '').trim();
        if (!artist && !album) { callback([]); return; }

        // Amazon Music search via their open search endpoint
        // Returns product pages — we extract the ASIN and build the image URL directly
        const query = [artist, album].filter(Boolean).join(' ');
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://www.amazon.com/s?k=${encodeURIComponent(query)}&i=popular&search-type=ss`,
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept-Language': 'en-US,en;q=0.9'
            },
            timeout: 12000,
            onload: function(r) {
                try {
                    const parser = new DOMParser();
                    const doc    = parser.parseFromString(r.responseText, 'text/html');
                    const results = [];
                    const seen    = new Set();

                    // Extract ASINs from search results
                    doc.querySelectorAll('[data-asin]').forEach(el => {
                        const asin = el.getAttribute('data-asin');
                        if (!asin || asin.length < 10 || seen.has(asin)) return;
                        seen.add(asin);

                        // Try to get the image from the result card
                        const img = el.querySelector('img.s-image');
                        let imageUrl = img && img.getAttribute('src');
                        // Upgrade to high-res: replace the size suffix
                        if (imageUrl) {
                            imageUrl = imageUrl.replace(/\._[A-Z0-9_,]+_\./, '.');
                        }

                        const titleEl = el.querySelector('h2 span, .a-text-normal');
                        const title   = titleEl ? titleEl.textContent.trim() : asin;

                        if (imageUrl && imageUrl.startsWith('https://')) {
                            results.push({ asin, imageUrl, title });
                        }
                    });

                    callback(results.slice(0, 10));
                } catch(e) { callback([]); }
            },
            onerror:   function() { callback([]); },
            ontimeout: function() { callback([]); }
        });
    }

    function createImagePickerOverlay(albumInfo, callback) {
        const overlay = document.createElement('div');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.92);z-index:100000;display:flex;align-items:center;justify-content:center;overflow:auto;';

        const container = document.createElement('div');
        container.style.cssText = 'background:#1a1a1a;padding:30px;border-radius:12px;border:2px solid #444;max-width:960px;width:92%;color:#fff;font-family:sans-serif;max-height:92vh;overflow-y:auto;';

        container.innerHTML = `
            <h2 style="margin-top:0;color:#4CAF50;">🎵 Select Artwork</h2>
            <p style="color:#ccc;margin-bottom:16px;">
                <strong>${albumInfo.artist}${albumInfo.album ? ' – ' + albumInfo.album : ''}${albumInfo.year ? ' (' + albumInfo.year + ')' : ''}</strong>
            </p>
            <div id="picker-tabs" style="display:flex;gap:6px;margin-bottom:20px;flex-wrap:wrap;">
                <button class="picker-tab active" data-tab="sources"
                    style="padding:7px 16px;border-radius:6px;border:none;cursor:pointer;font-size:13px;font-weight:bold;background:#4CAF50;color:#fff;">
                    📄 Page Sources <span id="sources-count" style="opacity:0.7;">(scanning…)</span>
                </button>
                <button class="picker-tab" data-tab="discogs"
                    style="padding:7px 16px;border-radius:6px;border:none;cursor:pointer;font-size:13px;font-weight:bold;background:#333;color:#aaa;">
                    🔍 Discogs Search <span id="discogs-count" style="opacity:0.7;">(loading…)</span>
                </button>
                <button class="picker-tab" data-tab="streaming"
                    style="padding:7px 16px;border-radius:6px;border:none;cursor:pointer;font-size:13px;font-weight:bold;background:#333;color:#aaa;">
                    🎵 Deezer / Qobuz / MB / BC <span id="streaming-count" style="opacity:0.7;">(loading…)</span>
                </button>
                <button class="picker-tab" data-tab="retail"
                    style="padding:7px 16px;border-radius:6px;border:none;cursor:pointer;font-size:13px;font-weight:bold;background:#333;color:#aaa;">
                    🛒 iTunes / Amazon <span id="retail-count" style="opacity:0.7;">(loading…)</span>
                </button>
            </div>
            <div id="panel-sources">
                <div id="sources-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;">
                    <div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;">Scanning page for source links…</div>
                </div>
            </div>
            <div id="panel-discogs" style="display:none;">
                <div id="discogs-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;">
                    <div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;">
                        <div style="font-size:32px;margin-bottom:8px;">🔍</div>Searching Discogs…
                    </div>
                </div>
            </div>
            <div id="panel-streaming" style="display:none;">
                <div id="streaming-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;">
                    <div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;">
                        <div style="font-size:32px;margin-bottom:8px;">🎵</div>Searching Deezer, Qobuz & MusicBrainz…
                    </div>
                </div>
            </div>
            <div id="panel-retail" style="display:none;">
                <div id="retail-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px;">
                    <div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;">
                        <div style="font-size:32px;margin-bottom:8px;">🛒</div>Searching iTunes & Amazon…
                    </div>
                </div>
            </div>
            <div style="margin-top:22px;border-top:1px solid #444;padding-top:18px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
                <div style="width:100%;font-size:11px;color:#888;margin-bottom:2px;">
                    Paste any direct image URL — or a Spotify track, album, or playlist URL and the artwork will be resolved automatically.
                </div>
                <input type="text" id="custom-url-input" placeholder="Or paste any image URL…"
                    style="flex:1;min-width:200px;padding:9px 12px;background:#222;border:1px solid #555;color:#fff;border-radius:4px;font-size:13px;">
                <button id="use-custom-url" style="padding:9px 18px;background:#2196F3;color:#fff;border:none;border-radius:4px;cursor:pointer;white-space:nowrap;">Use URL</button>
                <button id="upload-from-disk" style="padding:9px 18px;background:#7c3aed;color:#fff;border:none;border-radius:4px;cursor:pointer;white-space:nowrap;">📁 Upload from computer</button>
                <input type="file" id="disk-file-input" accept="image/*" style="display:none;">
                <button id="skip-picker"    style="padding:9px 18px;background:#ff9800;color:#fff;border:none;border-radius:4px;cursor:pointer;white-space:nowrap;">Use Original</button>
                <button id="cancel-picker"  style="padding:9px 18px;background:#555;color:#fff;border:none;border-radius:4px;cursor:pointer;">Cancel</button>
                ${!getDiscogsToken() ? '<button id="setup-discogs" style="padding:9px 18px;background:#7B1FA2;color:#fff;border:none;border-radius:4px;cursor:pointer;white-space:nowrap;">⚙ Set Discogs Token</button>' : ''}
            </div>
            <div id="url-preview" style="display:none;margin-top:12px;padding:12px;background:#1a1a1a;border:1px solid #444;border-radius:6px;align-items:center;gap:14px;">
                <img id="url-preview-img" src="" alt="preview" style="width:80px;height:80px;object-fit:contain;border-radius:4px;background:#111;">
                <div id="url-preview-info" style="font-size:12px;color:#aaa;line-height:1.6;"></div>
            </div>
        `;

        overlay.appendChild(container);
        document.body.appendChild(overlay);

        // --- Tab switching ---
        container.querySelectorAll('.picker-tab').forEach(btn => {
            btn.addEventListener('click', () => {
                container.querySelectorAll('.picker-tab').forEach(b => { b.style.background = '#333'; b.style.color = '#aaa'; });
                btn.style.background = '#4CAF50'; btn.style.color = '#fff';
                container.querySelector('#panel-sources').style.display   = btn.dataset.tab === 'sources'   ? '' : 'none';
                container.querySelector('#panel-discogs').style.display   = btn.dataset.tab === 'discogs'   ? '' : 'none';
                container.querySelector('#panel-streaming').style.display = btn.dataset.tab === 'streaming' ? '' : 'none';
                container.querySelector('#panel-retail').style.display    = btn.dataset.tab === 'retail'    ? '' : 'none';
            });
        });

        // --- Footer actions ---
        container.querySelector('#use-custom-url').onclick = () => {
            const url = container.querySelector('#custom-url-input').value.trim();
            if (url) { document.body.removeChild(overlay); callback(url); }
            else alert('Please enter a URL');
        };
        container.querySelector('#skip-picker').onclick   = () => { document.body.removeChild(overlay); callback('SKIP'); };
        container.querySelector('#cancel-picker').onclick = () => { document.body.removeChild(overlay); callback(''); };

        container.querySelector('#upload-from-disk').onclick = () => {
            container.querySelector('#disk-file-input').click();
        };
        container.querySelector('#disk-file-input').onchange = function() {
            const file = this.files && this.files[0];
            if (!file) return;
            document.body.removeChild(overlay);
            callback('__localfile__:' + URL.createObjectURL(file));
        };
        container.querySelector('#custom-url-input').addEventListener('keypress', e => {
            if (e.key === 'Enter') container.querySelector('#use-custom-url').click();
        });

        // Live URL preview — debounced, shows thumbnail + dimensions on paste/type
        let urlPreviewTimer = null;
        const urlPreviewDiv  = container.querySelector('#url-preview');
        const urlPreviewImg  = container.querySelector('#url-preview-img');
        const urlPreviewInfo = container.querySelector('#url-preview-info');

        function showUrlPreview(url) {
            if (!url) { urlPreviewDiv.style.display = 'none'; return; }
            urlPreviewInfo.textContent = 'Loading…';
            urlPreviewDiv.style.display = 'flex';
            urlPreviewImg.src = '';

            // For Spotify track/album/playlist URLs, resolve via oEmbed first
            // to get a real i.scdn.co image URL — then preview that
            if (/open\.spotify\.com\/(track|album|playlist)\//i.test(url)) {
                urlPreviewInfo.textContent = 'Resolving Spotify artwork…';
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://open.spotify.com/oembed?url=${encodeURIComponent(url)}`,
                    headers: { 'Accept': 'application/json' },
                    timeout: 8000,
                    onload: function(r) {
                        try {
                            const data = JSON.parse(r.responseText);
                            const imageUrl = data.thumbnail_url;
                            if (imageUrl) {
                                // Update the input field to the resolved image URL
                                // so "Use URL" submits the actual image, not the Spotify page URL
                                container.querySelector('#custom-url-input').value = imageUrl;
                                showUrlPreview(imageUrl);
                            } else {
                                showPreviewError(url, 'Spotify oEmbed returned no image');
                            }
                        } catch(e) { showPreviewError(url, 'Could not resolve Spotify URL'); }
                    },
                    onerror:   function() { showPreviewError(url, 'Could not reach Spotify'); },
                    ontimeout: function() { showPreviewError(url, 'Spotify request timed out'); }
                });
                return;
            }

            function showPreviewError(u, msg) {
                urlPreviewImg.src = '';
                urlPreviewInfo.innerHTML =
                    `<span style="color:#ccc;word-break:break-all;">${u.length > 80 ? u.slice(0,77)+'…' : u}</span><br>` +
                    `<span style="color:#f44;">${msg}</span>`;
            }

            const probe = new Image();
            probe.onload = () => {
                urlPreviewImg.src = url;
                const w = probe.naturalWidth, h = probe.naturalHeight;
                const sizeOk = w >= 500 && h >= 500;
                urlPreviewInfo.innerHTML =
                    `<span style="color:#ccc;word-break:break-all;">${url.length > 80 ? url.slice(0,77)+'…' : url}</span><br>` +
                    `<span style="color:${sizeOk ? '#4CAF50' : '#ff9800'};">${w} × ${h}px</span>` +
                    (sizeOk ? '' : ' <span style="color:#ff9800;">— may be too small</span>');
            };
            probe.onerror = () => showPreviewError(url, 'Could not load image — URL may not be a direct image link');
            probe.src = url;
        }

        container.querySelector('#custom-url-input').addEventListener('input', e => {
            clearTimeout(urlPreviewTimer);
            const val = e.target.value.trim();
            if (!val) { urlPreviewDiv.style.display = 'none'; return; }
            urlPreviewTimer = setTimeout(() => showUrlPreview(val), 600);
        });

        // Also trigger immediately on paste
        container.querySelector('#custom-url-input').addEventListener('paste', e => {
            clearTimeout(urlPreviewTimer);
            // Use setTimeout to let paste complete before reading value
            urlPreviewTimer = setTimeout(() => {
                const val = e.target.value.trim();
                if (val) showUrlPreview(val);
            }, 100);
        });
        const setupBtn = container.querySelector('#setup-discogs');
        if (setupBtn) {
            setupBtn.onclick = () => {
                document.body.removeChild(overlay);
                showDiscogsSetup(success => {
                    if (success) createImagePickerOverlay(albumInfo, callback);
                    else callback('');
                });
            };
        }

        // --- Card factory ---
        function makeCard(imageUrl, title, subtitle, badgeText, badgeColor, onClickUrl, detailLines) {
            const card = document.createElement('div');
            card.style.cssText = 'background:#222;border-radius:8px;padding:8px;cursor:pointer;transition:all 0.15s;border:2px solid transparent;position:relative;';
            card.innerHTML = `
                <img src="${imageUrl}" loading="lazy"
                    style="width:100%;height:160px;object-fit:cover;border-radius:5px;display:block;background:#333;">
                <div style="margin-top:8px;font-size:11px;">
                    <div style="font-weight:bold;color:#4CAF50;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${title}">${title}</div>
                    ${subtitle ? `<div style="color:#888;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${subtitle}">${subtitle}</div>` : ''}
                    ${detailLines && detailLines.length ? detailLines.map(l => `<div style="color:#aaa;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:10px;" title="${l}">${l}</div>`).join('') : ''}
                </div>
                <div style="position:absolute;top:12px;left:12px;background:${badgeColor};color:#fff;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:bold;border:1px solid rgba(255,255,255,0.15);">${badgeText}</div>
                <div class="res-badge" style="position:absolute;top:12px;right:12px;background:rgba(0,0,0,0.75);color:#fff;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:bold;">…</div>
            `;
            card.onmouseenter = () => { card.style.borderColor = '#4CAF50'; card.style.transform = 'scale(1.03)'; };
            card.onmouseleave = () => { card.style.borderColor = 'transparent'; card.style.transform = 'scale(1)'; };
            card.onclick = () => { document.body.removeChild(overlay); callback(onClickUrl); };
            const probe   = new Image();
            probe.onload  = () => {
                const badge = card.querySelector('.res-badge');
                badge.textContent = `${probe.width}×${probe.height}`;
                badge.style.background = (probe.width < MIN_RESOLUTION || probe.height < MIN_RESOLUTION)
                    ? 'rgba(244,67,54,0.85)' : 'rgba(76,175,80,0.85)';
            };
            probe.onerror = () => { card.querySelector('.res-badge').textContent = '?'; };
            probe.src = imageUrl;
            return card;
        }

        // --- Page Sources ---
        const sourcesGrid    = container.querySelector('#sources-grid');
        const sourcesCountEl = container.querySelector('#sources-count');
        const sourceLinks    = getPageSourceLinks();

        if (sourceLinks.length === 0) {
            sourcesGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;">No recognised source links found on this page.<br><small style="opacity:0.6;">Try the Discogs Search tab or paste a URL below.</small></div>';
            sourcesCountEl.textContent = '(0)';
        } else {
            sourcesGrid.innerHTML = '';
            sourcesCountEl.textContent = `(0/${sourceLinks.length})`;
            let resolved = 0, found = 0;

            const seenReleaseLists = new Set();

            sourceLinks.forEach(linkObj => {
                let firstCall = true;
                resolveSourceImage(linkObj, result => {
                    if (firstCall) { resolved++; firstCall = false; }

                    if (result && result.imageUrl) {
                        found++;
                        sourcesCountEl.textContent = `(${found})`;
                        const color = SOURCE_BADGE_COLORS[result.source] || '#555';
                        sourcesGrid.appendChild(makeCard(
                            result.imageUrl,
                            result.source,
                            result.label || linkObj.label,
                            result.label || result.source,
                            color,
                            result.imageUrl
                        ));
                    } else if (result && result.type === 'release_list' && Array.isArray(result.releases) && result.releases.length) {
                        // Render each release version directly as a card in the grid
                        result.releases.forEach(rel => {
                            const thumb = rel.coverImage || rel.thumb;
                            if (!thumb) return;
                            found++;
                            sourcesCountEl.textContent = `(${found})`;
                            const card = makeCard(
                                thumb,
                                rel.title || 'Release',
                                [rel.country, rel.year].filter(Boolean).join(' · '),
                                'Discogs',
                                SOURCE_BADGE_COLORS['Discogs Direct'] || '#e74c3c',
                                thumb,
                                [[rel.format && rel.format.join(', '), rel.label && rel.label.slice(0,2).join(', ')].filter(Boolean).join(' · ')]
                            );
                            // On click, fetch full-res image first
                            card.onclick = null;
                            card.addEventListener('click', () => {
                                const token = getDiscogsToken();
                                const headers = { 'User-Agent': 'CoverUp/7.0' };
                                if (token) headers['Authorization'] = `Discogs token=${token}`;
                                if (rel.country && rel.year && img) img.dataset.coverupSummary = [rel.country, rel.year].filter(Boolean).join(', ');
                                GM_xmlhttpRequest({
                                    method: 'GET',
                                    url: `https://api.discogs.com/releases/${rel.id}`,
                                    headers, timeout: 10000,
                                    onload: function(r) {
                                        try {
                                            const data = JSON.parse(r.responseText);
                                            const primary = (data.images || []).find(i => i.type === 'primary') || data.images && data.images[0];
                                            const srcUrl = (primary && primary.uri) || thumb;
                                            document.body.removeChild(overlay);
                                            callback(srcUrl);
                                        } catch(e) { document.body.removeChild(overlay); callback(thumb); }
                                    },
                                    onerror: function() { document.body.removeChild(overlay); callback(thumb); }
                                });
                            });
                            sourcesGrid.appendChild(card);
                        });
                    }

                    if (resolved === sourceLinks.length && found === 0) {
                        sourcesGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;">Source links found but no artwork could be extracted.<br><small style="opacity:0.6;">Try the Discogs Search tab or paste a URL below.</small></div>';
                        sourcesCountEl.textContent = '(0)';
                    }
                });
            });
        }

        // --- Discogs keyword search ---
        // Helpers to format Discogs release details matching the Discogs UI layout
        function buildDiscogsSubtitle(rel) {
            const parts = [];
            if (rel.format && rel.format.length) parts.push(rel.format.join(', '));
            return parts.join(' · ') || '';
        }
        function buildDiscogsDetailLines(rel) {
            const lines = [];
            // Label – Catalog Number
            const labels  = (rel.label  || []).slice(0, 2).join(', ');
            const catnos  = (rel.catno  || '').trim();
            if (labels || catnos) lines.push([labels, catnos].filter(Boolean).join(' – '));
            // Country  Year
            const country = rel.country || '';
            const year    = rel.year    || '';
            if (country || year) lines.push([country, year].filter(Boolean).join('  ·  '));
            return lines;
        }

        const discogsGrid    = container.querySelector('#discogs-grid');
        const discogsCountEl = container.querySelector('#discogs-count');

        if (!getDiscogsToken()) {
            discogsGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;">No Discogs token set.<br><small>Click "⚙ Set Discogs Token" below to enable.</small></div>';
            discogsCountEl.textContent = '(no token)';
        } else {
            const searchQuery = `${albumInfo.artist} ${albumInfo.album} ${albumInfo.year}`.trim();
            console.log('[Rehost] albumInfo:', JSON.stringify(albumInfo));
            console.log('[Rehost] searchQuery:', searchQuery);
            searchDiscogs(searchQuery, function(results) {
                discogsGrid.innerHTML = '';
                const filtered = results.filter(r => (r.cover_image && !r.cover_image.includes('spacer.gif')) || (r._allImages && r._allImages.length > 0));
                discogsCountEl.textContent = `(${filtered.length})`;
                if (filtered.length === 0) {
                    discogsGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;"><div style="font-size:32px;margin-bottom:8px;">❌</div>No Discogs results found.</div>';
                    return;
                }
                filtered.forEach(release => {
                    if (release._allImages && release._allImages.length > 1) {
                        release._allImages.forEach(function(img, idx) {
                            const typeLabel = img.type === 'primary' ? 'Primary' : ('Image ' + (idx + 1));
                            discogsGrid.appendChild(makeCard(
                                img.uri,
                                release.title + ' — ' + typeLabel,
                                buildDiscogsSubtitle(release),
                                'Discogs',
                                SOURCE_BADGE_COLORS['Discogs Direct'] || '#e74c3c',
                                img.uri,
                                buildDiscogsDetailLines(release)
                            ));
                        });
                        return;
                    }
                    discogsGrid.appendChild(makeCard(
                        release.cover_image,
                        release.title,
                        buildDiscogsSubtitle(release),
                        'Discogs',
                        '#e74c3c',
                        release.cover_image,
                        buildDiscogsDetailLines(release)
                    ));
                });
            }, albumInfo);
        }

        // --- Deezer / Qobuz / MusicBrainz search ---
        const streamingGrid    = container.querySelector('#streaming-grid');
        const streamingCountEl = container.querySelector('#streaming-count');

        let streamingFound = 0;
        let streamingDone  = 0;
        const STREAMING_SOURCES = 4;
        streamingGrid.innerHTML = '';

        // Relevance filter — artist must match unless VA/unknown
        const VA_PATTERN = /^(unknown artist|various artists?|va|various|multiple artists?)$/i;
        const searchArtist = (albumInfo.artist || '').trim().toLowerCase();
        const searchAlbum  = (albumInfo.album  || '').trim().toLowerCase();
        const isVA = !searchArtist || VA_PATTERN.test(searchArtist);

        function wordsOf(s) {
            return (s || '').toLowerCase().split(/[\s\-&,]+/).filter(w => w.length > 2);
        }
        function anyWordMatches(needle, haystack) {
            const nw = wordsOf(needle);
            if (!nw.length) return true;
            const hw = haystack.toLowerCase();
            return nw.some(w => hw.includes(w));
        }

        function isRelevant(resultTitle, resultArtist) {
            const ra = (resultArtist || '').toLowerCase();
            const rt = (resultTitle  || '').toLowerCase();

            if (!isVA && searchArtist) {
                if (ra) {
                    // Have artist field — it must match
                    if (!anyWordMatches(searchArtist, ra)) return false;
                } else {
                    // No artist field — require album title to match instead
                    if (searchAlbum && !anyWordMatches(searchAlbum, rt)) return false;
                }
            }

            // Album title check — always required when we have an album name
            if (searchAlbum) {
                const albumWords = wordsOf(searchAlbum);
                const distinctiveWords = albumWords.filter(w => w.length > 4);

                if (distinctiveWords.length > 0) {
                    // Album has distinctive long words — require at least one to match
                    if (!distinctiveWords.some(w => rt.includes(w))) return false;
                } else {
                    // Short album title (e.g. "Love", "Help!", "1") — require
                    // the full title to appear as a word boundary match in the result
                    const escaped = searchAlbum.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    const pattern = new RegExp('(?:^|[\\s\\-–—:])' + escaped + '(?:$|[\\s\\-–—:(])', 'i');
                    if (!pattern.test(rt)) return false;
                }
            }

            return true;
        }

        function onStreamingDone() {
            streamingDone++;
            if (streamingFound === 0 && streamingDone === STREAMING_SOURCES) {
                streamingGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;"><div style="font-size:32px;margin-bottom:8px;">❌</div>No results found on Deezer, Qobuz, MusicBrainz, or Bandcamp.</div>';
                streamingCountEl.textContent = '(0)';
            }
        }

        // Deezer
        searchDeezer(albumInfo, function(items) {
            items.filter(a => isRelevant(a.title, a.artist && a.artist.name)).forEach(a => {
                let imageUrl = a.cover_xl || a.cover_big || a.cover_medium || a.cover;
                if (!imageUrl && a.md5_image) {
                    imageUrl = `https://cdn-images.dzcdn.net/images/cover/${a.md5_image}/1000x1000-000000-80-0-0.jpg`;
                }
                if (!imageUrl) return;
                streamingGrid.appendChild(makeCard(
                    imageUrl,
                    a.title || 'Unknown',
                    a.artist && a.artist.name ? a.artist.name : '',
                    'Deezer',
                    '#a238ff',
                    imageUrl
                ));
                streamingFound++;
                streamingCountEl.textContent = `(${streamingFound})`;
            });
            onStreamingDone();
        });

        // Qobuz
        searchQobuz(albumInfo, function(items) {
                items.filter(a => isRelevant(a.title, (a.artist && (a.artist.name || a.artist)) || '')).forEach(a => {
                // Prefer API image fields; fall back to two-level CDN URL from ID
                const qImg = a.image && (a.image.mega || a.image.large || a.image.small);
                let imageUrl = (qImg && qImg.trim()) || '';
                if (!imageUrl && a.id) {
                    const aid = String(a.id);
                    const l1 = aid.slice(-2);
                    const l2 = aid.slice(-4, -2);
                    imageUrl = `https://static.qobuz.com/images/covers/${l1}/${l2}/${aid}_org.jpg`;
                }
                if (!imageUrl) return;
                streamingGrid.appendChild(makeCard(
                    imageUrl,
                    a.title || 'Unknown',
                    a.artist && a.artist.name ? a.artist.name : '',
                    'Qobuz',
                    '#1e90ff',
                    imageUrl
                ));
                streamingFound++;
                streamingCountEl.textContent = `(${streamingFound})`;
            });
            onStreamingDone();
        });

        // MusicBrainz
        searchMusicBrainz(albumInfo, function(results) {
            results.filter(r => isRelevant(r.title, r.artist)).forEach(r => {
                streamingGrid.appendChild(makeCard(
                    r.imageUrl,
                    r.title || 'Unknown',
                    [r.artist, r.date].filter(Boolean).join(' · '),
                    'MB ' + r.label,
                    '#eb743b',
                    r.imageUrl
                ));
                streamingFound++;
                streamingCountEl.textContent = `(${streamingFound})`;
            });
            onStreamingDone();
        });

        // Bandcamp
        searchBandcamp(albumInfo, function(items) {
            items.filter(a => isRelevant(a.title, a.artist)).forEach(a => {
                streamingGrid.appendChild(makeCard(
                    a.imageUrl,
                    a.title || 'Unknown',
                    a.artist || '',
                    'Bandcamp',
                    '#1da0c3',
                    a.imageUrl
                ));
                streamingFound++;
                streamingCountEl.textContent = `(${streamingFound})`;
            });
            onStreamingDone();
        });

        // --- iTunes / Amazon search ---
        const retailGrid    = container.querySelector('#retail-grid');
        const retailCountEl = container.querySelector('#retail-count');

        let retailFound = 0;
        let retailDone  = 0;
        const RETAIL_SOURCES = 2;
        retailGrid.innerHTML = '';

        function onRetailDone() {
            retailDone++;
            if (retailFound === 0 && retailDone === RETAIL_SOURCES) {
                retailGrid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:30px;color:#888;"><div style="font-size:32px;margin-bottom:8px;">❌</div>No results found on iTunes or Amazon.</div>';
                retailCountEl.textContent = '(0)';
            }
        }

        // iTunes
        searchItunes(albumInfo, function(items) {
            items.filter(a => isRelevant(a.collectionName, a.artistName)).forEach(a => {
                const imageUrl = a.artworkUrl100
                    .replace(/\d+x\d+bb\.jpg$/, '10000x10000bb.jpg')
                    .replace(/\d+x\d+\.jpg$/,   '10000x10000.jpg');
                retailGrid.appendChild(makeCard(
                    imageUrl,
                    a.collectionName || 'Unknown',
                    a.artistName || '',
                    'iTunes',
                    '#fc3c44',
                    imageUrl
                ));
                retailFound++;
                retailCountEl.textContent = `(${retailFound})`;
            });
            onRetailDone();
        });

        // Amazon
        searchAmazon(albumInfo, function(items) {
            items.filter(a => isRelevant(a.title, '')).forEach(a => {
                retailGrid.appendChild(makeCard(
                    a.imageUrl,
                    a.title || 'Unknown',
                    'Amazon',
                    'Amazon',
                    '#ff9900',
                    a.imageUrl
                ));
                retailFound++;
                retailCountEl.textContent = `(${retailFound})`;
            });
            onRetailDone();
        });

    }

    // ============================================================
    // --- COVER QUALITY VALIDATION ---
    // ============================================================

    const coverSelectors = ['.box_image_albumart #covers img', '.artist_profile img'];

    function checkImageQuality(img) {
        return new Promise((resolve) => {
            // Always use the real URL — Gazelle lazy-loads alt covers via data-gazelle-temp-src
            // so img.src may be blank until cover_art.js sets it.
            const actualUrl = getActualImageUrl(img);

            if (!actualUrl || actualUrl.includes('/static/common/noartwork/')) {
                resolve({ needsRehost: true, resizeOnly: false, reasons: ['No artwork'] });
                return;
            }

            // Check host against bad/trigger domains using the real URL
            let badHost = false;
            try {
                const urlObj = new URL(actualUrl);
                if (REHOST_TRIGGERS.some(h => urlObj.hostname.includes(h))) {
                    badHost = true;
                }
            } catch(e) {}

            function resolveFromDimensions(w, h, broken) {
                if (broken) {
                    resolve({ needsRehost: true, resizeOnly: false, reasons: ['Broken image'] });
                    return;
                }
                const issues = [];
                if (badHost) issues.push(`Hosted on ${new URL(actualUrl).hostname} — rehost recommended`);
                if (w > 0 && h > 0) {
                    if (w < MIN_RESOLUTION || h < MIN_RESOLUTION)
                        issues.push(`Low resolution (${w}×${h})`);
                    if (w > MAX_DIMENSION || h > MAX_DIMENSION)
                        issues.push(`Too large (${w}×${h})`);
                }
                const isOversized = w > MAX_DIMENSION || h > MAX_DIMENSION;
                const isLowRes    = w < MIN_RESOLUTION || h < MIN_RESOLUTION;
                const resizeOnly  = isOversized && !isLowRes && !badHost;
                resolve({ needsRehost: issues.length > 0, resizeOnly, reasons: issues });
            }

            // If the img element has already loaded with real dimensions, use them directly
            if (img.complete && img.naturalWidth > 0) {
                resolveFromDimensions(img.naturalWidth, img.naturalHeight, false);
                return;
            }

            // img.src is blank or not yet loaded — probe the actual URL via a fresh Image
            const probe = new Image();
            const timer = setTimeout(() => {
                probe.onload = probe.onerror = null;
                resolveFromDimensions(0, 0, true); // timeout = treat as broken
            }, 6000);
            probe.onload = () => {
                clearTimeout(timer);
                resolveFromDimensions(probe.naturalWidth, probe.naturalHeight, false);
            };
            probe.onerror = () => {
                clearTimeout(timer);
                resolveFromDimensions(0, 0, true);
            };
            probe.src = actualUrl;
        });
    }

    // ============================================================
    // --- ACTUAL IMAGE URL HELPER ---
    // ============================================================
    // Gazelle lazy-loads covers: the real URL lives in data-gazelle-temp-src
    // before img.src settles. Always prefer that over img.src to avoid
    // rehosting the torrent page URL instead of the actual image.
    function getActualImageUrl(img) {
        const badPrefix = window.location.origin + window.location.pathname + '?';
        const candidates = [
            img.getAttribute('data-gazelle-temp-src'),
            img.getAttribute('data-src'),
            img.currentSrc && !img.currentSrc.startsWith(badPrefix) ? img.currentSrc : null,
            img.src && !img.src.startsWith(badPrefix) ? img.src : null
        ].filter(u => u && u.trim());
        return candidates[0] || img.src || '';
    }

    // ============================================================
    // --- ATTACH REHOST / RESIZE LINKS ---
    // ============================================================

    // ============================================================
    // --- DISCOGS RELEASE PICKER ---
    // ============================================================
    function escHtml(s) {
        return String(s || '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
    }

    function openDiscogsReleasePicker(releases, linkEl, img, callback) {
        const prev = document.getElementById('coverup-discogs-picker');
        if (prev) prev.remove();

        const box = document.createElement('div');
        box.id = 'coverup-discogs-picker';
        Object.assign(box.style, {
            position:  'fixed', top: '60px', right: '20px', zIndex: '99999',
            width:     '420px', maxHeight: '72vh', overflowY: 'auto',
            background: '#1e1e1e', color: '#ddd',
            border: '1px solid #555', borderRadius: '8px',
            padding: '12px 14px', boxShadow: '0 8px 28px rgba(0,0,0,0.55)',
            fontFamily: 'sans-serif', fontSize: '13px'
        });

        const hdr = document.createElement('div');
        hdr.style.cssText = 'font-weight:bold;font-size:14px;margin-bottom:10px;padding-right:50px;';
        hdr.textContent = `Discogs releases (${releases.length})`;
        box.appendChild(hdr);

        const closeBtn = document.createElement('a');
        closeBtn.href = '#';
        closeBtn.textContent = '✕ close';
        closeBtn.style.cssText = 'position:absolute;top:12px;right:12px;color:#999;text-decoration:none;';
        closeBtn.addEventListener('click', e => { e.preventDefault(); box.remove(); });
        box.appendChild(closeBtn);

        releases.forEach(rel => {
            const row = document.createElement('div');
            row.style.cssText = 'display:flex;gap:10px;padding:8px 0;border-top:1px solid #2d2d2d;align-items:flex-start;';

            if (rel.coverImage || rel.thumb) {
                const thumb = document.createElement('img');
                thumb.src = rel.coverImage || rel.thumb;
                thumb.alt = '';
                thumb.style.cssText = 'width:54px;height:54px;object-fit:cover;flex:0 0 54px;background:#333;border-radius:3px;';
                row.appendChild(thumb);
            } else {
                const ph = document.createElement('div');
                ph.style.cssText = 'width:54px;height:54px;flex:0 0 54px;background:#333;border-radius:3px;display:flex;align-items:center;justify-content:center;color:#666;font-size:20px;';
                ph.textContent = '🎵';
                row.appendChild(ph);
            }

            const meta = document.createElement('div');
            meta.style.cssText = 'flex:1 1 auto;min-width:0;';

            meta.innerHTML = `<div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escHtml(rel.title)}</div>
                <div style="color:#aaa;font-size:0.87em;margin:3px 0 6px;">${escHtml(rel.summary)}</div>`;

            const btnRow = document.createElement('div');
            btnRow.style.cssText = 'display:flex;gap:10px;flex-wrap:wrap;';

            const openLink = document.createElement('a');
            openLink.href = rel.webUrl || '#';
            openLink.target = '_blank';
            openLink.rel = 'noopener noreferrer';
            openLink.textContent = '[open]';
            openLink.style.color = '#7ab8ff';
            btnRow.appendChild(openLink);

            if (rel.coverImage || rel.thumb) {
                const useBtn = document.createElement('a');
                useBtn.href = '#';
                useBtn.textContent = '[use this cover]';
                useBtn.style.color = '#7ddd5c';
                useBtn.addEventListener('click', function(e) {
                    e.preventDefault();
                    box.remove();

                    // Build summary tag from release metadata e.g. "UK, 1970"
                    const relParts = [rel.country, rel.year].filter(Boolean);
                    const relSummary = relParts.join(', ');
                    if (img && relSummary) img.dataset.coverupSummary = relSummary;

                    // Fetch the full release to get the proper full-res image URI
                    // (master version list only has thumbnails)
                    const token = getDiscogsToken();
                    const headers = { 'User-Agent': 'CoverUp/7.0' };
                    if (token) headers['Authorization'] = `Discogs token=${token}`;

                    useBtn.textContent = '[loading…]';
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: `https://api.discogs.com/releases/${rel.id}`,
                        headers,
                        timeout: 10000,
                        onload: function(r) {
                            try {
                                const data = JSON.parse(r.responseText);
                                const primary = (data.images || []).find(i => i.type === 'primary') || data.images && data.images[0];
                                const srcUrl = (primary && primary.uri) || rel.coverImage || rel.thumb;
                                if (typeof callback === 'function') {
                                    callback(srcUrl);
                                } else if (img) {
                                    handleUploadSuccess(srcUrl, linkEl && linkEl.dataset && linkEl.dataset.rehostOldUrl || srcUrl, linkEl, img);
                                }
                            } catch(e) {
                                // Fallback to thumb if fetch fails
                                const srcUrl = rel.coverImage || rel.thumb;
                                if (typeof callback === 'function') callback(srcUrl);
                                else if (img) handleUploadSuccess(srcUrl, srcUrl, linkEl, img);
                            }
                        },
                        onerror: function() {
                            const srcUrl = rel.coverImage || rel.thumb;
                            if (typeof callback === 'function') callback(srcUrl);
                            else if (img) handleUploadSuccess(srcUrl, srcUrl, linkEl, img);
                        }
                    });
                });
                btnRow.appendChild(useBtn);
            }

            meta.appendChild(btnRow);
            row.appendChild(meta);
            box.appendChild(row);
        });

        document.body.appendChild(box);
    }

    function attachRehostLink(img) {
        const parentP = img.closest('p') || img.parentNode;
        if (!parentP || img.dataset.rehostAttached) return;
        img.dataset.rehostOldUrl = getActualImageUrl(img);
        // Restore persisted rehosted state across page loads.
        // Use the actual image URL (not img.src which may be blank for lazy-loaded alt covers).
        // Only restore if the stored value points to a different URL (i.e. this was a source
        // that got rehosted elsewhere). Don't restore 'self' entries — those are rehost
        // *destinations* stored to prevent double-uploading, not source tracking.
        const actualUrlForLookup = getActualImageUrl(img);
        const persistedRehost = getRehostedUrl(actualUrlForLookup);
        if (persistedRehost && persistedRehost !== 'self') {
            img.dataset.rehostedUrl = persistedRehost;
        }

        const linkDiv = document.createElement('div');
        linkDiv.className = 'rehost-link-wrapper';
        linkDiv.style.cssText = 'text-align:center;margin-bottom:5px;';

        const link = document.createElement('a');
        link.href = 'javascript:void(0)';
        link.textContent = '[checking...]';
        link.style.cssText = 'cursor:pointer;font-weight:bold;';

        linkDiv.appendChild(link);
        // Insert the rehost link OUTSIDE the cover_div so Gazelle's cover_art.js jQuery
        // delegation never sees the click event — insertBefore puts it just before the cover_div.
        const coverDivAncestor = img.closest('[id^="cover_div_"]');
        if (coverDivAncestor && coverDivAncestor.parentNode) {
            coverDivAncestor.parentNode.insertBefore(linkDiv, coverDivAncestor);
        } else {
            parentP.insertBefore(linkDiv, parentP.firstChild);
        }

        checkImageQuality(img).then(result => {
            const currentUrl = getActualImageUrl(img);
            const isNoArtwork = result.reasons && result.reasons.includes('No artwork');
            if (isNoArtwork) {
                link.textContent = '[no artwork — search for some?]';
                link.style.color = '#aaa';
                link.addEventListener('click', (e) => { e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault(); rehostImage(img, link); });
                img.dataset.rehostAttached = 'true';
                return;
            }
            const isBroken = result.reasons && result.reasons.includes('Broken image');
            const rehostedByCoverup = !isBroken && (link.dataset.rehosted || img.dataset.rehostedUrl);
            const alreadyOnRehostDomain = !isBroken && !rehostedByCoverup && isOnRehostDomain(currentUrl);
            if (rehostedByCoverup || alreadyOnRehostDomain) {
                link.textContent = rehostedByCoverup ? '[rehosted via CoverUp — redo?]' : '[already on rehost domain — redo?]';
                link.style.color = '#888';
                link.addEventListener('click', (e) => { e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault(); rehostImage(img, link); });
                img.dataset.rehostAttached = 'true';
                return;
            }
            if (result.resizeOnly) {
                link.textContent = '[resize & rehost]';
                link.style.color = 'darkorange';
                const reasonSpan = document.createElement('span');
                reasonSpan.textContent = ' - ' + result.reasons.join(', ');
                reasonSpan.style.cssText = 'color:#ffaa44;font-size:0.9em;font-weight:normal;';
                linkDiv.appendChild(reasonSpan);
                link.addEventListener('click', (e) => {
                    e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault();
                    if (!getPtpimgKey() && !getImgbbKey()) { showApiKeySetup(img, link); return; }
                    processImage(getActualImageUrl(img), getActualImageUrl(img), link, img);
                });
            } else if (result.needsRehost) {
                const isBroken = result.reasons.includes('Broken image');
                if (isBroken) {
                    const realUrl = getActualImageUrl(img);
                    link.textContent = '[broken image — rehost?]';
                    link.style.color = '#ff4444';
                    // Show the actual broken image URL, not the torrent page URL
                    const urlSpan = document.createElement('span');
                    urlSpan.style.cssText = 'display:block;font-size:0.78em;font-weight:normal;color:#aaa;word-break:break-all;margin-top:3px;user-select:all;';
                    urlSpan.title = 'Broken image URL';
                    urlSpan.textContent = realUrl;
                    linkDiv.appendChild(urlSpan);
                } else {
                    link.textContent = '[rehost]';
                    link.style.color = 'red';
                    if (result.reasons.length > 0) {
                        const reasonSpan = document.createElement('span');
                        reasonSpan.textContent = ' - ' + result.reasons.join(', ');
                        reasonSpan.style.cssText = 'color:#ff6666;font-size:0.9em;font-weight:normal;';
                        linkDiv.appendChild(reasonSpan);
                    }
                }
                link.addEventListener('click', (e) => {
                    e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault();
                    rehostImage(img, link);
                });
            } else {
                if (img.dataset.rehostedUrl) {
                    link.textContent = '[rehosted via CoverUp — redo?]';
                    link.style.color = '#888';
                } else if (isOnRehostDomain(currentUrl)) {
                    link.textContent = '[already on rehost domain — redo?]';
                    link.style.color = '#888';
                } else if (isOnSourceDomain(currentUrl)) {
                    let sourceName = '';
                    try {
                        // Turn e.g. "i.discogs.com" → "Discogs", "coverartarchive.org" → "Cover Art Archive"
                        const h = new URL(currentUrl).hostname.replace(/^(i|images?|static|media|cdn)\./, '');
                        const base = h.split('.')[0];
                        const friendly = {
                            'discogs': 'Discogs', 'coverartarchive': 'Cover Art Archive',
                            'musicbrainz': 'MusicBrainz', 'lastfm': 'Last.fm',
                            'qobuz': 'Qobuz', 'mzstatic': 'Apple Music',
                            'archive': 'Archive.org', 'last': 'Last.fm', 'scdn': 'Spotify',
                            'dzcdn': 'Deezer', 'resources': 'Tidal', 'bcbits': 'Bandcamp',
                        };
                        sourceName = friendly[base] || h;
                    } catch(e) {}
                    link.textContent = sourceName ? `[rehost recommended — hosted on ${sourceName}]` : '[rehost recommended]';
                    link.style.color = 'darkorange';
                } else {
                    link.textContent = '[image ok — rehost anyway?]';
                    link.style.color = 'green';
                }
                link.addEventListener('click', (e) => { e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault(); rehostImage(img, link); });
            }
        });

        img.dataset.rehostAttached = 'true';
    }

    coverSelectors.forEach(selector => {
        document.querySelectorAll(selector).forEach(img => attachRehostLink(img));
    });

    // Alt cover rehost links: hide by default, show a summary nudge instead.
    // Reveal everything when the user clicks "Show all" (Gazelle's own button).
    (function setupAltCoverLinks() {
        const firstAltCoverDiv = document.getElementById('cover_div_1');
        if (!firstAltCoverDiv) return;
        const coversContainer = firstAltCoverDiv.parentNode;

        // Collect all rehost-link-wrappers that belong to alt covers (not cover_div_0)
        const altWrappers = Array.from(coversContainer.querySelectorAll('.rehost-link-wrapper'))
            .filter(w => {
                const cd = w.nextElementSibling || w.previousElementSibling;
                return cd && cd.id !== 'cover_div_0';
            });

        if (!altWrappers.length) return;

        // Hide all alt cover rehost links initially
        altWrappers.forEach(w => { w.dataset.coverupHidden = '1'; w.style.display = 'none'; });

        // Count how many need attention (anything that isn't "already rehosted")
        function countNeedingAttention() {
            return altWrappers.filter(w => {
                const a = w.querySelector('a');
                if (!a) return false;
                const t = a.textContent;
                return t !== '[rehosted via CoverUp — redo?]' && t !== '[already on rehost domain — redo?]' && t !== '[checking...]';
            }).length;
        }

        // Build the summary label
        const summaryDiv = document.createElement('div');
        summaryDiv.id = 'coverup-alt-summary';
        summaryDiv.style.cssText = 'text-align:center;font-size:0.85em;color:#aaa;margin:6px 0 4px;';

        function revealAll() {
            altWrappers.forEach(w => { w.style.display = ''; });
            summaryDiv.style.display = 'none';
        }

        function updateSummary() {
            const total = altWrappers.length;
            const needsWork = countNeedingAttention();
            summaryDiv.innerHTML = '';
            if (needsWork > 0) {
                const msg = document.createElement('span');
                msg.style.color = '#ffaa44';
                msg.textContent = `${needsWork} of ${total} alternate cover${total > 1 ? 's' : ''} may need rehosting `;
                summaryDiv.appendChild(msg);
            } else {
                const msg = document.createElement('span');
                msg.textContent = `${total} alternate cover${total > 1 ? 's' : ''} `;
                summaryDiv.appendChild(msg);
            }
            const showLink = document.createElement('a');
            showLink.href = '#';
            showLink.textContent = '[Show all]';
            showLink.style.cssText = 'color:#7ab8ff;font-weight:bold;';
            showLink.addEventListener('click', e => { e.preventDefault(); revealAll(); });
            summaryDiv.appendChild(showLink);
        }

        // Show a placeholder while image probes run, then update with real count
        summaryDiv.textContent = 'Checking alternate covers…';
        setTimeout(updateSummary, 7000);

        // Insert summary before the first alt rehost wrapper (or firstAltCoverDiv)
        const firstWrapper = altWrappers[0];
        coversContainer.insertBefore(summaryDiv, firstWrapper);

        // Also hook Gazelle's own "Show all" button so both paths work
        document.querySelectorAll('.show_all_covers').forEach(btn => {
            btn.addEventListener('click', revealAll, { once: true });
        });
    })();

    // ============================================================
    // --- AUTO-RESUME ---
    // ============================================================

    const pendingImgSrc = sessionStorage.getItem('rehost_pending_src');
    if (pendingImgSrc && getPtpimgKey()) {
        setTimeout(() => {
            const allImgs   = document.querySelectorAll(coverSelectors.join(','));
            const targetImg = Array.from(allImgs).find(img => img.src === pendingImgSrc);
            if (targetImg) {
                const cont = targetImg.closest('p') || targetImg.parentNode;
                const link = cont.querySelector('.rehost-link-wrapper a');
                if (link) { sessionStorage.removeItem('rehost_pending_src'); rehostImage(targetImg, link); }
            }
        }, 1000);
    }

    // ============================================================
    // --- AUTO-SUBMIT ON EDIT PAGE ---
    // ============================================================

    if (window.location.href.includes('action=editgroup')) {
        const oldUrl = sessionStorage.getItem('red_rehost_old_url');
        const newUrl = sessionStorage.getItem('red_rehost_new_url');
        if (newUrl) {
            sessionStorage.removeItem('red_rehost_old_url');
            sessionStorage.removeItem('red_rehost_new_url');
            // Fill the primary cover image input
            const imageInput = document.querySelector('input[name="image"]');
            const textarea   = document.querySelector('textarea[name="body"]') || document.querySelector('textarea#body');
            if (imageInput) { imageInput.value = newUrl; imageInput.style.backgroundColor = '#ffffcc'; }
            if (textarea && oldUrl && textarea.value.includes(oldUrl)) {
                textarea.value = textarea.value.replace(
                    new RegExp(oldUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newUrl
                );
                textarea.style.backgroundColor = '#ffffcc';
            }
            setTimeout(function() {
                const summaryInput = document.querySelector('input[name="summary"]');
                if (summaryInput) summaryInput.value = 'Rehosted cover image';
                const form = document.querySelector('form[name="torrent_group"]') || document.querySelector('form');
                if (form) form.submit();
            }, 400);
        }
    }

    // ============================================================
    // --- API SETUP PROMPT ---
    // ============================================================

    function showApiKeySetup(img, link) {
        const imgbbKey = prompt('No image hosting API key found.\n\nEnter your imgbb API key (get one free at https://api.imgbb.com):\n\n(Leave blank to use catbox.moe anonymously, or choose TheSunGod/custom host when prompted)');
        if (imgbbKey && imgbbKey.trim()) setImgbbKey(imgbbKey.trim());
        rehostImage(img, link);
    }

    // ============================================================
    // --- CORE REHOST LOGIC ---
    // ============================================================

    function rehostImage(img, link) {
        const oldUrl    = img.src;
        const albumInfo = parseAlbumInfo();
        createImagePickerOverlay(albumInfo, (selectedUrl) => {
            if (!selectedUrl) {
                link.textContent = '[rehost]';
                link.style.color = 'red';
            } else if (selectedUrl === 'SKIP') {
                checkAndProcess(img, oldUrl, link);
            } else if (selectedUrl.startsWith('__localfile__:')) {
                const objectUrl = selectedUrl.slice('__localfile__:'.length);
                link.textContent = 'Processing...';
                link.style.color = 'orange';
                fetch(objectUrl)
                    .then(r => r.blob())
                    .then(blob => {
                        URL.revokeObjectURL(objectUrl);
                        const tempImg = new Image();
                        const blobUrl = URL.createObjectURL(blob);
                        tempImg.onload = function() {
                            let w = tempImg.width, h = tempImg.height;
                            if (w > MAX_DIMENSION || h > MAX_DIMENSION) {
                                const ratio = Math.min(MAX_DIMENSION / w, MAX_DIMENSION / h);
                                w = Math.round(w * ratio); h = Math.round(h * ratio);
                            }
                            const canvas = document.createElement('canvas');
                            canvas.width = w; canvas.height = h;
                            const ctx = canvas.getContext('2d');
                            ctx.fillStyle = '#FFFFFF';
                            ctx.fillRect(0, 0, w, h);
                            ctx.drawImage(tempImg, 0, 0, w, h);
                            URL.revokeObjectURL(blobUrl);
                            canvas.toBlob(jpegBlob => {
                                link.textContent = 'Uploading...';
                                link.style.color = 'orange';
                                uploadWithFallback(jpegBlob, oldUrl, link, newUrl => handleUploadSuccess(newUrl, oldUrl, link, img));
                            }, 'image/jpeg', JPEG_QUALITY);
                        };
                        tempImg.src = blobUrl;
                    })
                    .catch(() => { link.textContent = 'File read failed'; link.style.color = 'red'; });
            } else {
                // If the URL is a direct image link, use it as-is without re-uploading.
                // Only re-upload if it comes from a source that needs processing
                // (streaming services, page URLs, Apple Music, etc.)
                const needsReupload = /apple\.com|mzstatic\.com|spotify\.com|scdn\.co|spotifycdn\.com|deezer\.com|dzcdn\.net|tidal\.com|resources\.tidal\.com|bandcamp\.com|bcbits\.com|amazon\.|m\.media-amazon\.com|qobuz\.com|i\.discogs\.com|coverartarchive\.org|musicbrainz\.org/i.test(selectedUrl);
                if (!needsReupload) {
                    link.textContent = 'Saving…';
                    link.style.color = 'orange';
                    handleUploadSuccess(selectedUrl, oldUrl, link, img);
                } else {
                    processImage(selectedUrl, oldUrl, link, img);
                }
            }
        });
    }

    function checkAndProcess(img, oldUrl, link) {
        const needsProcessing = oldUrl.includes('apple.com') || oldUrl.includes('mzstatic.com') ||
                                !oldUrl.toLowerCase().match(/\.jpe?g/) || img.naturalWidth > MAX_DIMENSION;
        if (needsProcessing) processImage(oldUrl, oldUrl, link, img);
        else fetchAndUpload(oldUrl, oldUrl, link, img);
    }

    function processImage(imageUrl, oldUrl, link, img) {
        link.textContent = 'Processing...';
        link.style.color = 'orange';
        // Some CDNs (e.g. Spotify) serve images without a file extension.
        // Append a hint so the fetch is treated as an image blob.
        const fetchUrl = imageUrl;
        GM_xmlhttpRequest({
            method: 'GET', url: fetchUrl, responseType: 'blob',
            headers: { 'Accept': 'image/jpeg,image/png,image/*,*/*' },
            onload: function(response) {
                const tempImg   = new Image();
                const objectUrl = URL.createObjectURL(response.response);
                tempImg.onload  = function() {
                    let w = tempImg.width, h = tempImg.height;
                    if (w > MAX_DIMENSION || h > MAX_DIMENSION) {
                        const ratio = Math.min(MAX_DIMENSION / w, MAX_DIMENSION / h);
                        w = Math.round(w * ratio); h = Math.round(h * ratio);
                    }
                    const canvas = document.createElement('canvas');
                    canvas.width = w; canvas.height = h;
                    const ctx = canvas.getContext('2d');
                    ctx.fillStyle = '#FFFFFF';
                    ctx.fillRect(0, 0, w, h);
                    ctx.drawImage(tempImg, 0, 0, w, h);
                    canvas.toBlob(blob => {
                        URL.revokeObjectURL(objectUrl);
                        link.textContent = 'Uploading...';
                        link.style.color = 'orange';
                        uploadWithFallback(blob, oldUrl, link, newUrl => handleUploadSuccess(newUrl, oldUrl, link, img));
                    }, 'image/jpeg', JPEG_QUALITY);
                };
                tempImg.src = objectUrl;
            },
            onerror: function() { link.textContent = 'Fetch failed'; link.style.color = 'red'; }
        });
    }

    function fetchAndUpload(imageUrl, oldUrl, link, img) {
        link.textContent = 'Uploading...';
        link.style.color = 'orange';
        GM_xmlhttpRequest({
            method: 'GET', url: imageUrl, responseType: 'blob',
            onload: function(response) {
                uploadWithFallback(response.response, oldUrl, link, newUrl => handleUploadSuccess(newUrl, oldUrl, link, img));
            },
            onerror: function() { link.textContent = 'Fetch failed'; link.style.color = 'red'; }
        });
    }


    // ============================================================
    // --- COVER TYPE DETECTION ---
    // ============================================================

    // Returns { isPrimary, coverArtId, groupId }
    // isPrimary = true  → lives in #cover_div_0 (the main cover, edit via editgroup)
    // isPrimary = false → alternative cover; coverArtId is extracted from its remove link
    function getCoverInfo(img) {
        const coverDiv = img.closest('[id^="cover_div_"]');
        if (!coverDiv) return { isPrimary: true, coverArtId: null, groupId: null };

        const isPrimary = coverDiv.id === 'cover_div_0';

        if (isPrimary) return { isPrimary: true, coverArtId: null, groupId: null };

        // Extract coverArtId and groupId from the remove link in this div
        // The remove link has href="#" but the action URL is inside the onclick attribute:
        // onclick="...ajax.get('torrents.php?action=remove_cover_art&auth=...&id=3973&groupid=3061')..."
        let coverArtId = null, groupId = null;
        const allLinks = coverDiv.querySelectorAll('a');
        let removeOnclick = '';
        allLinks.forEach(function(a) {
            const oc = a.getAttribute('onclick') || '';
            if (oc.includes('remove_cover_art')) removeOnclick = oc;
        });
        let authToken = null;
        if (removeOnclick) {
            const idMatch      = removeOnclick.match(/[?&]id=(\d+)/);
            const groupIdMatch = removeOnclick.match(/[?&]groupid=(\d+)/);
            const authMatch    = removeOnclick.match(/[?&]auth=([a-f0-9]+)/i);
            if (idMatch)      coverArtId = idMatch[1];
            if (groupIdMatch) groupId    = groupIdMatch[1];
            if (authMatch)    authToken  = authMatch[1];
        }
        // Fallback: groupId from page URL
        if (!groupId) {
            const urlMatch = window.location.href.match(/[?&]id=(\d+)/);
            if (urlMatch) groupId = urlMatch[1];
        }

        return { isPrimary: false, coverArtId, groupId, authToken };
    }

    function handleUploadSuccess(newUrl, oldUrl, link, img) {
        GM_setClipboard(newUrl);
        link.dataset.rehosted = '1';
        setRehostedUrl(oldUrl, newUrl);
        setRehostedUrl(newUrl, 'self');
        document.querySelectorAll(coverSelectors.join(',')).forEach(function(el) {
            if (el.src === oldUrl || el.dataset.rehostOldUrl === oldUrl) {
                el.dataset.rehostedUrl = newUrl;
            }
        });

        const coverInfo = img ? getCoverInfo(img) : { isPrimary: true };
        const groupIdMatch = window.location.href.match(/[?&]id=(\d+)/);
        const groupId = groupIdMatch ? groupIdMatch[1] : null;
        if (!groupId) return;

        if (coverInfo.isPrimary) {
            // Primary cover: must use editgroup form (no AJAX endpoint to update it)
            sessionStorage.setItem('red_rehost_old_url', oldUrl);
            sessionStorage.setItem('red_rehost_new_url', newUrl);
            link.textContent = 'Saving…';
            link.style.color = 'orange';
            window.location.href = 'torrents.php?action=editgroup&groupid=' + groupId;
            return;
        }

        // Alt cover: use AJAX add + remove, no page redirect needed
        if (!coverInfo.coverArtId || !coverInfo.authToken) {
            // Fallback: try to get auth from page-level JS var
            const pageAuth = (typeof authkey !== 'undefined') ? authkey : null;
            if (!pageAuth) {
                alert('Cover uploaded!\nCould not detect auth token — paste manually:\n\n' + newUrl);
                return;
            }
            coverInfo.authToken = pageAuth;
        }

        link.textContent = 'Saving…';
        link.style.color = 'orange';

        const coverDiv  = img ? img.closest('[id^="cover_div_"]') : null;
        // Use release metadata summary if set by Discogs picker (e.g. "UK, 1970"),
        // otherwise fall back to the existing img.alt attribute
        const coverTitle = (img && img.dataset && img.dataset.coverupSummary)
            ? img.dataset.coverupSummary
            : (img && img.alt) ? img.alt : '';
        if (img && img.dataset) delete img.dataset.coverupSummary; // consume it

        // Step 1: Fetch the live page to find the real input field names for add_cover_art.
        // Gazelle's cover_art.js adds image/summary fields dynamically — we need their names
        // from the live DOM, not from a POST response.
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'torrents.php?id=' + (coverInfo.groupId || groupId),
            onload: function(pageResp) {
                const parser = new DOMParser();
                const livePage = parser.parseFromString(pageResp.responseText, 'text/html');

                // Find image and summary field names from the live page
                // They sit near the add_cover div — look for text/url inputs adjacent to it
                let imageFieldName = 'image';
                let summaryFieldName = 'summary';
                const addCoverDiv = livePage.getElementById('add_cover');
                if (addCoverDiv) {
                    // Walk up to find sibling/parent inputs
                    const container = addCoverDiv.closest('form') || addCoverDiv.parentNode;
                    if (container) {
                        const textInputs = container.querySelectorAll('input[type="text"], input:not([type])');
                        textInputs.forEach(inp => {
                            const n = (inp.name || '').toLowerCase();
                            const p = (inp.placeholder || '').toLowerCase();
                            if (n.includes('image') || n.includes('url') || p.includes('image') || p.includes('url') || n === 'image') {
                                imageFieldName = inp.name;
                            }
                            if (n.includes('summary') || n.includes('title') || n.includes('caption') || p.includes('summary')) {
                                summaryFieldName = inp.name;
                            }
                        });
                    }
                }
                // Field names confirmed by inspecting addCoverField() output:
                // Redacted uses array notation: image[] and summary[]

                // Send as multipart FormData — Redacted uses image[]/summary[] array fields
                const addFormData = new FormData();
                addFormData.append('action',   'add_cover_art');
                addFormData.append('auth',     coverInfo.authToken);
                addFormData.append('groupid',  coverInfo.groupId || groupId);
                addFormData.append('image[]',  newUrl);
                addFormData.append('summary[]', coverTitle || 'Rehosted cover');


                // Step 2: GET remove_cover_art to delete the old entry FIRST.
                const removeUrl = 'torrents.php?action=remove_cover_art'
                    + '&auth='    + encodeURIComponent(coverInfo.authToken)
                    + '&id='      + encodeURIComponent(coverInfo.coverArtId)
                    + '&groupid=' + encodeURIComponent(coverInfo.groupId || groupId);

                GM_xmlhttpRequest({
                    method: 'GET',
                    url:    removeUrl,
                    onload: function(removeResp) {
                        console.log('[CoverUp] remove_cover_art status:', removeResp.status);

                        // Step 3: POST add_cover_art as multipart FormData

                GM_xmlhttpRequest({
                    method: 'POST',
                    url:    'torrents.php',
                    data:   addFormData,
                    onload: function(addResp) {
                        console.log('[CoverUp] add_cover_art status:', addResp.status);
                        console.log('[CoverUp] add_cover_art finalUrl:', addResp.finalUrl);
                        console.log('[CoverUp] add_cover_art response length:', addResp.responseText.length);
                        console.log('[CoverUp] add_cover_art newUrl present in response:', addResp.responseText.includes(newUrl));
                        console.log('[CoverUp] add_cover_art response (first 500 chars):', addResp.responseText.slice(0, 500));

                        // Parse the response page to find the add_cover_art form fields
                        // and any error messages — this tells us what Gazelle actually expects
                        try {
                            const parser = new DOMParser();
                            const doc = parser.parseFromString(addResp.responseText, 'text/html');
                            // Find any error notices
                            const notices = doc.querySelectorAll('.error, .notice, #notice, .alertbar');
                            notices.forEach(n => console.log('[CoverUp] Page notice:', n.textContent.trim().slice(0, 200)));
                            // Find the add cover art form and log all its fields
                            const forms = doc.querySelectorAll('form');
                            forms.forEach(form => {
                                const action = form.getAttribute('action') || '';
                                const inputs = Array.from(form.querySelectorAll('input, textarea, select'));
                                const hasAddCover = inputs.some(i => (i.value || '').includes('add_cover')) ||
                                                    action.includes('add_cover') ||
                                                    form.innerHTML.includes('add_cover_art');
                                if (hasAddCover || form.innerHTML.includes('cover')) {
                                    console.log('[CoverUp] Cover form action:', action);
                                    console.log('[CoverUp] Cover form HTML:', form.innerHTML.slice(0, 1200));
                                    inputs.forEach(i => {
                                        console.log('[CoverUp] Form field:', i.type, '|', i.name || '(no name)', '=', (i.value || '').slice(0, 100));
                                    });
                                }
                            });
                        } catch(e) { console.log('[CoverUp] Form parse error:', e.message); }
                        console.log('[CoverUp] Alt cover rehosted successfully:', newUrl);

                        if (addResp.status < 200 || addResp.status >= 400) {
                            link.textContent = '[add failed (HTTP ' + addResp.status + ') — old entry already removed!]';
                            link.style.color = '#f44';
                            console.error('[CoverUp] add_cover_art returned HTTP', addResp.status);
                            return;
                        }

                        link.textContent = '✓ Saved — reloading…';
                        link.style.color = '#4CAF50';
                        setTimeout(function() { location.reload(); }, 800);
                    },
                    onerror: function() {
                        link.textContent = '[add failed — old entry already removed!]';
                        link.style.color = '#f44';
                        console.error('[CoverUp] add_cover_art POST failed');
                    }
                });
            },
            onerror: function() {
                link.textContent = '[remove failed — add not attempted, old entry kept]';
                link.style.color = '#f44';
                console.error('[CoverUp] remove_cover_art GET failed');
            }
                });
            },
            onerror: function() {
                link.textContent = '[could not load page to detect form fields]';
                link.style.color = '#f44';
                console.error('[CoverUp] live page fetch failed');
            }
        });
    }

})();