mirror of
https://github.com/BradNut/graphbrainz
synced 2025-09-08 17:40:32 +00:00
153 lines
4.1 KiB
JavaScript
153 lines
4.1 KiB
JavaScript
|
|
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')
|
||
|
|
}
|