musicbrainz-api 0.15.0 → 0.17.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
@@ -69,7 +69,10 @@ const config = {
69
69
  },
70
70
 
71
71
  // Your e-mail address, required for submitting ISRCs
72
- appMail: string
72
+ appMail: string,
73
+
74
+ // Helpful if you have your own MusicBrainz server, default: false (optional)
75
+ disableRateLimiting: false,
73
76
  }
74
77
 
75
78
  const mbApi = new MusicbrainzApi(config);
@@ -391,6 +394,7 @@ await mbApi.addSpotifyIdToRecording(recording, '2AMysGXOe0zzZJMtH3Nizb');
391
394
 
392
395
  Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
393
396
 
397
+ ### Release Cover Art
394
398
  ```js
395
399
  import {CoverArtArchiveApi} from 'musicbrainz-api';
396
400
 
@@ -408,6 +412,25 @@ coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'back').then(releaseCover
408
412
 
409
413
  ```
410
414
 
415
+ ### Release Group Cover Art
416
+ ```js
417
+ import {CoverArtArchiveApi} from 'musicbrainz-api';
418
+
419
+ coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid).then(releaseGroupCoverInfo => {
420
+ console.log('Release cover info', releaseGroupCoverInfo);
421
+ });
422
+
423
+ coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'front').then(releaseGroupCoverInfo => {
424
+ console.log('Get best front cover', releaseGroupCoverInfo);
425
+ });
426
+
427
+ coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'back').then(releaseGroupCoverInfo => {
428
+ console.log('Get best back cover', releaseGroupCoverInfo);
429
+ });
430
+
431
+ ```
432
+
433
+
411
434
  ## Compatibility
412
435
 
413
436
  The JavaScript in runtime is compliant with [ECMAScript 2017 (ES8)](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017).
@@ -5,9 +5,9 @@ export class CoverArtArchiveApi {
5
5
  this.host = 'coverartarchive.org';
6
6
  }
