您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allows batch removing cover art from MusicBrainz releases.
当前为
// ==UserScript== // @name MusicBrainz: Batch Remove Cover Art // @namespace https://musicbrainz.org/user/chaban // @version 0.4.0 // @description Allows batch removing cover art from MusicBrainz releases. // @tag ai-created // @author chaban // @license MIT // @match *://musicbrainz.org/release/*/cover-art // @connect musicbrainz.org // @icon https://musicbrainz.org/static/images/favicons/android-chrome-512x512.png // @grant GM.xmlHttpRequest // @grant GM.addStyle // ==/UserScript== (function() { 'use strict'; GM.addStyle(` .batch-remove-container { margin-top: 1em; padding: 1em; border: 1px solid var(--border, #ccc); background-color: var(--background-accent, #f9f9f9); border-radius: 5px; } .batch-remove-container h3 { margin-top: 0; } .batch-remove-container textarea { width: 100%; min-height: 60px; margin-bottom: 10px; background-color: var(--background-dimmed, white); color: var(--text, black); border: 1px solid var(--border-accent, #aaa); border-radius: 3px; } .batch-remove-buttons button { margin-right: 10px; border-radius: 4px; padding: 8px 12px; font-weight: bold; cursor: pointer; border-width: 1px; border-style: solid; } .batch-remove-buttons button.positive { background-color: var(--positive-emphasis, #28a745); border-color: var(--positive, #252); color: var(--text, white); } .batch-remove-buttons button.negative { background-color: var(--negative-emphasis, #dc3545); border-color: var(--negative, #522); color: var(--text, white); } .batch-remove-buttons button:hover:not(:disabled) { filter: brightness(1.2); } .batch-remove-buttons button:disabled { background-color: var(--background-emphasis, #6c757d); color: var(--text-weak, #e1e1e1); border-color: var(--border-dimmed, #555); cursor: not-allowed; opacity: 0.7; } .mb-batch-remove-artwork-wrapper { position: relative; display: inline-block; vertical-align: top; } .mb-batch-remove-artwork-wrapper .cover-art-checkbox { position: absolute; top: 5px; left: 5px; z-index: 10; margin: 0; } .mb-batch-remove-artwork-wrapper .status { position: absolute; top: 5px; right: 5px; z-index: 10; margin: 0; font-size: 0.9em; font-weight: bold; text-align: right; width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .mb-batch-remove-artwork-wrapper .status-success { color: var(--positive-emphasis, green); } .mb-batch-remove-artwork-wrapper .status-error { color: var(--negative-emphasis, red); } /* Progress bar styling */ .progress-bar-container { width: 100%; background-color: var(--background-dimmed, #f3f3f3); border-radius: 5px; overflow: hidden; margin-top: 10px; height: 20px; } .progress-bar { height: 100%; width: 0%; background-color: var(--positive-emphasis, #4CAF50); text-align: center; color: var(--text, white); line-height: 20px; font-size: 0.8em; transition: width 0.3s ease-in-out; } `); const getReleaseId = () => { const pathParts = window.location.pathname.split('/'); if (pathParts.length >= 4 && pathParts[1] === 'release' && pathParts[3] === 'cover-art') { return pathParts[2]; } return null; }; const releaseId = getReleaseId(); if (!releaseId) { console.error('[MusicBrainz: Batch Remove Cover Art] Could not determine release ID. Script will not run.'); return; } let isAborting = false; /** * Observes the DOM for the presence of cover art elements and initializes the batch removal functionality. * Uses a MutationObserver for dynamic content loading, specifically waiting for CAA dimensions to load. */ const observeDOM = () => { const contentArea = document.getElementById('content'); if (!contentArea) { setTimeout(observeDOM, 500); return; } let initialized = false; const init = () => { if (initialized) return; initialized = true; observer.disconnect(); setTimeout(initBatchRemove, 100); }; const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'characterData' && mutation.target.parentElement?.classList.contains('ROpdebee_dimensions') && !mutation.target.textContent.includes('pending')) { console.log('[MusicBrainz: Batch Remove Cover Art] Detected CAA dimensions script has finished. Initializing...'); init(); return; } if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { const dimensionSpan = contentArea.querySelector('.artwork-cont .ROpdebee_dimensions'); if (dimensionSpan && !dimensionSpan.textContent.includes('pending')) { console.log('[MusicBrainz: Batch Remove Cover Art] Detected fully-formed CAA dimensions. Initializing...'); init(); return; } } } }); observer.observe(contentArea, { childList: true, subtree: true, characterData: true }); setTimeout(() => { if (!initialized && document.querySelector('.artwork-cont .buttons a[href*="/remove-cover-art/"]')) { console.log('[MusicBrainz: Batch Remove Cover Art] CAA dimensions script not detected, initializing as fallback.'); init(); } }, 3000); const existingDimensionSpan = contentArea.querySelector('.artwork-cont .ROpdebee_dimensions'); if (existingDimensionSpan && !existingDimensionSpan.textContent.includes('pending')) { console.log('[MusicBrainz: Batch Remove Cover Art] CAA dimensions script has already run. Initializing...'); init(); } }; /** * Initializes the batch removal UI and logic on the cover art page. */ const initBatchRemove = () => { const coverArtDivs = Array.from(document.querySelectorAll('.artwork-cont')); if (coverArtDivs.length === 0) { console.log('[MusicBrainz: Batch Remove Cover Art] No cover art found to process.'); return; } const batchControlsContainer = document.createElement('div'); batchControlsContainer.className = 'batch-remove-container'; batchControlsContainer.innerHTML = ` <h3>Batch Remove Cover Art</h3> <div><label><input type="checkbox" id="selectAllCovers"> Select All</label></div> <p>Edit Note (required):</p> <textarea id="editNote" placeholder="Reason for removal (e.g., 'Low quality', 'Duplicate', 'Not applicable')." required></textarea> <div class="batch-remove-buttons"> <button id="removeSelectedBtn" class="positive">Remove Selected Cover Art</button> <button id="abortBtn" class="negative" disabled>Abort</button> </div> <div class="progress-bar-container" style="display: none;"><div class="progress-bar" id="progressBar">0%</div></div> <div id="statusMessages"></div> `; const addCoverArtButton = document.querySelector('.buttons.ui-helper-clearfix'); if (addCoverArtButton) { addCoverArtButton.after(batchControlsContainer); } else { document.getElementById('content')?.appendChild(batchControlsContainer); } const selectAllCheckbox = document.getElementById('selectAllCovers'); const removeSelectedBtn = document.getElementById('removeSelectedBtn'); const abortBtn = document.getElementById('abortBtn'); const editNoteTextarea = document.getElementById('editNote'); const progressBarContainer = document.querySelector('.progress-bar-container'); const progressBar = document.getElementById('progressBar'); const statusMessages = document.getElementById('statusMessages'); let totalRemovals = 0; let completedRemovals = 0; coverArtDivs.forEach((artworkContDiv) => { if (artworkContDiv.closest('.mb-batch-remove-artwork-wrapper')) { return; } const removeLink = artworkContDiv.querySelector('.buttons a[href*="/remove-cover-art/"]'); if (removeLink) { const newWrapper = document.createElement('div'); newWrapper.className = 'mb-batch-remove-artwork-wrapper'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'cover-art-checkbox'; checkbox.dataset.removeUrl = removeLink.href; const statusSpan = document.createElement('span'); statusSpan.className = 'status'; artworkContDiv.parentNode.insertBefore(newWrapper, artworkContDiv); newWrapper.appendChild(artworkContDiv); newWrapper.appendChild(checkbox); newWrapper.appendChild(statusSpan); } }); /** * Resets the UI elements to their initial state after batch processing or abortion. * @param {boolean} aborted - True if the process was aborted by the user. */ const resetUI = (aborted = false) => { removeSelectedBtn.disabled = false; selectAllCheckbox.disabled = false; editNoteTextarea.disabled = false; abortBtn.disabled = true; progressBarContainer.style.display = 'none'; progressBar.style.width = '0%'; progressBar.textContent = '0%'; statusMessages.innerHTML = aborted ? '<p>Batch removal aborted.</p>' : ''; document.querySelectorAll('input.cover-art-checkbox').forEach(cb => { if (!cb.dataset.processed) { cb.disabled = false; } }); isAborting = false; }; selectAllCheckbox.addEventListener('change', (event) => { document.querySelectorAll('input.cover-art-checkbox:not(:disabled)').forEach(cb => { cb.checked = event.target.checked; }); }); removeSelectedBtn.addEventListener('click', async () => { const selectedCheckboxes = Array.from(document.querySelectorAll('input.cover-art-checkbox:checked')); if (selectedCheckboxes.length === 0) { alert('Please select at least one cover art to remove.'); return; } const editNote = editNoteTextarea.value.trim(); if (!editNote) { alert('Please provide an edit note for the removal.'); editNoteTextarea.focus(); return; } removeSelectedBtn.disabled = true; selectAllCheckbox.disabled = true; editNoteTextarea.disabled = true; abortBtn.disabled = false; progressBarContainer.style.display = 'block'; statusMessages.innerHTML = ''; totalRemovals = selectedCheckboxes.length; completedRemovals = 0; updateProgressBar(); for (const checkbox of selectedCheckboxes) { if (isAborting) { statusMessages.innerHTML += '<p>Batch process interrupted.</p>'; break; } const statusSpan = checkbox.closest('.mb-batch-remove-artwork-wrapper').querySelector('.status'); statusSpan.className = 'status'; statusSpan.textContent = 'Submitting...'; try { await submitRemoval(checkbox.dataset.removeUrl, editNote); statusSpan.textContent = 'Removal submitted.'; statusSpan.classList.add('status-success'); checkbox.dataset.processed = 'true'; } catch (error) { statusSpan.textContent = `Error: ${error.message || 'Failed to submit.'}`; statusSpan.classList.add('status-error'); console.error(`Error removing image:`, error); } finally { completedRemovals++; updateProgressBar(); checkbox.disabled = true; checkbox.checked = false; } await new Promise(resolve => setTimeout(resolve, 1000)); } resetUI(isAborting); if (!isAborting) { statusMessages.innerHTML += `<p>Batch removal complete. Processed ${completedRemovals} of ${totalRemovals} selected images.</p>`; } }); abortBtn.addEventListener('click', () => { isAborting = true; statusMessages.innerHTML = '<p>Aborting process, please wait...</p>'; abortBtn.disabled = true; }); /** * Updates the progress bar visually. */ const updateProgressBar = () => { const percentage = totalRemovals > 0 ? (completedRemovals / totalRemovals) * 100 : 0; progressBar.style.width = `${percentage}%`; progressBar.textContent = `${Math.round(percentage)}%`; if (completedRemovals > 0 && completedRemovals === totalRemovals) { progressBar.textContent = 'Complete!'; } }; /** * Submits the removal form for a single cover art via GM.xmlHttpRequest. * @param {string} url The URL to submit the removal request to. * @param {string} editNote The edit note for the removal. * @returns {Promise<any>} A promise that resolves on successful submission or rejects on error. */ const submitRemoval = (url, editNote) => { const formData = new URLSearchParams(); formData.append('confirm.edit_note', editNote); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'POST', url: url, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': window.location.href }, data: formData.toString(), onload: (response) => { if (response.status === 200 && response.finalUrl.includes('/cover-art')) { resolve(response); } else { reject(new Error(`Server returned status ${response.status}.`)); } }, onerror: () => reject(new Error('Network error or request failed.')), ontimeout: () => reject(new Error('Request timed out.')) }); }); }; }; observeDOM(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址