// ==UserScript== // @name MusicBrainz: Expand/collapse release groups // @description See what's inside a release group without having to follow its URL. Also adds convenient edit links for it. // @namespace http://userscripts.org/users/266906 // @author Michael Wiencek // @version 2022.1.6.1 // @license GPL // @downloadURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/expand-collapse-release-groups.user.js // @updateURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/expand-collapse-release-groups.user.js // @grant none // @include *://musicbrainz.org/artist/* // @include *://musicbrainz.org/label/* // @include *://musicbrainz.org/release-group/* // @include *://musicbrainz.org/series/* // @include *://beta.musicbrainz.org/artist/* // @include *://beta.musicbrainz.org/label/* // @include *://beta.musicbrainz.org/release-group/* // @include *://beta.musicbrainz.org/series/* // @include *://test.musicbrainz.org/artist/* // @include *://test.musicbrainz.org/label/* // @include *://test.musicbrainz.org/release-group/* // @include *://test.musicbrainz.org/series/* // @match *://musicbrainz.org/artist/* // @match *://musicbrainz.org/label/* // @match *://musicbrainz.org/release-group/* // @match *://musicbrainz.org/series/* // @match *://beta.musicbrainz.org/artist/* // @match *://beta.musicbrainz.org/label/* // @match *://beta.musicbrainz.org/release-group/* // @match *://beta.musicbrainz.org/series/* // @match *://test.musicbrainz.org/artist/* // @match *://test.musicbrainz.org/label/* // @match *://test.musicbrainz.org/release-group/* // @match *://test.musicbrainz.org/series/* // @exclude *musicbrainz.org/label/*/* // @exclude *musicbrainz.org/release-group/*/* // @exclude *musicbrainz.org/series/*/* // ==/UserScript== const MBID_REGEX = /[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}/; const releasesOrReleaseGroups = document.querySelectorAll("#content table.tbl > tbody > tr > td a[href^='/release']"); for (const entity of releasesOrReleaseGroups) { const entityLink = entity.getAttribute('href'); if (entityLink.match(/\/release-group\//)) { inject_release_group_button(entity.parentNode); } else if (!entityLink.match(/\/cover-art/)) { // avoid injecting a second button for a release's cover art link inject_release_button(entity.parentNode); } } function inject_release_group_button(parent) { let mbid = parent.querySelector('a').href.match(MBID_REGEX), table = document.createElement('table'); table.style.marginTop = '1em'; table.style.marginLeft = '1em'; table.style.paddingLeft = '1em'; let button = create_button( `/ws/2/release?release-group=${mbid}&limit=100&inc=media&fmt=json`, function (toggled) { if (toggled) parent.appendChild(table); else parent.removeChild(table); }, function (json) { parse_release_group(json, mbid, table); }, function (status) { table.innerHTML = `Error loading release group (HTTP status ${status})`; } ); parent.insertBefore(button, parent.firstChild); } function inject_release_button(parent, _table_parent, _table, _mbid) { let mbid = _mbid || parent.querySelector('a').href.match(MBID_REGEX), table = _table || document.createElement('table'); let table_parent = _table_parent || parent; // fallback for pages where we do not inject the release groups table.style.paddingLeft = '1em'; let button = create_button( `/ws/2/release/${mbid}?inc=media+recordings+artist-credits&fmt=json`, function (toggled) { if (toggled) table_parent.appendChild(table); else table_parent.removeChild(table); }, function (json) { parse_release(json, table); }, function (status) { table.innerHTML = `Error loading release (HTTP status ${status})`; } ); parent.insertBefore(button, parent.childNodes[0]); } function create_button(url, dom_callback, success_callback, error_callback) { let button = document.createElement('span'), toggled = false; button.innerHTML = '▶'; button.style.cursor = 'pointer'; button.style.marginRight = '4px'; button.style.color = '#777'; button.addEventListener( 'mousedown', function () { toggled = !toggled; if (toggled) button.innerHTML = '▼'; else button.innerHTML = '▶'; dom_callback(toggled); }, false ); button.addEventListener( 'mousedown', function () { let this_event = arguments.callee; button.removeEventListener('mousedown', this_event, false); let req = new XMLHttpRequest(); req.onreadystatechange = function () { if (req.readyState != 4) return; if (req.status == 200 && req.responseText) { success_callback(JSON.parse(req.responseText)); } else { button.addEventListener( 'mousedown', function () { button.removeEventListener('mousedown', arguments.callee, false); button.addEventListener('mousedown', this_event, false); }, false ); error_callback(req.status); } }; req.open('GET', url, true); req.send(null); }, false ); return button; } function format_time(ms) { let ts = ms / 1000, s = Math.round(ts % 60); return `${Math.floor(ts / 60)}:${s >= 10 ? s : `0${s}`}`; } function parse_release_group(json, mbid, table) { let releases = json.releases; table.innerHTML = ''; for (const release of releases) { let media = {}, tracks = [], formats = []; for (const medium of release.media) { let format = medium.format, count = medium['track-count']; if (format) { format in media ? (media[format] += 1) : (media[format] = 1); } tracks.push(count); } for (let format in media) { let count = media[format]; if (count > 1) formats.push(`${count.toString()}×${format}`); else formats.push(format); } release.tracks = tracks.join(' + '); release.formats = formats.join(' + '); } releases.sort(function (a, b) { if (a.date < b.date) return -1; if (a.date > b.date) return 1; return 0; }); for (const release of releases) { let track_tr = document.createElement('tr'), track_td = document.createElement('td'), track_table = document.createElement('table'), format_td = document.createElement('td'), tr = document.createElement('tr'), td = document.createElement('td'), a = createLink(`/release/${release.id}`, release.title); track_td.colSpan = 6; track_table.style.width = '100%'; track_table.style.marginLeft = '1em'; track_tr.appendChild(track_td); inject_release_button(td, track_td, track_table, release.id); td.appendChild(a); if (release.disambiguation) { td.appendChild(document.createTextNode(` (${release.disambiguation})`)); } tr.appendChild(td); format_td.innerHTML = release.formats; tr.appendChild(format_td); let columns = [release.tracks, release.date || '', release.country || '', release.status || '']; for (const column of columns) { tr.appendChild(createElement('td', column)); } table.appendChild(tr); table.appendChild(track_tr); } let bottom_tr = document.createElement('tr'), bottom_td = document.createElement('td'); bottom_td.colSpan = 6; bottom_td.style.padding = '1em'; bottom_td.appendChild(createNewTabLink(`/release-group/${mbid}/edit`, 'edit')); bottom_td.appendChild(document.createTextNode(' | ')); bottom_td.appendChild(createNewTabLink(`/release/add?release-group=${mbid}`, 'add release')); bottom_td.appendChild(document.createTextNode(' | ')); bottom_td.appendChild(createNewTabLink(`/release-group/${mbid}/edits`, 'editing history')); bottom_tr.appendChild(bottom_td); table.appendChild(bottom_tr); } function parse_release(json, table) { let media = json.media; table.innerHTML = ''; for (let i = 0; i < media.length; i++) { let medium = media[i], format = medium.format ? `${medium.format} ${i + 1}` : `Medium ${i + 1}`; table.innerHTML += `${format}`; for (let j = 0; j < medium.tracks.length; j++) { let track = medium.tracks[j], recording = track.recording, disambiguation = recording.disambiguation ? ` (${recording.disambiguation})` : '', length = track.length ? format_time(track.length) : '?:??', artist_credit = track['artist-credit'] || track.recording['artist-credit'], tr = document.createElement('tr'); tr.appendChild(createElement('td', j + 1)); let title_td = createElement('td', disambiguation); title_td.insertBefore(createLink(`/recording/${recording.id}`, recording.title), title_td.firstChild); tr.appendChild(title_td); tr.appendChild(createElement('td', length)); let ac_td = document.createElement('td'); ac_td.appendChild(createAC(artist_credit)); tr.appendChild(ac_td); table.appendChild(tr); } } let bottom_tr = document.createElement('tr'), bottom_td = document.createElement('td'); bottom_td.colSpan = 4; bottom_td.style.padding = '1em'; bottom_td.appendChild(createNewTabLink(`/release/${json.id}/edit`, 'edit')); bottom_td.appendChild(document.createTextNode(' | ')); bottom_td.appendChild(createNewTabLink(`/release/${json.id}/edit-relationships`, 'edit relationships')); bottom_td.appendChild(document.createTextNode(' | ')); bottom_td.appendChild(createNewTabLink(`/release/${json.id}/edits`, 'editing history')); bottom_td.appendChild(document.createTextNode(' | ')); bottom_td.appendChild(createNewTabLink(`/release/${json.id}/add-cover-art`, 'add cover art')); bottom_tr.appendChild(bottom_td); table.appendChild(bottom_tr); } function createAC(artist_credit_array) { let span = document.createElement('span'); for (const credit of artist_credit_array) { let artist = credit.artist, link = createLink(`/artist/${artist.id}`, credit.name || artist.name); link.setAttribute('title', artist['sort-name']); span.appendChild(link); if (credit.joinphrase) span.appendChild(document.createTextNode(credit.joinphrase)); } return span; } function createElement(name, text) { let element = document.createElement(name); element.textContent = text; return element; } function createLink(href, text) { let element = createElement('a', text); element.href = href; return element; } function createNewTabLink(href, text) { let link = createLink(href, text); link.target = '_blank'; return link; }