Skip to content

Commit 43ed89e

Browse files
committed
feat: add ampwall and subvert importers.
1 parent 7fef7c3 commit 43ed89e

3 files changed

Lines changed: 308 additions & 0 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ Add a button on Bandcamp's album pages to open MusicBrainz release editor with p
4141
[![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/bandcamp_importer.user.js)
4242
[![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/bandcamp_importer.user.js)
4343

44+
## <a name="ampwall_importer"></a> Import Ampwall releases to MusicBrainz
45+
46+
Add a button on Ampwall's album pages to open MusicBrainz release editor with pre-filled data for the selected release
47+
48+
[![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/ampwall_importer.user.js)
49+
[![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/ampwall_importer.user.js)
50+
4451
## <a name="beatport_importer"></a> <img src="assets/icons/typescript.svg" alt="TypeScript" width="16" height="16"> Import Beatport releases to MusicBrainz
4552

4653
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-
139146
[![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/qobuz_importer.user.js)
140147
[![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/qobuz_importer.user.js)
141148

149+
## <a name="subvert_importer"></a> Import Subvert.fm releases to MusicBrainz
150+
151+
Add a button on Subvert.fm's album pages to open MusicBrainz release editor with pre-filled data for the selected release
152+
153+
[![Source](assets/buttons/button-source.svg)](https://github.com/murdos/musicbrainz-userscripts/blob/master/subvert_importer.user.js)
154+
[![Install](assets/buttons/button-install.svg)](https://raw.github.com/murdos/musicbrainz-userscripts/master/subvert_importer.user.js)
155+
142156
## <a name="takealot_importer"></a> Import Takealot releases to MusicBrainz
143157

144158
Add a button to import https://www.takealot.com/ releases to MusicBrainz via API

ampwall_importer.user.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// ==UserScript==
2+
// @name Import Ampwall releases to Musicbrainz
3+
// @description Add a button on Ampwall release pages to open MusicBrainz release editor with pre-filled data for the selected release
4+
// @version 2026.6.20
5+
// @license X11
6+
// @downloadURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/ampwall_importer.user.js
7+
// @updateURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/ampwall_importer.user.js
8+
// @namespace https://github.com/murdos/musicbrainz-userscripts
9+
// @match https://ampwall.com/a/*/album/*
10+
// @require lib/mbimport.js
11+
// @require lib/logger.js
12+
// @require lib/mbimportstyle.js
13+
// ==/UserScript==
14+
15+
function onLoad() {
16+
MBImportStyle();
17+
const release = retrieveReleaseInfo();
18+
insertLink(release, window.location);
19+
}
20+
21+
function getArtistCredit(json) {
22+
return MBImport.makeArtistCredits([json['byArtist']['name']]);
23+
}
24+
25+
function getFormat() {
26+
// We only support digital downloads right now.
27+
return 'Digital Media';
28+
}
29+
30+
function getPackaging() {
31+
// We only support digital downloads right now.
32+
return 'None';
33+
}
34+
35+
function getReleaseType() {
36+
// It appears that Ampwall treats all releases as albums so just
37+
// return nothing for now and let the user decide.
38+
return '';
39+
}
40+
41+
function getTitle(json) {
42+
return json['albumRelease'][0]['name'];
43+
}
44+
45+
function getLabels() {
46+
// TODO this will probably explode on releases with multiple labels.
47+
const labels = document.querySelector('div[data-testid=labels-links] a');
48+
if (labels === null) {
49+
return [];
50+
}
51+
return [
52+
{
53+
name: labels.textContent,
54+
catno: '',
55+
},
56+
];
57+
}
58+
59+
function getDiscs(json) {
60+
let tracks = [];
61+
json['track']['itemListElement'].forEach(function (track) {
62+
tracks.push({
63+
artist_credit: MBImport.makeArtistCredits([json['byArtist']['name']]),
64+
title: track['name'],
65+
duration: parseDuration(track['duration']),
66+
});
67+
});
68+
return [
69+
{
70+
tracks: tracks,
71+
format: getFormat(json),
72+
},
73+
];
74+
}
75+
76+
function getURLs(json) {
77+
return [
78+
{
79+
url: json['url'],
80+
link_type: MBImport.URL_TYPES.purchase_for_download,
81+
},
82+
];
83+
}
84+
85+
function parseDuration(s) {
86+
// Durations are returned in the format "PT0H0M7S". Strip out the
87+
// leading/trailing characters and convert the rest to colons.
88+
return s.replace(/(^PT)|(S$)/g, '').replace(/[HM]/g, ':');
89+
}
90+
91+
function retrieveReleaseInfo() {
92+
const json = JSON.parse(document.querySelector("script[type='application/ld+json']").textContent);
93+
const releaseDate = new Date(json['datePublished']);
94+
const release = {
95+
artist_credit: getArtistCredit(json),
96+
title: getTitle(json),
97+
year: releaseDate.getUTCFullYear(),
98+
month: releaseDate.getUTCMonth() + 1,
99+
day: releaseDate.getUTCDate(),
100+
country: 'XW',
101+
status: 'official',
102+
language: 'eng',
103+
script: 'Latn',
104+
type: getReleaseType(json),
105+
urls: getURLs(json),
106+
labels: getLabels(),
107+
discs: getDiscs(json),
108+
packaging: getPackaging(),
109+
format: getFormat(),
110+
};
111+
LOGGER.info('Parsed release: ', release);
112+
return release;
113+
}
114+
115+
// Insert button into page
116+
function insertLink(release) {
117+
const edit_note = MBImport.makeEditNote(window.location, 'Ampwall');
118+
const div = document.createElement('div');
119+
120+
const formButton = document.createElement('span');
121+
formButton.className = 'tab-title musicbrainz-import';
122+
formButton.innerHTML = MBImport.buildFormHTML(MBImport.buildFormParameters(release, edit_note));
123+
formButton.style.display = 'inline-block';
124+
125+
const searchButton = document.createElement('span');
126+
searchButton.className = 'tab-title musicbrainz-import';
127+
searchButton.innerHTML = MBImport.buildSearchButton(release);
128+
searchButton.style.display = 'inline-block';
129+
130+
div.appendChild(formButton);
131+
div.appendChild(searchButton);
132+
133+
// Set a slight delay to stop breaking React/Next.
134+
window.setTimeout(function () {
135+
const container = document.querySelector('div.d_flex.ai_center.flex-wrap_wrap.gap_space1');
136+
container.appendChild(formButton);
137+
container.appendChild(searchButton);
138+
}, 2000);
139+
}
140+
141+
if (document.readyState !== 'loading') {
142+
onLoad();
143+
} else {
144+
document.addEventListener('DOMContentLoaded', onLoad);
145+
}

subvert_importer.user.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// ==UserScript==
2+
// @name Import Subvert.fm releases to Musicbrainz
3+
// @description Add a button on Subvert.fm release pages to open MusicBrainz release editor with pre-filled data for the selected release
4+
// @version 2026.6.20
5+
// @license X11
6+
// @downloadURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/subvert_importer.user.js
7+
// @updateURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/subvert_importer.user.js
8+
// @namespace https://github.com/murdos/musicbrainz-userscripts
9+
// @match https://subvert.fm/*/*
10+
// @match https://www.subvert.fm/*/*
11+
// @require lib/mbimport.js
12+
// @require lib/logger.js
13+
// @require lib/mbimportstyle.js
14+
// ==/UserScript==
15+
16+
function onLoad() {
17+
MBImportStyle();
18+
const release = retrieveReleaseInfo();
19+
insertLink(release, window.location);
20+
}
21+
22+
function getArtistCredit(data) {
23+
let artists = [];
24+
data['release']['artists'].forEach(function (artist) {
25+
artists.push(artist['name']);
26+
});
27+
return MBImport.makeArtistCredits(artists);
28+
}
29+
30+
function getFormat() {
31+
// We only support digital downloads right now.
32+
return 'Digital Media';
33+
}
34+
35+
function getPackaging() {
36+
// We only support digital downloads right now.
37+
return 'None';
38+
}
39+
40+
function getReleaseType(data) {
41+
return data['release']['releaseType'];
42+
}
43+
44+
function getTitle(data) {
45+
return data['release']['name'];
46+
}
47+
48+
function getLabels(data) {
49+
let labels = [];
50+
data['release']['labelsOnReleases'].forEach(function (label) {
51+
labels.push({
52+
label: label['label']['name'],
53+
catno: label['catalogNumber'],
54+
});
55+
});
56+
return labels;
57+
}
58+
59+
function getDiscs(data) {
60+
let tracks = [];
61+
data['release']['tracks'].forEach(function (track) {
62+
let artists = [];
63+
track['track']['artists'].forEach(function (artist) {
64+
artists.push(artist['name']);
65+
});
66+
tracks.push({
67+
artist_credit: MBImport.makeArtistCredits(artists),
68+
title: track['track']['name'],
69+
duration: track['track']['duration'] * 1000,
70+
});
71+
});
72+
return [
73+
{
74+
tracks: tracks,
75+
format: getFormat(data),
76+
},
77+
];
78+
}
79+
80+
function getURLs(data) {
81+
let urls = [];
82+
if (data['release']['isDownloadAllowed']) {
83+
urls.push({
84+
url: window.location,
85+
link_type: MBImport.URL_TYPES.purchase_for_download,
86+
});
87+
}
88+
if (data['release']['isStreamingAllowed']) {
89+
urls.push({
90+
url: window.location,
91+
link_type: MBImport.URL_TYPES.stream_for_free,
92+
});
93+
}
94+
if (data['release']['priceCents'] > 0) {
95+
urls.push({
96+
url: window.location,
97+
link_type: MBImport.URL_TYPES.download_for_free,
98+
});
99+
}
100+
return urls;
101+
}
102+
103+
function retrieveReleaseInfo() {
104+
let json = JSON.parse(document.querySelector('script#__NEXT_DATA__').textContent);
105+
106+
// Skip non-release pages
107+
if (json['page'] !== '/[artistSlug]/[releaseSlug]') {
108+
return;
109+
}
110+
111+
const data = json['props']['pageProps'];
112+
const releaseDate = new Date(data['release']['releaseDate']);
113+
const release = {
114+
artist_credit: getArtistCredit(data),
115+
title: getTitle(data),
116+
year: releaseDate.getUTCFullYear(),
117+
month: releaseDate.getUTCMonth() + 1,
118+
day: releaseDate.getUTCDate(),
119+
country: 'XW',
120+
status: 'official',
121+
language: 'eng',
122+
script: 'Latn',
123+
type: getReleaseType(data),
124+
urls: getURLs(data),
125+
labels: getLabels(data),
126+
discs: getDiscs(data),
127+
packaging: getPackaging(data),
128+
format: getFormat(data),
129+
};
130+
LOGGER.info('Parsed release: ', release);
131+
return release;
132+
}
133+
134+
// Insert button into page
135+
function insertLink(release, url) {
136+
const edit_note = MBImport.makeEditNote(url, 'Subvert');
137+
const container = document.querySelector('div.releaseRailMetaWrapper.leftRail.hideMobile');
138+
const sourceNode = container.children[container.children.length - 3];
139+
const importContainer = sourceNode.cloneNode(true);
140+
importContainer.children[0].innerHTML = MBImport.buildFormHTML(MBImport.buildFormParameters(release, edit_note));
141+
importContainer.children[1].innerHTML = MBImport.buildSearchButton(release);
142+
sourceNode.after(importContainer);
143+
}
144+
145+
if (document.readyState !== 'loading') {
146+
onLoad();
147+
} else {
148+
document.addEventListener('DOMContentLoaded', onLoad);
149+
}

0 commit comments

Comments
 (0)