nodebrainz
Version:
A MusicBrainz JSON Web Service Version 2 client
365 lines (303 loc) • 10.1 kB
JavaScript
"use strict";
const querystring = require('querystring')
, request = require('./request')
, mockRequest = require('./mocker').mockRequest;
const pack = require('../package.json');
const VERSION = pack.version;
const _host = 'musicbrainz.org';
const _port = 80;
const _path = '/ws/2/';
const _limit = 25;
const _userAgent = `nodebrainz/${VERSION} ( https://github.com/jbraithwaite/nodebrainz/ )`;
/**
* Initialize Nodebrainz
*
* The configuration:
* - host: custom host for querying data
* - basePath: custom path to the data
* - defaultLimit: default limit on request that require it
* - userAgent: User agent sent in the request
*
* @param {Object} options The application configuration
*/
class BaseNodeBrainz {
constructor(options) {
this.request = request;
if (options && options.mock) {
this.mock = options.mock;
this.request = mockRequest.bind(this);
}
options = options || {};
this.host = options.host || _host;
this.port = options.port || _port;
this.basePath = options.basePath || _path;
this.limit = options.defaultLimit || _limit;
this.userAgent = options.userAgent || _userAgent;
this.retryOn = options.retryOn || false;
this.retryDelay = options.retryDelay || 2000;
this.retryCount = options.retryCount || 3;
this.version = VERSION;
}
executeRequest(count, callback) {
if (typeof count === 'function') {
callback = count;
count = 0;
}
this.request({
host: this.host,
port: this.port,
path: this.path,
userAgent: this.userAgent
}, (err, data) => {
if (this.retryOn && count < this.retryCount && err && (err.statusCode === 502 || err.statusCode === 503)) {
setTimeout(() => this.executeRequest(count + 1, callback), this.retryDelay);
} else {
callback(err, data);
}
});
}
/**
* Search
*
* Provides a way to search for entities. Behind the scenes, results are provided by a search server using Lucene technology.
* Note there are different search fields depending on the entity.
*
* http://musicbrainz.org/doc/Development/XML_Web_Service/Version_2/Search
*
* @param {String} type The type of search (e.g. 'artist', 'releasegroup', 'release', 'recording', etc)
* @param {Object} searchFields What to search for (e.g. {artist:'tool'})
* @param {Function} callback function(err, response)
*/
search(type, searchFields, callback) {
let query = '';
let limit = this.limit;
let offset = 0;
let and = '';
searchFields = searchFields || {};
// See if the limit has been included in the search fields
if (searchFields.hasOwnProperty('limit')) {
// Set the limit
limit = searchFields.limit;
// Delete it from search fields
delete searchFields.limit;
}
// See if offset has been included in the search fields
if (searchFields.hasOwnProperty('offset')) {
// Set the offset
offset = searchFields.offset;
// Delete it from search fields
delete searchFields.offset;
}
// Set the query
// https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description
for (let key in searchFields) {
query += and + key + ':' + '"'+ encodeURIComponent(searchFields[key]) +'"';
and = '%20AND%20';
}
// Set the path
this.path = this.basePath + type + '/?query=' + query + '&limit=' + limit + '&offset=' + offset + '&fmt=json';
if (query === '') {
return callback(new Error('You must search for something'));
}
this.executeRequest(callback);
}
/**
* Lucene Search
*
* Direct access to the Lucene search query
*
* https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description
*
* @param {String} type Entity type
* @param {Oject} data {query:'something AND something'}
* @param {Function} callback function(err, response)
*/
luceneSearch(type, data, callback) {
let limit = this.limit;
let offset = 0;
let query = '';
data = data || {};
if (!data.hasOwnProperty('query')) {
return callback(new Error('Missing query'));
}
// Encode the query
query = encodeURIComponent(data.query);
// See if the limit has been included in the search fields
if (data.hasOwnProperty('limit')) {
// Set the limit
limit = data.limit;
}
// See if offset has been included in the search fields
if (data.hasOwnProperty('offset')) {
// Set the offset
offset = data.offset;
}
// Set the path
this.path = this.basePath + type + '/?query=' + query + '&limit=' + limit + '&offset=' + offset + '&fmt=json';
if (query === '') {
return callback(new Error('You must search for something'));
}
this.executeRequest(callback);
}
/**
* NodeBrainsBrowse
*
* Browse requests are a direct lookup of all the entities directly linked to another entity.
* Browsed entities are always ordered alphabetically by gid. If you need to sort the entities,
* you will have to fetch all entities and sort them yourself. For pagination, set a limit and offset.
* Note that browse requests are not searches, in order to browse all the releases-groups for an artist,
* you need to provide the MBID for the artist.
*
* http://musicbrainz.org/doc/Development/XML_Web_Service/Version_2/#Browse
*
* @param {String} type The type of search (e.g. 'artist', 'releasegroup', 'release', 'recording', etc)
* @param {Object} data What to filter by and include
* @param {Function} callback function(err, response)
*/
browse(type, data, callback) {
let query = {};
let inc = '';
data = data || {};
// JSON response
query.fmt = 'json';
// Include
if (data.hasOwnProperty('inc')) {
// Can't pass this to the querystring object because it turns '+' into '%2B'
inc = '&inc=' + data.inc;
// Remove the include
delete data.inc;
}
// Everything else (e.g. limit, offset, type, status)
for (let key in data) {
query[key] = data[key];
}
// Make the query string;
query = querystring.stringify(query);
// Set the path
this.path = this.basePath + type + '?' + query + inc;
this.executeRequest(callback);
}
/**
* NodeBrainsLookup
*
* http://musicbrainz.org/doc/Development/XML_Web_Service/Version_2/#Lookups
*
* @param {String} type The type of search (e.g. 'artist', 'releasegroup', 'release', 'recording', etc)
* @param {String} mbid The id for the type
* @param {Object} data The lookup criteria
* @param {Function} callback function(err, response)
*/
_lookup(type, mbid, data, callback) {
data = data || {};
// You are allowed to not include data
if (typeof data === 'function') {
callback = data;
data = {};
}
if (!mbid) {
return callback(new Error('Missing mbid'));
}
let query = {};
let inc = '';
data = data || {};
// JSON response
query.fmt = 'json';
// Include
if (data.hasOwnProperty('inc')) {
// Can't pass this to the querystring object because it turns '+' into '%2B'
inc = '&inc=' + data.inc;
// Remove the include
delete data.inc;
}
// Everything else (e.g. limit, offset, type, status)
for (let key in data) {
query[key] = data[key];
}
// Make the query string;
query = querystring.stringify(query);
// Set the path
this.path = this.basePath + type + '/' + mbid + '?' + query + inc;
this.executeRequest(callback);
}
/**
* Artist Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
artist(mbid, data, callback) {
this._lookup('artist', mbid, data, callback);
}
/**
* Release Group Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
releaseGroup(mbid, data, callback) {
this._lookup('release-group', mbid, data, callback);
}
/**
* Release Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
release(mbid, data, callback) {
this._lookup('release', mbid, data, callback);
}
/**
* Recording Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
recording(mbid, data, callback) {
this._lookup('recording', mbid, data, callback);
}
/**
* Work Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
work(mbid, data, callback) {
this._lookup('work', mbid, data, callback);
}
/**
* Label Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
label(mbid, data, callback) {
this._lookup('label', mbid, data, callback);
}
/**
* Area Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
area(mbid, data, callback) {
this._lookup('area', mbid, data, callback);
}
/**
* URL Lookup
*
* @param {String} mbid MusicBrainz ID
* @param {Object} data Filtering and subqueries information
* @param {Function} callback function(err, response)
*/
url(mbid, data, callback) {
this._lookup('url', mbid, data, callback);
}
}
module.exports = BaseNodeBrainz;