Skip to content

Commit 7ba1cf3

Browse files
authored
feat(bandcamp_imorter): add MB linking on Bandcamp's Discovery page (#1004)
<img width="1322" height="1159" alt="image" src="https://github.com/user-attachments/assets/3d67c930-485f-4138-903e-06e0c34dae28" />
1 parent 7e03d1f commit 7ba1cf3

1 file changed

Lines changed: 102 additions & 3 deletions

File tree

bandcamp_importer.user.js

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// ==UserScript==
22
// @name Import Bandcamp releases to MusicBrainz
33
// @description Add a button on Bandcamp's album pages to open MusicBrainz release editor with pre-filled data for the selected release
4-
// @version 2026.7.7.1
4+
// @version 2026.7.20.1
55
// @namespace http://userscripts.org/users/22504
66
// @downloadURL https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js
77
// @updateURL https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js
88
// @include /^https:\/\/[^/]+\/(?:(?:(?:album|track))\/[^/]+|music)$/
99
// @include /^https:\/\/([^.]+)\.bandcamp\.com((?:\/(?:(?:album|track))\/[^/]+|\/|\/music)?)$/
1010
// @match https://*.bandcamp.com/*
11+
// @match https://bandcamp.com/discover*
1112
// @include /^https:\/\/bandcamp\.com\/private\//
1213
// @include /^https:\/\/([^.]+)\.bandcamp\.com\/private\//
1314
// @include /^https?:\/\/web\.archive\.org\/web\/\d+\/https?:\/\/[^/]+(?:\/(?:album|track)\/[^/]+\/?|\/music\/?|\/?)$/
@@ -21,6 +22,8 @@
2122
// @run-at document-start
2223
// ==/UserScript==
2324

25+
// @ts-nocheck
26+
2427
if (!unsafeWindow) {
2528
/* oxlint-disable-next-line no-global-assign */
2629
unsafeWindow = window;
@@ -502,6 +505,94 @@ const collectDiscographyReleaseLinks = ({ linksMatcher, hostnames, insertionLoca
502505
return urls_data;
503506
};
504507

508+
const isDiscoverPage = () =>
509+
unsafeWindow.location.hostname === 'bandcamp.com' && /^\/discover(?:\/|$)/.test(unsafeWindow.location.pathname);
510+
511+
/**
512+
* Return the canonical URL for an album displayed in a Bandcamp Discover card.
513+
* Discover-specific query parameters are intentionally omitted because MusicBrainz
514+
* stores relationships against the album URL itself.
515+
*/
516+
const getDiscoverAlbumUrl = link => {
517+
try {
518+
const url = new URL(link.href, unsafeWindow.location.origin);
519+
const isBandcampUrl = url.hostname === 'bandcamp.com' || url.hostname.endsWith('.bandcamp.com');
520+
if (!isBandcampUrl || !url.pathname.startsWith('/album/')) return null;
521+
return `${url.protocol}//${url.hostname}${url.pathname.replace(/\/$/, '')}`;
522+
} catch {
523+
return null;
524+
}
525+
};
526+
527+
/**
528+
* Add MusicBrainz release links to the current and subsequently loaded Discover cards.
529+
*/
530+
const initDiscoverPage = () => {
531+
const mblinks = new MBLinks('BCI_MBLINKS_CACHE', 2);
532+
const processedLinks = new WeakSet();
533+
const albumLinkSelector = '.results-grid-item .meta p > a[href]';
534+
535+
const collectAlbumLinks = root => {
536+
const albumLinks = [];
537+
if (root instanceof Element && root.matches(albumLinkSelector)) albumLinks.push(root);
538+
if (root.querySelectorAll) albumLinks.push(...root.querySelectorAll(albumLinkSelector));
539+
return albumLinks;
540+
};
541+
542+
const addReleaseLinks = roots => {
543+
const urlsData = [];
544+
545+
roots.flatMap(collectAlbumLinks).forEach(albumLink => {
546+
if (processedLinks.has(albumLink)) return;
547+
548+
const albumUrl = getDiscoverAlbumUrl(albumLink);
549+
if (!albumUrl) return;
550+
processedLinks.add(albumLink);
551+
552+
const seenReleaseMbids = new Set();
553+
urlsData.push({
554+
url: albumUrl,
555+
mb_type: 'release',
556+
key: `release:${albumUrl}`,
557+
insert_func: link => {
558+
const mbUrl = link.match(/href="([^"]+)"/)?.[1];
559+
if (!mbUrl) return;
560+
const mbid = mbUrl.slice(-36);
561+
if (seenReleaseMbids.has(mbid) || !albumLink.isConnected) return;
562+
seenReleaseMbids.add(mbid);
563+
// Make our MB link render inline with the Bandcamp album link
564+
Object.assign(albumLink.parentElement.style, {
565+
alignItems: 'flex-start',
566+
columnGap: '4px',
567+
display: 'flex',
568+
});
569+
albumLink.insertAdjacentHTML('beforebegin', link);
570+
albumLink.previousElementSibling.style.flexShrink = '0';
571+
},
572+
});
573+
});
574+
575+
if (urlsData.length > 0) mblinks.searchAndDisplayMbLinks(urlsData);
576+
};
577+
578+
addReleaseLinks([document]);
579+
580+
const observer = new MutationObserver(mutations => {
581+
const changedRoots = mutations.flatMap(mutation =>
582+
mutation.type === 'attributes'
583+
? [mutation.target]
584+
: Array.from(mutation.addedNodes).filter(node => node.nodeType === Node.ELEMENT_NODE),
585+
);
586+
if (changedRoots.length > 0) addReleaseLinks(changedRoots);
587+
});
588+
observer.observe(document.body, {
589+
childList: true,
590+
subtree: true,
591+
attributes: true,
592+
attributeFilter: ['href'],
593+
});
594+
};
595+
505596
function init() {
506597
/* keep the following line as first, it is required to skip
507598
* pages which aren't actually a bandcamp page, since we support
@@ -793,8 +884,16 @@ function init() {
793884
}
794885
}
795886

887+
const run = () => {
888+
if (isDiscoverPage()) {
889+
initDiscoverPage();
890+
} else {
891+
init();
892+
}
893+
};
894+
796895
if (document.readyState === 'loading') {
797-
document.addEventListener('DOMContentLoaded', init);
896+
document.addEventListener('DOMContentLoaded', run);
798897
} else {
799-
init();
898+
run();
800899
}

0 commit comments

Comments
 (0)