MusicBrainz: Batch Remove Cover Art

Allows batch removing cover art from MusicBrainz releases.

当前为 2025-07-15 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MusicBrainz: Batch Remove Cover Art
// @namespace    https://musicbrainz.org/user/chaban
// @version      0.2
// @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(`
        /* General container styling */
        .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;
        }

        /* Specific button styling for better contrast and theme compatibility */
        .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;
        }

        /* Per-item styling */
        .cover-art-item {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
            border: 1px solid var(--border-dimmed, #eee);
            padding: 5px;
            border-radius: 3px;
        }
        .cover-art-item input[type="checkbox"] {
            margin-right: 10px;
        }
        .cover-art-item .artwork-cont {
            flex-grow: 1;
            display: flex;
            align-items: center;
        }
        .cover-art-item .status {
            margin-left: 20px;
            font-weight: bold;
        }
        .cover-art-item .status-success {
            color: var(--positive-emphasis, green);
        }
        .cover-art-item .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;

    const observeDOM = () => {
        const contentArea = document.getElementById('content');
        if (!contentArea) {
            setTimeout(observeDOM, 500);
            return;
        }
        const observerConfig = { childList: true, subtree: true };
        const observerCallback = (mutationsList, observer) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && document.querySelector('.artwork-cont .buttons a[href*="/remove-cover-art/"]')) {
                    observer.disconnect();
                    initBatchRemove();
                    break;
                }
            }
        };
        const observer = new MutationObserver(observerCallback);
        observer.observe(contentArea, observerConfig);
        if (document.querySelector('.artwork-cont .buttons a[href*="/remove-cover-art/"]')) {
            observer.disconnect();
            initBatchRemove();
        }
    };

    const initBatchRemove = () => {
        const coverArtDivs = Array.from(document.querySelectorAll('.artwork-cont'));
        if (coverArtDivs.length === 0) 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.before(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((div) => {
            const removeLink = div.querySelector('.buttons a[href*="/remove-cover-art/"]');
            if (removeLink) {
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.className = 'cover-art-checkbox';
                checkbox.dataset.removeUrl = removeLink.href;
                const wrapper = document.createElement('div');
                wrapper.className = 'cover-art-item';
                wrapper.appendChild(checkbox);
                wrapper.appendChild(div.cloneNode(true));
                const statusSpan = document.createElement('span');
                statusSpan.className = 'status';
                wrapper.appendChild(statusSpan);
                div.parentNode.replaceChild(wrapper, div);
            }
        });

        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('.cover-art-checkbox').forEach(cb => {
                if (!cb.dataset.processed) cb.disabled = false;
            });
            isAborting = false;
        };

        selectAllCheckbox.addEventListener('change', (event) => {
            document.querySelectorAll('.cover-art-checkbox:not(:disabled)').forEach(cb => {
                cb.checked = event.target.checked;
            });
        });

        removeSelectedBtn.addEventListener('click', async () => {
            const selectedCheckboxes = Array.from(document.querySelectorAll('.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('.cover-art-item').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;
        });

        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!';
            }
        };

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