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 getReactContainer(elem) {
        const properties = Object.getOwnPropertyNames(elem);
        const name = properties.find(x => x.startsWith("__reactContainer$"));
        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 container = getReactContainer(bubble);
            if (container == null)
                return alert("Failed to get React container");
            const tbody = bubble.querySelector("tbody");
            if (tbody == null)
                return alert("Failed to get tbody");
            const props = container.memoizedState.element.props;
            console.log(props);
            const currentCredit = props.artistCredit.names.map(name => name.name + (name.joinPhrase ?? "")).join("");
            const splittedCredits = splitCredit(currentCredit);
            if (!confirm("次のように指定します。よろしいですか?\n\n" + JSON.stringify(splittedCredits, null, 4)))
                return;
            for (let i = props.artistCredit.names.length; i < splittedCredits.length; i++) {
                props.addName();
                await waitDOMByObserve(tbody, () => tbody.childNodes.item(i), { subtree: false });
            }
            // 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(splittedCredits.map(([name, joinPhrase], i) => {
                return {
                    joinPhrase,
                    name,
                    // artist: {
                    //     entityType: "artist",
                    //     uniqueID: stubArtistCredit[i].artist.uniqueID,
                    //     name,
                    // }
                };
            })));
            props.pasteArtistCredit();
            alert("finish!");
        });
        buttons.appendChild(button);
    })();

}());