您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Import recording-url relationship to release's recordings.
// ==UserScript== // @name MusicBrainz: Seed URLs to Release Recordings // @namespace https://rinsuki.net // @version 0.2.1 // @description Import recording-url relationship to release's recordings. // @author rinsuki // @match https://musicbrainz.org/release/*/edit-relationships // @match https://*.musicbrainz.org/release/*/edit-relationships // @grant none // @contributionURL https://rinsuki.fanbox.cc/ // @contributionURL https://github.com/sponsors/rinsuki // @homepageURL https://github.com/rinsuki/userscripts // @supportURL https://github.com/rinsuki/userscripts/issues // @require https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.js#sha256-JWI6HDMt5FcbdaKm+4vh+uQBgMj9/XtEIPCb6nJ87hw=,sha512-w/o1QQewvZigMuaxE5iaUXhSUomguLoRvf4n95QwcYXWxEs7xdMzM0Wmhqwt+PCcUkmE8prQjzcqDOIMhVD6ew== // ==/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({ version: Zod.literal(1), recordings: Zod.record(Zod.string(), // recording id Zod.object({ url: Zod.string().url(), types: Zod.array(Zod.string()), // link_type UUID })), note: Zod.string(), }).or(Zod.object({ version: Zod.literal(2), recordings: Zod.record(Zod.string(), // recording id Zod.array(Zod.object({ url: Zod.string().url(), types: Zod.array(Zod.string()), // link_type UUID }))), note: Zod.string(), })); const zSeedJSONFallback = Zod.object({ version: Zod.number(), }); function applyRelationships(relationships, editNote, relationshipEditor) { relationshipEditor.dispatch({ type: "update-edit-note", editNote: (relationshipEditor.state.editNoteField.value + "\n" + editNote + "\n''Powered by \"" + GM_info.script.name + "\" script (" + GM_info.script.version + ")''").trim(), }); for (const relationship of relationships) { // 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: relationship.recording, oldRelationshipState: null, newRelationshipState: { id: relationshipEditor.getRelationshipStateId(null), linkOrder: 0, linkTypeID: relationship.linkTypeID, _lineage: ["added"], _original: null, _status: 1, attributes: null, begin_date: null, // TODO: support? end_date: null, // TODO: support? editsPending: false, ended: false, entity0: relationship.recording, entity0_credit: "", entity1: { decoded: "", editsPending: false, entityType: "url", gid: "", name: relationship.url, id: relationshipEditor.getRelationshipStateId(null), last_updated: null, href_url: "", pretty_name: "", }, entity1_credit: "", }, batchSelectionCount: undefined, creditsToChangeForSource: "", creditsToChangeForTarget: "", }); } } 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 anyJSON = JSON.parse(rawJson); const baseJSON = zSeedJSONFallback.parse(anyJSON); if (![1, 2].includes(baseJSON.version)) { alert(`Unsupported version: ${baseJSON.version}, please update the script (or contact the seeder's developer)`); return; } const json = zSeedJSON.parse(anyJSON); const errors = []; const preparedRelationships = []; for (const track of relationshipEditor.state.entity.mediums.flatMap(m => m.tracks ?? [])) { if (!(track.recording.gid in json.recordings)) continue; const rels = json.recordings[track.recording.gid]; delete json.recordings[track.recording.gid]; const alreadyAddedDomains = new Set(); for (const rel of Array.isArray(rels) ? rels : [rels]) { const relUrl = new URL(rel.url); if (alreadyAddedDomains.has(relUrl.hostname)) { errors.push(`You can't add multiple same domain URLs for a recording at once! URL: ${rel.url} Recording: ${track.recording.gid}`); continue; } alreadyAddedDomains.add(relUrl.hostname); 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; } preparedRelationships.push({ recording: track.recording, url: rel.url, linkTypeID, }); } } } for (const remainingRecordingId of Object.keys(json.recordings)) { errors.push(`Failed to find recording: ${remainingRecordingId}, you might be need to expand mediums before press seed button.`); } if (errors.length === 0) { applyRelationships(preparedRelationships, json.note, relationshipEditor); button.textContent = "URLs seeded successfully!"; button.disabled = true; } else { alert("Failed to seed urls:\n" + errors.map(x => "* " + x).join("\n")); } }); const before = document.querySelector("#content > p"); before.parentElement.insertBefore(button, before); button.focus(); console.log("done"); } main(); })();