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 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
@@ -0,0 +1,2 @@
1
+ export * from './coverartarchive-api';
2
+ export * from './musicbrainz-api';
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
@@ -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
@@ -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(60, 50);
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
- do {
111
- response = await got_1.default.get('ws/2' + relUrl, Object.assign({ searchParams: query, responseType: 'json' }, this.options));
112
- if (response.statusCode !== 503)
113
- break;
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 }, this.options));
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 }, this.options));
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
- } }, this.options));
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 }, this.options));
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' }, this.options));
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;
@@ -3,6 +3,6 @@ export declare class RateLimiter {
3
3
  static sleep(ms: number): Promise<void>;
4
4
  queue: number[];
5
5
  private readonly period;
6
- constructor(period: number, maxCalls: number);
6
+ constructor(maxCalls: number, period: number);
7
7
  limit(): Promise<void>;
8
8
  }
@@ -7,9 +7,10 @@ class RateLimiter {
7
7
  static sleep(ms) {
8
8
  return new Promise(resolve => setTimeout(resolve, ms));
9
9
  }
10
- constructor(period, maxCalls) {
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.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "MusicBrainz API client for reading and submitting metadata",
5
- "main": "lib/musicbrainz-api",
6
- "types": "lib/musicbrainz-api",
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,