426 lines
17 KiB
JavaScript
426 lines
17 KiB
JavaScript
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
// MusicBrainz Import helper functions
|
||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
|
|
||
|
/*
|
||
|
* How to use this module?
|
||
|
*
|
||
|
* - First build a release object (see expected format below) that you'll fill in from source of data
|
||
|
* - Call as follows, e.g.:
|
||
|
* var parameters = MBImport.buildFormParameters(parsedRelease, optionalEditNote);
|
||
|
* - Then build the HTML that you'll inject into source site page:
|
||
|
* var formHtml = MBImport.buildFormHTML(parameters);
|
||
|
* - Addinionally, you can inject a search link to verify that the release is not already known by MusicBrainz:
|
||
|
* var linkHtml = MBImport.buildSearchLink(parsedRelease);
|
||
|
*
|
||
|
* Expected format of release object:
|
||
|
*
|
||
|
* release = {
|
||
|
* title,
|
||
|
* artist_credit,
|
||
|
* type,
|
||
|
* status,
|
||
|
* secondary_types,
|
||
|
* language,
|
||
|
* script,
|
||
|
* packaging,
|
||
|
* country,
|
||
|
* year,
|
||
|
* month,
|
||
|
* day,
|
||
|
* labels = [ { name, mbid, catno }, ... ],
|
||
|
* barcode,
|
||
|
* comment,
|
||
|
* annotation,
|
||
|
* urls = [ {url, link_type }, ... ],
|
||
|
* discs = [
|
||
|
* {
|
||
|
* title,
|
||
|
* format,
|
||
|
* tracks = [
|
||
|
* { number, title, duration, artist_credit },
|
||
|
* ...
|
||
|
* ]
|
||
|
* },
|
||
|
* ...
|
||
|
* ],
|
||
|
* }
|
||
|
*
|
||
|
* where 'artist_credit' has the following format:
|
||
|
*
|
||
|
* artist_credit = [
|
||
|
* {
|
||
|
* credited_name,
|
||
|
* artist_name,
|
||
|
* mbid,
|
||
|
* joinphrase
|
||
|
* },
|
||
|
* ...
|
||
|
* ]
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
var MBImport = (function () {
|
||
|
// --------------------------------------- publics ----------------------------------------- //
|
||
|
|
||
|
let special_artists = {
|
||
|
various_artists: {
|
||
|
name: 'Various Artists',
|
||
|
mbid: '89ad4ac3-39f7-470e-963a-56509c546377',
|
||
|
},
|
||
|
unknown: {
|
||
|
name: '[unknown]',
|
||
|
mbid: '125ec42a-7229-4250-afc5-e057484327fe',
|
||
|
},
|
||
|
};
|
||
|
|
||
|
let url_types = {
|
||
|
purchase_for_download: 74,
|
||
|
download_for_free: 75,
|
||
|
discogs: 76,
|
||
|
purchase_for_mail_order: 79,
|
||
|
other_databases: 82,
|
||
|
stream_for_free: 85,
|
||
|
license: 301,
|
||
|
};
|
||
|
|
||
|
function fnSpecialArtist(key, ac) {
|
||
|
let credited_name = '';
|
||
|
let joinphrase = '';
|
||
|
if (typeof ac !== 'undefined') {
|
||
|
joinphrase = ac.joinphrase;
|
||
|
}
|
||
|
return {
|
||
|
artist_name: special_artists[key].name,
|
||
|
credited_name: credited_name,
|
||
|
joinphrase: joinphrase,
|
||
|
mbid: special_artists[key].mbid,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// compute HTML of search link
|
||
|
function fnBuildSearchLink(release) {
|
||
|
let parameters = searchParams(release);
|
||
|
let url_params = [];
|
||
|
parameters.forEach(function (parameter) {
|
||
|
let value = `${parameter.value}`;
|
||
|
url_params.push(encodeURI(`${parameter.name}=${value}`));
|
||
|
});
|
||
|
return `<a class="musicbrainz_import" href="//musicbrainz.org/search?${url_params.join('&')}">Search in MusicBrainz</a>`;
|
||
|
}
|
||
|
|
||
|
// compute HTML of search button
|
||
|
function fnBuildSearchButton(release) {
|
||
|
let parameters = searchParams(release);
|
||
|
let html = `<form class="musicbrainz_import musicbrainz_import_search" action="//musicbrainz.org/search" method="get" target="_blank" accept-charset="UTF-8" charset="${document.characterSet}">`;
|
||
|
parameters.forEach(function (parameter) {
|
||
|
let value = `${parameter.value}`;
|
||
|
html += `<input type='hidden' value='${value.replace(/'/g, ''')}' name='${parameter.name}'/>`;
|
||
|
});
|
||
|
html += '<button type="submit" title="Search for this release in MusicBrainz (open a new tab)"><span>Search in MB</span></button>';
|
||
|
html += '</form>';
|
||
|
return html;
|
||
|
}
|
||
|
|
||
|
function fnSearchUrlFor(type, what) {
|
||
|
type = type.replace('-', '_');
|
||
|
|
||
|
let params = [`query=${luceneEscape(what)}`, `type=${type}`, 'indexed=1'];
|
||
|
return `//musicbrainz.org/search?${params.join('&')}`;
|
||
|
}
|
||
|
|
||
|
// compute HTML of import form
|
||
|
function fnBuildFormHTML(parameters) {
|
||
|
// Build form
|
||
|
let innerHTML = `<form class="musicbrainz_import musicbrainz_import_add" action="//musicbrainz.org/release/add" method="post" target="_blank" accept-charset="UTF-8" charset="${document.characterSet}">`;
|
||
|
parameters.forEach(function (parameter) {
|
||
|
let value = `${parameter.value}`;
|
||
|
innerHTML += `<input type='hidden' value='${value.replace(/'/g, ''')}' name='${parameter.name}'/>`;
|
||
|
});
|
||
|
|
||
|
innerHTML +=
|
||
|
'<button type="submit" title="Import this release into MusicBrainz (open a new tab)"><img src="//musicbrainz.org/favicon.ico" /><span>Import into MB</span></button>';
|
||
|
innerHTML += '</form>';
|
||
|
|
||
|
return innerHTML;
|
||
|
}
|
||
|
|
||
|
// build form POST parameters that MB is waiting
|
||
|
function fnBuildFormParameters(release, edit_note) {
|
||
|
// Form parameters
|
||
|
let parameters = new Array();
|
||
|
appendParameter(parameters, 'name', release.title);
|
||
|
|
||
|
// Release Artist credits
|
||
|
buildArtistCreditsFormParameters(parameters, '', release.artist_credit);
|
||
|
|
||
|
if (release['secondary_types']) {
|
||
|
for (let i = 0; i < release.secondary_types.length; i++) {
|
||
|
appendParameter(parameters, 'type', release.secondary_types[i]);
|
||
|
}
|
||
|
}
|
||
|
appendParameter(parameters, 'status', release.status);
|
||
|
appendParameter(parameters, 'language', release.language);
|
||
|
appendParameter(parameters, 'script', release.script);
|
||
|
appendParameter(parameters, 'packaging', release.packaging);
|
||
|
|
||
|
// ReleaseGroup
|
||
|
appendParameter(parameters, 'release_group', release.release_group_mbid);
|
||
|
|
||
|
// Date + country
|
||
|
appendParameter(parameters, 'country', release.country);
|
||
|
if (!isNaN(release.year) && release.year != 0) {
|
||
|
appendParameter(parameters, 'date.year', release.year);
|
||
|
}
|
||
|
if (!isNaN(release.month) && release.month != 0) {
|
||
|
appendParameter(parameters, 'date.month', release.month);
|
||
|
}
|
||
|
if (!isNaN(release.day) && release.day != 0) {
|
||
|
appendParameter(parameters, 'date.day', release.day);
|
||
|
}
|
||
|
|
||
|
// Barcode
|
||
|
appendParameter(parameters, 'barcode', release.barcode);
|
||
|
|
||
|
// Disambiguation comment
|
||
|
appendParameter(parameters, 'comment', release.comment);
|
||
|
|
||
|
// Annotation
|
||
|
appendParameter(parameters, 'annotation', release.annotation);
|
||
|
|
||
|
// Label + catnos
|
||
|
if (Array.isArray(release.labels)) {
|
||
|
for (let i = 0; i < release.labels.length; i++) {
|
||
|
let label = release.labels[i];
|
||
|
appendParameter(parameters, `labels.${i}.name`, label.name);
|
||
|
appendParameter(parameters, `labels.${i}.mbid`, label.mbid);
|
||
|
if (label.catno != 'none') {
|
||
|
appendParameter(parameters, `labels.${i}.catalog_number`, label.catno);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// URLs
|
||
|
if (Array.isArray(release.urls)) {
|
||
|
for (let i = 0; i < release.urls.length; i++) {
|
||
|
let url = release.urls[i];
|
||
|
appendParameter(parameters, `urls.${i}.url`, url.url);
|
||
|
appendParameter(parameters, `urls.${i}.link_type`, url.link_type);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Mediums
|
||
|
let total_tracks = 0;
|
||
|
let total_tracks_with_duration = 0;
|
||
|
let total_duration = 0;
|
||
|
for (let i = 0; i < release.discs.length; i++) {
|
||
|
let disc = release.discs[i];
|
||
|
appendParameter(parameters, `mediums.${i}.format`, disc.format);
|
||
|
appendParameter(parameters, `mediums.${i}.name`, disc.title);
|
||
|
|
||
|
// Tracks
|
||
|
for (let j = 0; j < disc.tracks.length; j++) {
|
||
|
let track = disc.tracks[j];
|
||
|
total_tracks++;
|
||
|
appendParameter(parameters, `mediums.${i}.track.${j}.number`, track.number);
|
||
|
appendParameter(parameters, `mediums.${i}.track.${j}.name`, track.title);
|
||
|
let tracklength = '?:??';
|
||
|
let duration_ms = hmsToMilliSeconds(track.duration);
|
||
|
if (!isNaN(duration_ms)) {
|
||
|
tracklength = duration_ms;
|
||
|
total_tracks_with_duration++;
|
||
|
total_duration += duration_ms;
|
||
|
}
|
||
|
appendParameter(parameters, `mediums.${i}.track.${j}.length`, tracklength);
|
||
|
appendParameter(parameters, `mediums.${i}.track.${j}.recording`, track.recording);
|
||
|
buildArtistCreditsFormParameters(parameters, `mediums.${i}.track.${j}.`, track.artist_credit);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Guess release type if not given
|
||
|
if (!release.type && release.title && total_tracks == total_tracks_with_duration) {
|
||
|
release.type = fnGuessReleaseType(release.title, total_tracks, total_duration);
|
||
|
}
|
||
|
appendParameter(parameters, 'type', release.type);
|
||
|
|
||
|
// Add Edit note parameter
|
||
|
appendParameter(parameters, 'edit_note', edit_note);
|
||
|
|
||
|
return parameters;
|
||
|
}
|
||
|
|
||
|
// Convert a list of artists to a list of artist credits with joinphrases
|
||
|
function fnArtistCredits(artists_list) {
|
||
|
let artists = artists_list.map(function (item) {
|
||
|
return { artist_name: item };
|
||
|
});
|
||
|
if (artists.length > 2) {
|
||
|
let last = artists.pop();
|
||
|
last.joinphrase = '';
|
||
|
let prev = artists.pop();
|
||
|
prev.joinphrase = ' & ';
|
||
|
for (let i = 0; i < artists.length; i++) {
|
||
|
artists[i].joinphrase = ', ';
|
||
|
}
|
||
|
artists.push(prev);
|
||
|
artists.push(last);
|
||
|
} else if (artists.length == 2) {
|
||
|
artists[0].joinphrase = ' & ';
|
||
|
}
|
||
|
let credits = [];
|
||
|
// re-split artists if featuring or vs
|
||
|
artists.map(function (item) {
|
||
|
let c = item.artist_name.replace(/\s*\b(?:feat\.?|ft\.?|featuring)\s+/gi, ' feat. ');
|
||
|
c = c.replace(/\s*\(( feat. )([^\)]+)\)/g, '$1$2');
|
||
|
c = c.replace(/\s*\b(?:versus|vs\.?)\s+/gi, ' vs. ');
|
||
|
c = c.replace(/\s+/g, ' ');
|
||
|
let splitted = c.split(/( feat\. | vs\. )/);
|
||
|
if (splitted.length == 1) {
|
||
|
credits.push(item); // nothing to split
|
||
|
} else {
|
||
|
let new_items = [];
|
||
|
let n = 0;
|
||
|
for (let i = 0; i < splitted.length; i++) {
|
||
|
if (n && (splitted[i] == ' feat. ' || splitted[i] == ' vs. ')) {
|
||
|
new_items[n - 1].joinphrase = splitted[i];
|
||
|
} else {
|
||
|
new_items[n++] = {
|
||
|
artist_name: splitted[i].trim(),
|
||
|
joinphrase: '',
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
new_items[n - 1].joinphrase = item.joinphrase;
|
||
|
new_items.map(function (newit) {
|
||
|
credits.push(newit);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
return credits;
|
||
|
}
|
||
|
|
||
|
// Try to guess release type using number of tracks, title and total duration (in millisecs)
|
||
|
function fnGuessReleaseType(title, num_tracks, duration_ms) {
|
||
|
if (num_tracks < 1) return '';
|
||
|
let has_single = !!title.match(/\bsingle\b/i);
|
||
|
let has_EP = !!title.match(/\bEP\b/i);
|
||
|
if (has_single && has_EP) {
|
||
|
has_single = false;
|
||
|
has_EP = false;
|
||
|
}
|
||
|
let perhaps_single = (has_single && num_tracks <= 4) || num_tracks <= 2;
|
||
|
let perhaps_EP = has_EP || (num_tracks > 2 && num_tracks <= 6);
|
||
|
let perhaps_album = num_tracks > 8;
|
||
|
if (isNaN(duration_ms)) {
|
||
|
// no duration, try to guess with title and number of tracks
|
||
|
if (perhaps_single && !perhaps_EP && !perhaps_album) return 'single';
|
||
|
if (!perhaps_single && perhaps_EP && !perhaps_album) return 'EP';
|
||
|
if (!perhaps_single && !perhaps_EP && perhaps_album) return 'album';
|
||
|
return '';
|
||
|
}
|
||
|
let duration_mn = duration_ms / (60 * 1000);
|
||
|
if (perhaps_single && duration_mn >= 1 && duration_mn < 7) return 'single';
|
||
|
if (perhaps_EP && duration_mn > 7 && duration_mn <= 30) return 'EP';
|
||
|
if (perhaps_album && duration_mn > 30) return 'album';
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
// convert HH:MM:SS or MM:SS to milliseconds
|
||
|
function hmsToMilliSeconds(str) {
|
||
|
/* eslint-disable-next-line use-isnan */
|
||
|
if (typeof str == 'undefined' || str === null || str === NaN || str === '') return NaN;
|
||
|
if (typeof str == 'number') return str;
|
||
|
let t = str.split(':');
|
||
|
let s = 0;
|
||
|
let m = 1;
|
||
|
while (t.length > 0) {
|
||
|
s += m * parseInt(t.pop(), 10);
|
||
|
m *= 60;
|
||
|
}
|
||
|
return s * 1000;
|
||
|
}
|
||
|
|
||
|
// convert ISO8601 duration (limited to hours/minutes/seconds) to milliseconds
|
||
|
// format looks like PT1H45M5.789S (note: floats can be used)
|
||
|
// https://en.wikipedia.org/wiki/ISO_8601#Durations
|
||
|
function fnISO8601toMilliSeconds(str) {
|
||
|
let regex = /^PT(?:(\d*\.?\d*)H)?(?:(\d*\.?\d*)M)?(?:(\d*\.?\d*)S)?$/,
|
||
|
m = str.replace(',', '.').match(regex);
|
||
|
if (!m) return NaN;
|
||
|
return (3600 * parseFloat(m[1] || 0) + 60 * parseFloat(m[2] || 0) + parseFloat(m[3] || 0)) * 1000;
|
||
|
}
|
||
|
|
||
|
function fnMakeEditNote(release_url, importer_name, format, home = 'https://github.com/murdos/musicbrainz-userscripts') {
|
||
|
return `Imported from ${release_url}${format ? ` (${format})` : ''} using ${importer_name} import script from ${home}`;
|
||
|
}
|
||
|
|
||
|
// --------------------------------------- privates ----------------------------------------- //
|
||
|
|
||
|
function appendParameter(parameters, paramName, paramValue) {
|
||
|
if (!paramValue) return;
|
||
|
parameters.push({ name: paramName, value: paramValue });
|
||
|
}
|
||
|
|
||
|
function luceneEscape(text) {
|
||
|
let newtext = text.replace(/[-[\]{}()*+?~:\\^!"\/]/g, '\\$&');
|
||
|
return newtext.replace('&&', '&&').replace('||', '||');
|
||
|
}
|
||
|
|
||
|
function buildArtistCreditsFormParameters(parameters, paramPrefix, artist_credit) {
|
||
|
if (!artist_credit) return;
|
||
|
for (let i = 0; i < artist_credit.length; i++) {
|
||
|
let ac = artist_credit[i];
|
||
|
appendParameter(parameters, `${paramPrefix}artist_credit.names.${i}.name`, ac.credited_name);
|
||
|
appendParameter(parameters, `${paramPrefix}artist_credit.names.${i}.artist.name`, ac.artist_name);
|
||
|
appendParameter(parameters, `${paramPrefix}artist_credit.names.${i}.mbid`, ac.mbid);
|
||
|
if (typeof ac.joinphrase != 'undefined' && ac.joinphrase != '') {
|
||
|
appendParameter(parameters, `${paramPrefix}artist_credit.names.${i}.join_phrase`, ac.joinphrase);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function searchParams(release) {
|
||
|
let params = [];
|
||
|
|
||
|
const totaltracks = release.discs.reduce((acc, { tracks }) => acc + tracks.length, 0);
|
||
|
let release_artist = '';
|
||
|
for (let i = 0; i < release.artist_credit.length; i++) {
|
||
|
let ac = release.artist_credit[i];
|
||
|
release_artist += ac.artist_name;
|
||
|
if (typeof ac.joinphrase != 'undefined' && ac.joinphrase != '') {
|
||
|
release_artist += ac.joinphrase;
|
||
|
} else {
|
||
|
if (i != release.artist_credit.length - 1) release_artist += ', ';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let query =
|
||
|
`artist:(${luceneEscape(release_artist)})` +
|
||
|
` release:(${luceneEscape(release.title)})` +
|
||
|
` tracks:(${totaltracks})${release.country ? ` country:${release.country}` : ''}`;
|
||
|
|
||
|
appendParameter(params, 'query', query);
|
||
|
appendParameter(params, 'type', 'release');
|
||
|
appendParameter(params, 'advanced', '1');
|
||
|
return params;
|
||
|
}
|
||
|
|
||
|
// ---------------------------------- expose publics here ------------------------------------ //
|
||
|
|
||
|
return {
|
||
|
buildSearchLink: fnBuildSearchLink,
|
||
|
buildSearchButton: fnBuildSearchButton,
|
||
|
buildFormHTML: fnBuildFormHTML,
|
||
|
buildFormParameters: fnBuildFormParameters,
|
||
|
makeArtistCredits: fnArtistCredits,
|
||
|
guessReleaseType: fnGuessReleaseType,
|
||
|
hmsToMilliSeconds: hmsToMilliSeconds,
|
||
|
ISO8601toMilliSeconds: fnISO8601toMilliSeconds,
|
||
|
makeEditNote: fnMakeEditNote,
|
||
|
searchUrlFor: fnSearchUrlFor,
|
||
|
URL_TYPES: url_types,
|
||
|
SPECIAL_ARTISTS: special_artists,
|
||
|
specialArtist: fnSpecialArtist,
|
||
|
};
|
||
|
})();
|