Click buttons across tabs

Clicks specified buttons across tabs using the Broadcast Channel API and closes tabs after successful submission.

От 06.09.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Click buttons across tabs
// @namespace    https://musicbrainz.org/user/chaban
// @version      4.2.1
// @tag          ai-created
// @description  Clicks specified buttons across tabs using the Broadcast Channel API and closes tabs after successful submission.
// @author       chaban
// @license      MIT
// @match        *://*.musicbrainz.org/*
// @match        *://magicisrc.kepstin.ca/*
// @match        *://magicisrc-beta.kepstin.ca/*
// @match        *://isrchunt.com/*
// @run-at       document-start
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        window.close
// ==/UserScript==

(async function () {
    'use strict';

    const scriptName = GM.info.script.name;
    const tabId = `[${Math.random().toString(36).substring(2, 6)}]`;
    console.log(`%c[${scriptName}] ${tabId} Script initialization started on ${location.href}`, 'font-weight: bold;');

    /**
     * @typedef {Object} SiteConfig
     * @property {string|string[]} hostnames - Hostnames where this configuration applies.
     * @property {string|string[]} paths - URL paths where this configuration is active.
     * @property {string} buttonSelector - The CSS selector for the button to be clicked.
     * @property {string} [channelName] - The BroadcastChannel name for cross-tab communication.
     * @property {string} [messageTrigger] - The message that triggers the action on the channel.
     * @property {string} [menuCommandName] - The name for the userscript menu command.
     * @property {(RegExp|string)[]} [successUrlPatterns] - URL patterns that indicate a successful submission.
     * @property {boolean} [shouldCloseAfterSuccess=false] - Whether to close the tab after a successful submission.
     * @property {boolean} [autoClick=false] - Whether to click the button automatically on page load.
     * @property {() => boolean} [isNoOp] - A function that checks if the current page state represents a no-op submission (e.g., a "no changes" banner).
     * @property {(config: SiteConfig, triggerAction: () => Promise<boolean>) => void} [submissionHandler] - Custom logic to execute when a submission is triggered, like rate-limiting or pre-flight checks.
     */

    const siteConfigurations = [
        {
            hostnames: ['musicbrainz.org'],
            paths: ['/edit-relationships'],
            buttonSelector: '.rel-editor > button',
            autoClick: true,
            successUrlPatterns: [],
            shouldCloseAfterSuccess: false,
        },
        {
            hostnames: ['musicbrainz.org'],
            paths: ['/edit', '/edit-relationships', '/add-cover-art'],
            channelName: 'mb_edit_channel',
            messageTrigger: 'submit-edit',
            buttonSelector: 'button.submit.positive[type="submit"]',
            menuCommandName: 'MusicBrainz: Submit Edit (All Tabs)',
            successUrlPatterns: [/^https?:\/\/(?:beta\.)?musicbrainz\.org\/(?!collection\/)[^/]+\/[a-f0-9\-]{36}(?:\/cover-art)?\/?$/],
            shouldCloseAfterSuccess: true,
            isNoOp: () => {
                const noChangesBanner = document.querySelector('.banner.warning-header');
                return noChangesBanner?.textContent.includes(
                    'The data you have submitted does not make any changes to the data already present.'
                );
            },
            submissionHandler: (_config, triggerAction) => {
                rateLimitedMBSubmit(triggerAction);
            },
        },
        {
            hostnames: ['magicisrc.kepstin.ca', 'magicisrc-beta.kepstin.ca'],
            paths: ['/'],
            channelName: 'magicisrc_submit_channel',
            messageTrigger: 'submit-isrcs',
            buttonSelector: '[onclick^="doSubmitISRCs"]',
            menuCommandName: 'MagicISRC: Submit ISRCs (All Tabs)',
            successUrlPatterns: [/\?.*submit=1/],
            shouldCloseAfterSuccess: true,
            submissionHandler: (config, triggerAction) => {
                onDOMLoaded(() => {
                    const performCheck = (obs) => {
                        const cleanupAndExit = () => {
                            if (obs) obs.disconnect();
                            return true;
                        };

                        const submitButton = document.querySelector(config.buttonSelector);
                        const isrcForm = document.querySelector('form#check-isrcs');
                        const loginButton = document.querySelector('button[onclick^="doLogin();"]');
                        const logoutButton = document.querySelector('button[onclick^="doLogout();"]');

                        if (isSubmissionSuccessful(config, true)) {
                            checkAndCloseOnSuccess(config, 'Trigger received on an already successful page.');
                            return cleanupAndExit();
                        }
                        if (submitButton) {
                            debugLog('MagicISRC submit button found. Proceeding with submission.', 'green');
                            sessionStorage.removeItem(RELOAD_ATTEMPTS_KEY);
                            navigator.locks.request(MAGICISRC_SUBMIT_LOCK_KEY, async () => {
                                debugLog(`Acquired MagicISRC submit lock. Waiting 1s before submission.`, 'green');
                                await new Promise(resolve => setTimeout(resolve, 1000));
                                triggerAction();
                            });
                            return cleanupAndExit();
                        }
                        if (isrcForm && logoutButton && !submitButton) {
                            checkAndCloseOnSuccess(config, 'Detected MagicISRC page with no new ISRCs to submit.');
                            return cleanupAndExit();
                        }
                        if (loginButton) {
                            debugLog('User is not logged into MagicISRC. Aborting submission on this tab.', 'orange');
                            return cleanupAndExit();
                        }

                        return false;
                    };

                    if (performCheck(null)) {
                        return;
                    }

                    const observer = new MutationObserver(() => performCheck(observer));
                    observer.observe(document.body, { childList: true, subtree: true });
                });
            },
        },
        {
            hostnames: ['isrchunt.com'],
            paths: ['/spotify/importisrc', '/deezer/importisrc'],
            channelName: 'isrc_hunt_submit_channel',
            messageTrigger: 'submit-isrcs',
            buttonSelector: 'form[action$="/importisrc"][method="post"] button[type="submit"]',
            menuCommandName: 'ISRC Hunt: Submit ISRCs (All Tabs)',
            successUrlPatterns: [/\?.*submitted=1/],
            shouldCloseAfterSuccess: true,
            submissionHandler: (_config, triggerAction) => {
                debugLog(`Requesting ISRC Hunt submit lock...`);
                navigator.locks.request(ISRC_HUNT_SUBMIT_LOCK_KEY, async () => {
                    debugLog(`Acquired ISRC Hunt submit lock. Waiting 1s before submission.`, 'green');
                    await new Promise(resolve => setTimeout(resolve, 1000));
                    triggerAction();
                });
            },
        },
    ];

    const SUBMISSION_TRIGGERED_FLAG = 'broadcastChannelSubmissionState';
    const RELOAD_ATTEMPTS_KEY = 'magicisrc_reload_attempts';
    const RELOAD_LOCK_KEY = 'magicisrc-reload-lock';
    const MAGICISRC_SUBMIT_LOCK_KEY = 'magicisrc-submit-lock';
    const ISRC_HUNT_SUBMIT_LOCK_KEY = 'isrc-hunt-submit-lock';
    const MB_SUBMIT_COORDINATION_LOCK_KEY = 'mb-submit-coordination-lock';
    const MB_LAST_SUBMIT_TIMESTAMP_KEY = 'mb_last_submit_timestamp';
    const DEBUG_LOG_CHANNEL_NAME = `${scriptName}_debug_log`;
    const MUSICBRAINZ_SUBMITS_PER_SECOND_SETTING = 'mb_submits_per_second';
    const MUSICBRAINZ_DISABLE_RATE_LIMITER_SETTING = 'mb_disable_rate_limiter';
    const DISABLE_AUTO_CLOSE_SETTING = 'mb_button_clicker_disableAutoClose';
    const MAGICISRC_ENABLE_AUTO_RELOAD = 'magicisrc_enableAutoReload';
    const DEBUG_LOGGING_SETTING = 'debug_logging_enabled';

    let registeredMenuCommandIDs = [];
    let debugLogChannel;

    /**
     * @summary Sends a log message to all tabs if debug logging is enabled.
     * @param {string} message The message to log.
     * @param {string} [color] Optional CSS color for the message.
     */
    async function debugLog(message, color = 'blue') {
        const debugEnabled = await GM.getValue(DEBUG_LOGGING_SETTING, false);
        if (!debugEnabled) return;

        if (!debugLogChannel) {
            debugLogChannel = new BroadcastChannel(DEBUG_LOG_CHANNEL_NAME);
        }

        debugLogChannel.postMessage({
            tabId,
            message,
            color,
            timestamp: new Date().toISOString(),
        });
    }

    /**
     * @summary Executes a callback when the DOM is ready, or immediately if it's already loaded.
     * @param {Function} callback The function to execute.
     */
    function onDOMLoaded(callback) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', callback);
        } else {
            callback();
        }
    }

    /**
     * @summary Finds all site configurations that are active for the current page URL path.
     * @returns {SiteConfig[]} An array of active configurations.
     */
    function getActiveConfigs() {
        const currentHostname = location.hostname;
        const currentPathname = location.pathname;
        return siteConfigurations.filter(config => {
            const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames];
            const paths = Array.isArray(config.paths) ? config.paths : [config.paths];
            const hostnameMatches = hostnames.some(h => currentHostname.includes(h));
            const pathMatches = paths.some(p => currentPathname.endsWith(p));
            return hostnameMatches && pathMatches;
        });
    }

    /**
     * @summary Waits for a button to appear and become enabled, then clicks it.
     * @param {SiteConfig} config - The configuration object for the button.
     * @param {Function} [onClick] - An optional callback to execute immediately after the click.
     * @returns {Promise<boolean>} Resolves to true if clicked, false otherwise.
     */
    async function waitForButtonAndClick(config, onClick) {
        return new Promise(resolve => {
            const checkAndClick = (obs) => {
                const btn = document.querySelector(config.buttonSelector);
                if (btn && !btn.disabled) {
                    debugLog(`Button "${config.buttonSelector}" found and enabled. Clicking.`, 'green');
                    btn.click();
                    onClick?.(btn);
                    if (obs) obs.disconnect();
                    resolve(true);
                    return true;
                }
                return false;
            };

            onDOMLoaded(() => {
                if (checkAndClick(null)) return;
                const observer = new MutationObserver((_, obs) => checkAndClick(obs));
                observer.observe(document.body, { childList: true, subtree: true, attributes: true });
            });
        });
    }

    /**
     * @summary Checks if the current URL matches a success pattern for a given configuration.
     * @param {SiteConfig} config - The site configuration.
     * @param {boolean} [quiet=false] - If true, suppresses console logs.
     * @returns {boolean} True if the URL matches a success pattern.
     */
    function isSubmissionSuccessful(config, quiet = false) {
        if (!config?.successUrlPatterns?.length) return false;
        const url = location.href;
        const isSuccess = config.successUrlPatterns.some(pattern =>
            (typeof pattern === 'string' ? url.includes(pattern) : pattern.test(url))
        );
        if (isSuccess && !quiet) {
            debugLog(`URL "${url}" matches success pattern.`);
        }
        return isSuccess;
    }

    /**
     * @summary Checks if a submission was successful and closes the tab if configured to do so.
     * @param {SiteConfig} config - The site configuration for success checking.
     * @param {string|null} [preSubmissionNoOpReason=null] - A string indicating a no-op reason detected before submission.
     */
    async function checkAndCloseOnSuccess(config, preSubmissionNoOpReason = null) {
        if (!config || !sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG)) return;

        const isPostSubmitNoOp = config.isNoOp?.() ?? false;

        if (isSubmissionSuccessful(config) || isPostSubmitNoOp || preSubmissionNoOpReason) {
            if (isPostSubmitNoOp) {
                debugLog(`Detected a post-submission no-op state. Treating as success.`);
            } else if (preSubmissionNoOpReason) {
                debugLog(`${preSubmissionNoOpReason} Treating as success.`);
            }

            debugLog(`Submission successful. Clearing flag.`);
            sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG);

            const disableAutoClose = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false);
            if (disableAutoClose) {
                debugLog(`Auto-closing is DISABLED by user setting.`, 'orange');
            } else {
                debugLog(`Closing tab.`, 'green');
                setTimeout(() => window.close(), 200);
            }
        }
    }

    /**
     * @summary Handles the reload logic for MagicISRC pages with exponential backoff and a Web Lock.
     * @param {boolean} [manual=false] - If true, bypasses the 'enableReload' check and forces the reload logic.
     */
    async function handleMagicISRCReload(manual = false) {
        const enableReload = await GM.getValue(MAGICISRC_ENABLE_AUTO_RELOAD, true);
        if (!enableReload && !manual) {
            debugLog(`MagicISRC automatic reload is DISABLED.`, 'orange');
            return;
        }

        debugLog(`An error occurred. Requesting reload lock...`, 'red');
        navigator.locks.request(RELOAD_LOCK_KEY, async () => {
            debugLog(`Acquired reload lock. This tab will handle the reload.`, 'red');
            let attempts = parseInt(sessionStorage.getItem(RELOAD_ATTEMPTS_KEY) || '0');
            attempts++;

            const backoffSeconds = Math.pow(2, Math.min(attempts, 6));
            const jitter = Math.random();
            const delay = (backoffSeconds + jitter) * 1000;

            debugLog(`MagicISRC error detected. Reload attempt ${attempts}. Retrying in ${Math.round(delay / 1000)}s.`, 'red');
            sessionStorage.setItem(RELOAD_ATTEMPTS_KEY, attempts.toString());

            await new Promise(resolve => setTimeout(resolve, delay));

            debugLog(`Falling back to a full page reload to re-trigger logic.`, 'red');
            location.reload();
        });
    }

    /**
     * @summary Sets up listeners and handlers specific to MagicISRC pages.
     * @description Injects a script to intercept fetch/render errors and sets up a message listener to handle them.
     */
    function setupMagicISRC() {
        if (!location.hostname.includes('magicisrc')) return;

        debugLog(`MagicISRC page detected. Setting up special handlers.`);

        const script = document.createElement('script');
        script.textContent = `
            (() => {
                const post = (type) => window.postMessage({ source: '${scriptName}', type }, location.origin);
                const origFetch = window.fetch;
                window.fetch = (...args) => origFetch(...args).catch(err => { post('FETCH_ERROR'); throw err; });
                const origRenderError = window.renderError;
                window.renderError = (...args) => {
                    post('RENDER_ERROR');
                    if(origRenderError) origRenderError.apply(this, args);
                };
            })();
        `;
        document.documentElement.appendChild(script);
        script.remove();

        window.addEventListener('message', (event) => {
            if (event.origin !== location.origin || event.data?.source !== scriptName) return;
            if (event.data.type === 'FETCH_ERROR' || event.data.type === 'RENDER_ERROR') {
                handleMagicISRCReload();
            }
        });
    }


    /**
     * @summary Registers all userscript menu commands and settings toggles.
     */
    async function setupMenuCommands() {
        for (const commandId of registeredMenuCommandIDs) {
            try {
                GM_unregisterMenuCommand(commandId);
            } catch (e) { /* ignore */ }
        }
        registeredMenuCommandIDs = [];

        const registerCommand = (name, func) => {
            const id = GM_registerMenuCommand(name, func);
            registeredMenuCommandIDs.push(id);
        };

        const settings = [
            {
                key: DISABLE_AUTO_CLOSE_SETTING,
                getLabel: async (value) => `Auto Close Tabs: ${value ? 'DISABLED' : 'ENABLED'}`,
                onClick: async (currentValue) => GM.setValue(DISABLE_AUTO_CLOSE_SETTING, !currentValue),
                defaultValue: false,
            },
            {
                key: MAGICISRC_ENABLE_AUTO_RELOAD,
                getLabel: async (value) => `MagicISRC Auto Reload: ${value ? 'ENABLED' : 'DISABLED'}`,
                onClick: async (currentValue) => GM.setValue(MAGICISRC_ENABLE_AUTO_RELOAD, !currentValue),
                defaultValue: true,
            },
            {
                key: DEBUG_LOGGING_SETTING,
                getLabel: async (value) => `Debug Logging: ${value ? 'ENABLED' : 'DISABLED'}`,
                onClick: async (currentValue) => GM.setValue(DEBUG_LOGGING_SETTING, !currentValue),
                defaultValue: false,
            },
            {
                key: MUSICBRAINZ_SUBMITS_PER_SECOND_SETTING,
                getLabel: async (value) => `MusicBrainz Edit Submits / sec (Current: ${value})`,
                onClick: async (currentValue) => {
                    const newValue = prompt(`Enter new max submissions per second for MusicBrainz:`, currentValue);
                    const newRate = parseInt(newValue, 10);
                    if (!isNaN(newRate) && newRate > 0) {
                        await GM.setValue(MUSICBRAINZ_SUBMITS_PER_SECOND_SETTING, newRate);
                    } else if (newValue !== null) {
                        alert('Please enter a valid positive number.');
                    }
                },
                defaultValue: 10,
            },
            {
                key: MUSICBRAINZ_DISABLE_RATE_LIMITER_SETTING,
                getLabel: async (value) => `MusicBrainz Rate Limiter: ${value ? 'DISABLED' : 'ENABLED'}`,
                onClick: async (currentValue) => GM.setValue(MUSICBRAINZ_DISABLE_RATE_LIMITER_SETTING, !currentValue),
                defaultValue: false,
            },
        ];

        for (const setting of settings) {
            const value = await GM.getValue(setting.key, setting.defaultValue);
            registerCommand(await setting.getLabel(value), async () => {
                await setting.onClick(value);
                await setupMenuCommands();
            });
        }

        const activeConfigs = getActiveConfigs();
        const configsForMenu = activeConfigs.filter(c => !c.autoClick && c.menuCommandName);

        for (const config of configsForMenu) {
            registerCommand(config.menuCommandName, () => {
                const channel = new BroadcastChannel(config.channelName);
                channel.postMessage(config.messageTrigger);
                channel.close();
            });
        }

        debugLog(`Menu commands updated.`);
    }

    /**
     * @summary Executes a callback after ensuring the configured rate limit is not exceeded.
     * @param {Function} callback The function to execute.
     */
    async function rateLimitedMBSubmit(callback) {
        const limiterDisabled = await GM.getValue(MUSICBRAINZ_DISABLE_RATE_LIMITER_SETTING, false);
        if (limiterDisabled) {
            debugLog('MusicBrainz rate limiter is disabled. Submitting immediately.', 'orange');
            callback();
            return;
        }

        const submitsPerSecond = await GM.getValue(MUSICBRAINZ_SUBMITS_PER_SECOND_SETTING, 10);
        const requiredInterval = 1000 / submitsPerSecond;

        debugLog(`Requesting MB submission lock...`);
        navigator.locks.request(MB_SUBMIT_COORDINATION_LOCK_KEY, async () => {
            debugLog(`Acquired MB submission lock.`, 'green');
            const lastSubmit = await GM.getValue(MB_LAST_SUBMIT_TIMESTAMP_KEY, 0);
            const now = Date.now();
            const elapsed = now - lastSubmit;

            if (elapsed < requiredInterval) {
                const waitTime = requiredInterval - elapsed;
                debugLog(`Rate limiting: waiting ${waitTime.toFixed(0)}ms...`, 'orange');
                await new Promise(resolve => setTimeout(resolve, waitTime));
            }

            await GM.setValue(MB_LAST_SUBMIT_TIMESTAMP_KEY, Date.now().toString());
            debugLog(`Executing submission.`, 'darkgreen');
            callback();
        });
    }

    /**
     * @summary Sets up listeners for specified configurations, handling auto-clicks or broadcast channel messages.
     * @param {SiteConfig[]} configs - An array of configuration objects.
     */
    function setupConfigListeners(configs) {
        const pendingSubmissionJSON = sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG);
        if (pendingSubmissionJSON) {
            try {
                const pendingState = JSON.parse(pendingSubmissionJSON);
                const pendingConfig = siteConfigurations.find(c => c.channelName === pendingState.channel && c.messageTrigger === pendingState.trigger);

                if (pendingConfig && (isSubmissionSuccessful(pendingConfig, true) || pendingConfig.isNoOp?.())) {
                     debugLog(`Found pending submission flag on a success/no-op page. Letting success handler take over.`, 'purple');
                } else {
                    const activePendingConfig = configs.find(c => c.channelName === pendingState.channel && c.messageTrigger === pendingState.trigger);
                    if (activePendingConfig && activePendingConfig.submissionHandler) {
                         debugLog(`Found pending submission flag on page load for "${activePendingConfig.menuCommandName}". Re-triggering handler.`, 'purple');
                         const triggerAction = () => waitForButtonAndClick(activePendingConfig);
                         activePendingConfig.submissionHandler(activePendingConfig, triggerAction);
                    }
                }
            } catch (e) {
                console.error(`[${scriptName}] Error parsing pending submission state:`, e);
                sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG);
            }
        }

        for (const config of configs) {
            const triggerAction = () => waitForButtonAndClick(config);

            if (config.autoClick) {
                debugLog(`Setting up auto-click for "${config.buttonSelector}".`);
                triggerAction();
                continue;
            }

            if (config.channelName) {
                const channel = new BroadcastChannel(config.channelName);

                channel.onmessage = async (event) => {
                    if (event.data !== config.messageTrigger) return;

                    debugLog(`Received trigger "${event.data}".`);
                    const triggerState = JSON.stringify({ channel: config.channelName, trigger: config.messageTrigger });
                    sessionStorage.setItem(SUBMISSION_TRIGGERED_FLAG, triggerState);

                    if (config.submissionHandler) {
                        config.submissionHandler(config, triggerAction);
                    } else {
                        triggerAction();
                    }
                };
            }
        }
    }

    /**
     * @summary Wraps a method on the history object to call a callback after it executes.
     * @param {'pushState'|'replaceState'} methodName The name of the history method to wrap.
     * @param {Function} callback The function to call after the original method.
     */
    function wrapHistoryMethod(methodName, callback) {
        const original = history[methodName];
        history[methodName] = function (...args) {
            original.apply(this, args);
            callback();
        };
    }

    /**
     * @summary Sets up listeners for URL changes to check for submission success.
     * @description This is a global handler that runs on all matched pages. It checks if a submission
     * was triggered and then determines if the current page is a success page for any configuration.
     */
    function setupSuccessHandling() {
        const potentialSuccessConfigs = siteConfigurations.filter(c => c.shouldCloseAfterSuccess);
        if (potentialSuccessConfigs.length === 0) return;

        const runCheck = () => {
            if (!sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG)) return;

            for (const config of potentialSuccessConfigs) {
                const isSuccess = isSubmissionSuccessful(config, true);
                const isNoOp = config.isNoOp?.() ?? false;

                if (isSuccess || isNoOp) {
                    checkAndCloseOnSuccess(config);
                    return;
                }
            }
        };

        onDOMLoaded(runCheck);
        wrapHistoryMethod('pushState', runCheck);
        wrapHistoryMethod('replaceState', runCheck);
        window.addEventListener('popstate', runCheck);
    }

    /**
     * @summary Main script entry point.
     */
    async function main() {
        await setupMenuCommands();

        const debugEnabled = await GM.getValue(DEBUG_LOGGING_SETTING, false);
        if (debugEnabled) {
            const logReceiver = new BroadcastChannel(DEBUG_LOG_CHANNEL_NAME);
            logReceiver.onmessage = (event) => {
                const { tabId: msgTabId, message, color, timestamp } = event.data;
                console.log(`%c[${scriptName}] [${timestamp}] ${msgTabId} ${message}`, `color: ${color}`);
            };
        }

        const activeConfigs = getActiveConfigs();
        if (activeConfigs.length > 0) {
            setupConfigListeners(activeConfigs);
        }

        setupMagicISRC();
        setupSuccessHandling();

        debugLog(`Initialization finished.`);
    }

    main();

})();