Harmony: Enhancements

Adds some convenience features, various UI and behavior settings, as well as an improved language detection to Harmony.

// ==UserScript==
// @name         Harmony: Enhancements
// @namespace    https://musicbrainz.org/user/chaban
// @version      1.3.1
// @tag          ai-created
// @description  Adds some convenience features, various UI and behavior settings, as well as an improved language detection to Harmony.
// @author       chaban
// @license      MIT
// @match        https://harmony.pulsewidth.org.uk/*
// @connect      none
// @icon         https://harmony.pulsewidth.org.uk/harmony-logo.svg
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_info
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_NAME = GM_info.script.name;
    const TOOLTIP_DISPLAY_DURATION = 2000;
    const DATA_ATTRIBUTE_APPLIED = 'data-he-applied';

    // --- CONFIGURATION ---
    // The top-level keys of this object MUST match the function names in the `enhancements` object.
    const SETTINGS_CONFIG = {
        // Seeder Behavior
        skipConfirmation: { key: 'enhancements.seeder.skipConfirmation', label: 'Skip MusicBrainz confirmation page when adding a new release', description: 'Automatically skips the interstitial page when seeding data from external pages.', defaultValue: false, section: 'Seeder Behavior', type: 'checkbox' },
        updateProperties: { key: 'enhancements.seeder.updateProperties', label: 'Include GTIN and packaging when updating an existing release', description: 'When using the "Update external links in MusicBrainz" button, also include the GTIN (barcode) and set packaging to "None".', defaultValue: false, section: 'Seeder Behavior', type: 'checkbox' },
        // UI Settings
        hideDebugMessages: { key: 'enhancements.ui.hideDebugMessages', label: 'Hide debug messages on release pages', description: 'Hides the boxes containing debug information from Harmony, such as guessed languages and scripts.', defaultValue: false, section: 'UI Settings', type: 'checkbox' },
        toggleReleaseInfo: { key: 'enhancements.ui.hideReleaseInfo', label: 'Hide Availability, Sources, and External Links sections', description: 'Hides the verbose and redundant release info sections.', defaultValue: false, section: 'UI Settings', type: 'checkbox', paths: [/^\/release(?!\/actions)/] },
        // Convenience Features
        addSearchLinks: { key: 'enhancements.ui.addSearchLinks', label: 'Add external search links (Qobuz, YouTube Music, etc.)', description: 'Adds quick search links for the release on various external sites.', defaultValue: false, section: 'Convenience Features', type: 'checkbox', paths: [/^\/release(?!\/actions)/] },
        makePermalinkCopyable: { key: 'enhancements.ui.copyPermalink', label: 'Enable copying permalink on click', description: 'Makes the "Permanent link to this version" clickable to copy the URL to the clipboard.', defaultValue: false, section: 'Convenience Features', type: 'checkbox', paths: [/^\/release(?!\/actions)/] },
        addClipboardButton: { key: 'enhancements.ui.clipboardRelookup', label: `Add 'Re-Lookup with MBID from Clipboard' button`, description: 'Adds a button to the "Release Lookup" page to redo the lookup using a MusicBrainz Release ID found in the clipboard.', defaultValue: true, section: 'Convenience Features', type: 'checkbox', paths: [/^\/$/, /^\/release(?!\/actions)/] },
        addActionsRelookupLink: { key: 'enhancements.ui.actionsRelookup', label: 'Add "Re-Lookup" link on Release Actions page', description: 'Adds a link to re-lookup a release from the Harmony release actions page.', defaultValue: true, section: 'Convenience Features', type: 'checkbox', paths: [/^\/release\/actions/] },
        makeTracklistCopyable: { key: 'enhancements.ui.copyTracklist', label: `Enable copying tracklist by clicking "Track" header`, description: 'Copies the tracklist in MusicBrainz track parser compatible format to clipboard when clicking on track header', defaultValue: true, section: 'Convenience Features', type: 'checkbox', paths: [/^\/release(?!\/actions)/] },
        // Language Detection
        runLanguageDetection: { key: 'enhancements.lang.enabled', label: 'Enable browser-based language detection', description: 'Uses your browser\'s built-in API for a secondary language analysis, which can be more accurate than Harmony\'s default.<br>Only works in <a href="https://developer.mozilla.org/en-US/docs/Web/API/LanguageDetector#browser_compatibility" rel="noopener noreferrer">Chrome >138</a> as of July 2025', defaultValue: true, section: 'Language Detection', type: 'checkbox', paths: [/^\/release(?!\/actions)/] },
        confidenceThreshold: { key: 'enhancements.lang.confidenceThreshold', label: 'Confidence Threshold', description: 'The minimum confidence level (in percent) required for the browser-detected language to be applied.', defaultValue: 50, section: 'Language Detection', type: 'range' },
        conflictThreshold: { key: 'enhancements.lang.conflictThreshold', label: 'Harmony Conflict Threshold', description: 'If Harmony\'s confidence is below this level, this script will overwrite its guess. Otherwise, it will not.', defaultValue: 90, section: 'Language Detection', type: 'range' },
        detectSingles: { key: 'enhancements.lang.detectSingles', label: 'Analyze single-track releases', description: 'By default, language detection is skipped for releases with only one track. Enable this to analyze them.', defaultValue: false, section: 'Language Detection', type: 'checkbox' },
        ignoreHarmony: { key: 'enhancements.lang.ignoreHarmony', label: `Force overwrite Harmony's guess`, description: 'Always replace Harmony\'s language guess result with the browser-detected result, regardless of confidence scores.', defaultValue: false, section: 'Language Detection', type: 'checkbox' },
        stopWords: { key: 'enhancements.lang.stopWords', label: 'Stop Words (one per line)', description: 'These common words will be ignored during language analysis to improve accuracy. Add or remove words as needed.', defaultValue: ['a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'bye', 'for', 'from', 'is', 'it', 'of', 'off', 'on', 'the', 'to', 'was', 'with'], section: 'Language Detection', type: 'textarea' },
        techTerms: { key: 'enhancements.lang.techTerms', label: 'Technical Terms (one per line, regex supported)', description: 'Terms that are not specific to any language (like "remix" or "live") will be removed from titles before analysis.', defaultValue: ['live', 'remix(es)?', 'edit(ion)?', 'medley', 'mix', 'version(s)?', 'instrumental', 'album', 'radio', 'single', 'vocal', 'dub', 'club', 'extended', 'original', 'acoustic', 'unplugged', 'mono', 'stereo', 'demo', 'remaster(ed)?', 'f(ea)?t\\.?', 'sped up', 'slowed', 'chopped', 'screwed', '8d'], section: 'Language Detection', type: 'textarea' },
        // Internal, non-configurable features
        setupFormSubmitListener: { key: 'enhancements.internal.formListener', defaultValue: true, paths: [/^\/release(?!\/actions)/] },
        removeHardcodedBy: { key: 'enhancements.internal.removeHardcodedBy', defaultValue: true, paths: [/^\/release/] },
    };

    const ISO_639_1_TO_3_MAP = {'aa':'aar','ab':'abk','ae':'ave','af':'afr','ak':'aka','am':'amh','an':'arg','ar':'ara','as':'asm','av':'ava','ay':'aym','az':'aze','ba':'bak','be':'bel','bg':'bul','bi':'bis','bm':'bam','bn':'ben','bo':'bod','br':'bre','bs':'bos','ca':'cat','ce':'che','ch':'cha','co':'cos','cr':'cre','cs':'ces','cu':'chu','cv':'chv','cy':'cym','da':'dan','de':'deu','dv':'div','dz':'dzo','ee':'ewe','el':'ell','en':'eng','eo':'epo','es':'spa','et':'est','eu':'eus','fa':'fas','ff':'ful','fi':'fin','fj':'fij','fo':'fao','fr':'fra','fy':'fry','ga':'gle','gd':'gla','gl':'glg','gn':'grn','gu':'guj','gv':'glv','ha':'hau','he':'heb','hi':'hin','ho':'hmo','hr':'hrv','ht':'hat','hu':'hun','hy':'hye','hz':'her','ia':'ina','id':'ind','ie':'ile','ig':'ibo','ii':'iii','ik':'ipk','io':'ido','is':'isl','it':'ita','iu':'iku','ja':'jpn','jv':'jav','ka':'kat','kg':'kon','ki':'kik','kj':'kua','kk':'kaz','kl':'kal','km':'khm','kn':'kan','ko':'kor','kr':'kau','ks':'kas','ku':'kur','kv':'kom','kw':'cor','ky':'kir','la':'lat','lb':'ltz','lg':'lug','li':'lim','ln':'lin','lo':'lao','lt':'lit','lu':'lub','lv':'lav','mg':'mlg','mh':'mah','mi':'mri','mk':'mkd','ml':'mal','mn':'mon','mr':'mar','ms':'msa','mt':'mlt','my':'mya','na':'nau','nb':'nob','nd':'nde','ne':'nep','ng':'ndo','nl':'nld','nn':'nno','no':'nor','nr':'nbl','nv':'nav','ny':'nya','oc':'oci','oj':'oji','om':'orm','or':'ori','os':'oss','pa':'pan','pi':'pli','pl':'pol','ps':'pus','pt':'por','qu':'que','rm':'roh','rn':'run','ro':'ron','ru':'rus','rw':'kin','sa':'san','sc':'srd','sd':'snd','se':'sme','sg':'sag','si':'sin','sk':'slv','sl':'slv','sm':'smo','sn':'sna','so':'som','sq':'sqi','sr':'srp','ss':'ssw','st':'sot','su':'sun','sv':'swe','sw':'swa','ta':'tam','te':'tel','tg':'tgk','th':'tha','ti':'tir','tk':'tuk','tl':'tgl','tn':'tsn','to':'ton','tr':'tur','ts':'tso','tt':'tat','tw':'twi','ty':'tah','ug':'uig','uk':'ukr','ur':'urd','uz':'uzb','ve':'ven','vi':'vie','vo':'vol','wa':'wln','wo':'wol','xh':'xho','yi':'yid','yo':'yor','za':'zha','zh':'zho','zu':'zul'};
    const getISO639_3_Code = (code) => ISO_639_1_TO_3_MAP[code] || null;

    const langDetectState = {
        code: null,
        detector: null,
        apiFailed: false,
        result: null,
    };

    // --- UTILITY FUNCTIONS ---

    function log(message, ...args) {
        console.log(`%c[${SCRIPT_NAME}] %c${message}`, 'color: #337ab7; font-weight: bold;', 'color: unset;', ...args);
    }

    function warn(message, ...args) {
        console.warn(`%c[${SCRIPT_NAME}] %c${message}`, 'color: #f0ad4e; font-weight: bold;', 'color: unset;', ...args);
    }

    function error(message, ...args) {
        console.error(`%c[${SCRIPT_NAME}] %c${message}`, 'color: #d9534f; font-weight: bold;', 'color: unset;', ...args);
    }

    async function getSettings() {
        const settings = {};
        for (const config of Object.values(SETTINGS_CONFIG)) {
            settings[config.key] = await GM_getValue(config.key, config.defaultValue);
        }
        return settings;
    }

    const getReleaseDataFromJSON = (() => {
        let releaseData = null;
        let hasRun = false;

        return () => {
            if (hasRun) return releaseData;
            hasRun = true;

            const scriptElement = document.querySelector('script[id^="__FRSH_STATE_"]');
            if (!scriptElement?.textContent) {
                warn('Could not find Fresh state JSON script tag.');
                return null;
            }
            try {
                const data = JSON.parse(scriptElement.textContent);
                releaseData = data.v?.flat().find(prop => prop?.release)?.release;
                if (!releaseData) {
                    warn('Could not find release data within Fresh state JSON.');
                }
                return releaseData;
            } catch (e) {
                error('Failed to parse Fresh state JSON.', e);
                return null;
            }
        };
    })();

    function showTooltip(message, type, event) {
        const tooltip = document.createElement('div');
        tooltip.textContent = message;
        tooltip.className = `he-tooltip ${type === 'success' ? 'he-tooltip-success' : 'he-tooltip-error'}`;
        document.body.appendChild(tooltip);
        tooltip.style.left = `${event.clientX - (tooltip.offsetWidth / 2)}px`;
        tooltip.style.top = `${event.clientY - tooltip.offsetHeight - 10}px`;
        setTimeout(() => { tooltip.style.opacity = '1'; }, 10);
        setTimeout(() => {
            tooltip.style.opacity = '0';
            tooltip.addEventListener('transitionend', () => tooltip.remove());
        }, TOOLTIP_DISPLAY_DURATION);
    }

    function showConfirmationModal({ title, message, confirmText = 'Confirm', cancelText = 'Cancel' }) {
        return new Promise((resolve) => {
            const overlay = document.createElement('div');
            overlay.className = 'he-modal-overlay';

            const modal = document.createElement('div');
            modal.className = 'he-modal-content';

            modal.innerHTML = `
                <h3>${title}</h3>
                <p>${message}</p>
                <div class="he-modal-actions">
                    <button class="he-modal-cancel-button">${cancelText}</button>
                    <button class="he-modal-confirm-button">${confirmText}</button>
                </div>
            `;

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

            const close = (value) => {
                overlay.remove();
                resolve(value);
            };

            modal.querySelector('.he-modal-confirm-button').onclick = () => close(true);
            modal.querySelector('.he-modal-cancel-button').onclick = () => close(false);
            overlay.onclick = (e) => {
                if (e.target === overlay) close(false);
            };
        });
    }


    // --- SETTINGS PAGE ---

    function initSettingsPage(settings) {
        const main = document.querySelector('main');
        if (!main || main.querySelector('.he-settings-container')) return;

        const sections = Object.values(SETTINGS_CONFIG)
            .filter(config => config.section) // Filter out internal settings
            .reduce((acc, config) => {
                (acc[config.section] = acc[config.section] || []).push(config);
                return acc;
            }, {});

        const container = document.createElement('div');
        container.className = 'he-settings-container';

        for (const [name, configs] of Object.entries(sections)) {
            const header = document.createElement('div');
            header.className = 'he-settings-header';

            const h3 = document.createElement('h3');
            h3.textContent = name;
            header.appendChild(h3);

            if (name === 'Language Detection') {
                const resetButton = document.createElement('button');
                resetButton.textContent = 'Reset Language Settings';
                resetButton.className = 'he-reset-button';
                resetButton.onclick = async (e) => {
                    e.preventDefault();
                    const confirmed = await showConfirmationModal({
                        title: 'Reset Language Settings',
                        message: 'Are you sure you want to reset all language detection settings to their defaults? This cannot be undone.',
                        confirmText: 'Reset'
                    });
                    if (confirmed) {
                        await resetLanguageSettings();
                        showTooltip('Language settings have been reset.', 'success', e);
                    }
                };
                header.appendChild(resetButton);
            }

            container.appendChild(header);

            configs.forEach(config => {
                const wrap = document.createElement('div');
                wrap.className = 'row he-setting-row';

                const textContainer = document.createElement('div');
                textContainer.className = 'he-setting-text-container';

                const lbl = document.createElement('label');
                lbl.htmlFor = config.key;
                lbl.textContent = config.label;
                lbl.className = 'he-setting-label';
                textContainer.appendChild(lbl);

                let input;
                let descriptionEl;

                if (config.description) {
                    descriptionEl = document.createElement('small');
                    descriptionEl.id = `${config.key}-desc`;
                    descriptionEl.innerHTML = config.description;
                    descriptionEl.className = 'he-setting-description';
                    textContainer.appendChild(descriptionEl);
                }

                switch (config.type) {
                    case 'checkbox':
                        input = document.createElement('input');
                        input.type = 'checkbox';
                        input.checked = settings[config.key];
                        input.className = 'he-checkbox';
                        wrap.append(input, textContainer);
                        break;
                    case 'range':
                        wrap.classList.add('he-setting-row-column');
                        input = document.createElement('input');
                        input.type = 'range';
                        input.min = 0;
                        input.max = 100;
                        input.value = settings[config.key];
                        const val = document.createElement('span');
                        val.textContent = ` ${settings[config.key]}%`;
                        input.addEventListener('input', () => val.textContent = ` ${input.value}%`);
                        const rangeWrap = document.createElement('div');
                        rangeWrap.className = 'he-range-wrap';
                        rangeWrap.append(input, val);
                        wrap.append(textContainer, rangeWrap);
                        break;
                    case 'textarea':
                        wrap.classList.add('he-setting-row-column');
                        input = document.createElement('textarea');
                        input.rows = 5;
                        input.value = Array.isArray(settings[config.key]) ? settings[config.key].join('\n') : '';
                        input.className = 'he-textarea';
                        wrap.append(textContainer, input);
                        break;
                }

                if (input) {
                    input.id = config.key;
                    if (descriptionEl) {
                        input.setAttribute('aria-describedby', descriptionEl.id);
                    }
                    const save = () => {
                        let value;
                        if (config.type === 'checkbox') value = input.checked;
                        else if (config.type === 'range') value = parseInt(input.value, 10);
                        else if (config.type === 'textarea') value = input.value.split('\n').map(s => s.trim()).filter(Boolean);
                        GM_setValue(config.key, value);
                    };
                    input.addEventListener('change', save);
                    if (config.type === 'range' || config.type === 'textarea') {
                        input.addEventListener('input', save);
                    }
                }
                container.appendChild(wrap);
            });
        }
        main.appendChild(container);
    }

    async function resetLanguageSettings() {
        const langConfigs = Object.values(SETTINGS_CONFIG).filter(c => c.section === 'Language Detection');
        for (const config of langConfigs) {
            await GM_setValue(config.key, config.defaultValue);
            const input = document.getElementById(config.key);
            if (!input) continue;

            switch (config.type) {
                case 'checkbox':
                    input.checked = config.defaultValue;
                    break;
                case 'range':
                    input.value = config.defaultValue;
                    input.nextElementSibling.textContent = ` ${config.defaultValue}%`;
                    break;
                case 'textarea':
                    input.value = config.defaultValue.join('\n');
                    break;
            }
        }
    }


    // --- ENHANCEMENT MODULES ---

    const enhancements = {
        _copyHandler: async (event, text, name) => {
            try {
                await navigator.clipboard.writeText(text);
                showTooltip(`${name} copied!`, 'success', event);
            } catch (err) {
                error(`Failed to copy ${name}:`, err);
                showTooltip(`Failed to copy ${name}!`, 'error', event);
            }
        },

        removeHardcodedBy: () => {
            const releaseArtistNode = document.querySelector('.release-artist');
            if (releaseArtistNode && releaseArtistNode.firstChild && releaseArtistNode.firstChild.nodeType === Node.TEXT_NODE && releaseArtistNode.firstChild.textContent.trim().startsWith('by')) {
                releaseArtistNode.firstChild.textContent = releaseArtistNode.firstChild.textContent.replace(/^by\s+/, '');
            }
        },
        toggleReleaseInfo: (settings) => {
            const terms = ['Availability', 'Sources', 'External links'];
            document.querySelectorAll('.release-info > tbody > tr').forEach(row => {
                const header = row.querySelector('th');
                if (header && terms.includes(header.innerText.trim())) {
                    row.style.display = 'none';
                }
            });
        },

        addClipboardButton: (settings) => {
            if (document.getElementById('he-redo-lookup-mbid-button')) return;
            const lookupBtn = document.querySelector('input[type="submit"][value="Lookup"]');
            const container = lookupBtn?.closest('.input-with-overlay');
            if (!container) return;

            const newBtn = document.createElement('input');
            newBtn.type = 'submit';
            newBtn.value = 'Re-Lookup with MBID from Clipboard';
            newBtn.id = 'he-redo-lookup-mbid-button';
            newBtn.className = lookupBtn.className;

            container.parentElement.insertBefore(newBtn, container.nextSibling);

            newBtn.addEventListener('click', async (e) => {
                e.preventDefault();
                try {
                    const text = await navigator.clipboard.readText();
                    const mbid = (text.match(/musicbrainz\.org\/release\/([a-f0-9\-]{36})/i) || [])[1];
                    if (mbid) {
                        const url = new URL(window.location.href);
                        url.searchParams.set('musicbrainz', mbid);
                        window.location.href = url.toString();
                    } else {
                        showTooltip('No MBID Found in Clipboard!', 'error', e);
                    }
                } catch (err) {
                    const message = err.name === 'NotAllowedError' ? 'Clipboard permission denied!' : 'Could not read clipboard!';
                    showTooltip(message, 'error', e);
                }
            });
        },

        addActionsRelookupLink: (settings) => {
            if (document.getElementById('he-relookup-link-container')) return;
            const h2 = Array.from(document.querySelectorAll('h2')).find(h => h.textContent.includes('Release Actions'));
            if (!h2) return;

            const mbid = new URLSearchParams(window.location.search).get('release_mbid');
            if (!mbid) return;

            const params = new URLSearchParams({ musicbrainz: mbid });
            document.querySelectorAll('.provider-list li').forEach(item => {
                const pName = item.getAttribute('data-provider')?.toLowerCase();
                const pId = item.querySelector('.provider-id')?.textContent.trim();
                if (pName && pId) params.set(pName, pId);
            });

            const url = `/release?${params.toString()}`;
            const container = document.createElement('div');
            container.id = 'he-relookup-link-container';
            container.className = 'message';
            container.innerHTML = `<svg class="icon" width="24" height="24" stroke-width="2"><use xlink:href="/icon-sprite.svg#brand-metabrainz"></use></svg><p><a href="${url}">Re-Lookup with Harmony</a></p>`;
            h2.parentNode.insertBefore(container, h2.nextSibling);
        },

        makeTracklistCopyable: (settings) => {
            const header = Array.from(document.querySelectorAll('table.tracklist th')).find(th => th.textContent.trim() === 'Track');
            if (!header || header.hasAttribute(DATA_ATTRIBUTE_APPLIED)) return;

            header.setAttribute(DATA_ATTRIBUTE_APPLIED, 'true');
            header.title = 'Click to copy this tracklist';
            header.classList.add('copyable-header');

            header.addEventListener('click', async (e) => {
                const table = header.closest('table.tracklist');
                if (!table) return;

                const getCleanText = (element, selectorsToRemove) => {
                    if (!element) return '';
                    const clone = element.cloneNode(true);
                    clone.querySelectorAll(selectorsToRemove.join(',')).forEach(el => el.remove());
                    return clone.textContent.trim();
                };

                const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
                const requiredCols = ['Track', 'Title', 'Artists', 'Length'];
                const colIndices = Object.fromEntries(requiredCols.map(name => [name, headers.indexOf(name)]));

                if (Object.values(colIndices).some(i => i === -1)) {
                    showTooltip(`Required columns missing: ${requiredCols.join(', ')}`, 'error', e);
                    return;
                }

                const lines = Array.from(table.querySelectorAll('tbody tr')).map(row => {
                    const cells = row.children;
                    const num = cells[colIndices.Track]?.textContent.trim() || '';
                    const title = getCleanText(cells[colIndices.Title], ['ul.alt-values']);
                    const artist = getCleanText(cells[colIndices.Artists]?.querySelector('.artist-credit'), ['ul.alt-values']);
                    const len = (getCleanText(cells[colIndices.Length], ['ul.alt-values'])).split('.')[0];
                    return `${num}. ${title}${artist ? ` - ${artist}` : ''} (${len})`;
                });

                if (lines.length > 0) {
                    enhancements._copyHandler(e, lines.join('\n'), 'Tracklist');
                }
            });
        },

        makePermalinkCopyable: (settings) => {
            const permaLink = document.querySelector('p.center > a');
            if (!permaLink || permaLink.hasAttribute(DATA_ATTRIBUTE_APPLIED)) return;

            permaLink.setAttribute(DATA_ATTRIBUTE_APPLIED, 'true');
            permaLink.classList.add('copyable-permalink');
            permaLink.title = 'Click to copy URL';

            permaLink.addEventListener('click', async (e) => {
                e.preventDefault();
                const url = permaLink.href;
                enhancements._copyHandler(e, url, 'Permalink');
            });
        },

        addSearchLinks: (settings) => {
            if (document.getElementById('he-search-links')) return;

            const releaseData = getReleaseDataFromJSON();
            if (!releaseData || !releaseData.title || !releaseData.artists) return;

            const isVariousArtists = releaseData.artists.length >= 5;
            const releaseArtist = isVariousArtists ? 'Various Artists' : releaseData.artists.map(a => a.name).join(' ');
            const releaseTitle = releaseData.title;

            const encodedArtist = encodeURIComponent(releaseArtist);
            const encodedTitle = encodeURIComponent(releaseTitle);

            const searchLinks = [];

            // --- Qobuz Search Logic ---
            const regionInput = document.querySelector('#region-input');
            const currentRegion = regionInput ? regionInput.value.toLowerCase() : '';
            const defaultQbzRegion = 'us-en';
            const regionMap = new Map([
                ['ar','ar-es'], ['au','au-en'], ['at','at-de'], ['be','be-nl'], ['br','br-pt'],
                ['ca','ca-en'], ['cl','cl-es'], ['co','co-es'], ['dk','dk-en'], ['fi','fi-en'],
                ['fr','fr-fr'], ['de','de-de'], ['ie','ie-en'], ['it','it-it'], ['jp','jp-ja'],
                ['lu','lu-de'], ['mx','mx-es'], ['nl','nl-nl'], ['nz','nz-en'], ['no','no-en'],
                ['pt','pt-pt'], ['es','es-es'], ['se','se-en'], ['ch','ch-de'], ['gb','gb-en'],
                ['us','us-en'],
            ]);

            let qbzRegion = defaultQbzRegion;
            if (currentRegion.includes(',')) {
                const regions = currentRegion.split(',');
                for (const region of regions) {
                    if (regionMap.has(region)) {
                        qbzRegion = regionMap.get(region);
                        break;
                    }
                }
            } else if (regionMap.has(currentRegion)) {
                qbzRegion = regionMap.get(currentRegion);
            }

            searchLinks.push({
                name: 'Search Qobuz',
                url: `https://www.qobuz.com/${qbzRegion}/search?q=${encodedArtist}%20${encodedTitle}&type=album`
            });

            // --- YouTube Music Search (by barcode if available) ---
            if (releaseData.gtin) {
                const barcode = releaseData.gtin.replace(/^0+/, '');
                searchLinks.push({
                    name: 'Search YouTube Music',
                    url: `https://music.youtube.com/search?q="${encodeURIComponent(barcode)}"`
                });
            }

            // --- Beatsource Search ---
            searchLinks.push({
                name: 'Search Beatsource',
                url: `https://www.beatsource.com/search/releases?q=${encodedArtist}%20${encodedTitle}`
            });

            // --- Placement ---
            const targetElement = document.querySelector('h2.center');
            if (!targetElement || !targetElement.nextElementSibling) return;

            const container = document.createElement('div');
            container.id = 'he-search-links';
            container.style.textAlign = 'center';
            container.style.marginBottom = '1em';

            searchLinks.forEach((link, index) => {
                const anchor = document.createElement('a');
                anchor.href = link.url;
                anchor.textContent = link.name;
                anchor.target = '_blank';
                container.appendChild(anchor);

                if (index < searchLinks.length - 1) {
                    container.appendChild(document.createTextNode(' | '));
                }
            });

            targetElement.nextElementSibling.append(container);
        },

        runLanguageDetection: async (settings) => {
            if (langDetectState.result !== null) {
                enhancements.applyLanguageDetectionResult(settings, langDetectState.result);
                return;
            }

            if (!langDetectState.detector && !langDetectState.apiFailed) {
                if ('LanguageDetector' in window) {
                    try {
                        const nativeDetector = await window.LanguageDetector.create();
                        langDetectState.detector = (text) => nativeDetector.detect(text);
                    } catch (error) {
                        error('LanguageDetector API failed to initialize.', error);
                        langDetectState.apiFailed = true;
                    }
                } else {
                    warn('LanguageDetector API not available in this browser.');
                    langDetectState.apiFailed = true;
                }
            }

            if (langDetectState.apiFailed) {
                langDetectState.result = { skipped: true, debugInfo: { analyzedText: 'LanguageDetector API not available or failed to load.' } };
                enhancements.applyLanguageDetectionResult(settings, langDetectState.result);
                return;
            }

            const releaseData = getReleaseDataFromJSON();
            if (!releaseData) return;

            const { title: releaseTitle, media } = releaseData;
            const trackTitles = media?.flatMap(m => m.tracklist.map(t => t.title)) || [];
            const trackCount = media?.reduce((sum, m) => sum + (m.tracklist?.length || 0), 0) || 0;

            if (trackCount === 1 && !settings[SETTINGS_CONFIG.detectSingles.key]) {
                langDetectState.result = { skipped: true, debugInfo: { analyzedText: 'Skipped: Single track release detection is disabled.' } };
                enhancements.applyLanguageDetectionResult(settings, langDetectState.result);
                return;
            }

            const allTitles = [releaseTitle, ...trackTitles].filter(Boolean);
            if (allTitles.length === 0) return;

            // --- Text Cleaning & Analysis Setup ---
            const techTerms = settings[SETTINGS_CONFIG.techTerms.key];
            const stopWords = new Set(settings[SETTINGS_CONFIG.stopWords.key]);
            const enclosedRegex = new RegExp(`\\s*(?:\\([^)]*\\b(${techTerms.join('|')})\\b[^)]*\\)|\\[[^\\]]*\\b(${techTerms.join('|')})\\b[^\\]]*\\])`, 'ig');
            const trailingRegex = new RegExp(`\\s+[-–]\\s+.*(?:${techTerms.map(t => `\\b${t}\\b`).join('|')}).*`, 'ig');
            const stopWordsRegex = new RegExp(`\\b(${Array.from(stopWords).join('|')})\\b`, 'gi');

            // --- Multi-stage Filtering Algorithm ---

            // Stage 1: Initial Cleaning (remove bracketed/hyphenated technical terms)
            let cleanedTitles = allTitles.map(title =>
                title.replace(enclosedRegex, '').replace(trailingRegex, '').trim()
            ).filter(Boolean);

            // Stage 2: Contextual "Core Title" Cleaning (find a common base title)
            const titleCounts = new Map();
            cleanedTitles.forEach(title => titleCounts.set(title, (titleCounts.get(title) || 0) + 1));
            let coreTitle = null;
            let maxCount = 0;
            if (titleCounts.size > 1) {
                for (const [title, count] of titleCounts.entries()) {
                    if (count > maxCount) {
                        maxCount = count;
                        coreTitle = title;
                    }
                }
            }
            if (maxCount > 1) {
                cleanedTitles = cleanedTitles.map(title => (title.startsWith(coreTitle) && title !== coreTitle) ? coreTitle : title);
            }

            // Stage 3: Stop Word Removal (from within titles)
            let surgicallyCleanedTitles = cleanedTitles.map(title =>
                title.replace(stopWordsRegex, '').replace(/\s{2,}/g, ' ').trim()
            );

            // Stage 4: Whole Title Filtering (remove titles that are now just stop words)
            const finalFilteredTitles = surgicallyCleanedTitles.filter(title => {
                if (!title) return false;
                const normalizedTitle = title.toLowerCase().replace(/[\s.]+/g, '');
                return !stopWords.has(normalizedTitle);
            });

            // Stage 5: De-duplication & Analysis
            const uniqueTitles = [...new Set(finalFilteredTitles)];
            const titlesToAnalyze = uniqueTitles.length > 0 ? uniqueTitles : [...new Set(allTitles)];

            let textToAnalyze = titlesToAnalyze.join(' . ');
            if (titlesToAnalyze.length <= 3) {
                textToAnalyze += ' .';
            }

            if (!textToAnalyze.replaceAll(/\P{Letter}/gu, '')) {
                langDetectState.result = { languageName: '[No linguistic content]', confidence: 100, languageCode3: 'zxx', isZxx: true, debugInfo: { allResults: [], analyzedText: textToAnalyze } };
            } else {
                const results = await langDetectState.detector(textToAnalyze);
                if (results.length === 0) return;
                const final = results[0];
                langDetectState.result = {
                    languageName: new Intl.DisplayNames(['en'], { type: 'language' }).of(final.detectedLanguage),
                    confidence: Math.round(final.confidence * 100),
                    languageCode3: getISO639_3_Code(final.detectedLanguage),
                    isZxx: false,
                    skipped: false,
                    debugInfo: { allResults: results, analyzedText: textToAnalyze }
                };
            }
            enhancements.applyLanguageDetectionResult(settings, langDetectState.result);
        },

        applyLanguageDetectionResult: (settings, result) => {
            if (!result) return;
            const container = document.querySelector('div.release');
            if (!container) return;
            document.getElementById('he-language-analysis')?.remove();

            // --- Define all variables at the top scope ---
            const { languageName, confidence, languageCode3, isZxx, skipped, debugInfo } = result;
            const confidenceThreshold = settings[SETTINGS_CONFIG.confidenceThreshold.key];
            const conflictThreshold = settings[SETTINGS_CONFIG.conflictThreshold.key];

            // Get Harmony's info once
            const langRow = Array.from(document.querySelectorAll('.release-info th')).find(th => th.textContent.trim() === 'Language')?.parentElement;
            let harmonyConfidence = 0;
            let originalLang = '';
            let originalText = '';
            if (langRow) {
                originalText = langRow.querySelector('td').textContent.trim();
                originalLang = originalText.replace(/\s*\(.*\)/, '').trim();
                harmonyConfidence = parseInt((originalText.match(/\((\d+)%\sconfidence\)/) || [])[1] || '0', 10);
            }

            // Calculate shouldOverwrite once
            const shouldOverwrite = settings[SETTINGS_CONFIG.ignoreHarmony.key] || originalLang.toLowerCase() === languageName.toLowerCase() || harmonyConfidence < conflictThreshold;

            // --- Build and insert the debug message ---
            const messageDiv = document.createElement('div');
            messageDiv.id = 'he-language-analysis';
            messageDiv.className = 'message debug';
            messageDiv.innerHTML = `<svg class="icon" width="24" height="24" stroke-width="2"><use xlink:href="/icon-sprite.svg#bug"></use></svg><div><p></p></div>`;
            const p = messageDiv.querySelector('p');

            if (skipped) {
                p.textContent = debugInfo.analyzedText;
            } else {
                const b = document.createElement('b');
                b.textContent = languageName;
                p.append('Guessed language (LanguageDetector API): ', b, ` (${confidence}% confidence)`);

                // Use the pre-calculated variables
                if (!shouldOverwrite && originalLang.toLowerCase() !== languageName.toLowerCase()) {
                    const i = document.createElement('i');
                    i.textContent = ` - Harmony's confidence meets the conflict threshold (${conflictThreshold}%) and force overwrite is off, no changes applied.`;
                    p.append(i);
                } else if (confidence < confidenceThreshold) {
                    const i = document.createElement('i');
                    i.textContent = ` - below ${confidenceThreshold}% threshold, no changes applied.`;
                    p.append(i);
                }

                p.append(document.createElement('br'), `Analyzed block: "${debugInfo.analyzedText}"`);
            }

            const findInsertionAnchor = () => {
                const messages = container.querySelectorAll('.message.debug');
                let langGuessMsg = null;
                let scriptGuessMsg = null;
                for (const msg of messages) {
                    const text = msg.textContent;
                    if (text.includes('Guessed language of the titles:')) langGuessMsg = msg;
                    else if (text.includes('Detected scripts of the titles:')) scriptGuessMsg = msg;
                }
                if (langGuessMsg) return langGuessMsg.nextSibling;
                if (scriptGuessMsg) return scriptGuessMsg.nextSibling;
                return container.querySelector('.message');
            };

            const insertionAnchor = findInsertionAnchor();
            const parent = (insertionAnchor?.parentElement || container.querySelector('.message')?.parentElement) || container;
            parent.insertBefore(messageDiv, insertionAnchor);

            // --- Update the UI and Seeder ---
            const updateSeeder = (code) => {
                langDetectState.code = code;
            };

            if (isZxx) {
                if (langRow) langRow.querySelector('td').textContent = '[No linguistic content]';
                updateSeeder('zxx');
                return;
            }
            if (skipped || confidence < confidenceThreshold) return;

            const newContent = `${languageName} (${confidence}% confidence)`;
            if (langRow) {
                if (shouldOverwrite) {
                    if (originalLang.toLowerCase() !== languageName.toLowerCase()) {
                        const cell = langRow.querySelector('td');
                        const overwrittenSpan = document.createElement('span');
                        overwrittenSpan.className = 'he-overwritten-label';
                        overwrittenSpan.title = `Harmony's original guess: ${originalText}`;
                        overwrittenSpan.textContent = '(overwritten)';

                        cell.textContent = '';
                        cell.append(newContent, ' ', overwrittenSpan);
                        cell.setAttribute(DATA_ATTRIBUTE_APPLIED, 'true');
                        updateSeeder(languageCode3);
                    }
                }
            } else if (!document.getElementById('he-language-row')) {
                const table = document.querySelector('.release-info tbody');
                const scriptRow = Array.from(table.querySelectorAll('th')).find(th => th.textContent.trim() === 'Script')?.parentElement;
                const newRow = table.insertRow(scriptRow ? scriptRow.rowIndex + 1 : -1);
                newRow.id = 'he-language-row';
                newRow.innerHTML = `<th>Language</th><td>${newContent}</td>`;
                updateSeeder(languageCode3);
            }
        },

        setupFormSubmitListener: (settings) => {
            document.body.addEventListener('submit', (e) => {
                const form = e.target.closest('form');
                if (form && (form.getAttribute('name') === 'release-seeder' || form.getAttribute('name') === 'release-update-seeder')) {
                    handleSeederFormSubmit(e, settings);
                }
            });
        }
    };

    // --- FORM SUBMISSION HANDLER ---

    function handleSeederFormSubmit(event, settings) {
        const form = event.target.closest('form');
        if (!form) {
            warn('Event target has no parent form, ignoring.');
            return;
        }

        let modificationsMade = false;
        const formName = form.getAttribute('name');

        if (formName === 'release-seeder') {
            if (settings[SETTINGS_CONFIG.skipConfirmation.key]) {
                const url = new URL(form.action);
                if (!url.searchParams.has('skip_confirmation')) {
                    url.searchParams.set('skip_confirmation', '1');
                    form.action = url.toString();
                    modificationsMade = true;
                }
            }

            if (langDetectState.code) {
                let input = form.querySelector('input[name="language"]');
                if (!input) {
                    input = document.createElement('input');
                    input.type = 'hidden';
                    input.name = 'language';
                    form.appendChild(input);
                }
                if (input.value !== langDetectState.code) {
                   input.value = langDetectState.code;
                   modificationsMade = true;
                }
            }
        }

        if (formName === 'release-update-seeder' && settings[SETTINGS_CONFIG.updateProperties.key]) {
            const append = (name, value) => {
                if (!value) return;
                let input = form.querySelector(`input[type="hidden"][name="${name}"]`);
                if (!input) {
                    input = document.createElement('input');
                    input.type = 'hidden';
                    input.name = name;
                    form.appendChild(input);
                }
                if (input.value !== value) {
                    input.value = value;
                    modificationsMade = true;
                }
            };
            const gtin = Array.from(document.querySelectorAll('th')).find(th => th.textContent?.trim() === 'GTIN')?.nextElementSibling?.textContent?.trim();
            append('barcode', gtin);
            append('packaging', 'None');
        }

        if (modificationsMade) {
            event.preventDefault();
            event.stopPropagation();
            form.submit();
        }
    }

    // --- INITIALIZATION AND ROUTING ---

    function applyGlobalStyles(settings) {
        const css = `
            .release-artist::before { content: "by "; }
            .release-artist > :first-child { margin-left: 0.25em; }
            ${settings[SETTINGS_CONFIG.hideDebugMessages.key] ? '.message.debug { display: none !important; }' : ''}
            .copyable-header {
                cursor: pointer;
                text-decoration: underline dotted;
            }
            .copyable-permalink {
                cursor: pointer;
            }
            .he-overwritten-label {
                color: #d9534f;
                font-size: 0.8em;
                font-weight: bold;
                cursor: help;
                border-bottom: 1px dotted #d9534f;
            }
            .he-reset-button {
                padding: 4px 8px;
                font-size: 12px;
                border-radius: 4px;
                border: 1px solid #ccc;
                background-color: #f0f0f0;
                cursor: pointer;
            }
            .he-tooltip {
                position: fixed;
                color: white;
                padding: 5px 10px;
                border-radius: 4px;
                font-size: 12px;
                z-index: 10002;
                opacity: 0;
                transition: opacity 0.3s;
                pointer-events: none;
                white-space: nowrap;
            }
            .he-tooltip-success { background-color: #4CAF50; }
            .he-tooltip-error { background-color: #f44336; }
            .he-modal-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0, 0, 0, 0.6); z-index: 10000;
                display: flex; align-items: center; justify-content: center;
            }
            .he-modal-content {
                background-color: var(--theme-fill); padding: 20px; border-radius: 8px;
                box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); max-width: 400px;
                text-align: center;
            }
            .he-modal-content h3 { margin-top: 0; }
            .he-modal-actions { margin-top: 20px; }
            .he-modal-cancel-button {
                margin-right: 10px; padding: 8px 16px; border-radius: 4px;
                border: 1px solid #ccc; background-color: #f0f0f0; cursor: pointer;
            }
            .he-modal-confirm-button {
                padding: 8px 16px; border-radius: 4px; border: none;
                background-color: #f44336; color: white; cursor: pointer;
            }
            .he-settings-container { margin-top: 2em; }
            .he-settings-header { display: flex; justify-content: space-between; align-items: center; }
            .he-setting-row { align-items: flex-start !important; }
            .he-setting-row-column { flex-direction: column; align-items: stretch !important; }
            .he-setting-text-container { flex: 1; text-align: left; }
            .he-setting-label { cursor: pointer; display: inline-block; }
            .he-setting-description { display: block; color: #666; margin-top: 4px; }
            .he-checkbox { margin-top: 4px; }
            .he-range-wrap { display: flex; align-items: center; }
            .he-textarea {
                width: 100%;
                margin-top: 4px;
                color: var(--text);
                background-color: var(--input-fill);
            }
        `;
        GM_addStyle(css);
    }

    async function main() {
        const settings = await getSettings();
        const path = window.location.pathname;

        applyGlobalStyles(settings);

        if (path.startsWith('/settings')) {
            initSettingsPage(settings);
            return;
        }

        // Centralized routing for all enhancements
        for (const [funcName, config] of Object.entries(SETTINGS_CONFIG)) {
            if (settings[config.key] && config.paths && enhancements[funcName]) {
                if (config.paths.some(p => p.test(path))) {
                    enhancements[funcName](settings);
                }
            }
        }
    }

    main().catch(e => error(`An unhandled error occurred in main execution:`, e));

})();