|
1 | 1 | // ==UserScript== |
2 | 2 | // @name Import Bandcamp releases to MusicBrainz |
3 | 3 | // @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 |
5 | 5 | // @namespace http://userscripts.org/users/22504 |
6 | 6 | // @downloadURL https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js |
7 | 7 | // @updateURL https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js |
8 | 8 | // @include /^https:\/\/[^/]+\/(?:(?:(?:album|track))\/[^/]+|music)$/ |
9 | 9 | // @include /^https:\/\/([^.]+)\.bandcamp\.com((?:\/(?:(?:album|track))\/[^/]+|\/|\/music)?)$/ |
10 | 10 | // @match https://*.bandcamp.com/* |
| 11 | +// @match https://bandcamp.com/discover* |
11 | 12 | // @include /^https:\/\/bandcamp\.com\/private\// |
12 | 13 | // @include /^https:\/\/([^.]+)\.bandcamp\.com\/private\// |
13 | 14 | // @include /^https?:\/\/web\.archive\.org\/web\/\d+\/https?:\/\/[^/]+(?:\/(?:album|track)\/[^/]+\/?|\/music\/?|\/?)$/ |
|
21 | 22 | // @run-at document-start |
22 | 23 | // ==/UserScript== |
23 | 24 |
|
| 25 | +// @ts-nocheck |
| 26 | + |
24 | 27 | if (!unsafeWindow) { |
25 | 28 | /* oxlint-disable-next-line no-global-assign */ |
26 | 29 | unsafeWindow = window; |
@@ -502,6 +505,94 @@ const collectDiscographyReleaseLinks = ({ linksMatcher, hostnames, insertionLoca |
502 | 505 | return urls_data; |
503 | 506 | }; |
504 | 507 |
|
| 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 | + |
505 | 596 | function init() { |
506 | 597 | /* keep the following line as first, it is required to skip |
507 | 598 | * pages which aren't actually a bandcamp page, since we support |
@@ -793,8 +884,16 @@ function init() { |
793 | 884 | } |
794 | 885 | } |
795 | 886 |
|
| 887 | +const run = () => { |
| 888 | + if (isDiscoverPage()) { |
| 889 | + initDiscoverPage(); |
| 890 | + } else { |
| 891 | + init(); |
| 892 | + } |
| 893 | +}; |
| 894 | + |
796 | 895 | if (document.readyState === 'loading') { |
797 | | - document.addEventListener('DOMContentLoaded', init); |
| 896 | + document.addEventListener('DOMContentLoaded', run); |
798 | 897 | } else { |
799 | | - init(); |
| 898 | + run(); |
800 | 899 | } |
0 commit comments