您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Users of Harmony Release Actions can include/exclude/modify release choices from each of the vendors
// ==UserScript== // @name Harmony Link Preferences // @namespace https://musicbrainz.org/user/DenizC // @version 2.6 // @description Users of Harmony Release Actions can include/exclude/modify release choices from each of the vendors // @match https://harmony.pulsewidth.org.uk/release/actions* // @grant GM_xmlhttpRequest // @connect musicbrainz.org // ==/UserScript== (function() { 'use strict'; const SERVICES = ['spotify','deezer','itunes','tidal','bandcamp','beatport']; const MB_API = 'https://musicbrainz.org/ws/2/release/'; const style = 'margin:1em 0;padding:1em;border:1px solid #ccc;border-radius:6px;background:#f9f9f9'; const urlParams = new URLSearchParams(location.search); function getMBID() { const mbid = decodeURIComponent(urlParams.get('release_mbid') || ''); const match = mbid.match(/([a-f0-9-]{36})/i); return match ? match[1] : null; } function getServiceValue(service) { return urlParams.has(service) ? decodeURIComponent(urlParams.get(service)) : null; } function getRegionParam() { return urlParams.get('region') || ''; } function isInitialVisitOnlyReleaseMBID() { return urlParams.keys().next().value === 'release_mbid' && [...urlParams.keys()].length === 1; } function parseAppleRegion(url) { const m = url.match(/(?:itunes|music)\.apple\.com\/([a-z]{2})\//i); return m ? m[1].toUpperCase() : ''; } function normalizeAppleURL(url) { const idMatch = url.match(/id(\d+)/) || url.match(/\/(\d+)(?:[/?#]|$)/); const id = idMatch ? idMatch[1] : null; const region = parseAppleRegion(url); return id && region ? `${id}_${region}` : null; } function bandcampSlug(url) { try { const u = new URL(url); if (!u.hostname.endsWith('bandcamp.com')) return null; if (!u.pathname.startsWith('/album/')) return null; const artist = u.hostname.split('.')[0]; const path = u.pathname.replace(/^\/album\/+/, ''); return artist && path ? `${artist}/${path}` : null; } catch (e) { return null; } } function extractLinks(rels) { const map = {}; const seen = new Set(); let fallbackRegion = ''; rels.forEach(rel => { if (rel.ended) return; const url = rel.url?.resource; if (!url) return; let service, id, label, region, dedupeKey; if (url.includes('spotify.com/album/')) { service = 'spotify'; id = url.split('/album/')[1]?.split('?')[0]; label = id; dedupeKey = `${service}_${id}`; } else if (url.includes('deezer.com/album/')) { service = 'deezer'; id = url.split('/album/')[1]?.split('?')[0]; label = id; dedupeKey = `${service}_${id}`; } else if (/(itunes|music)\.apple\.com/.test(url)) { service = 'itunes'; const normalized = normalizeAppleURL(url); if (!normalized) return; [id, region] = normalized.split('_'); label = `${id} [${region}]`; dedupeKey = `${service}_${normalized}`; if (!fallbackRegion && region) fallbackRegion = region; } else if (url.includes('tidal.com/album/')) { service = 'tidal'; id = url.split('/album/')[1]?.split('?')[0]; label = id; dedupeKey = `${service}_${id}`; } else if (url.includes('bandcamp.com')) { const slug = bandcampSlug(url); if (!slug) return; service = 'bandcamp'; id = slug; label = slug; dedupeKey = `${service}_${slug}`; } else if (url.includes('beatport.com/release/')) { const parts = url.split('/release/')[1]?.split('/'); service = 'beatport'; id = parts?.[1]?.split('?')[0]; label = id; dedupeKey = `${service}_${id}`; } if (service && id && !seen.has(dedupeKey)) { seen.add(dedupeKey); map[service] ||= []; map[service].push({id, label, region}); } }); return { map, fallbackRegion }; } function renderUI(map, fallbackRegion, mbid) { const details = document.createElement('details'); details.style = style; const summary = document.createElement('summary'); summary.innerHTML = '<strong>🎚 Link Preferences</strong>'; details.appendChild(summary); const form = document.createElement('form'); form.style = 'margin-top:1em'; let regionInput; SERVICES.forEach(s => { const list = map[s] || []; if (list.length === 0) return; const div = document.createElement('div'); div.style = 'margin-bottom:1em'; const lbl = document.createElement('label'); lbl.style = 'font-weight:bold;margin-left:0.3em'; const chk = document.createElement('input'); chk.type = 'checkbox'; chk.name = s; const selectedVal = getServiceValue(s); const showAllChecked = isInitialVisitOnlyReleaseMBID(); chk.checked = showAllChecked || !!selectedVal; lbl.appendChild(chk); lbl.appendChild(document.createTextNode(s)); div.appendChild(lbl); if (list.length > 1) { list.forEach((entry, i) => { const rad = document.createElement('input'); rad.type = 'radio'; rad.name = s + '_choice'; rad.value = entry.id; if (selectedVal) { rad.checked = (entry.id === selectedVal); } else if (i === 0 || entry.region === fallbackRegion) { rad.checked = true; } const rlbl = document.createElement('label'); rlbl.style = 'margin-left:1.5em;display:block'; rlbl.appendChild(rad); rlbl.appendChild(document.createTextNode(' ' + entry.label)); div.appendChild(rlbl); chk.addEventListener('change', () => { rad.disabled = !chk.checked; }); rad.disabled = !chk.checked; if (s === 'itunes' && entry.region) { rad.addEventListener('change', () => { if (rad.checked && regionInput) { regionInput.value = entry.region; } }); } }); } else { chk.dataset.id = list[0].id; } form.appendChild(div); }); const regLbl = document.createElement('label'); regLbl.textContent = 'Preferred Region: '; regionInput = document.createElement('input'); regionInput.type = 'text'; regionInput.name = 'region'; regionInput.placeholder = 'US'; regionInput.value = getRegionParam() || fallbackRegion || ''; regLbl.appendChild(regionInput); form.appendChild(regLbl); form.appendChild(document.createElement('br')); form.appendChild(document.createElement('br')); const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'Generate URL'; btn.addEventListener('click', () => { const url = new URL('https://harmony.pulsewidth.org.uk/release/actions'); url.searchParams.set('release_mbid', mbid); url.searchParams.set('musicbrainz', mbid); SERVICES.forEach(s => { const chk = form.querySelector(`input[name="${s}"]`); if (chk && chk.checked) { const rads = form.querySelectorAll(`input[name="${s}_choice"]`); let val = chk.dataset.id; if (rads.length > 0) { const sel = [...rads].find(r => r.checked); if (sel) val = sel.value; } if (val) url.searchParams.set(s, val); } }); const rr = regionInput.value.trim(); if (rr) url.searchParams.set('region', rr.toUpperCase()); location.href = url.toString(); }); form.appendChild(btn); details.appendChild(form); document.querySelector('main')?.prepend(details); } const mbid = getMBID(); if (mbid) { GM_xmlhttpRequest({ method: 'GET', url: MB_API + mbid + '?inc=url-rels&fmt=json', headers: { Accept: 'application/json' }, onload(r) { const data = JSON.parse(r.responseText); const { map, fallbackRegion } = extractLinks(data.relations || []); renderUI(map, fallbackRegion, mbid); }, onerror() { console.error('Failed to fetch MB relations'); } }); } })();