Skip to content
Open
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
264 changes: 135 additions & 129 deletions deezer_importer.user.js
Original file line number Diff line number Diff line change
@@ -1,159 +1,165 @@
// ==UserScript==
// @name Import Deezer releases into MusicBrainz
// @name Import Deezer releases into MusicBrainz (API & UI Fix)
// @namespace https://github.com/murdos/musicbrainz-userscripts/
// @description One-click importing of releases from deezer.com into MusicBrainz
// @version 2025.9.28
// @description One-click importing of releases from deezer.com into MusicBrainz using the Deezer API
// @version 2026.02.03.2
// @downloadURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/deezer_importer.user.js
// @updateURL https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/deezer_importer.user.js
// @match https://www.deezer.com/*/album/*
// @match https://www.deezer.com/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
// @require lib/mbimport.js
// @require lib/logger.js
// @require lib/mbimportstyle.js
// @require https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/lib/mbimport.js
// @require https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/lib/logger.js
// @require https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/lib/mbimportstyle.js
// @icon https://raw.githubusercontent.com/murdos/musicbrainz-userscripts/master/assets/images/Musicbrainz_import_logo.png
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// ==/UserScript==

// prevent JQuery conflicts, see http://wiki.greasespot.net/@grant
this.$ = this.jQuery = jQuery.noConflict(true);

