MusicBrainz: Hotkeys for selected entities

Adds hotkeys to perform actions on selected entities. "A" = Artwork, "D" = Delete, "E" = Edit, "W" = Merge

当前为 2025-05-22 提交的版本,查看 最新版本

// ==UserScript==
// @name         MusicBrainz: Hotkeys for selected entities
// @namespace    https://musicbrainz.org/user/chaban
// @version      1.0.1
// @description  Adds hotkeys to perform actions on selected entities. "A" = Artwork, "D" = Delete, "E" = Edit, "W" = Merge
// @tag          ai-created
// @author       chaban
// @license      MIT
// @include      https://*musicbrainz.org/artist*
// @include      https://*musicbrainz.org/area/*
// @include      https://*musicbrainz.org/release-group/*
// @include      https://*musicbrainz.org/label/*
// @include      https://*musicbrainz.org/place/*
// @include      https://*musicbrainz.org/isrc/*
// @include      https://*musicbrainz.org/iswc/*
// @include      https://*musicbrainz.org/report/*
// @include      https://*musicbrainz.org/*/*/artists
// @include      https://*musicbrainz.org/*/*/releases
// @include      https://*musicbrainz.org/*/*/recordings
// @include      https://*musicbrainz.org/*/*/release-groups
// @include      https://*musicbrainz.org/*/*/events
// @include      https://*musicbrainz.org/*/*/labels
// @include      https://*musicbrainz.org/*/*/places
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const entityTypes = {
        release: { actions: ['delete', 'edit', 'viewArtwork'] },
        recording: { actions: ['delete', 'edit'] },
        work: { actions: ['edit'] },
        area: { actions: ['delete', 'edit'] },
        instrument: { actions: ['delete', 'edit'] },
        genre: { actions: ['delete', 'edit'] },
        'release-group': { actions: ['edit'] },
        event: { actions: ['edit', 'viewArtwork'] },
        place: { actions: ['edit'] },
        label: { actions: ['edit'] },
        series: { actions: ['edit'] }
    };

    /**
     * Extracts the entity type from the link.
     * @param {HTMLAnchorElement} link The link element.
     * @returns {string|null} The entity type or null if not detectable.
     */
    function getEntityTypeFromLink(link) {
        if (!link || !link.href) {
            return null;
        }
        const href = link.href;
        const parts = href.split('/').filter(Boolean);
        const type = parts[2];
        return entityTypes[type] ? type : null;
    }

    /**
     * Extracts the MBID from a URL.
     * @param {string} url The URL from which the MBID should be extracted.
     * @returns {string|null} The MBID or null if not found.
     */
    function extractMBID(url) {
        const mbidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
        const match = url.match(mbidRegex);
        return match ? match[0] : null;
    }

    /**
     * Opens pages based on action.
     * @param {NodeListOf<HTMLInputElement>} checkboxes - Checkboxes of entities.
     * @param {string} action - Type of action (edit, delete, viewArtwork).
     */
    function openPages(checkboxes, action) {
        checkboxes.forEach((checkbox, index) => {
            const row = checkbox.closest('tr');
            if (row) {
                const entityLink = row.querySelector('a[href]');
                const entityType = getEntityTypeFromLink(entityLink);
                const mbid = extractMBID(entityLink.href);
                if (entityType && entityTypes[entityType].actions.includes(action) && mbid) {
                    let url = `/${entityType}/${mbid}/${action}`;
                    if (action === 'viewArtwork') {
                        url = entityType === 'release' ? `/release/${mbid}/cover-art` : `/event/${mbid}/event-art`;
                    }
                    setTimeout(() => {
                        window.open(url, '_blank');
                    }, index * 1000);
                }
            }
        });
    }

    /**
     * Handles the keydown event for triggering actions.
     * @param {KeyboardEvent} event - The keydown event.
     */
    function handleKeyDown(event) {
        const checkedSelector = 'input[name="add-to-merge"]:checked';
        const checkboxes = document.querySelectorAll(checkedSelector);
        switch (event.key) {
            case 'w':
                if (checkboxes.length > 1) {
                    const container = document.querySelector('.list-merge-buttons-row-container');
                    if (container) {
                        const buttons = container.querySelectorAll('button');
                        if (buttons.length > 0) {
                            buttons[buttons.length - 1].click();
                        }
                    }
                }
                break;
            case 'd':
                if (checkboxes.length > 0) {
                    openPages(checkboxes, 'delete');
                }
                break;
            case 'e':
                if (checkboxes.length > 0) {
                    openPages(checkboxes, 'edit');
                }
                break;
            case 'a':
                if (checkboxes.length > 0) {
                    openPages(checkboxes, 'viewArtwork');
                }
                break;
        }
    }

    document.addEventListener('keydown', handleKeyDown);
})();