MusicBrainz: Seed URLs to Release Recordings

Import recording-url relationship to release's recordings.

目前為 2025-06-29 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        MusicBrainz: Seed URLs to Release Recordings
// @namespace   https://rinsuki.net
// @match       https://musicbrainz.org/release/*/edit-relationships
// @require     https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.js#sha256=25623a1c332de4571b75a2a6fb8be1fae40180c8fdfd7b4420f09bea727cee1c
// @grant       none
// @version     0.1.1
// @author      rinsuki
// @description Import recording-url relationship to release's recordings.
// ==/UserScript==

(function () {
    'use strict';

    //#region node_modules/.pnpm/[email protected]/node_modules/typedbrainz/lib/index.js
    // SPDX-License-Identifier: MIT
    function isReleaseRelationshipEditor(relationshipEditor) {
        return relationshipEditor.state?.entity.entityType === "release";
    }

    //#endregion

    const zSeedJSON = Zod.object({
        recordings: Zod.record(Zod.string(), // recording id
        Zod.object({
            url: Zod.string().url(),
            types: Zod.array(Zod.string()), // link_type UUID
        })),
        note: Zod.string(),
        version: Zod.number(),
    });
    async function main() {
        // check hash
        const urlParams = new URLSearchParams(location.hash.slice(1));
        const rawJson = urlParams.get("seed-urls-v1");
        if (rawJson == null)
            return;
        while (window.MB?.relationshipEditor?.state == null) {
            console.log("Waiting for window.MB.relationshipEditor?.state to be defined...");
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        console.log("!", window.MB.relationshipEditor);
        if (!isReleaseRelationshipEditor(window.MB.relationshipEditor)) {
            return;
        }
        const { linkedEntities, relationshipEditor } = window.MB;
        const button = document.createElement("button");
        button.textContent = "Seed URLs to Recordings";
        button.style.zoom = "2";
        button.addEventListener("click", () => {
            const json = zSeedJSON.parse(JSON.parse(rawJson));
            if (json.version !== 1) {
                alert("Unsupported version: " + json.version + ", please update the script (or contacat to seeder's developer)");
                return;
            }
            console.log(json);
            const errors = [];
            relationshipEditor.dispatch({
                type: "update-edit-note",
                editNote: (relationshipEditor.state.editNoteField.value + "\n" + json.note + "\n''Powered by \"" + GM_info.script.name + "\" script''").trim(),
            });
            for (const medium of relationshipEditor.state.entity.mediums) {
                console.log(medium);
                for (const track of medium.tracks ?? []) {
                    console.log(track);
                    if (track.recording.gid in json.recordings) {
                        const rel = json.recordings[track.recording.gid];
                        delete json.recordings[track.recording.gid];
                        for (const relType of rel.types) {
                            let linkTypeID;
                            if (relType in linkedEntities.link_type && linkedEntities.link_type[relType].type0 === "recording" && linkedEntities.link_type[relType].type1 === "url") {
                                linkTypeID = linkedEntities.link_type[relType].id;
                            }
                            if (linkTypeID == null) {
                                for (const lt of Object.values(linkedEntities.link_type)) {
                                    if (lt.type0 !== "recording")
                                        continue;
                                    if (lt.type1 !== "url")
                                        continue;
                                    console.log(lt);
                                    if (lt.name === relType) {
                                        linkTypeID = lt.id;
                                        break;
                                    }
                                }
                            }
                            if (linkTypeID == null) {
                                errors.push(`Failed to find link type ${JSON.stringify(relType)} for recording ${track.recording.gid}`);
                                continue;
                            }
                            // it will be marked as "incomplete" in the UI, but actually working?
                            // @see https://github.com/metabrainz/musicbrainz-server/blob/e214b4d3c13f7ee6b2eb2f9c186ecab310354a5b/root/static/scripts/relationship-editor/components/RelationshipItem.js#L153-L163
                            relationshipEditor.dispatch({
                                type: "update-relationship-state",
                                sourceEntity: track.recording,
                                oldRelationshipState: null,
                                newRelationshipState: {
                                    id: relationshipEditor.getRelationshipStateId(null),
                                    linkOrder: 0,
                                    linkTypeID,
                                    _lineage: ["added"],
                                    _original: null,
                                    _status: 1,
                                    attributes: null,
                                    begin_date: null, // TODO: support?
                                    end_date: null, // TODO: support?
                                    editsPending: false,
                                    ended: false,
                                    entity0: track.recording,
                                    entity0_credit: "",
                                    entity1: {
                                        decoded: "",
                                        editsPending: false,
                                        entityType: "url",
                                        gid: "",
                                        name: rel.url,
                                        id: relationshipEditor.getRelationshipStateId(null),
                                        last_updated: null,
                                        href_url: "",
                                        pretty_name: "",
                                    },
                                    entity1_credit: "",
                                },
                                batchSelectionCount: undefined,
                                creditsToChangeForSource: "",
                                creditsToChangeForTarget: "",
                            });
                        }
                    }
                }
            }
            for (const remainingRecordingId of Object.keys(json.recordings)) {
                errors.push(`Can't find ${remainingRecordingId}, skipped`);
            }
            if (errors.length === 0) {
                alert("All URLs seeded successfully!");
            }
            else {
                alert("URLs seeded, but with some errors:\n" + errors.map(x => "* " + x).join("\n"));
            }
        });
        const before = document.querySelector("#content > p");
        before.parentElement.insertBefore(button, before);
        button.focus();
        console.log("done");
    }
    main();

})();