您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Clicks specified buttons across tabs using the BroadcastChannel API and closes tabs after successful submission.
当前为
// ==UserScript== // @name Click buttons across tabs // @namespace https://musicbrainz.org/user/chaban // @version 2.0 // @tag ai-created // @description Clicks specified buttons across tabs using the BroadcastChannel API and closes tabs after successful submission. // @author chaban // @license MIT // @match *://*.musicbrainz.org/* // @match *://magicisrc.kepstin.ca/* // @match *://magicisrc-beta.kepstin.ca/* // @run-at document-start // @grant GM.info // @grant GM_registerMenuCommand // @grant window.close // ==/UserScript== (function () { 'use strict'; const scriptName = GM.info.script.name; /** * @typedef {Object} SiteConfig * @property {string|string[]} hostnames - A single hostname string or an array of hostname strings. * @property {string|string[]} paths - A single path string or an array of path strings (must match ending of pathname). * @property {string} channelName - The name of the BroadcastChannel to use for this site's button click. * @property {string} messageTrigger - The message data that triggers the button click. * @property {string} buttonSelector - The CSS selector for the button to be clicked. * @property {string} menuCommandName - The name to display in the Tampermonkey/Greasemonkey menu for the click action. * @property {(RegExp|string)[]} [successUrlPatterns] - An array of RegExp or string patterns to match against the URL to indicate a successful submission. * @property {boolean} [shouldCloseAfterSuccess=false] - Whether to attempt closing the tab after a successful submission. Note: Browser security heavily restricts `window.close()`. */ /** * Configuration for different websites and their button click settings. * @type {SiteConfig[]} */ const siteConfigurations = [ { hostnames: ['musicbrainz.org'], paths: [ '/edit', '/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\/[^/]+\/[a-f0-9\-]{36}(?:\/cover-art)?\/?$/, ], shouldCloseAfterSuccess: true }, { 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 } ]; const SUBMISSION_TRIGGERED_FLAG = 'broadcastChannelSubmissionState'; const GLOBAL_CLOSE_TAB_CHANNEL_NAME = 'global_close_tab_channel'; const GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER = 'close-this-tab'; const GLOBAL_CLOSE_TAB_MENU_COMMAND_NAME = 'Global: Close This Tab (All Tabs)'; /** * Sends a message to the specified BroadcastChannel. * @param {string} channelName * @param {string} message */ function sendMessageToChannel(channelName, message) { try { new BroadcastChannel(channelName).postMessage(message); console.log(`[${scriptName}] Sent message "${message}" to channel "${channelName}".`); } catch (error) { console.error(`[${scriptName}] Error sending message to channel "${channelName}":`, error); } } /** * Checks if the current page indicates a successful submission based on the given config's URL patterns. * @param {SiteConfig} config - The site configuration. * @returns {boolean} */ function isSubmissionSuccessful(config) { if (!config.successUrlPatterns || config.successUrlPatterns.length === 0) { return false; } for (const pattern of config.successUrlPatterns) { const matchResult = (typeof pattern === 'string') ? location.href.includes(pattern) : pattern.test(location.href); if (matchResult) { console.log(`[${scriptName}] URL "${location.href}" matches success pattern "${pattern}".`); return true; } } console.log(`[${scriptName}] URL "${location.href}" does not match any success pattern.`); return false; } /** * Attempts to close the current tab. * @param {number} delayMs - Optional delay in milliseconds before attempting to close. * @returns {void} */ function attemptCloseTab(delayMs = 200) { console.log(`[${scriptName}] Attempting to close tab in ${delayMs}ms.`); setTimeout(() => { try { window.close(); console.log(`[${scriptName}] Successfully called window.close() after delay.`); } catch (e) { console.warn(`[${scriptName}] Failed to close tab automatically via window.close(). This is expected due to browser security restrictions.`, e); } }, delayMs); } /** * Checks the current page against the success criteria for the relevant site, * and closes the tab if successful. * @param {SiteConfig} currentConfigForSuccessCheck - The site config relevant to the current hostname. */ function checkAndCloseIfSuccessful(currentConfigForSuccessCheck) { const storedSubmissionState = sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG); if (!storedSubmissionState) { return; // No pending submission } let submissionData; try { submissionData = JSON.parse(storedSubmissionState); } catch (e) { console.error(`[${scriptName}] Error parsing submission state from sessionStorage:`, e); sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG); return; } const { triggered } = submissionData; if (triggered && currentConfigForSuccessCheck.shouldCloseAfterSuccess) { console.log(`[${scriptName}] Checking for submission success on "${location.href}".`); if (isSubmissionSuccessful(currentConfigForSuccessCheck)) { console.log(`%c[${scriptName}] Submission successful. Closing tab.`, 'color: green; font-weight: bold;'); sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG); attemptCloseTab(); } else { console.info(`%c[${scriptName}] Submission was triggered, but URL does not match success criteria.`, 'color: orange;'); } } else { console.log(`[${scriptName}] Not checking for success: triggered is false, or closing is not configured for this site.`); } } /** * Main initialization function for the userscript. */ function initializeScript() { console.log(`%c[${scriptName}] Script initialized on ${location.href}`, 'color: blue; font-weight: bold;'); const currentHostname = location.hostname; const currentPathname = location.pathname; let configForButtonClicking = null; let configForSuccessCheck = null; // --- Find relevant configurations --- for (const config of siteConfigurations) { const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames]; const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname)); if (hostnameMatches) { const paths = Array.isArray(config.paths) ? config.paths : [config.paths]; const pathMatchesForClicking = paths.some(pathPattern => currentPathname.endsWith(pathPattern)); if (pathMatchesForClicking) { configForButtonClicking = config; console.log(`[${scriptName}] Found button-clicking config: ${config.channelName}`); } if (config.shouldCloseAfterSuccess) { configForSuccessCheck = config; console.log(`[${scriptName}] Identified success-check config for this hostname: ${config.channelName}`); } } } // --- Part 1: Setup BroadcastChannel listener and GM_registerMenuCommand for button clicks --- if (configForButtonClicking) { console.log(`[${scriptName}] Setting up button-clicking logic for "${configForButtonClicking.channelName}".`); try { const channel = new BroadcastChannel(configForButtonClicking.channelName); channel.addEventListener('message', (event) => { if (event.data === configForButtonClicking.messageTrigger) { console.log(`[${scriptName}] Received trigger message "${event.data}".`); const btn = document.querySelector(configForButtonClicking.buttonSelector); if (btn) { const submissionState = { triggered: true }; sessionStorage.setItem(SUBMISSION_TRIGGERED_FLAG, JSON.stringify(submissionState)); console.log(`[${scriptName}] Stored submission state: ${JSON.stringify(submissionState)}.`); btn.click(); } else { console.warn(`[${scriptName}] Button with selector "${configForButtonClicking.buttonSelector}" not found.`); } } }); console.log(`[${scriptName}] Listener active for button clicks on channel "${configForButtonClicking.channelName}".`); } catch (error) { console.error(`[${scriptName}] Error initializing BroadcastChannel:`, error); } if (typeof GM_registerMenuCommand !== 'undefined' && configForButtonClicking.menuCommandName) { GM_registerMenuCommand(configForButtonClicking.menuCommandName, () => { const submissionState = { triggered: true }; sessionStorage.setItem(SUBMISSION_TRIGGERED_FLAG, JSON.stringify(submissionState)); console.log(`[${scriptName}] Menu command triggered. Stored submission state: ${JSON.stringify(submissionState)}.`); sendMessageToChannel(configForButtonClicking.channelName, configForButtonClicking.messageTrigger); }); console.log(`[${scriptName}] Registered menu command "${configForButtonClicking.menuCommandName}".`); } } else { console.log(`[${scriptName}] No button-clicking config found for this page.`); } // --- Part 2: Immediate check for pending submission success & URL change monitoring --- if (configForSuccessCheck) { // Immediate check on page load (due to @run-at document-start) checkAndCloseIfSuccessful(configForSuccessCheck); // Setup URL change monitoring for pages that might update via history API let lastUrl = location.href; const urlChangedHandler = () => { if (location.href !== lastUrl) { console.log(`%c[${scriptName}] URL changed from "${lastUrl}" to "${location.href}". Re-checking for success.`, 'color: purple; font-weight: bold;'); lastUrl = location.href; checkAndCloseIfSuccessful(configForSuccessCheck); } }; // Listen for popstate events window.addEventListener('popstate', urlChangedHandler); // Monkey-patch history API methods const originalPushState = history.pushState; history.pushState = function() { originalPushState.apply(this, arguments); urlChangedHandler(); }; const originalReplaceState = history.replaceState; history.replaceState = function() { originalReplaceState.apply(this, arguments); urlChangedHandler(); }; console.log(`[${scriptName}] URL change listeners activated.`); // --- Part 3: Setup GLOBAL BroadcastChannel listener and GM_registerMenuCommand for closing tabs --- if (isSubmissionSuccessful(configForSuccessCheck)) { console.log(`[${scriptName}] Current URL matches a success pattern. Setting up global close functionality.`); try { const globalCloseChannel = new BroadcastChannel(GLOBAL_CLOSE_TAB_CHANNEL_NAME); globalCloseChannel.addEventListener('message', (event) => { if (event.data === GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER) { console.log(`[${scriptName}] Received global close request.`); attemptCloseTab(50); } }); console.log(`[${scriptName}] Global close channel listener active.`); } catch (error) { console.error(`[${scriptName}] Error initializing global close BroadcastChannel:`, error); } if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand(GLOBAL_CLOSE_TAB_MENU_COMMAND_NAME, () => { console.log(`[${scriptName}] Global close menu command triggered.`); sendMessageToChannel(GLOBAL_CLOSE_TAB_CHANNEL_NAME, GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER); }); console.log(`[${scriptName}] Registered global close menu command.`); } } else { console.log(`[${scriptName}] Current URL does NOT match a success pattern. Global close functionality skipped.`); } } else { console.log(`[${scriptName}] No success-check config found for this hostname. Not monitoring for closure or registering global close command.`); sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG); } console.log(`%c[${scriptName}] Script initialization finished.`, 'color: blue; font-weight: bold;'); } initializeScript(); })();