Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 102 additions & 3 deletions bandcamp_importer.user.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// ==UserScript==
// @name Import Bandcamp releases to MusicBrainz
// @description Add a button on Bandcamp's album pages to open MusicBrainz release editor with pre-filled data for the selected release
// @version 2026.7.7.1
// @version 2026.7.20.1
// @namespace http://userscripts.org/users/22504
// @downloadURL https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js
// @updateURL https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js
// @include /^https:\/\/[^/]+\/(?:(?:(?:album|track))\/[^/]+|music)$/

Check warning on line 8 in bandcamp_importer.user.js

View workflow job for this annotation

GitHub Actions / lint checks

Using @include is potentially unsafe and may be obsolete in Manifest v3. Please switch to @match
// @include /^https:\/\/([^.]+)\.bandcamp\.com((?:\/(?:(?:album|track))\/[^/]+|\/|\/music)?)$/

Check warning on line 9 in bandcamp_importer.user.js

View workflow job for this annotation

GitHub Actions / lint checks

Using @include is potentially unsafe and may be obsolete in Manifest v3. Please switch to @match
// @match https://*.bandcamp.com/*
// @match https://bandcamp.com/discover*
// @include /^https:\/\/bandcamp\.com\/private\//

Check warning on line 12 in bandcamp_importer.user.js

View workflow job for this annotation

GitHub Actions / lint checks

Using @include is potentially unsafe and may be obsolete in Manifest v3. Please switch to @match
// @include /^https:\/\/([^.]+)\.bandcamp\.com\/private\//

Check warning on line 13 in bandcamp_importer.user.js

View workflow job for this annotation

GitHub Actions / lint checks

Using @include is potentially unsafe and may be obsolete in Manifest v3. Please switch to @match
// @include /^https?:\/\/web\.archive\.org\/web\/\d+\/https?:\/\/[^/]+(?:\/(?:album|track)\/[^/]+\/?|\/music\/?|\/?)$/

Check warning on line 14 in bandcamp_importer.user.js

View workflow job for this annotation

GitHub Actions / lint checks

Using @include is potentially unsafe and may be obsolete in Manifest v3. Please switch to @match
// @match https://web.archive.org/web/*
// @require lib/mbimport.js?version=v2026.05.30.1
// @require lib/logger.js
Expand All @@ -21,6 +22,8 @@
// @run-at document-start
// ==/UserScript==

// @ts-nocheck

if (!unsafeWindow) {
/* oxlint-disable-next-line no-global-assign */
unsafeWindow = window;
Expand Down Expand Up @@ -502,6 +505,94 @@
return urls_data;
};

const isDiscoverPage = () =>
unsafeWindow.location.hostname === 'bandcamp.com' && /^\/discover(?:\/|$)/.test(unsafeWindow.location.pathname);

/**
* Return the canonical URL for an album displayed in a Bandcamp Discover card.
* Discover-specific query parameters are intentionally omitted because MusicBrainz
* stores relationships against the album URL itself.
*/
const getDiscoverAlbumUrl = link => {
try {
const url = new URL(link.href, unsafeWindow.location.origin);
const isBandcampUrl = url.hostname === 'bandcamp.com' || url.hostname.endsWith('.bandcamp.com');
if (!isBandcampUrl || !url.pathname.startsWith('/album/')) return null;
return `${url.protocol}//${url.hostname}${url.pathname.replace(/\/$/, '')}`;
} catch {
return null;
}
};

/**
* Add MusicBrainz release links to the current and subsequently loaded Discover cards.
*/
const initDiscoverPage = () => {
const mblinks = new MBLinks('BCI_MBLINKS_CACHE', 2);
const processedLinks = new WeakSet();
const albumLinkSelector = '.results-grid-item .meta p > a[href]';

const collectAlbumLinks = root => {
const albumLinks = [];
if (root instanceof Element && root.matches(albumLinkSelector)) albumLinks.push(root);
if (root.querySelectorAll) albumLinks.push(...root.querySelectorAll(albumLinkSelector));
return albumLinks;
};

const addReleaseLinks = roots => {
const urlsData = [];

roots.flatMap(collectAlbumLinks).forEach(albumLink => {
if (processedLinks.has(albumLink)) return;

const albumUrl = getDiscoverAlbumUrl(albumLink);
if (!albumUrl) return;
processedLinks.add(albumLink);

const seenReleaseMbids = new Set();
urlsData.push({
url: albumUrl,
mb_type: 'release',
key: `release:${albumUrl}`,
insert_func: link => {
const mbUrl = link.match(/href="([^"]+)"/)?.[1];
if (!mbUrl) return;
const mbid = mbUrl.slice(-36);
if (seenReleaseMbids.has(mbid) || !albumLink.isConnected) return;
seenReleaseMbids.add(mbid);
// Make our MB link render inline with the Bandcamp album link
Object.assign(albumLink.parentElement.style, {
alignItems: 'flex-start',
columnGap: '4px',
display: 'flex',
});
albumLink.insertAdjacentHTML('beforebegin', link);
albumLink.previousElementSibling.style.flexShrink = '0';
},
});
});

if (urlsData.length > 0) mblinks.searchAndDisplayMbLinks(urlsData);
};

addReleaseLinks([document]);

const observer = new MutationObserver(mutations => {
const changedRoots = mutations.flatMap(mutation =>
mutation.type === 'attributes'
? [mutation.target]
: Array.from(mutation.addedNodes).filter(node => node.nodeType === Node.ELEMENT_NODE),
);
if (changedRoots.length > 0) addReleaseLinks(changedRoots);
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['href'],
});
};

function init() {
/* keep the following line as first, it is required to skip
* pages which aren't actually a bandcamp page, since we support
Expand Down Expand Up @@ -793,8 +884,16 @@
}
}

const run = () => {
if (isDiscoverPage()) {
initDiscoverPage();
} else {
init();
}
};

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', run);
} else {
init();
run();
}
Loading