-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathmb_qol_inline_recording_tracks.user.js
153 lines (132 loc) · 5.76 KB
/
mb_qol_inline_recording_tracks.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// ==UserScript==
// @name MB: QoL: Inline all recording's tracks on releases
// @version 2024.7.25
// @description Display all tracks and releases on which a recording appears from the release page.
// @author ROpdebee
// @license MIT; https://opensource.org/licenses/MIT
// @namespace https://github.com/ROpdebee/mb-userscripts
// @downloadURL https://raw.github.com/ROpdebee/mb-userscripts/main/mb_qol_inline_recording_tracks.user.js
// @updateURL https://raw.github.com/ROpdebee/mb-userscripts/main/mb_qol_inline_recording_tracks.user.js
// @match *://*.musicbrainz.eu/release/*
// @match *://*.musicbrainz.org/release/*
// @exclude */release/*/*
// @exclude */release/add
// @run-at document-end
// @grant none
// ==/UserScript==
function splitChunks(arr, chunkSize) {
let chunks = [];
for (let i = 0; i < arr.length; i += chunkSize) {
chunks.push(arr.slice(i, i + chunkSize));
}
return chunks;
}
const queuedFetch = (() => {
let fetchQueue = [];
// This may make multiple concurrent requests if an old one is still pending
// while the new one is queued.
// FIXME: This runs continuously even though no fetches are queued.
setInterval(async () => {
let url, resolve;
try {
[url, resolve] = fetchQueue.shift();
} catch {
return;
}
try {
let resp = await fetch(url);
if (!resp.ok) fetchQueue.push([url, resolve]);
else resolve(resp);
} catch {
fetchQueue.push([url, resolve]);
}
}, 500);
return function(url) {
return new Promise((resolve) => fetchQueue.push([url, resolve]));
}
})();
async function loadRecordingInfo(rids) {
const query = rids.map((rid) => 'rid:' + rid).join(' OR ');
const url = location.origin + '/ws/2/recording?fmt=json&query=' + query;
let resp = await (await queuedFetch(url)).json();
let perRecId = {};
resp.recordings.forEach((rec) => perRecId[rec.id] = rec);
return perRecId;
}
function getTrackIndex(track, mediumPosition, mediumTrackCount) {
return `<a href="/track/${track.id}" title="track ${track.number} of ${mediumTrackCount}">#${mediumPosition}.${track.number}</a>`;
}
function getTrackIndices(media) {
return media.flatMap((medium) =>
medium.track.map((track) => getTrackIndex(track, medium.position, medium['track-count'])))
.join(', ');
}
function getReleaseName(release) {
return `<a href="/release/${release.id}" title="` + (release.date ? `released on ${release.date}` : 'unknown release date') + `">${release.title}</a>` + (release.disambiguation ? ` <span class="comment">(${release.disambiguation})</span>` : '');
}
function formatRow(release) {
return `${getReleaseName(release)} (${getTrackIndices(release.media)})`;
}
function insertRows(recordingTd, recordingInfo) {
let rowElements = recordingInfo.releases
.sort(compareReleases)
.map(formatRow)
.map(row => '<dl class="ars"><dt>appears on:</dt><dd>' + row + '</dd></dl>')
.join('\n');
rowElements = '<div class="ars ROpdebee_inline_tracks">' + rowElements + '</div>';
let existingArs = recordingTd.querySelector('div.ars');
if (existingArs) {
existingArs.insertAdjacentHTML('beforebegin', rowElements);
} else {
recordingTd.insertAdjacentHTML('beforeend', rowElements);
}
}
function compareReleases(a, b) {
if (releaseOrderingString(a) < releaseOrderingString(b)) {
return -1;
} else {
return 1;
}
}
function releaseOrderingString(release) {
return `[${release.date || ''}] ${release.title} ${release.disambiguation || ''} ${release.media[0].position.toString().padStart(4, '0')}.${release.media[0].track[0].number.toString().padStart(10, '0')}`;
}
function loadAndInsert() {
let recAnchors = document.querySelectorAll('table.medium td > a[href^="/recording/"], table.medium td > span > a[href^="/recording/"], table.medium td > span > span > a[href^="/recording/"]');
let todo = [...recAnchors]
.map((a) => [a.closest('td'), a.href.split('/recording/')[1]])
.filter(([td]) => !td.querySelector('div.ars.ROpdebee_inline_tracks'));
let chunks = splitChunks(todo, 20);
chunks.forEach(async (chunk) => {
let recInfo = await loadRecordingInfo(chunk.map(([, recId]) => recId));
chunk.forEach(([td, recId]) => insertRows(td, recInfo[recId]));
});
}
// MBS will fire a custom `mb-hydration` event whenever a React component gets
// hydrated. We need to wait for hydration to complete before modifying the
// component, React gets mad otherwise.
// Multiple `mb-hydration` events will fire on a release page, so make sure we're
// listening for the correct one.
function onReactHydrated(element, callback) {
var alreadyHydrated = Object.keys(element).some(function (propertyName) {
return propertyName.startsWith('_reactListening') && element[propertyName];
});
if (alreadyHydrated) {
callback();
} else if (window.__MB__.DBDefs.GIT_BRANCH === 'production' && window.__MB__.DBDefs.GIT_SHA === '923237cf73') {
// Current production version does not have this custom event yet.
// TODO: Remove this when prod is updated.
window.addEventListener('load', callback);
} else {
element.addEventListener('mb-hydration', callback);
}
}
onReactHydrated(document.querySelector('.tracklist-and-credits'), () => {
const button = document.createElement('button');
button.classList.add('btn-link');
button.type = 'button';
button.textContent = 'Display track info for recordings';
button.addEventListener('click', loadAndInsert);
document.querySelector('span#medium-toolbox')
.firstChild.before(button, ' | ');
});