musicbrainz-api 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/lib/coverartarchive-api.d.ts +31 -0
- package/lib/coverartarchive-api.js +37 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +19 -0
- package/lib/musicbrainz-api.d.ts +5 -0
- package/lib/musicbrainz-api.js +20 -34
- package/lib/musicbrainz.types.d.ts +6 -0
- package/lib/rate-limiter.d.ts +1 -1
- package/lib/rate-limiter.js +3 -1
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -115,6 +115,20 @@ Lookup an `artist` and include their `releases`, `release-groups` and `aliases`
|
|
|
115
115
|
const artist = await mbApi.lookupArtist('ab2528d9-719f-4261-8098-21849222a0f2');
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
### Lookup collection
|
|
119
|
+
|
|
120
|
+
Lookup an instrument
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
const collection = await mbApi.lookupCollection('de4fdfc4-53aa-458a-b463-8761cc7f5af8');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Lookup an event
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const event = await mbApi.lookupEvent('6d32c658-151e-45ec-88c4-fb8787524d61');
|
|
130
|
+
```
|
|
131
|
+
|
|
118
132
|
### Lookup instrument
|
|
119
133
|
|
|
120
134
|
Lookup an instrument
|
|
@@ -137,6 +151,10 @@ const label = await mbApi.lookupLabel('25dda9f9-f069-4898-82f0-59330a106c7f');
|
|
|
137
151
|
const place = await mbApi.lookupPlace('e6cfb74d-d69b-44c3-b890-1b3f509816e4');
|
|
138
152
|
```
|
|
139
153
|
|
|
154
|
+
```js
|
|
155
|
+
const place = await mbApi.lookupSeries('1ae6c9bc-2931-4d75-bee4-3dc53dfd246a');
|
|
156
|
+
```
|
|
157
|
+
|
|
140
158
|
The second argument can be used to pass [subqueries](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#Subqueries), which will return more (nested) information:
|
|
141
159
|
```js
|
|
142
160
|
const artist = await mbApi.lookupArtist('ab2528d9-719f-4261-8098-21849222a0f2', ['releases', 'recordings', 'url-rels']);
|
|
@@ -475,6 +493,27 @@ assert.isTrue(succeed, 'Login successful');
|
|
|
475
493
|
await mbApi.addSpotifyIdToRecording(recording, '2AMysGXOe0zzZJMtH3Nizb');
|
|
476
494
|
```
|
|
477
495
|
|
|
496
|
+
## Cover Art Archive API
|
|
497
|
+
|
|
498
|
+
Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
502
|
+
|
|
503
|
+
coverArtArchiveApiClient.getReleaseCovers(releaseMbid).then(releaseCoverInfo => {
|
|
504
|
+
console.log('Release cover info', releaseCoverInfo);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'front').then(releaseCoverInfo => {
|
|
508
|
+
console.log('Get best front cover', releaseCoverInfo);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'back').then(releaseCoverInfo => {
|
|
512
|
+
console.log('Get best back cover', releaseCoverInfo);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
|
|
478
517
|
## Compatibility
|
|
479
518
|
|
|
480
519
|
The JavaScript in runtime is compliant with [ECMAScript 2017 (ES8)](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CovertType = 'Front' | 'Back' | 'Booklet' | 'Medium' | 'Obi' | 'Spine' | 'Track' | 'Tray' | 'Sticker' | 'Poster' | 'Liner' | 'Watermark' | 'Raw/Unedited' | 'Matrix/Runout' | 'Top' | 'Bottom' | 'Other';
|
|
2
|
+
export interface IImage {
|
|
3
|
+
types: CovertType[];
|
|
4
|
+
front: boolean;
|
|
5
|
+
back: boolean;
|
|
6
|
+
edit: number;
|
|
7
|
+
image: string;
|
|
8
|
+
comment: string;
|
|
9
|
+
approved: boolean;
|
|
10
|
+
id: string;
|
|
11
|
+
thumbnails: {
|
|
12
|
+
large: string;
|
|
13
|
+
small: string;
|
|
14
|
+
'250': string;
|
|
15
|
+
'500'?: string;
|
|
16
|
+
'1200'?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface ICoverInfo {
|
|
20
|
+
images: IImage[];
|
|
21
|
+
release: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class CoverArtArchiveApi {
|
|
24
|
+
private host;
|
|
25
|
+
private getJson;
|
|
26
|
+
/**
|
|
27
|
+
*
|
|
28
|
+
* @param releaseId MusicBrainz Release MBID
|
|
29
|
+
*/
|
|
30
|
+
getReleaseCovers(releaseId: string, coverType?: 'front' | 'back'): Promise<ICoverInfo>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CoverArtArchiveApi = void 0;
|
|
4
|
+
/* eslint-disable-next-line */
|
|
5
|
+
const got_1 = require("got");
|
|
6
|
+
class CoverArtArchiveApi {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.host = 'coverartarchive.org';
|
|
9
|
+
}
|
|
10
|
+
async getJson(path) {
|
|
11
|
+
const response = await got_1.default.get('https://' + this.host + path, {
|
|
12
|
+
headers: {
|
|
13
|
+
Accept: `application/json`
|
|
14
|
+
},
|
|
15
|
+
responseType: 'json'
|
|
16
|
+
});
|
|
17
|
+
return response.body;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param releaseId MusicBrainz Release MBID
|
|
22
|
+
*/
|
|
23
|
+
async getReleaseCovers(releaseId, coverType) {
|
|
24
|
+
const path = ['release', releaseId];
|
|
25
|
+
if (coverType) {
|
|
26
|
+
path.push(coverType);
|
|
27
|
+
}
|
|
28
|
+
const info = await this.getJson('/' + path.join('/'));
|
|
29
|
+
// Hack to correct http addresses into https
|
|
30
|
+
if (info.release && info.release.startsWith('http:')) {
|
|
31
|
+
info.release = 'https' + info.release.substring(4);
|
|
32
|
+
}
|
|
33
|
+
return info;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.CoverArtArchiveApi = CoverArtArchiveApi;
|
|
37
|
+
//# sourceMappingURL=coverartarchive-api.js.map
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./coverartarchive-api"), exports);
|
|
18
|
+
__exportStar(require("./musicbrainz-api"), exports);
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
package/lib/musicbrainz-api.d.ts
CHANGED
|
@@ -145,6 +145,11 @@ export declare class MusicBrainzApi {
|
|
|
145
145
|
* @param inc Include: artist-credits, isrcs
|
|
146
146
|
*/
|
|
147
147
|
lookupRecording(recordingId: string, inc?: RecordingIncludes[]): Promise<mb.IRecording>;
|
|
148
|
+
/**
|
|
149
|
+
* Lookup series
|
|
150
|
+
* @param seriesId Series MBID
|
|
151
|
+
*/
|
|
152
|
+
lookupSeries(seriesId: string): Promise<mb.ISeries>;
|
|
148
153
|
/**
|
|
149
154
|
* Lookup work
|
|
150
155
|
* @param workId Work MBID
|
package/lib/musicbrainz-api.js
CHANGED
|
@@ -35,7 +35,6 @@ const got_1 = require("got");
|
|
|
35
35
|
const tough_cookie_1 = require("tough-cookie");
|
|
36
36
|
__exportStar(require("./musicbrainz.types"), exports);
|
|
37
37
|
const util_1 = require("util");
|
|
38
|
-
const retries = 3;
|
|
39
38
|
const debug = Debug('musicbrainz-api');
|
|
40
39
|
class MusicBrainzApi {
|
|
41
40
|
static escapeText(text) {
|
|
@@ -101,35 +100,15 @@ class MusicBrainzApi {
|
|
|
101
100
|
},
|
|
102
101
|
cookieJar: cookieJar
|
|
103
102
|
};
|
|
104
|
-
this.rateLimiter = new rate_limiter_1.RateLimiter(
|
|
103
|
+
this.rateLimiter = new rate_limiter_1.RateLimiter(15, 18);
|
|
105
104
|
}
|
|
106
105
|
async restGet(relUrl, query = {}, attempt = 1) {
|
|
107
106
|
query.fmt = 'json';
|
|
108
|
-
let response;
|
|
109
107
|
await this.rateLimiter.limit();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
debug('Rate limiter kicked in, slowing down...');
|
|
115
|
-
await rate_limiter_1.RateLimiter.sleep(500);
|
|
116
|
-
} while (true);
|
|
117
|
-
switch (response.statusCode) {
|
|
118
|
-
case http_status_codes_1.StatusCodes.OK:
|
|
119
|
-
return response.body;
|
|
120
|
-
case http_status_codes_1.StatusCodes.BAD_REQUEST:
|
|
121
|
-
case http_status_codes_1.StatusCodes.NOT_FOUND:
|
|
122
|
-
throw new Error(`Got response status ${response.statusCode}: ${(0, http_status_codes_1.getReasonPhrase)(response.status)}`);
|
|
123
|
-
case http_status_codes_1.StatusCodes.SERVICE_UNAVAILABLE: // 503
|
|
124
|
-
default:
|
|
125
|
-
const msg = `Got response status ${response.statusCode} on attempt #${attempt} (${(0, http_status_codes_1.getReasonPhrase)(response.status)})`;
|
|
126
|
-
debug(msg);
|
|
127
|
-
if (attempt < retries) {
|
|
128
|
-
return this.restGet(relUrl, query, attempt + 1);
|
|
129
|
-
}
|
|
130
|
-
else
|
|
131
|
-
throw new Error(msg);
|
|
132
|
-
}
|
|
108
|
+
const response = await got_1.default.get('ws/2' + relUrl, Object.assign(Object.assign({}, this.options), { searchParams: query, responseType: 'json', retry: {
|
|
109
|
+
limit: 10
|
|
110
|
+
} }));
|
|
111
|
+
return response.body;
|
|
133
112
|
}
|
|
134
113
|
// -----------------------------------------------------------------------------------------------------------------
|
|
135
114
|
// Lookup functions
|
|
@@ -216,6 +195,13 @@ class MusicBrainzApi {
|
|
|
216
195
|
lookupRecording(recordingId, inc = []) {
|
|
217
196
|
return this.lookupEntity('recording', recordingId, inc);
|
|
218
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Lookup series
|
|
200
|
+
* @param seriesId Series MBID
|
|
201
|
+
*/
|
|
202
|
+
lookupSeries(seriesId) {
|
|
203
|
+
return this.lookupEntity('series', seriesId);
|
|
204
|
+
}
|
|
219
205
|
/**
|
|
220
206
|
* Lookup work
|
|
221
207
|
* @param workId Work MBID
|
|
@@ -360,10 +346,10 @@ class MusicBrainzApi {
|
|
|
360
346
|
const postData = xmlMetadata.toXml();
|
|
361
347
|
do {
|
|
362
348
|
await this.rateLimiter.limit();
|
|
363
|
-
const response = await got_1.default.post(path, Object.assign({ searchParams: { client: clientId }, headers: {
|
|
349
|
+
const response = await got_1.default.post(path, Object.assign(Object.assign({}, this.options), { searchParams: { client: clientId }, headers: {
|
|
364
350
|
authorization: digest,
|
|
365
351
|
'Content-Type': 'application/xml'
|
|
366
|
-
}, body: postData, throwHttpErrors: false }
|
|
352
|
+
}, body: postData, throwHttpErrors: false }));
|
|
367
353
|
if (response.statusCode === http_status_codes_1.StatusCodes.UNAUTHORIZED) {
|
|
368
354
|
// Respond to digest challenge
|
|
369
355
|
const auth = new digest_auth_1.DigestAuth(this.config.botAccount);
|
|
@@ -395,9 +381,9 @@ class MusicBrainzApi {
|
|
|
395
381
|
csrf_token: this.session.csrf.token,
|
|
396
382
|
remember_me: 1
|
|
397
383
|
};
|
|
398
|
-
const response = await got_1.default.post('login', Object.assign({ followRedirect: false, searchParams: {
|
|
384
|
+
const response = await got_1.default.post('login', Object.assign(Object.assign({}, this.options), { followRedirect: false, searchParams: {
|
|
399
385
|
returnto: redirectUri
|
|
400
|
-
}, form: formData }
|
|
386
|
+
}, form: formData }));
|
|
401
387
|
const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri;
|
|
402
388
|
if (success) {
|
|
403
389
|
this.session.loggedIn = true;
|
|
@@ -409,9 +395,9 @@ class MusicBrainzApi {
|
|
|
409
395
|
*/
|
|
410
396
|
async logout() {
|
|
411
397
|
const redirectUri = '/success';
|
|
412
|
-
const response = await got_1.default.get('logout', Object.assign({ followRedirect: false, searchParams: {
|
|
398
|
+
const response = await got_1.default.get('logout', Object.assign(Object.assign({}, this.options), { followRedirect: false, searchParams: {
|
|
413
399
|
returnto: redirectUri
|
|
414
|
-
} }
|
|
400
|
+
} }));
|
|
415
401
|
const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri;
|
|
416
402
|
if (success && this.session) {
|
|
417
403
|
this.session.loggedIn = true;
|
|
@@ -432,7 +418,7 @@ class MusicBrainzApi {
|
|
|
432
418
|
formData.username = this.config.botAccount.username;
|
|
433
419
|
formData.password = this.config.botAccount.password;
|
|
434
420
|
formData.remember_me = 1;
|
|
435
|
-
const response = await got_1.default.post(`${entity}/${mbid}/edit`, Object.assign({ form: formData, followRedirect: false }
|
|
421
|
+
const response = await got_1.default.post(`${entity}/${mbid}/edit`, Object.assign(Object.assign({}, this.options), { form: formData, followRedirect: false }));
|
|
436
422
|
if (response.statusCode === http_status_codes_1.StatusCodes.OK)
|
|
437
423
|
throw new Error(`Failed to submit form data`);
|
|
438
424
|
if (response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY)
|
|
@@ -531,7 +517,7 @@ class MusicBrainzApi {
|
|
|
531
517
|
return this.search('url', query);
|
|
532
518
|
}
|
|
533
519
|
async getSession() {
|
|
534
|
-
const response = await got_1.default.get('login', Object.assign({ followRedirect: false, responseType: 'text' }
|
|
520
|
+
const response = await got_1.default.get('login', Object.assign(Object.assign({}, this.options), { followRedirect: false, responseType: 'text' }));
|
|
535
521
|
return {
|
|
536
522
|
csrf: MusicBrainzApi.fetchCsrf(response.body)
|
|
537
523
|
};
|
|
@@ -220,6 +220,12 @@ export interface ILabel extends IEntity {
|
|
|
220
220
|
export interface IPlace extends IEntity {
|
|
221
221
|
name: string;
|
|
222
222
|
}
|
|
223
|
+
export interface ISeries extends IEntity {
|
|
224
|
+
name: string;
|
|
225
|
+
type: string;
|
|
226
|
+
disambiguation: string;
|
|
227
|
+
'type-id': string;
|
|
228
|
+
}
|
|
223
229
|
export interface IUrl extends IEntity {
|
|
224
230
|
id: string;
|
|
225
231
|
resource: string;
|
package/lib/rate-limiter.d.ts
CHANGED
package/lib/rate-limiter.js
CHANGED
|
@@ -7,9 +7,10 @@ class RateLimiter {
|
|
|
7
7
|
static sleep(ms) {
|
|
8
8
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
9
9
|
}
|
|
10
|
-
constructor(
|
|
10
|
+
constructor(maxCalls, period) {
|
|
11
11
|
this.maxCalls = maxCalls;
|
|
12
12
|
this.queue = [];
|
|
13
|
+
debug(`Rate limiter initialized with max ${maxCalls} calls in ${period} seconds.`);
|
|
13
14
|
this.period = 1000 * period;
|
|
14
15
|
}
|
|
15
16
|
async limit() {
|
|
@@ -18,6 +19,7 @@ class RateLimiter {
|
|
|
18
19
|
while (this.queue.length > 0 && this.queue[0] < t0) {
|
|
19
20
|
this.queue.shift();
|
|
20
21
|
}
|
|
22
|
+
// debug(`Current rate is ${this.queue.length} per ${this.period / 1000} sec`);
|
|
21
23
|
if (this.queue.length >= this.maxCalls) {
|
|
22
24
|
const delay = this.queue[0] + this.period - now;
|
|
23
25
|
debug(`Client side rate limiter activated: cool down for ${delay / 1000} s...`);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musicbrainz-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "MusicBrainz API client for reading and submitting metadata",
|
|
5
|
-
"main": "lib/
|
|
6
|
-
"types": "lib/
|
|
5
|
+
"main": "lib/index",
|
|
6
|
+
"types": "lib/index",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Borewit",
|
|
9
9
|
"url": "https://github.com/Borewit"
|
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
"web",
|
|
21
21
|
"service",
|
|
22
22
|
"submit",
|
|
23
|
-
"metabrainz"
|
|
23
|
+
"metabrainz",
|
|
24
|
+
"Cover Art Archive",
|
|
25
|
+
"coverartarchive",
|
|
26
|
+
"coverartarchive.org",
|
|
27
|
+
"album art",
|
|
28
|
+
"covers",
|
|
29
|
+
"download covers"
|
|
24
30
|
],
|
|
25
31
|
"license": "MIT",
|
|
26
32
|
"private": false,
|