7
7
  async getJson(path) {
8
- const response = await got.get('https://' + this.host + path, {
8
+ const response = await got.get(`https://${this.host}${path}`, {
9
9
  headers: {
10
- Accept: `application/json`
10
+ Accept: "application/json"
11
11
  },
12
12
  responseType: 'json'
13
13
  });
@@ -39,14 +39,15 @@ export class CoverArtArchiveApi {
39
39
  * @param coverType Cover type
40
40
  */
41
41
  async getCovers(releaseId, releaseType = 'release', coverType) {
42
+ var _a;
42
43
  const path = [releaseType, releaseId];
43
44
  if (coverType) {
44
45
  path.push(coverType);
45
46
  }
46
- const info = await this.getJson('/' + path.join('/'));
47
+ const info = await this.getJson(`/${path.join('/')}`);
47
48
  // Hack to correct http addresses into https
48
- if (info.release && info.release.startsWith('http:')) {
49
- info.release = 'https' + info.release.substring(4);
49
+ if ((_a = info.release) === null || _a === void 0 ? void 0 : _a.startsWith('http:')) {
50
+ info.release = `https${info.release.substring(4)}`;
50
51
  }
51
52
  return info;
52
53
  }
@@ -1,5 +1,5 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
- import * as crypto from 'crypto';
2
+ import * as crypto from 'node:crypto';
3
3
  function md5(str) {
4
4
  return crypto.createHash('md5').update(str).digest('hex'); // lgtm [js/insufficient-password-hash]
5
5
  }
@@ -13,8 +13,8 @@ export class DigestAuth {
13
13
  * HA1=MD5(MD5(username:realm:password):nonce:cnonce)
14
14
  */
15
15
  static ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
16
- const ha1 = md5(user + ':' + realm + ':' + pass); // lgtm [js/insufficient-password-hash]
17
- return algorithm && algorithm.toLowerCase() === 'md5-sess' ? md5(ha1 + ':' + nonce + ':' + cnonce) : ha1;
16
+ const ha1 = md5(`${user}:${realm}:${pass}`); // lgtm [js/insufficient-password-hash]
17
+ return algorithm && algorithm.toLowerCase() === 'md5-sess' ? md5(`${ha1}:${nonce}:${cnonce}`) : ha1;
18
18
  }
19
19
  constructor(credentials) {
20
20
  this.credentials = credentials;
@@ -44,10 +44,10 @@ export class DigestAuth {
44
44
  const nc = qop && '00000001';
45
45
  const cnonce = qop && uuidv4().replace(/-/g, '');
46
46
  const ha1 = DigestAuth.ha1Compute(challenge.algorithm, this.credentials.username, challenge.realm, this.credentials.password, challenge.nonce, cnonce);
47
- const ha2 = md5(method + ':' + path); // lgtm [js/insufficient-password-hash]
47
+ const ha2 = md5(`${method}:${path}`); // lgtm [js/insufficient-password-hash]
48
48
  const digestResponse = qop
49
- ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) // lgtm [js/insufficient-password-hash]
50
- : md5(ha1 + ':' + challenge.nonce + ':' + ha2); // lgtm [js/insufficient-password-hash]
49
+ ? md5(`${ha1}:${challenge.nonce}:${nc}:${cnonce}:${qop}:${ha2}`) // lgtm [js/insufficient-password-hash]
50
+ : md5(`${ha1}:${challenge.nonce}:${ha2}`); // lgtm [js/insufficient-password-hash]
51
51
  const authValues = {
52
52
  username: this.credentials.username,
53
53
  realm: challenge.realm,
@@ -64,16 +64,16 @@ export class DigestAuth {
64
64
  Object.entries(authValues).forEach(([key, value]) => {
65
65
  if (value) {
66
66
  if (key === 'qop' || key === 'nc' || key === 'algorithm') {
67
- parts.push(key + '=' + value);
67
+ parts.push(`${key}=${value}`);
68
68
  }
69
69
  else {
70
- parts.push(key + '="' + value + '"');
70
+ parts.push(`${key}="${value}"`);
71
71
  }
72
72
  }
73
73
  });
74
- authHeader = 'Digest ' + parts.join(', ');
74
+ const digest = `Digest ${parts.join(', ')}`;
75
75
  this.sentAuth = true;
76
- return authHeader;
76
+ return digest;
77
77
  }
78
78
  }
79
79
  //# sourceMappingURL=digest-auth.js.map
@@ -2,7 +2,7 @@ export { XmlMetadata } from './xml/xml-metadata.js';
2
2
  export { XmlIsrc } from './xml/xml-isrc.js';
3
3
  export { XmlIsrcList } from './xml/xml-isrc-list.js';
4
4
  export { XmlRecording } from './xml/xml-recording.js';
5
- import { XmlMetadata } from './xml/xml-metadata.js';
5
+ import type { XmlMetadata } from './xml/xml-metadata.js';
6
6
  import * as mb from './musicbrainz.types.js';
7
7
  export * from './musicbrainz.types.js';
8
8
  export type RelationsIncludes = 'area-rels' | 'artist-rels' | 'event-rels' | 'instrument-rels' | 'label-rels' | 'place-rels' | 'recording-rels' | 'release-rels' | 'release-group-rels' | 'series-rels' | 'url-rels' | 'work-rels';
@@ -61,6 +61,7 @@ export interface IMusicBrainzConfig {
61
61
  * User e-mail address or application URL
62
62
  */
63
63
  appContactInfo?: string;
64
+ disableRateLimiting?: boolean;
64
65
  }
65
66
  export interface ICsrfSession {
66
67
  sessionKey: string;
@@ -174,5 +175,6 @@ export declare class MusicBrainzApi {
174
175
  */
175
176
  addSpotifyIdToRecording(recording: mb.IRecording, spotifyId: string, editNote: string): Promise<void>;
176
177
  private getSession;
178
+ private applyRateLimiter;
177
179
  }
178
180
  export declare function makeAndQueryString(keyValuePairs: IFormData): string;
@@ -1,4 +1,4 @@
1
- import * as assert from 'assert';
1
+ import * as assert from 'node:assert';
2
2
  import { StatusCodes as HttpStatus } from 'http-status-codes';
3
3
  import Debug from 'debug';
4
4
  export { XmlMetadata } from './xml/xml-metadata.js';
@@ -11,7 +11,7 @@ import * as mb from './musicbrainz.types.js';
11
11
  import got from 'got';
12
12
  import { CookieJar } from 'tough-cookie';
13
13
  export * from './musicbrainz.types.js';
14
- import { promisify } from 'util';
14
+ import { promisify } from 'node:util';
15
15
  const debug = Debug('musicbrainz-api');
16
16
  export class MusicBrainzApi {
17
17
  static fetchCsrf(html) {
@@ -53,9 +53,8 @@ export class MusicBrainzApi {
53
53
  }
54
54
  async restGet(relUrl, query = {}) {
55
55
  query.fmt = 'json';
56
- const delay = await this.rateLimiter.limit();
57
- debug(`Client side rate limiter activated: cool down for ${Math.round(delay / 100) / 10} s...`);
58
- const response = await got.get('ws/2' + relUrl, {
56
+ await this.applyRateLimiter();
57
+ const response = await got.get(`ws/2${relUrl}`, {
59
58
  ...this.options,
60
59
  searchParams: query,
61
60
  responseType: 'json',
@@ -79,7 +78,7 @@ export class MusicBrainzApi {
79
78
  if (Array.isArray(query.inc)) {
80
79
  urlQuery.inc = urlQuery.inc.join(' ');
81
80
  }
82
- return this.restGet('/' + entity + '/', urlQuery);
81
+ return this.restGet(`/${entity}/`, urlQuery);
83
82
  }
84
83
  // ---------------------------------------------------------------------------
85
84
  async postRecording(xmlMetadata) {
@@ -87,7 +86,7 @@ export class MusicBrainzApi {
87
86
  }
88
87
  async post(entity, xmlMetadata) {
89
88
  if (!this.config.appName || !this.config.appVersion) {
90
- throw new Error(`XML-Post requires the appName & appVersion to be defined`);
89
+ throw new Error("XML-Post requires the appName & appVersion to be defined");
91
90
  }
92
91
  const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
93
92
  const path = `ws/2/${entity}/`;
@@ -96,7 +95,7 @@ export class MusicBrainzApi {
96
95
  let n = 1;
97
96
  const postData = xmlMetadata.toXml();
98
97
  do {
99
- await this.rateLimiter.limit();
98
+ await this.applyRateLimiter();
100
99
  const response = await got.post(path, {
101
100
  ...this.options,
102
101
  searchParams: { client: clientId },
@@ -120,10 +119,10 @@ export class MusicBrainzApi {
120
119
  } while (n++ < 5);
121
120
  }
122
121
  async login() {
123
- var _a, _b;
122
+ var _a, _b, _c;
124
123
  assert.ok((_a = this.config.botAccount) === null || _a === void 0 ? void 0 : _a.username, 'bot username should be set');
125
124
  assert.ok((_b = this.config.botAccount) === null || _b === void 0 ? void 0 : _b.password, 'bot password should be set');
126
- if (this.session && this.session.loggedIn) {
125
+ if ((_c = this.session) === null || _c === void 0 ? void 0 : _c.loggedIn) {
127
126
  for (const cookie of await this.getCookies(this.options.prefixUrl)) {
128
127
  if (cookie.key === 'remember_login') {
129
128
  return true;
@@ -179,7 +178,7 @@ export class MusicBrainzApi {
179
178
  */
180
179
  async editEntity(entity, mbid, formData) {
181
180
  var _a, _b;
182
- await this.rateLimiter.limit();
181
+ await this.applyRateLimiter();
183
182
  this.session = await this.getSession();
184
183
  formData.csrf_session_key = this.session.csrf.sessionKey;
185
184
  formData.csrf_token = this.session.csrf.token;
@@ -192,7 +191,7 @@ export class MusicBrainzApi {
192
191
  followRedirect: false
193
192
  });
194
193
  if (response.statusCode === HttpStatus.OK)
195
- throw new Error(`Failed to submit form data`);
194
+ throw new Error("Failed to submit form data");
196
195
  if (response.statusCode === HttpStatus.MOVED_TEMPORARILY)
197
196
  return;
198
197
  throw new Error(`Unexpected status code: ${response.statusCode}`);
@@ -224,7 +223,7 @@ export class MusicBrainzApi {
224
223
  */
225
224
  async addIsrc(recording, isrc) {
226
225
  const formData = {};
227
- formData[`edit-recording.name`] = recording.title; // Required
226
+ formData["edit-recording.name"] = recording.title; // Required
228
227
  if (!recording.isrcs) {
229
228
  throw new Error('You must retrieve recording with existing ISRC values');
230
229
  }
@@ -250,7 +249,7 @@ export class MusicBrainzApi {
250
249
  assert.strictEqual(spotifyId.length, 22);
251
250
  return this.addUrlToRecording(recording, {
252
251
  linkTypeId: mb.LinkType.stream_for_free,
253
- text: 'https://open.spotify.com/track/' + spotifyId
252
+ text: `https://open.spotify.com/track/${spotifyId}`
254
253
  }, editNote);
255
254
  }
256
255
  async getSession() {
@@ -263,6 +262,12 @@ export class MusicBrainzApi {
263
262
  csrf: MusicBrainzApi.fetchCsrf(response.body)
264
263
  };
265
264
  }
265
+ async applyRateLimiter() {
266
+ if (!this.config.disableRateLimiting) {
267
+ const delay = await this.rateLimiter.limit();
268
+ debug(`Client side rate limiter activated: cool down for ${Math.round(delay / 100) / 10} s...`);
269
+ }
270
+ }
266
271
  }
267
272
  export function makeAndQueryString(keyValuePairs) {
268
273
  return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
@@ -36,7 +36,7 @@ export interface IArtist extends IEntity {
36
36
  'gender-id'?: string;
37
37
  'life-span'?: IPeriod;
38
38
  country?: string;
39
- ipis?: any[];
39
+ ipis?: string[];
40
40
  isnis?: string[];
41
41
  aliases?: IAlias[];
42
42
  gender?: string;
@@ -200,13 +200,13 @@ export interface IUrlList extends ISearchResult {
200
200
  }
201
201
  export type RelationDirection = 'backward' | 'forward';
202
202
  export interface IRelation {
203
- 'attribute-ids': any;
203
+ 'attribute-ids': unknown[];
204
204
  direction: RelationDirection;
205
205
  'target-credit': string;
206
206
  end: null | unknown;
207
207
  'source-credit': string;
208
208
  ended: boolean;
209
- 'attribute-values': unknown;
209
+ 'attribute-values': unknown[];
210
210
  attributes?: any[];
211
211
  type: string;
212
212
  begin?: null | unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musicbrainz-api",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "MusicBrainz API client for reading and submitting metadata",
5
5
  "exports": "./lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -45,9 +45,10 @@
45
45
  "url": "https://github.com/Borewit/musicbrainz-api/issues"
46
46
  },
47
47
  "dependencies": {
48
+ "@biomejs/biome": "1.8.3",
48
49
  "@types/caseless": "^0.12.1",
49
50
  "@types/request-promise-native": "^1.0.17",
50
- "@types/uuid": "^9.0.0",
51
+ "@types/uuid": "^10.0.0",
51
52
  "caseless": "^0.12.0",
52
53
  "debug": "^4.3.4",
53
54
  "got": "^14.2.1",
@@ -57,43 +58,36 @@
57
58
  "rate-limit-threshold": "^0.1.5",
58
59
  "source-map-support": "^0.5.16",
59
60
  "tough-cookie": "^4.1.3",
60
- "uuid": "^9.0.0"
61
+ "uuid": "^10.0.0"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@types/chai": "^4.3.0",
64
65
  "@types/jsontoxml": "^1.0.5",
65
66
  "@types/mocha": "^10.0.4",
66
- "@types/node": "^20.8.10",
67
- "@typescript-eslint/eslint-plugin": "^5.13.0",
68
- "@typescript-eslint/parser": "^5.13.0",
69
- "c8": "^9.1.0",
67
+ "@types/node": "^22.1.0",
68
+ "@types/sinon": "^17.0.3",
69
+ "c8": "^10.1.2",
70
70
  "chai": "^5.1.0",
71
71
  "del-cli": "^5.0.0",
72
- "eslint": "^8.10.0",
73
- "eslint-config-prettier": "^9.0.0",
74
- "eslint-import-resolver-typescript": "^3.3.0",
75
- "eslint-plugin-import": "^2.25.4",
76
- "eslint-plugin-jsdoc": "^48.2.2",
77
- "eslint-plugin-node": "^11.1.0",
78
- "eslint-plugin-unicorn": "^49.0.0",
79
72
  "mocha": "^10.1.0",
80
73
  "remark-cli": "^12.0.0",
81
- "remark-preset-lint-recommended": "^6.1.2",
74
+ "remark-preset-lint-recommended": "^7.0.0",
75
+ "sinon": "^18.0.0",
82
76
  "ts-node": "^10.0.0",
83
77
  "typescript": "^5.0.2"
84
78
  },
85
79
  "scripts": {
86
- "clean": "del-cli lib/**/*.js lib/**/*.js.map lib/**/*.d.ts test/**/*.js test/**/*.js.map",
80
+ "clean": "del-cli 'lib/**/*.js' 'lib/**/*.js.map' 'lib/**/*.d.ts' 'test/**/*.js' 'test/**/*.js.map'",
87
81
  "compile-lib": "tsc -p lib",
88
82
  "compile-test": "tsc -p test",
89
- "compile": "npm run compile-lib && npm run compile-test",
90
- "eslint": "eslint lib/**/*.ts --ignore-pattern lib/**/*.d.ts test/**/*.ts",
83
+ "compile": "yarn run compile-lib && yarn run compile-test",
91
84
  "lint-md": "remark -u preset-lint-recommended .",
92
- "lint": "npm run lint-md && npm run eslint",
85
+ "lint-ts": "biome check",
86
+ "lint": "yarn run lint-md && yarn run lint-ts",
93
87
  "test": "mocha",
94
- "build": "npm run clean && npm run compile",
95
- "start": "npm-run-all compile lint cover-test",
96
- "test-coverage": "c8 npm run test",
88
+ "build": "yarn run clean && yarn run compile",
89
+ "start": "yarn run compile && yarn run lint && yarn run cover-test",
90
+ "test-coverage": "c8 yarn run test",
97
91
  "send-codacy": "nyc report --reporter=text-lcov | codacy-coverage"
98
92
  },
99
93
  "nyc": {