Harmony: Enhancements

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

当前为 2025-08-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Harmony: Enhancements
// @namespace    https://musicbrainz.org/user/chaban
// @version      1.6.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
// @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';
    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);

        if (cleanLevel === 'light') {
            return cleanedTitles.map(title => title.split('-')[0].trim()).filter(Boolean);
        }

        // --- Deep Cleaning Stages ---

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


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

            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 releaseData = getReleaseDataFromJSON();
            if (!releaseData) 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 { releaseContainer, releaseInfoTable } = AppState.dom;
            if (!releaseContainer || !releaseInfoTable) return;
            document.getElementById('he-language-analysis')?.remove();

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

                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 = 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;
                return releaseContainer.querySelector('.message');
            };

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

            // --- Update the UI and Seeder ---
            const updateSeeder = (code) => {
                AppState.lang.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 {
                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) {
                return;
            }

            const currentType = AppState.data.release.types[0];
            const totalTracks = AppState.data.release.media.reduce((sum, m) => sum + ((Array.isArray(m.tracklist) ? m.tracklist.length : 0)), 0);

            if (!['EP', 'Album'].includes(currentType) || totalTracks <= 1) {
                return;
            }

            const allTitles = (AppState.data.release.media || []).flatMap(m => (Array.isArray(m.tracklist) ? m.tracklist.map(t => t.title) : []));
            const coreTitles = getCleanedTitles(allTitles, { cleanLevel: 'light' });

            const uniqueCoreTitles = new Set(coreTitles);

            if (uniqueCoreTitles.size === 1) {
                AppState.data.release.types[0] = 'Single';

                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: ${currentType}`;
                overwrittenSpan.textContent = '(overwritten)';

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

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

        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.dom.gtinHeader?.nextElementSibling?.textContent?.trim();
            append('barcode', gtin);
            append('packaging', 'None');
        }

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

    // --- INITIALIZATION AND ROUTING ---

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

        if (path === '/' || path.startsWith('/release')) {
            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.gtinHeader = Array.from(document.querySelectorAll('th')).find(th => th.textContent?.trim() === 'GTIN');
                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; }' : ''}
            .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() {
        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));

})();