您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays per-recording streaming/download links on MusicBrainz with toggle buttons (collapsed by default).
// ==UserScript== // @name MB: Inline per-recording streaming & download links // @namespace https://chat.openai.com/ // @version 1.4 // @description Displays per-recording streaming/download links on MusicBrainz with toggle buttons (collapsed by default). // @match *://*.musicbrainz.org/release/* // @grant none // ==/UserScript== (function () { 'use strict'; function getServiceIcon(domain) { domain = domain.toLowerCase().replace(/^www\./, ''); if (domain.includes('bandcamp.com')) return '🎵'; if (domain.includes('spotify.com')) return '🟢'; if (domain.includes('apple.com')) return '🍎'; if (domain.includes('deezer.com')) return '🟣'; if (domain.includes('tidal.com')) return '🌊'; if (domain.includes('youtube.com') || domain.includes('youtu.be')) return '▶️'; if (domain.includes('soundcloud.com')) return '☁️'; if (domain.includes('archive.org')) return '🗄️'; if (domain.includes('qobuz.com')) return '🎼'; if (domain.includes('beatport.com')) return '🧩'; if (domain.includes('amazon.com')) return '🅰️'; return '🌐'; } function mergeRelations(relations) { const merged = {}; for (const rel of relations) { const url = rel.url?.resource; if (!url) continue; if (!merged[url]) merged[url] = { href: url, types: new Set() }; merged[url].types.add(rel.type); } return Object.values(merged).map(entry => ({ href: entry.href, typeText: Array.from(entry.types).join(', ') })); } function groupByDomain(mergedLinks) { const grouped = {}; for (const item of mergedLinks) { try { const domain = new URL(item.href).hostname.replace(/^www\./, ''); const icon = getServiceIcon(domain); const isKnown = icon !== '🌐'; if (!grouped[domain]) grouped[domain] = { links: [], icon, known: isKnown }; grouped[domain].links.push(item); } catch (e) { console.warn('Invalid URL:', item.href); } } return grouped; } function injectLinks(row, relations) { const merged = mergeRelations(relations); const grouped = groupByDomain(merged); const td = row.querySelectorAll('td')[1]; if (!td) return; const dl = td.querySelector('dl.ar') || Object.assign(document.createElement('dl'), { className: 'ar' }); if (!td.contains(dl)) td.appendChild(dl); const dt = document.createElement('dt'); const toggle = document.createElement('button'); toggle.textContent = '►'; toggle.className = 'recording-toggle'; toggle.style.marginRight = '0.5em'; toggle.style.border = 'none'; toggle.style.background = 'none'; toggle.style.cursor = 'pointer'; toggle.style.fontSize = '0.9em'; const dd = document.createElement('dd'); dd.classList.add('recording-url-links'); dd.style.display = 'none'; toggle.addEventListener('click', () => { const isHidden = dd.style.display === 'none'; dd.style.display = isHidden ? 'block' : 'none'; toggle.textContent = isHidden ? '▼' : '►'; }); dt.appendChild(toggle); dt.appendChild(document.createTextNode('Streaming/Downloads:')); if (merged.length === 0) { dd.textContent = 'No streaming or download links found.'; } else { for (const [domain, { links, icon }] of Object.entries(grouped).sort(([, a], [, b]) => Number(b.known) - Number(a.known))) { const label = document.createElement('strong'); label.textContent = `${icon} ${domain}: `; dd.appendChild(label); for (const link of links) { const a = document.createElement('a'); a.href = link.href; a.target = '_blank'; a.rel = 'noopener'; a.textContent = `[${link.typeText}]`; a.style.marginRight = '0.5em'; dd.appendChild(a); } dd.appendChild(document.createElement('br')); } } dl.appendChild(dt); dl.appendChild(dd); } async function fetchReleaseRecordingData() { const releaseMBID = window.location.pathname.match(/release\/([a-f0-9-]{36})/)?.[1]; if (!releaseMBID) return null; const url = `https://musicbrainz.org/ws/2/release/${releaseMBID}?inc=recordings+recording-level-rels+url-rels&fmt=json`; const res = await fetch(url); return await res.json(); } function getTrackRows() { return Array.from(document.querySelectorAll('.tracklist-and-credits table.tbl.medium tbody tr')) .filter(tr => !tr.classList.contains('subh')); } function onReactHydrated(element, callback) { const alreadyHydrated = Object.keys(element).some(key => key.startsWith('_reactListening') && element[key]); if (alreadyHydrated) callback(); else element.addEventListener('mb-hydration', callback); } onReactHydrated(document.querySelector('.tracklist-and-credits'), async () => { const toolbox = document.querySelector('#medium-toolbox'); if (!toolbox) return; const status = document.createElement('span'); status.style.marginLeft = '1em'; const toggleAllBtn = document.createElement('button'); toggleAllBtn.classList.add('btn-link'); toggleAllBtn.textContent = 'Expand all streaming/download links'; let allExpanded = false; toggleAllBtn.addEventListener('click', () => { const dds = document.querySelectorAll('.tracklist-and-credits td dd.recording-url-links'); const toggles = document.querySelectorAll('.tracklist-and-credits td button.recording-toggle'); for (const dd of dds) { dd.style.display = allExpanded ? 'none' : 'block'; } for (const toggle of toggles) { toggle.textContent = allExpanded ? '►' : '▼'; } allExpanded = !allExpanded; toggleAllBtn.textContent = allExpanded ? 'Collapse all streaming/download links' : 'Expand all streaming/download links'; }); toolbox.firstChild?.before(toggleAllBtn, ' | '); toolbox.firstChild?.before(status); status.textContent = 'Fetching recording links…'; const releaseData = await fetchReleaseRecordingData(); const trackRows = getTrackRows(); const tracks = releaseData?.media?.flatMap(m => m.tracks) || []; for (let i = 0; i < trackRows.length; i++) { const row = trackRows[i]; const recording = tracks[i]?.recording; const relations = recording?.relations || []; injectLinks(row, relations); } status.textContent = ''; }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址