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
77 changes: 56 additions & 21 deletions bandcamp_importer.user.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// ==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.06.06.3
// @version 2026.06.06.4
// @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
// @include /^https:\/\/bandcamp\.com\/private\//

Check warning on line 10 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 11 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 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
// @require lib/mbimport.js?version=v2026.05.30.1
// @require lib/logger.js
// @require lib/mblinks.js?version=v2026.05.31.1
Expand Down Expand Up @@ -92,6 +92,23 @@
return result;
};

/**
* Resolve hostnames for discography page lookups from the page location and TralbumData.url.
*/
const resolveDiscographyHostnames = tralbumUrl => {
const pageHostname = normalizeBandcampUrl(window.location.origin);
const tralbumHostname = tralbumUrl ? normalizeBandcampUrl(tralbumUrl.replace(/\/music\/?$/, '').replace(/\/indexpage\/?$/, '')) : '';
const hostnames = [pageHostname];
if (tralbumHostname && normalizeUrlForComparison(tralbumHostname) !== normalizeUrlForComparison(pageHostname)) {
LOGGER.info('TralbumData discography hostname differs from page hostname; looking up both', {
pageHostname,
tralbumHostname,
});
hostnames.push(tralbumHostname);
}
return hostnames;
};

const BandcampImport = {
// Analyze Bandcamp data and return a release object
retrieveReleaseInfo: function (isPrivateStream) {
Expand Down Expand Up @@ -427,33 +444,51 @@
* Collects discography release link data from elements matching the given selector.
* @param {Object} options
* @param {string} options.linksMatcher - CSS selector for release/track links
* @param {string} options.hostname - Base hostname for constructing full URLs
* @param {string[]} options.hostnames - Base hostnames for constructing full URLs
* @param {string} [options.insertionLocationMatcher] - Optional selector for insertion point (e.g. 'p.title' for music format)
* @returns {Array} Array of url_data objects for mblinks.searchAndDisplayMbLinks
*/
const collectDiscographyReleaseLinks = ({ linksMatcher, hostname, insertionLocationMatcher }) => {
const collectDiscographyReleaseLinks = ({ linksMatcher, hostnames, insertionLocationMatcher }) => {
const urls_data = [];
document.querySelectorAll(linksMatcher).forEach(linkEl => {
const bandcampReleaseUrl = linkEl.getAttribute('href');
const pathName = getPathName(bandcampReleaseUrl);

if (pathName && pathName.match(/^(\/album|\/track)/)) {
const isRelative = bandcampReleaseUrl.startsWith('/');
const linkHostname = isRelative ? hostname : getHostname(bandcampReleaseUrl);
const full_url = linkHostname + pathName;

urls_data.push({
url: full_url,
mb_type: 'release',
insert_func: function (link) {
const target = insertionLocationMatcher ? linkEl.querySelector(insertionLocationMatcher) : linkEl;
if (target) {
target.insertAdjacentHTML('afterbegin', link);
} else {
linkEl.insertAdjacentHTML('afterbegin', link);
const lookupUrls = [];
if (bandcampReleaseUrl.startsWith('/')) {
hostnames.forEach(hostname => {
const full_url = hostname + pathName;
if (!lookupUrls.some(url => normalizeUrlForComparison(url) === normalizeUrlForComparison(full_url))) {
lookupUrls.push(full_url);
}
},
key: `release:${full_url}`,
});
} else {
lookupUrls.push(getHostname(bandcampReleaseUrl) + pathName);
}

const seenReleaseMbids = new Set();
const insertReleaseLink = link => {
const mb_url = link.match(/href="([^"]+)"/)?.[1];
if (!mb_url) return;
const mbid = mb_url.slice(-36);
if (seenReleaseMbids.has(mbid)) return;
seenReleaseMbids.add(mbid);
const target = insertionLocationMatcher ? linkEl.querySelector(insertionLocationMatcher) : linkEl;
if (target) {
target.insertAdjacentHTML('afterbegin', link);
} else {
linkEl.insertAdjacentHTML('afterbegin', link);
}
};

lookupUrls.forEach(full_url => {
urls_data.push({
url: full_url,
mb_type: 'release',
insert_func: insertReleaseLink,
key: `release:${full_url}`,
});
});
}
});
Expand Down Expand Up @@ -488,17 +523,17 @@
: unsafeWindow.TralbumData.url.match(/\/indexpage\/?$/)
? 'indexpage'
: null;
const hostname = unsafeWindow.TralbumData.url.replace('/music', '').replace('/indexpage', '');
const hostnames = resolveDiscographyHostnames(unsafeWindow.TralbumData.url);
const releaseLinksMatcher = discographyFormat === 'music' ? 'ol#music-grid > li > a' : 'span.indexpage_list div.ipCellLabel1 a';
const insertionLocationMatcher = discographyFormat === 'music' ? 'p.title' : undefined;

const urls_data = [
...collectDiscographyReleaseLinks({
linksMatcher: 'ol.featured-grid > li.featured-item > a',
hostname,
hostnames,
insertionLocationMatcher,
}),
...collectDiscographyReleaseLinks({ linksMatcher: releaseLinksMatcher, hostname, insertionLocationMatcher }),
...collectDiscographyReleaseLinks({ linksMatcher: releaseLinksMatcher, hostnames, insertionLocationMatcher }),
];

if (urls_data.length > 0) {
Expand Down
Loading