$(document).ready(function () {
let gmXHR;
(function () {
'use strict';

if (typeof GM_xmlhttpRequest != 'undefined') {
gmXHR = GM_xmlhttpRequest;
} else if (GM.xmlHttpRequest != 'undefined') {
gmXHR = GM.xmlHttpRequest;
} else {
LOGGER.error('Userscript requires GM_xmlHttpRequest or GM.xmlHttpRequest');
return;
// --- Helpers ---
function getAlbumId() {
const match = window.location.pathname.match(/\/album\/(\d+)/);
return match ? match[1] : null;
}

// allow 1 second for Deezer SPA to initialize
window.setTimeout(function () {
MBImportStyle();
let releaseUrl = window.location.href.replace(/\?.*$/, '').replace(/#.*$/, '');
let releaseId = releaseUrl.replace(/^https?:\/\/www\.deezer\.com\/[^/]+\/album\//i, '');
let deezerApiUrl = `https://api.deezer.com/album/${releaseId}`;

gmXHR({
method: 'GET',
url: deezerApiUrl,
onload: function (resp) {
try {
let release = parseDeezerRelease(releaseUrl, JSON.parse(resp.responseText));
insertLink(release, releaseUrl);
} catch (e) {
LOGGER.error('Failed to parse release: ', e);
function parseDuration(duration) {
return duration * 1000;
}

// --- Main Logic ---
async function fetchAndImport(albumId) {
const apiUrl = `https://api.deezer.com/album/${albumId}`;

GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
onload: function (response) {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
if (data.error) return;
renderButton(data);
}
},
onerror: function (resp) {
LOGGER.error('AJAX status:', resp.status);
LOGGER.error('AJAX response:', resp.responseText);
},
}
});
}, 1000);
});

function parseDeezerRelease(releaseUrl, data) {
let releaseDate = data.release_date.split('-');

let release = {
artist_credit: [],
title: data.title,
year: releaseDate[0],
month: releaseDate[1],
day: releaseDate[2],
packaging: 'None',
country: 'XW',
status: 'official',
language: 'eng',
script: 'Latn',
type: '',
urls: [],
labels: [],
discs: [],
};

$.each(data.contributors, function (index, artist) {
if (artist.role != 'Main') return true;

let ac = {
artist_name: artist.name,
joinphrase: index == data.contributors.length - 1 ? '' : ', ',
}

function renderButton(data) {
const release = {
title: data.title,
artist_credit: MBImport.makeArtistCredits([data.artist.name]),
type: data.record_type === 'single' ? 'single' : 'album',
status: 'official',
language: 'eng',
script: 'Latn',
barcode: data.upc,
labels: data.label ? [{ name: data.label, catno: '' }] : [],
urls: [{ url: data.link, link_type: MBImport.URL_TYPES.stream_for_free }],
discs: []
};

if (artist.name == 'Various Artists') {
ac = MBImport.specialArtist('various_artists', ac);
if (data.release_date) {
const dateParts = data.release_date.split('-');
release.year = dateParts[0];
release.month = dateParts[1];
release.day = dateParts[2];
}

release.artist_credit.push(ac);
});
const tracks = data.tracks.data;
const discs = {};

let disc = {
format: 'Digital Media',
title: '',
tracks: [],
};

$.each(data.tracks.data, function (index, track) {
let t = {
number: index + 1,
title: track.title_short,
duration: track.duration * 1000,
artist_credit: [],
};
tracks.forEach(track => {
const discNum = track.disk_number || 1;
if (!discs[discNum]) {
discs[discNum] = { tracks: [], format: 'Digital Media' };
}

// ignore pointless "(Original Mix)" in title version
if (track.title_version && !track.title_version.match(/^\s*\(Original Mix\)\s*$/i)) {
t.title += ` ${track.title_version}`;
}
const trackObj = {
title: track.title,
duration: parseDuration(track.duration),
artist_credit: []
};

t.artist_credit.push({ artist_name: track.artist.name });
if (track.artist.name !== data.artist.name && data.artist.name === "Various Artists") {
trackObj.artist_credit = MBImport.makeArtistCredits([track.artist.name]);
}

disc.tracks.push(t);
});
if (trackObj.title.match(/\(Original Mix\)/i)) {
trackObj.title = trackObj.title.replace(/\s*\(Original Mix\)\s*/i, "");
}

discs[discNum].tracks.push(trackObj);
});

release.discs.push(disc);
Object.keys(discs).sort().forEach(k => {
release.discs.push(discs[k]);
});

release.urls.push({
link_type: MBImport.URL_TYPES.stream_for_free,
url: releaseUrl,
});
release.labels.push({ name: data.label });
release.type = data.record_type;
release.barcode = data.upc;

return release;
}

function waitForEl(selector, callback) {
if (jQuery(selector).length) {
callback();
} else {
setTimeout(function () {
waitForEl(selector, callback);
}, 100);
insertLink(release, data.link);
}
}

function insertLink(release, release_url) {
let editNote = MBImport.makeEditNote(release_url, 'Deezer');
let parameters = MBImport.buildFormParameters(release, editNote);

let mbUI = $(
`<div class="toolbar-item">
${MBImport.buildFormHTML(parameters)}
</div><div class="toolbar-item">
${MBImport.buildSearchButton(release)}
</div>`,
).hide();
waitForEl('[data-testid="toolbar"]', function () {
$('[data-testid="toolbar"]').css({

function insertLink(release, releaseUrl) {
$('#mb_import_container').remove();

const editNote = MBImport.makeEditNote(releaseUrl, 'Deezer');
const parameters = MBImport.buildFormParameters(release, editNote);

const mbUI = $(`
<div id="mb_import_container" style="display:inline-flex; align-items:center; margin-right:8px;">
${MBImport.buildFormHTML(parameters)}
${MBImport.buildSearchButton(release)}
</div>
`);

// Apply Native Deezer Styling
// We add the class "tempo-btn" and "tempo-btn-hollow-neutral" which are standard Deezer buttons
const $btns = mbUI.find('button');
$btns.addClass('tempo-btn tempo-btn-hollow-neutral tempo-btn-s');

// Custom tweaks to ensure the logo fits nicely inside the rounded Deezer button
$btns.css({
'padding': '0 12px',
'height': '32px', // Matches Deezer small button height
'min-height': '32px',
'display': 'flex',
'align-items': 'center',
'justify-content': 'center',
'border-radius': '500px' // Native pill shape
});
$('[data-testid="toolbar"]').append(mbUI);
mbUI.show();

// Add a text label or icon adjustment if needed
$btns.eq(0).html('<span style="font-weight:700; font-size:12px;">Import to MB</span>');
$btns.eq(1).html('<span style="font-weight:700; font-size:12px;">Search MB</span>');

// Inject into the action bar
const target = $('.tempo-topbar-actions').first();

if (target.length) {
target.prepend(mbUI);
} else {
$('body').prepend(mbUI.css({
'position': 'fixed', 'top': '80px', 'right': '20px', 'z-index': '9999', 'background': 'white', 'padding': '5px', 'border-radius': '5px'
}));
}
}

// --- Init ---
let lastUrl = location.href;
function checkUrl() {
if (location.href !== lastUrl) {
lastUrl = location.href;
init();
}
}
function init() {
const albumId = getAlbumId();
if (albumId) setTimeout(() => fetchAndImport(albumId), 1000);
}
$(document).ready(function() {
init();
setInterval(checkUrl, 1000);
});
}

})();