From 43ed89e1d69cf79bab26ada6927749ae33947bee Mon Sep 17 00:00:00 2001 From: David Logie Date: Sat, 20 Jun 2026 13:03:03 +0100 Subject: [PATCH] feat: add ampwall and subvert importers. --- README.md | 14 ++++ ampwall_importer.user.js | 145 +++++++++++++++++++++++++++++++++++++ subvert_importer.user.js | 149 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 ampwall_importer.user.js create mode 100644 subvert_importer.user.js diff --git a/README.md b/README.md index 5d92e24..2ae006a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,13 @@ Add a button on Bandcamp's album pages to open MusicBrainz release editor with p [![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/bandcamp_importer.user.js) [![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js) +## Import Ampwall releases to MusicBrainz + +Add a button on Ampwall's album pages to open MusicBrainz release editor with pre-filled data for the selected release + +[![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/ampwall_importer.user.js) +[![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/ampwall_importer.user.js) + ## TypeScript Import Beatport releases to MusicBrainz One-click importing of releases from beatport.com/release pages into MusicBrainz @@ -139,6 +146,13 @@ Add a button on Qobuz's album pages to open MusicBrainz release editor with pre- [![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/qobuz_importer.user.js) [![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/qobuz_importer.user.js) +## Import Subvert.fm releases to MusicBrainz + +Add a button on Subvert.fm's album pages to open MusicBrainz release editor with pre-filled data for the selected release + +[![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/subvert_importer.user.js) +[![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/subvert_importer.user.js) + ## Import Takealot releases to MusicBrainz Add a button to import https://www.takealot.com/ releases to MusicBrainz via API diff --git a/ampwall_importer.user.js b/ampwall_importer.user.js new file mode 100644 index 0000000..8ba8266 --- /dev/null +++ b/ampwall_importer.user.js @@ -0,0 +1,145 @@ +// ==UserScript== +// @name Import Ampwall releases to Musicbrainz +// @description Add a button on Ampwall release pages to open MusicBrainz release editor with pre-filled data for the selected release +// @version 2026.6.20 +// @license X11 +// @downloadURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/ampwall_importer.user.js +// @updateURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/ampwall_importer.user.js +// @namespace https://github.com/murdos/musicbrainz-userscripts +// @match https://ampwall.com/a/*/album/* +// @require lib/mbimport.js +// @require lib/logger.js +// @require lib/mbimportstyle.js +// ==/UserScript== + +function onLoad() { + MBImportStyle(); + const release = retrieveReleaseInfo(); + insertLink(release, window.location); +} + +function getArtistCredit(json) { + return MBImport.makeArtistCredits([json['byArtist']['name']]); +} + +function getFormat() { + // We only support digital downloads right now. + return 'Digital Media'; +} + +function getPackaging() { + // We only support digital downloads right now. + return 'None'; +} + +function getReleaseType() { + // It appears that Ampwall treats all releases as albums so just + // return nothing for now and let the user decide. + return ''; +} + +function getTitle(json) { + return json['albumRelease'][0]['name']; +} + +function getLabels() { + // TODO this will probably explode on releases with multiple labels. + const labels = document.querySelector('div[data-testid=labels-links] a'); + if (labels === null) { + return []; + } + return [ + { + name: labels.textContent, + catno: '', + }, + ]; +} + +function getDiscs(json) { + let tracks = []; + json['track']['itemListElement'].forEach(function (track) { + tracks.push({ + artist_credit: MBImport.makeArtistCredits([json['byArtist']['name']]), + title: track['name'], + duration: parseDuration(track['duration']), + }); + }); + return [ + { + tracks: tracks, + format: getFormat(json), + }, + ]; +} + +function getURLs(json) { + return [ + { + url: json['url'], + link_type: MBImport.URL_TYPES.purchase_for_download, + }, + ]; +} + +function parseDuration(s) { + // Durations are returned in the format "PT0H0M7S". Strip out the + // leading/trailing characters and convert the rest to colons. + return s.replace(/(^PT)|(S$)/g, '').replace(/[HM]/g, ':'); +} + +function retrieveReleaseInfo() { + const json = JSON.parse(document.querySelector("script[type='application/ld+json']").textContent); + const releaseDate = new Date(json['datePublished']); + const release = { + artist_credit: getArtistCredit(json), + title: getTitle(json), + year: releaseDate.getUTCFullYear(), + month: releaseDate.getUTCMonth() + 1, + day: releaseDate.getUTCDate(), + country: 'XW', + status: 'official', + language: 'eng', + script: 'Latn', + type: getReleaseType(json), + urls: getURLs(json), + labels: getLabels(), + discs: getDiscs(json), + packaging: getPackaging(), + format: getFormat(), + }; + LOGGER.info('Parsed release: ', release); + return release; +} + +// Insert button into page +function insertLink(release) { + const edit_note = MBImport.makeEditNote(window.location, 'Ampwall'); + const div = document.createElement('div'); + + const formButton = document.createElement('span'); + formButton.className = 'tab-title musicbrainz-import'; + formButton.innerHTML = MBImport.buildFormHTML(MBImport.buildFormParameters(release, edit_note)); + formButton.style.display = 'inline-block'; + + const searchButton = document.createElement('span'); + searchButton.className = 'tab-title musicbrainz-import'; + searchButton.innerHTML = MBImport.buildSearchButton(release); + searchButton.style.display = 'inline-block'; + + div.appendChild(formButton); + div.appendChild(searchButton); + + // Set a slight delay to stop breaking React/Next. + window.setTimeout(function () { + const container = document.querySelector('div.d_flex.ai_center.flex-wrap_wrap.gap_space1'); + container.appendChild(formButton); + container.appendChild(searchButton); + }, 2000); +} + +if (document.readyState !== 'loading') { + onLoad(); +} else { + document.addEventListener('DOMContentLoaded', onLoad); +} diff --git a/subvert_importer.user.js b/subvert_importer.user.js new file mode 100644 index 0000000..ac0f997 --- /dev/null +++ b/subvert_importer.user.js @@ -0,0 +1,149 @@ +// ==UserScript== +// @name Import Subvert.fm releases to Musicbrainz +// @description Add a button on Subvert.fm release pages to open MusicBrainz release editor with pre-filled data for the selected release +// @version 2026.6.20 +// @license X11 +// @downloadURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/subvert_importer.user.js +// @updateURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/subvert_importer.user.js +// @namespace https://github.com/murdos/musicbrainz-userscripts +// @match https://subvert.fm/*/* +// @match https://www.subvert.fm/*/* +// @require lib/mbimport.js +// @require lib/logger.js +// @require lib/mbimportstyle.js +// ==/UserScript== + +function onLoad() { + MBImportStyle(); + const release = retrieveReleaseInfo(); + insertLink(release, window.location); +} + +function getArtistCredit(data) { + let artists = []; + data['release']['artists'].forEach(function (artist) { + artists.push(artist['name']); + }); + return MBImport.makeArtistCredits(artists); +} + +function getFormat() { + // We only support digital downloads right now. + return 'Digital Media'; +} + +function getPackaging() { + // We only support digital downloads right now. + return 'None'; +} + +function getReleaseType(data) { + return data['release']['releaseType']; +} + +function getTitle(data) { + return data['release']['name']; +} + +function getLabels(data) { + let labels = []; + data['release']['labelsOnReleases'].forEach(function (label) { + labels.push({ + label: label['label']['name'], + catno: label['catalogNumber'], + }); + }); + return labels; +} + +function getDiscs(data) { + let tracks = []; + data['release']['tracks'].forEach(function (track) { + let artists = []; + track['track']['artists'].forEach(function (artist) { + artists.push(artist['name']); + }); + tracks.push({ + artist_credit: MBImport.makeArtistCredits(artists), + title: track['track']['name'], + duration: track['track']['duration'] * 1000, + }); + }); + return [ + { + tracks: tracks, + format: getFormat(data), + }, + ]; +} + +function getURLs(data) { + let urls = []; + if (data['release']['isDownloadAllowed']) { + urls.push({ + url: window.location, + link_type: MBImport.URL_TYPES.purchase_for_download, + }); + } + if (data['release']['isStreamingAllowed']) { + urls.push({ + url: window.location, + link_type: MBImport.URL_TYPES.stream_for_free, + }); + } + if (data['release']['priceCents'] > 0) { + urls.push({ + url: window.location, + link_type: MBImport.URL_TYPES.download_for_free, + }); + } + return urls; +} + +function retrieveReleaseInfo() { + let json = JSON.parse(document.querySelector('script#__NEXT_DATA__').textContent); + + // Skip non-release pages + if (json['page'] !== '/[artistSlug]/[releaseSlug]') { + return; + } + + const data = json['props']['pageProps']; + const releaseDate = new Date(data['release']['releaseDate']); + const release = { + artist_credit: getArtistCredit(data), + title: getTitle(data), + year: releaseDate.getUTCFullYear(), + month: releaseDate.getUTCMonth() + 1, + day: releaseDate.getUTCDate(), + country: 'XW', + status: 'official', + language: 'eng', + script: 'Latn', + type: getReleaseType(data), + urls: getURLs(data), + labels: getLabels(data), + discs: getDiscs(data), + packaging: getPackaging(data), + format: getFormat(data), + }; + LOGGER.info('Parsed release: ', release); + return release; +} + +// Insert button into page +function insertLink(release, url) { + const edit_note = MBImport.makeEditNote(url, 'Subvert'); + const container = document.querySelector('div.releaseRailMetaWrapper.leftRail.hideMobile'); + const sourceNode = container.children[container.children.length - 3]; + const importContainer = sourceNode.cloneNode(true); + importContainer.children[0].innerHTML = MBImport.buildFormHTML(MBImport.buildFormParameters(release, edit_note)); + importContainer.children[1].innerHTML = MBImport.buildSearchButton(release); + sourceNode.after(importContainer); +} + +if (document.readyState !== 'loading') { + onLoad(); +} else { + document.addEventListener('DOMContentLoaded', onLoad); +}