graphbrainz/src/client.js

153 lines
4.1 KiB
JavaScript
Raw Normal View History

2016-08-08 07:54:06 +00:00
import request from 'request'
import retry from 'retry'
import qs from 'qs'
import chalk from 'chalk'
import ExtendableError from 'es6-error'
import RateLimit from './rate-limit'
import pkg from '../package.json'
const RETRY_CODES = {
ECONNRESET: true,
ENOTFOUND: true,
ESOCKETTIMEDOUT: true,
ETIMEDOUT: true,
ECONNREFUSED: true,
EHOSTUNREACH: true,
EPIPE: true,
EAI_AGAIN: true
}
export class MusicBrainzError extends ExtendableError {
constructor (message, statusCode) {
super(message)
this.statusCode = statusCode
}
}
export default class MusicBrainz {
constructor (options = {}) {
options = {
baseURL: 'http://musicbrainz.org/ws/2/',
userAgent: `${pkg.name}/${pkg.version} ` +
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
timeout: 60000,
limit: 3,
limitPeriod: 3000,
maxConcurrency: 10,
retries: 10,
minRetryDelay: 100,
maxRetryDelay: 60000,
randomizeRetry: true,
...options
}
this.baseURL = options.baseURL
this.userAgent = options.userAgent
this.timeout = options.timeout
this.limiter = new RateLimit({
limit: options.limit,
period: options.limitPeriod,
maxConcurrency: options.maxConcurrency
})
// Even though `minTimeout` is lower than one second, the `Limiter` is
// making sure we don't exceed the API rate limit anyway. So we're not doing
// exponential backoff to wait for the rate limit to subside, but rather
// to be kind to MusicBrainz in case some other error occurred.
this.retryOptions = {
retries: options.retries,
minTimeout: options.minRetryTimeout,
maxTimeout: options.maxRetryDelay,
randomize: options.randomizeRetry
}
}
shouldRetry (err) {
if (err instanceof MusicBrainzError) {
return err.statusCode >= 500 && err.statusCode < 600
}
return RETRY_CODES[err.code] || false
}
_get (path, params) {
return new Promise((resolve, reject) => {
const options = {
baseUrl: this.baseURL,
url: path,
qs: params,
json: true,
gzip: true,
headers: { 'User-Agent': this.userAgent },
timeout: this.timeout
}
console.log('GET:', path, params)
request(options, (err, response, body) => {
if (err) {
reject(err)
} else if (response.statusCode !== 200) {
const message = (body && body.error) || ''
reject(new MusicBrainzError(message, response.statusCode))
} else {
resolve(body)
}
})
})
}
get (path, params) {
return new Promise((resolve, reject) => {
const fn = this._get.bind(this)
const operation = retry.operation(this.retryOptions)
operation.attempt(currentAttempt => {
const priority = currentAttempt
this.limiter.enqueue(fn, [path, params], priority)
.then(resolve)
.catch(err => {
if (!this.shouldRetry(err) || !operation.retry(err)) {
reject(operation.mainError() || err)
}
})
})
})
}
getLookupURL (entity, id, params) {
let url = `${entity}/${id}`
if (typeof params.inc === 'object') {
params = {
...params,
inc: params.inc.join('+')
}
}
const query = qs.stringify(params, {
skipNulls: true,
filter: (key, value) => value === '' ? undefined : value
})
if (query) {
url += `?${query}`
}
return url
}
lookup (entity, id, params = {}) {
const url = this.getLookupURL(entity, id, params)
return this.get(url)
}
}
if (require.main === module) {
const client = new MusicBrainz()
const fn = (id) => {
return client.lookup('artist', id).then(artist => {
console.log(chalk.green(`Done: ${id}${artist.name}`))
}).catch(err => {
console.log(chalk.red(`Error: ${id}${err}`))
})
}
fn('f1106b17-dcbb-45f6-b938-199ccfab50cc')
fn('a74b1b7f-71a5-4011-9441-d0b5e4122711')
fn('9b5ae4cc-15ae-4f0b-8a4e-8c44e42ba52a')
fn('26f77379-968b-4435-b486-fc9acb4590d3')
fn('8538e728-ca0b-4321-b7e5-cff6565dd4c0')
}