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.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Advertisement:

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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');
            }
        });
    }

})();