您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
从条目页面、搜索结果或现有收藏页面批量复制MBID或添加项目到MusicBrainz收藏。
当前为
// ==UserScript== // @name MusicBrainz Batch Add to Collection // @namespace https://github.com/y-young/userscripts // @version 2021.8.25.1 // @description Batch add entities to MusicBrainz collection and copy MBIDs from entity pages, search result or existing collections. // @author y-young // @licence MIT; https://opensource.org/licenses/MIT // @supportURL https://github.com/y-young/userscripts/labels/mb-batch-add-to-collection // @include /^https?:\/\/(.*\.)?musicbrainz.org\/(artist|collection|label|release|release-group|series|work)\/[\w-]+(\/disc\/.*)?\/?(\?page=\d+)?$/ // @include /^https?:\/\/(.*\.)?musicbrainz.org\/area\/[\w-]+\/(artists|events|labels|releases|recordings|places|works)\/?(\?page=\d+)?$/ // @include /^https?:\/\/(.*\.)?musicbrainz.org\/artist\/[\w-]+\/(events|releases|recordings|works)\/?(\?page=\d+)?$/ // @include /^https?:\/\/(.*\.)?musicbrainz.org\/place\/[\w-]+\/events\/?(\?page=\d+)?$/ // @include /^https?:\/\/(.*\.)?musicbrainz.org\/search\?query=.*&type=(artist|event|label|instrument|place|recording|release_group|release|series|work)/ // @exclude /^https?:\/\/(.*\.)?musicbrainz.org\/collection/create/ // @grant GM_setClipboard // @run-at document-idle // @name:zh-CN MusicBrainz批量添加收藏 // @description:zh-CN 从条目页面、搜索结果或现有收藏页面批量复制MBID或添加项目到MusicBrainz收藏。 // ==/UserScript== 'use strict'; // To enable "Copy MBIDs" button, set this option to true const SHOW_COPY_BUTTON = false; // Whether to close dialog when successfully submitted const CLOSE_DIALOG_AFTER_SUBMIT = true; const IDENTIFIER = "batch-add-to-collection"; const CLIENT = "BatchAddToCollection/2021.8.25.1(https://github.com/y-young)"; const ENTITY_TYPE_MAPPING = { artist: "release-group", label: "release", place: "event", "release-group": "release", release: "recording", work: "recording" }; const SUPPORTED_TYPES = [ "artist", "event", "label", "instrument", "place", "recording", "release", "release-group", "series", "work" ]; const DIALOG_LOADING_NOTICE = ` <div class="banner loading-message" style="background-position: right;"> Loading your collections... </div> `; const url = new URL(location.href); const origin = url.origin; const path = url.pathname.split('/'); // Determine entity type of current page and target collection type const entityType = path[1]; let collectionType = ENTITY_TYPE_MAPPING[entityType]; switch (entityType) { case "area": case "artist": { const subType = path[3]; if (subType) { // Convert plural form to singular form collectionType = subType.substr(0, subType.length - 1); } break; } case "search": collectionType = url.searchParams.get("type"); if (collectionType === "release_group") { collectionType = "release-group"; } break; case "collection": { const type = document.querySelector("dd[class='type']").innerText; collectionType = type.toLowerCase().replaceAll(' ', '-'); if (collectionType === "owned-music" || collectionType === "wishlist") { collectionType = "release"; } else if (collectionType === "attending" || collectionType === "maybe-attending") { collectionType = "event"; } break; } case "series": { const type = document.querySelector("dd[class='type']").innerText; collectionType = type.toLowerCase().replaceAll(' ', '-').replaceAll("-series", ""); break; } } let collections = undefined; // Initialize "Batch add to collection" dialog const dialogElement = document.createElement("div"); dialogElement.id = IDENTIFIER + "-dialog"; dialogElement.title = "Batch add to collection"; dialogElement.innerHTML = DIALOG_LOADING_NOTICE; dialogElement.style.overflow = "auto"; document.querySelector("body").appendChild(dialogElement); const dialog = $("#" + IDENTIFIER + "-dialog").dialog({ autoOpen: false, height: 400, width: 350, open: function() { loadCollections(); }, buttons: { Refresh: function() { collections = undefined; dialogElement.innerHTML = DIALOG_LOADING_NOTICE; loadCollections(); }, Cancel: function() { $(this).dialog("close"); } } }); function request(url, options = {}) { return fetch(origin + url, { ...options, headers: { "user-agent": CLIENT, "accept": "application/json" } }); } function getGidFromUrl(url) { const path = new URL(url).pathname.split('/'); return path[2]; } function getCollectionTypePlural() { if (collectionType === "series") { return "series"; } return collectionType + "s"; } function createCheckbox(recordingId) { const checkbox = document.createElement("input"); checkbox.setAttribute("type", "checkbox"); checkbox.classList.add(IDENTIFIER); checkbox.dataset.id = recordingId; const cell = document.createElement("td"); cell.prepend(checkbox); return cell; } function getSelectedIds() { const entityIds = Array.from( document.querySelectorAll("input:checked[type='checkbox']." + IDENTIFIER) ).map(checkbox => checkbox.dataset.id); return entityIds; } function addToCollection(event) { let target = event.target; // Compatibility with katakana-terminator if (target.nodeName === "RUBY") { target = target.parentElement; } else if (target.nodeName === "RT") { target = target.parentElement.parentElement; } const collectionId = target.dataset.id; const ids = getSelectedIds(); if (ids.length === 0) { alert("No item is selected."); dialog.dialog("close"); return; } if (ids.length > 400) { alert("Too many items, you can select at most 400 items."); dialog.dialog("close"); return; } const loadingNotice = document.querySelector("#" + IDENTIFIER + "-dialog div.loading-message"); loadingNotice.style.display = "block"; request( `/ws/2/collection/${collectionId}/${getCollectionTypePlural()}/${ids.join(';')}?client=${encodeURIComponent(CLIENT)}`, {method: "PUT"} ).then(response => { loadingNotice.style.display = "none"; if (response.status === 200) { alert(`Successfully added ${ids.length} item(s) to collection.`); if (CLOSE_DIALOG_AFTER_SUBMIT) { dialog.dialog("close"); } } else { alert("An error occurred."); console.error(response); } }); } function loadCollections() { if (collections) { return collections; } return request("/ws/2/collection") .then(response => response.json()) .then(data => data.collections.filter( collection => collection["entity-type"] === (collectionType === "release-group" ? "release_group" : collectionType) ) ) .then(result => { collections = result; document.querySelector("#" + IDENTIFIER + "-dialog").innerHTML = ` <div>You have the following ${collectionType} collection(s), click to add selected items:</div> <div class="banner loading-message" style="background-position: right; display: none;"> Adding to collection... </div> <table class="tbl"> <thead> <th>Name</th> <th>Action</th> </thead> <tbody> ${result.map((collection, index) => ` <tr class="${index % 2 ? "odd" : "even"}"> <td> <a href="/collection/${collection.id}" target="_blank" rel="noreferrer"> ${collection.name} </a> </td> <td> <a name="add" data-id="${collection.id}" href="javascript:void(0)"> Add </a> </td> </tr>`).join('')} <tr> <td> <a href="/collection/create" target="_blank" rel="noreferrer"> Create a new collection </a> </td> </tr> </tbody> </table>`; document.querySelectorAll("#" + IDENTIFIER + "-dialog a[name='add']") .forEach(element => element.addEventListener("click", addToCollection)); }); } function toggleSelection(event) { const target = event.target; let context = target; while (context.nodeName !== "TABLE") { context = context.parentNode; } const checked = target.checked; context.querySelectorAll("input[type='checkbox']." + IDENTIFIER).forEach(checkbox => { checkbox.checked = checked; }); } function createToggleSelectionCheckbox() { const headCell = document.createElement("th"); const checkbox = document.createElement("input"); checkbox.setAttribute("type", "checkbox"); checkbox.addEventListener("click", toggleSelection); headCell.appendChild(checkbox); headCell.className = "checkbox-cell"; return headCell; } function initTableCheckboxes(table) { // Get rows const rows = Array.from(table.querySelectorAll("tr.odd, tr.even")); rows.forEach(row => { const entityLink = row.querySelector(`td a[href^='/${collectionType}']`); if (!entityLink) { // Some rows in search result are grouped together row.prepend(document.createElement("td")); return; } const gid = getGidFromUrl(entityLink.href); // Use existing checkboxes if possible const checkbox = row.querySelector("td input[type='checkbox']"); if (checkbox) { checkbox.classList.add(IDENTIFIER); checkbox.dataset.id = gid; } else { row.prepend(createCheckbox(gid)); } }); // Update table headers switch (entityType) { case "release": table.querySelectorAll("thead th[colspan]").forEach(header => { header.setAttribute("colspan", Number(header.getAttribute("colspan")) + 1); }); table.querySelectorAll("tr.subh").forEach(header => { header.prepend(createToggleSelectionCheckbox()); }); break; case "work": table.querySelectorAll("tr.subh th[colspan]").forEach(header => { header.setAttribute("colspan", "5"); }); table.querySelector("thead tr").prepend(createToggleSelectionCheckbox()); break; case "search": case "series": table.querySelector("thead tr").prepend(createToggleSelectionCheckbox()); break; case "collection": if (!table.querySelector("thead th input[type='checkbox']")) { table.querySelector("thead tr").prepend(createToggleSelectionCheckbox()); } break; } } function initCheckboxes() { document.querySelectorAll("table.tbl").forEach(table => { initTableCheckboxes(table); }); } function openDialog() { dialog.dialog("open"); } function copyMBIDs() { const entityIds = getSelectedIds(); GM_setClipboard(entityIds.join("\n")); alert(`Copied ${entityIds.length} MBIDs to clipboard.`); } function initButton() { const button = document.createElement("button"); button.setAttribute("type", "button"); button.innerText = "Batch add to collection"; button.addEventListener("click", openDialog); const copyButton = document.createElement("button"); copyButton.setAttribute("type", "button"); copyButton.innerText = "Copy MBIDs"; copyButton.addEventListener("click", copyMBIDs); let container = document.querySelector("form div.row span.buttons"); if (container) { container.appendChild(button); if (SHOW_COPY_BUTTON) { container.appendChild(copyButton); } } else { container = document.createElement("form"); container.innerHTML = `<div class="row"><span class="buttons"></span></div>`; const tables = document.querySelectorAll("table.tbl"); // Insert after the last table if there're multiple ones const precedent = tables[tables.length - 1]; if (!precedent) { // Empty collection return; } container.querySelector("span.buttons").appendChild(button); if (SHOW_COPY_BUTTON) { container.querySelector("span.buttons").appendChild(copyButton); } precedent.parentNode.insertBefore(container, precedent.nextSibling); } } console.log("[Batch add to collection]", entityType, collectionType); if (SUPPORTED_TYPES.includes(collectionType)) { initCheckboxes(); initButton(); }