MB: Artist Credit Splitter

いい感じに MusicBrainz のアーティストクレジットを分割します (失敗することもあります)

// ==UserScript==
// @name        MB: Artist Credit Splitter
// @version     1.0.2
// @description いい感じに MusicBrainz のアーティストクレジットを分割します (失敗することもあります)
// @namespace   https://rinsuki.net/
// @author      rinsuki
// @match       https://musicbrainz.org/*
// @grant       none
// ==/UserScript==

(function () {
    'use strict';

    function getReactProps(elem) {
        const properties = Object.getOwnPropertyNames(elem);
        const name = properties.find(x => x.startsWith("__reactProps$"));
        if (name != null)
            return elem[name];
    }

    function waitDOMByObserve(root, check, options) {
        const firstRes = check();
        if (firstRes != null)
            return Promise.resolve(firstRes);
        return new Promise(resolve => {
            const observer = new MutationObserver(() => {
                const res = check();
                if (res != null) {
                    observer.disconnect();
                    resolve(res);
                }
            });
            observer.observe(root, {
                childList: true,
                subtree: options.subtree
            });
        });
    }

    function splitCredit(input) {
        const RE = /([  ]*((?:CV|cv)[.:.:] *|[\((]((?:CV|cv)[.:.:] *)?(?=[^)]{3,})|(?<=[^(]{3})[\))]\/?|[、,\//[]\[\]]|(?: & |&)| feat[.: .: ] *)[  ]*)+/g;
        const splittedCredits = [];
        let lastIndex = 0;
        for (const match of input.matchAll(RE)) {
            splittedCredits.push([input.slice(lastIndex, match.index), match[0]]);
            lastIndex = match.index + match[0].length;
        }
        if (input.slice(lastIndex).length > 0)
            splittedCredits.push([input.slice(lastIndex), ""]);
        return splittedCredits;
    }

    const LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT = "copiedArtistCredit";
    (async () => {
        const bubble = await waitDOMByObserve(document.body, () => document.querySelector("#artist-credit-bubble"), { subtree: false });
        const buttons = await waitDOMByObserve(bubble, () => bubble.querySelector(".buttons"), { subtree: false });
        const button = document.createElement("button");
        button.type = "button";
        button.style.float = "left";
        button.textContent = "USERJS: Split Automatically";
        button.addEventListener("click", async () => {
            const props = getReactProps(bubble);
            console.log(props);
            if (props == null)
                return alert("Failed to get React container");
            const tbody = bubble.querySelector("tbody");
            if (tbody == null)
                return alert("Failed to get tbody");
            const dispatch = props.children[0].props.children.props.dispatch;
            dispatch({ type: "copy" });
            await new Promise(resolve => requestAnimationFrame(resolve));
            const currentCredit = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT) ?? "").names.map(name => name.name + (name.joinPhrase ?? "")).join("");
            const splittedCredits = splitCredit(currentCredit);
            if (!confirm("次のように指定します。よろしいですか?\n\n" + JSON.stringify(splittedCredits, null, 4)))
                return;
            // localStorage.removeItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT)
            // let p = waitLocalStorage(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT)
            // props.copyArtistCredit()
            // await p
            // const stubArtistCredit = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT)!)
            localStorage.setItem(LOCALSTORAGE_KEY_COPIED_ARTIST_CREDIT, JSON.stringify({ names: splittedCredits.map(([name, joinPhrase], i) => {
                    return {
                        joinPhrase,
                        name,
                        // artist: {
                        //     entityType: "artist",
                        //     uniqueID: stubArtistCredit[i].artist.uniqueID,
                        //     name,
                        // }
                    };
                }) }));
            dispatch({ type: "paste" });
            alert("finish!");
        });
        buttons.appendChild(button);
    })();

}());