Greasy Fork 还支持 简体中文。

Harmony: Enhancements

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

目前為 2025-09-06 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Harmony: Enhancements
// @namespace    https://musicbrainz.org/user/chaban
// @version      1.7.2
// @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
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_NAME = GM_info.script.name;
    const TOOLTIP_DISPLAY_DURATION = 2000;
    const DATA_ATTRIBUTE_APPLIED = 'data-he-applied';
    const NO_LABEL = {
        name: '[no label]',
        mbid: '157afde4-4bf5-4039-8ad2-5a15acc85176',
    };

    // --- 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/] },
        // Release Property Detection
        improveReleaseTypeDetection: { key: 'enhancements.releaseType.enabled', label: 'Improve release type detection', description: 'Uses the technical terms list to determine if a release with multiple tracks is actually a single with multiple versions.', defaultValue: false, section: 'Release Property Detection', type: 'checkbox', paths: [/^\/release(?!\/actions)/] },
        setNoLabel: { key: 'enhancements.label.setNoLabel', label: 'Set label to [no label] for self-releases', description: 'If a release appears to be self-released (label name is the same as the artist name), automatically set the label to the special purpose label "[no label]".', defaultValue: false, section: 'Release Property Detection', 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 AppState = {
        settings: {},
        dom: {},
        data: {
            release: null,
        },
        lang: {
            code: null,
            detector: null,
            apiFailed: false,
            result: null,
        },
        path: window.location.pathname,
    };

    // --- 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;
    }

    function getReleaseDataFromJSON() {
        if (AppState.data.release) return AppState.data.release;

        const { freshStateScript } = AppState.dom;
        if (!freshStateScript?.textContent) {
            warn('Could not find Fresh state JSON script tag.');
            return null;
        }
        try {
            const data = JSON.parse(freshStateScript.textContent);
            const releaseObj = data.v?.flat().find(prop => prop?.release)?.release;

            const trackArrays = data.v?.flat().filter(prop => prop?.tracks).map(prop => prop.tracks);

            if (!releaseObj) {
                warn('Could not find release data within Fresh state JSON.');
                return null;
            }

            if (trackArrays.length > 0 && Array.isArray(releaseObj.media)) {
                if (trackArrays.length === releaseObj.media.length) {
                    releaseObj.media.forEach((medium, index) => {
                        if (Array.isArray(trackArrays[index])) {
                            medium.tracklist = trackArrays[index];
                        } else {
                            warn(`Track data for medium ${index + 1} is not an array.`, trackArrays[index]);
                            medium.tracklist = [];
                        }
                    });
                } else {
                    warn(`Mismatch between number of media (${releaseObj.media.length}) and number of tracklists (${trackArrays.length}). Falling back to single tracklist assignment.`);
                    if (releaseObj.media.length > 0 && trackArrays.length > 0) {
                         releaseObj.media[0].tracklist = trackArrays.flat();
                    }
                }
            }

            AppState.data.release = releaseObj;
            return AppState.data.release;
        } catch (e) {
            error('Failed to parse Fresh state JSON.', e);
            return null;
        }
    }


    /**
     * Cleans an array of titles for analysis.
     * @param {string[]} allTitles - The array of titles to clean.
     * @param {object} options - Cleaning options.
     * @param {'light' | 'deep'} options.cleanLevel - The depth of cleaning. 'light' for release type, 'deep' for language detection.
     * @returns {string[]} The cleaned array of titles.
     */
    function getCleanedTitles(allTitles, { cleanLevel = 'deep' }) {
        const techTerms = AppState.settings[SETTINGS_CONFIG.techTerms.key];
        const stopWords = new Set(AppState.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');


        // 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 && coreTitle) {
            cleanedTitles = cleanedTitles.map(title => (title.startsWith(coreTitle) && title !== coreTitle) ? coreTitle : title);
        }

        if (cleanLevel === 'light') {
            return cleanedTitles;
        }

        // --- Deep Cleaning Stages (for language detection) ---

        // Stage 3: Stop Word Removal (from within titles)
        const stopWordsRegex = new RegExp(`\\b(${Array.from(stopWords).join('|')})\\b`, 'gi');
        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 & Final Selection
        const uniqueTitles = [...new Set(finalFilteredTitles)];
        return uniqueTitles.length > 0 ? uniqueTitles : [...new Set(allTitles)];
    }


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

    /**
     * Finds the best position to insert a new message element in the release view.
     * It prioritizes inserting after specific known elements to group related messages.
     * @param {string[]} [priorityAnchorIds=[]] - An array of element IDs to try anchoring to first.
     * @returns {Node | null} The node *before which* the new element should be inserted.
     */
    function findInsertionAnchor(priorityAnchorIds = []) {
        const { releaseContainer } = AppState.dom;
        if (!releaseContainer) return null;

        // 1. Try to find a priority anchor if provided.
        for (const id of priorityAnchorIds) {
            const anchor = document.getElementById(id);
            if (anchor) {
                return anchor.nextSibling;
            }
        }

        // 2. Fallback to finding Harmony's own debug messages.
        const messages = releaseContainer.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;

        // 3. Fallback for "no linguistic content" case where Harmony provides no language guess message.
        const noLettersMsg = Array.from(messages).find(msg => msg.textContent.includes('Titles contain no letters'));
        if (noLettersMsg) return noLettersMsg.nextSibling;

        // 4. Final fallback to the first message of any kind.
        return releaseContainer.querySelector('.message');
    }

    /**
     * Creates and inserts a message element into the release view.
     * @param {string} id - The ID for the new message element.
     * @param {string} message - The raw text or HTML content for the message.
     * @param {'debug' | 'info' | 'warning' | 'error'} [type='debug'] - The type of the message.
     * @param {string[]} [priorityAnchorIds=[]] - An array of element IDs to try anchoring to first.
     */
    function createAndInsertMessage(id, message, type = 'debug', priorityAnchorIds = []) {
        const { releaseContainer } = AppState.dom;
        if (!releaseContainer) return;

        document.getElementById(id)?.remove();

        const messageDiv = document.createElement('div');
        messageDiv.id = id;
        messageDiv.className = `message ${type}`;

        const iconName = type === 'debug' ? 'bug' : 'info-circle';

        // Split the message by <br> tags to handle multi-line content and format it nicely.
        const lines = message.split(/<br\s*\/?>/i);
        const messageLinesHtml = lines.map(line => `<p>${line}</p>`).join('');

        const finalContent = `
            <div class="he-message-content-wrapper">
                <div class="he-message-prefix"><b>[${SCRIPT_NAME}]</b></div>
                <div class="he-message-lines">${messageLinesHtml}</div>
            </div>`;

        messageDiv.innerHTML = `<svg class="icon" width="24" height="24" stroke-width="2"><use xlink:href="/icon-sprite.svg#${iconName}"></use></svg>
            <div>${finalContent}</div>`;

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


    // --- SETTINGS PAGE ---

    function initSettingsPage() {
        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 = AppState.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 = AppState.settings[config.key];
                        const val = document.createElement('span');
                        val.textContent = ` ${AppState.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(AppState.settings[config.key]) ? AppState.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 } = AppState.dom;
            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: () => {
            const { releaseInfoRows } = AppState.dom;
            if (!releaseInfoRows) return;
            const terms = ['Availability', 'Sources', 'External links'];
            releaseInfoRows.forEach(row => {
                const header = row.querySelector('th');
                if (header && terms.includes(header.innerText.trim())) {
                    row.style.display = 'none';
                }
            });
        },

        addClipboardButton: () => {
            if (document.getElementById('he-redo-lookup-mbid-button')) return;
            const { lookupBtn } = AppState.dom;
            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: () => {
            if (document.getElementById('he-relookup-link-container')) return;
            const { actionsHeader } = AppState.dom;
            if (!actionsHeader) 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>`;
            actionsHeader.parentNode.insertBefore(container, actionsHeader.nextSibling);
        },

        makePermalinkCopyable: () => {
            const { permaLink } = AppState.dom;
            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: () => {
            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 = [];

            const { regionInput } = AppState.dom;
            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'],
            ]);

            const regionKey = currentRegion.split(',').map(code => code.trim()).find(code => regionMap.has(code));
            const qbzRegion = regionMap.get(regionKey) || defaultQbzRegion;

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

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

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

            searchLinks.push({
                name: 'Search Apple Music (ISRCeam)',
                url: `https://isrceam.rinsuki.net/apple/jp/search?q=${encodedArtist}%20${encodedTitle}`
            });

            searchLinks.push({
                name: 'Search OTOTOY',
                url: `https://ototoy.jp/find/?q=${encodedArtist}%20${encodedTitle}`
            });

            searchLinks.push({
                name: 'Search mora',
                url: `https://mora.jp/search/top?keyWord=${encodedArtist}%20${encodedTitle}`
            });

            // --- Placement ---
            const { permalinkHeader } = AppState.dom;
            if (!permalinkHeader || !permalinkHeader.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(' | '));
                }
            });

            permalinkHeader.nextElementSibling.append(container);
        },

        runLanguageDetection: async () => {
            if (AppState.lang.result !== null) {
                enhancements.applyLanguageDetectionResult(AppState.lang.result);
                return;
            }

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

            if (releaseData.language?.code === 'zxx') {
                log('Harmony has already determined no linguistic content (zxx). Skipping language detection.');
                return;
            }

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

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

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

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

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

            const titlesToAnalyze = getCleanedTitles(allTitles, { cleanLevel: 'deep' });

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

            if (!textToAnalyze.replaceAll(/\P{Letter}/gu, '')) {
                AppState.lang.result = { languageName: '[No linguistic content]', confidence: 100, languageCode3: 'zxx', isZxx: true, debugInfo: { allResults: [], analyzedText: textToAnalyze } };
            } else {
                const results = await AppState.lang.detector(textToAnalyze);
                if (results.length === 0) return;
                const final = results[0];
                AppState.lang.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(AppState.lang.result);
        },

        applyLanguageDetectionResult: (result) => {
            if (!result) return;
            const { releaseInfoTable } = AppState.dom;
            if (!releaseInfoTable) return;

            const { languageName, confidence, languageCode3, isZxx, skipped, debugInfo } = result;
            const confidenceThreshold = AppState.settings[SETTINGS_CONFIG.confidenceThreshold.key];
            const conflictThreshold = AppState.settings[SETTINGS_CONFIG.conflictThreshold.key];

            const langRow = Array.from(releaseInfoTable.querySelectorAll('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);
            }

            const shouldOverwrite = AppState.settings[SETTINGS_CONFIG.ignoreHarmony.key] || harmonyConfidence < conflictThreshold;

            // --- Build and insert the debug message ---
            let messageContent = '';
            if (skipped) {
                messageContent = debugInfo.analyzedText;
            } else {
                const b = document.createElement('b');
                b.textContent = languageName;
                messageContent = `Guessed language (LanguageDetector API): ${b.outerHTML} (${confidence}% confidence)`;

                if (!shouldOverwrite && originalLang.toLowerCase() !== languageName.toLowerCase()) {
                    messageContent += ` <i>- Harmony's confidence meets the conflict threshold (${conflictThreshold}%) and force overwrite is off, no changes applied.</i>`;
                } else if (confidence < confidenceThreshold) {
                    messageContent += ` <i>- below ${confidenceThreshold}% threshold, no changes applied.</i>`;
                }

                messageContent += `<br>Analyzed block: "${debugInfo.analyzedText}"`;
            }
            createAndInsertMessage('he-language-analysis', messageContent);


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

            if (isZxx) {
                 if (langRow) {
                    langRow.querySelector('td').textContent = '[No linguistic content]';
                } else {
                    const scriptRow = Array.from(releaseInfoTable.querySelectorAll('th')).find(th => th.textContent.trim() === 'Script')?.parentElement;
                    const newRow = releaseInfoTable.insertRow(scriptRow ? scriptRow.rowIndex + 1 : -1);
                    newRow.id = 'he-language-row';
                    newRow.innerHTML = `<th>Language</th><td>[No linguistic content]</td>`;
                }
                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 {
                const scriptRow = Array.from(releaseInfoTable.querySelectorAll('th')).find(th => th.textContent.trim() === 'Script')?.parentElement;
                const newRow = releaseInfoTable.insertRow(scriptRow ? scriptRow.rowIndex + 1 : -1);
                newRow.id = 'he-language-row';
                newRow.innerHTML = `<th>Language</th><td>${newContent}</td>`;
                updateSeeder(languageCode3);
            }
        },

        improveReleaseTypeDetection: () => {
            const releaseData = getReleaseDataFromJSON();
            if (!releaseData || !releaseData.media || !releaseData.types?.[0]) {
                return;
            }

            const { title: releaseTitle, media, types } = releaseData;
            const originalType = types[0];
            let detectedType = null;
            let detectionReason = '';

            // 1. More flexible title-based detection for EPs.
            if (/\bEP\b/i.test(releaseTitle) && ['Album', 'Single'].includes(originalType)) {
                detectedType = 'EP';
                detectionReason = `Detected "EP" in release title`;
            }

            // 2. Original logic to detect singles from track titles (overrides EP detection).
            const totalTracks = media.reduce((sum, m) => sum + ((Array.isArray(m.tracklist) ? m.tracklist.length : 0)), 0);
            if (totalTracks > 1) {
                const allTitles = media.flatMap(m => (Array.isArray(m.tracklist) ? m.tracklist.map(t => t.title) : []));
                const coreTitles = getCleanedTitles(allTitles, { cleanLevel: 'light' });
                if (new Set(coreTitles).size === 1) {
                    detectedType = 'Single';
                    detectionReason = 'All tracks appear to be versions of the same title';
                }
            }

            // 3. Apply the change if a new type was detected and it's different from the original.
            if (detectedType && detectedType !== originalType) {
                AppState.data.release.types[0] = detectedType;

                const releaseTypeRow = Array.from(AppState.dom.releaseInfoTable.querySelectorAll('th')).find(th => th.textContent.trim() === 'Types')?.parentElement;
                if (!releaseTypeRow) return;

                const currentTypeCell = releaseTypeRow.querySelector('td');
                const altValuesList = currentTypeCell.querySelector('ul.alt-values');

                const overwrittenSpan = document.createElement('span');
                overwrittenSpan.className = 'he-overwritten-label';
                overwrittenSpan.title = `Harmony's original guess: ${originalType}`;
                overwrittenSpan.textContent = '(overwritten)';

                currentTypeCell.textContent = '';
                currentTypeCell.append(detectedType, ' ', overwrittenSpan);

                if (altValuesList) {
                    currentTypeCell.appendChild(altValuesList);
                }

                const messageContent = `Changed release type from "${originalType}" to "${detectedType}". Reason: ${detectionReason}.`;
                createAndInsertMessage('he-release-type-override', messageContent, 'debug', ['he-language-analysis']);
            }
        },

        setNoLabel: () => {
            const releaseData = getReleaseDataFromJSON();
            if (!releaseData || !releaseData.labels?.length || !releaseData.artists?.length) {
                return;
            }

            const artistNames = new Set(releaseData.artists.map(artist => artist.name));
            const originalLabel = { ...releaseData.labels[0] };

            if (artistNames.has(originalLabel.name)) {
                AppState.data.release.labels[0] = { ...originalLabel, ...NO_LABEL };

                const labelRow = Array.from(AppState.dom.releaseInfoTable.querySelectorAll('th'))
                    .find(th => th.textContent.trim() === 'Labels')?.parentElement;
                if (!labelRow) return;

                const labelCell = labelRow.querySelector('td');
                const mainLabelList = labelCell.querySelector('ul.release-labels');
                if (!mainLabelList) return;

                const overwrittenSpan = document.createElement('span');
                overwrittenSpan.className = 'he-overwritten-label';
                overwrittenSpan.title = `Original label: ${originalLabel.name}`;
                overwrittenSpan.textContent = '(overwritten)';

                mainLabelList.innerHTML = `<li><span class="entity-links">${NO_LABEL.name}</span> </li>`;
                mainLabelList.querySelector('li').append(' ', overwrittenSpan);
            }
        },

        setupFormSubmitListener: () => {
            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);
                }
            });
        }
    };

    // --- FORM SUBMISSION HANDLER ---

    function handleSeederFormSubmit(event) {
        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 (AppState.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 (AppState.lang.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 !== AppState.lang.code) {
                   input.value = AppState.lang.code;
                   modificationsMade = true;
                }
            }

            if (AppState.data.release?.types?.[0]) {
                let input = form.querySelector('input[name="type.0"]');
                if (!input) {
                    input = document.createElement('input');
                    input.type = 'hidden';
                    input.name = 'type.0';
                    form.appendChild(input);
                }

                if (input.value !== AppState.data.release.types[0]) {
                    input.value = AppState.data.release.types[0];
                    modificationsMade = true;
                }
            }

            if (AppState.data.release?.labels?.[0]) {
                const label = AppState.data.release.labels[0];
                let nameInput = form.querySelector('input[name="labels.0.name"]');
                let mbidInput = form.querySelector('input[name="labels.0.mbid"]');

                if (!nameInput) {
                    nameInput = document.createElement('input');
                    nameInput.type = 'hidden';
                    nameInput.name = 'labels.0.name';
                    form.appendChild(nameInput);
                }
                if (!mbidInput && label.mbid) {
                    mbidInput = document.createElement('input');
                    mbidInput.type = 'hidden';
                    mbidInput.name = 'labels.0.mbid';
                    form.appendChild(mbidInput);
                }

                if (nameInput.value !== label.name) {
                    nameInput.value = label.name;
                    modificationsMade = true;
                }
                if (mbidInput && label.mbid && mbidInput.value !== label.mbid) {
                    mbidInput.value = label.mbid;
                    modificationsMade = true;
                }
            }
        }

        if (formName === 'release-update-seeder' && AppState.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 = AppState.data.release?.gtin;
            append('barcode', gtin);
            append('packaging', 'None');
        }

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

    // --- INITIALIZATION AND ROUTING ---

    function initDOMCache() {
        const { path } = AppState;

        if (['/', '/release'].includes(path)) {
            AppState.dom.lookupBtn = document.querySelector('input[type="submit"][value="Lookup"]');
        }

        switch (true) {
            case path.startsWith('/release') && !path.startsWith('/release/actions'):
                AppState.dom.freshStateScript = document.querySelector('script[id^="__FRSH_STATE_"]');
                AppState.dom.releaseContainer = document.querySelector('div.release');
                AppState.dom.releaseArtistNode = AppState.dom.releaseContainer?.querySelector('.release-artist');
                AppState.dom.permalinkHeader = document.querySelector('h2.center');
                AppState.dom.permaLink = document.querySelector('p.center > a');
                AppState.dom.regionInput = document.querySelector('#region-input');
                AppState.dom.releaseInfoRows = document.querySelectorAll('.release-info > tbody > tr');
                AppState.dom.releaseInfoTable = document.querySelector('.release-info tbody');
                break;
            case path.startsWith('/release/actions'):
                AppState.dom.actionsHeader = Array.from(document.querySelectorAll('h2')).find(h => h.textContent.includes('Release Actions'));
                break;
        }
    }

    function applyGlobalStyles() {
        const css = `
            .release-artist::before { content: "by "; }
            .release-artist > :first-child { margin-left: 0.25em; }
            ${AppState.settings[SETTINGS_CONFIG.hideDebugMessages.key] ? '.message.debug { display: none !important; }' : ''}
            .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;
                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; cursor: pointer;
            }
            .he-modal-confirm-button {
                padding: 8px 16px; border-radius: 4px; border: none;
                background-color: #f44336; color: white; cursor: pointer;
            }
            .he-modal-confirm-button:hover {
                background-color: #d32f2f;
            }
            .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);
            }
            .he-message-content-wrapper {
                display: table;
                width: 100%;
            }
            .he-message-prefix {
                display: table-cell;
                padding-right: 0.5em;
                white-space: nowrap;
                vertical-align: middle;
            }
            .he-message-lines {
                display: table-cell;
                width: 100%;
            }
            .he-message-lines > p {
                margin: 0;
                padding: 0;
            }
        `;
        GM_addStyle(css);
    }

    async function main() {
        AppState.settings = await getSettings();

        initDOMCache();
        applyGlobalStyles();

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

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

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

})();