musicbrainz-api 0.17.0 → 0.18.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
@@ -1,24 +1,38 @@
1
1
  [![Node.js CI](https://github.com/Borewit/musicbrainz-api/actions/workflows/nodejs-ci.yml/badge.svg)](https://github.com/Borewit/musicbrainz-api/actions/workflows/nodejs-ci.yml)
2
+ [![CodeQL](https://github.com/Borewit/musicbrainz-api/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/Borewit/musicbrainz-api/actions/workflows/github-code-scanning/codeql)
2
3
  [![NPM version](https://img.shields.io/npm/v/musicbrainz-api.svg)](https://npmjs.org/package/musicbrainz-api)
3
4
  [![npm downloads](http://img.shields.io/npm/dm/musicbrainz-api.svg)](https://npmcharts.com/compare/musicbrainz-api?interval=30&start=365)
4
5
  [![Coverage Status](https://coveralls.io/repos/github/Borewit/musicbrainz-api/badge.svg?branch=master)](https://coveralls.io/github/Borewit/musicbrainz-api?branch=master)
5
6
  [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2bc47b2006454bae8c737991f152e518)](https://app.codacy.com/gh/Borewit/musicbrainz-api/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
6
- [![CodeQL](https://github.com/Borewit/musicbrainz-api/actions/workflows/codeql.yml/badge.svg)](https://github.com/Borewit/musicbrainz-api/actions/workflows/codeql.yml)
7
7
  [![Known Vulnerabilities](https://snyk.io/test/github/Borewit/musicbrainz-api/badge.svg?targetFile=package.json)](https://snyk.io/test/github/Borewit/musicbrainz-api?targetFile=package.json)
8
8
  [![DeepScan grade](https://deepscan.io/api/teams/5165/projects/6991/branches/63373/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=5165&pid=6991&bid=63373)
9
9
  [![Discord](https://img.shields.io/discord/460524735235883049.svg)](https://discord.gg/958xT5X)
10
+ [![bundlejs.com badge](https://deno.bundlejs.com/?q=musicbrainz-api&badge)](https://bundlejs.com/?q=musicbrainz-api)
10
11
 
11
12
  # musicbrainz-api
12
13
 
13
14
  A MusicBrainz-API-client for reading and submitting metadata
14
15
 
15
16
  ## Features
16
- * Access metadata from MusicBrainz
17
- * Submit metadata
18
- * Smart and adjustable throttling, like MusicBrainz, it allows a bursts of requests
19
- * Build in TypeScript definitions
17
+ - Access metadata from MusicBrainz
18
+ - Submit metadata
19
+ - Smart and adjustable throttling, like MusicBrainz, it allows a bursts of requests
20
+ - Build in TypeScript definitions
20
21
 
21
- ### Hint
22
+ ## Compatibility
23
+
24
+ Module: [Pure ECMAScript Module (ESM)](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).
25
+ The distributed JavaScript codebase is compliant with the [ECMAScript 2020 (11th Edition)](https://en.wikipedia.org/wiki/ECMAScript_version_history#11th_Edition_%E2%80%93_ECMAScript_2020) standard.
26
+
27
+ This module requires a [Node.js ≥ 20](https://nodejs.org/en/about/previous-releases) engine.
28
+
29
+ > [!NOTE]
30
+ > We are looking into making this package usable in the browser as well.
31
+
32
+ ## Sponsor
33
+ If you appreciate my work and want to support the development of open-source projects like [musicbrainz-api](https://github.com/Borewit/musicbrainz-api), [music-metadata](https://github.com/Borewit/music-metadata), [file-type](https://github.com/sindresorhus/file-type), [listFix()](https://github.com/Borewit/listFix), [lizzy](https://github.com/Borewit/lizzy), [strtok3](https://github.com/Borewit/strtok3), or [tokenizer-s3](https://github.com/Borewit/tokenizer-s3), please consider becoming a sponsor or making a small contribution.
34
+ Your support helps sustain ongoing development and improvements.
35
+ [Become a sponsor to Borewit](https://github.com/sponsors/Borewit)
22
36
 
23
37
  This package is currently only developed for the use in a [node.js environment](http://nodejs.org/).
24
38
  We are looking into making this package usable in the browser as well.
@@ -85,15 +99,14 @@ MusicBrainz API documentation: [XML Web Service/Version 2 Lookups](https://wiki.
85
99
  ### Generic lookup function
86
100
 
87
101
  Arguments:
88
- * entity: `'area'` | `'artist'` | `'collection'` | `'instrument'` | `'label'` | `'place'` | `'release'` | `'release-group'` | `'recording'` | `'series'` | `'work'` | `'url'` | `'event'`
89
- * MBID [(MusicBrainz identifier)](https://wiki.musicbrainz.org/MusicBrainz_Identifier)
90
- * query
102
+ - entity: `'area'` | `'artist'` | `'collection'` | `'instrument'` | `'label'` | `'place'` | `'release'` | `'release-group'` | `'recording'` | `'series'` | `'work'` | `'url'` | `'event'`
103
+ - MBID [(MusicBrainz identifier)](https://wiki.musicbrainz.org/MusicBrainz_Identifier)
104
+ - query
91
105
 
92
106
  ```js
93
107
  const artist = await mbApi.lookup('artist', 'ab2528d9-719f-4261-8098-21849222a0f2');
94
108
  ```
95
109
 
96
-
97
110
  | Query argument | Query value |
98
111
  |-----------------------|-----------------|
99
112
  | `query.collection` | Collection MBID |
@@ -263,19 +276,19 @@ There are different search fields depending on the entity.
263
276
  Searches can be performed using the generic search function: `query(entity: mb.EntityType, query: string | IFormData, offset?: number, limit?: number): Promise<entity>`
264
277
 
265
278
  Arguments:
266
- * Entity type, which can be one of:
267
- * `artist`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Artist)
268
- * `label`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Label)
269
- * `recording`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Recording)
270
- * `release`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release)
271
- * `release-group`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release_Group)
272
- * `work`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Work)
273
- * `area`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Area)
274
- * `url`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#URL)
275
- * `query {query: string, offset: number, limit: number}`
276
- * `query.query`: supports the full Lucene Search syntax; you can find a detailed guide at [Lucene Search Syntax](https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description). For example, you can set conditions while searching for a name with the AND operator.
277
- * `query.offset`: optional, return search results starting at a given offset. Used for paging through more than one page of results.
278
- * `limit.query`: optional, an integer value defining how many entries should be returned. Only values between 1 and 100 (both inclusive) are allowed. If not given, this defaults to 25.
279
+ - Entity type, which can be one of:
280
+ - `artist`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Artist)
281
+ - `label`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Label)
282
+ - `recording`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Recording)
283
+ - `release`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release)
284
+ - `release-group`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release_Group)
285
+ - `work`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Work)
286
+ - `area`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Area)
287
+ - `url`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#URL)
288
+ - `query {query: string, offset: number, limit: number}`
289
+ - `query.query`: supports the full Lucene Search syntax; you can find a detailed guide at [Lucene Search Syntax](https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description). For example, you can set conditions while searching for a name with the AND operator.
290
+ - `query.offset`: optional, return search results starting at a given offset. Used for paging through more than one page of results.
291
+ - `limit.query`: optional, an integer value defining how many entries should be returned. Only values between 1 and 100 (both inclusive) are allowed. If not given, this defaults to 25.
279
292
 
280
293
  For example, to find any recordings of _'We Will Rock You'_ by Queen:
281
294
  ```js
@@ -348,8 +361,7 @@ For all of the following function you need to use a dedicated bot account.
348
361
 
349
362
  ## Submitting ISRC via post user form-data
350
363
 
351
- <img width="150" src="http://www.clker.com/cliparts/i/w/L/q/u/1/work-in-progress.svg" alt="Work in progress"/>
352
- Use with caution, and only on a test server, it may clear existing metadata as side effect.
364
+ Use with caution, and only on a test server, it may clear existing metadata has side effect.
353
365
 
354
366
  ```js
355
367
 
@@ -396,37 +408,48 @@ Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_
396
408
 
397
409
  ### Release Cover Art
398
410
  ```js
399
- import {CoverArtArchiveApi} from 'musicbrainz-api';
411
+ import { CoverArtArchiveApi } from 'musicbrainz-api';
400
412
 
401
- coverArtArchiveApiClient.getReleaseCovers(releaseMbid).then(releaseCoverInfo => {
402
- console.log('Release cover info', releaseCoverInfo);
403
- });
413
+ const coverArtArchiveApiClient = new CoverArtArchiveApi();
404
414
 
405
- coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'front').then(releaseCoverInfo => {
406
- console.log('Get best front cover', releaseCoverInfo);
407
- });
408
-
409
- coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'back').then(releaseCoverInfo => {
410
- console.log('Get best back cover', releaseCoverInfo);
411
- });
415
+ async function getReleaseCoverArt(releaseMbid, coverType = '') {
416
+ try {
417
+ const coverInfo = await coverArtArchiveApiClient.getReleaseCovers(releaseMbid, coverType);
418
+ console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
419
+ } catch (error) {
420
+ console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
421
+ }
422
+ }
412
423
 
424
+ (async () => {
425
+ const releaseMbid = 'your-release-mbid-here'; // Replace with actual MBID
426
+ await getReleaseCoverArt(releaseMbid); // Get all covers
427
+ await getReleaseCoverArt(releaseMbid, 'front'); // Get best front cover
428
+ await getReleaseCoverArt(releaseMbid, 'back'); // Get best back cover
429
+ })();
413
430
  ```
414
431
 
415
432
  ### Release Group Cover Art
416
433
  ```js
417
- import {CoverArtArchiveApi} from 'musicbrainz-api';
434
+ import { CoverArtArchiveApi } from 'musicbrainz-api';
418
435
 
419
- coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid).then(releaseGroupCoverInfo => {
420
- console.log('Release cover info', releaseGroupCoverInfo);
421
- });
436
+ const coverArtArchiveApiClient = new CoverArtArchiveApi();
422
437
 
423
- coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'front').then(releaseGroupCoverInfo => {
424
- console.log('Get best front cover', releaseGroupCoverInfo);
425
- });
438
+ async function getCoverArt(releaseGroupMbid, coverType = '') {
439
+ try {
440
+ const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, coverType);
441
+ console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
442
+ } catch (error) {
443
+ console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
444
+ }
445
+ }
426
446
 
427
- coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'back').then(releaseGroupCoverInfo => {
428
- console.log('Get best back cover', releaseGroupCoverInfo);
429
- });
447
+ (async () => {
448
+ const releaseGroupMbid = 'your-release-group-mbid-here'; // Replace with actual MBID
449
+ await getCoverArt(releaseGroupMbid); // Get all covers
450
+ await getCoverArt(releaseGroupMbid, 'front'); // Get best front cover
451
+ await getCoverArt(releaseGroupMbid, 'back'); // Get best back cover
452
+ })();
430
453
 
431
454
  ```
432
455
 
@@ -21,7 +21,7 @@ export interface ICoverInfo {
21
21
  release: string;
22
22
  }
23
23
  export declare class CoverArtArchiveApi {
24
- private host;
24
+ private httpClient;
25
25
  private getJson;
26
26
  /**
27
27
  * Fetch release
@@ -1,17 +1,16 @@
1
1
  /* eslint-disable-next-line */
2
- import got from 'got';
2
+ import { HttpClient } from "./httpClient.js";
3
3
  export class CoverArtArchiveApi {
4
4
  constructor() {
5
- this.host = 'coverartarchive.org';
5
+ this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 20000 });
6
6
  }
7
7
  async getJson(path) {
8
- const response = await got.get(`https://${this.host}${path}`, {
8
+ const response = await this.httpClient.get(path, {
9
9
  headers: {
10
10
  Accept: "application/json"
11
- },
12
- responseType: 'json'
11
+ }
13
12
  });
14
- return response.body;
13
+ return response.json();
15
14
  }
16
15
  /**
17
16
  * Fetch release
@@ -39,14 +38,13 @@ export class CoverArtArchiveApi {
39
38
  * @param coverType Cover type
40
39
  */
41
40
  async getCovers(releaseId, releaseType = 'release', coverType) {
42
- var _a;
43
41
  const path = [releaseType, releaseId];
44
42
  if (coverType) {
45
43
  path.push(coverType);
46
44
  }
47
45
  const info = await this.getJson(`/${path.join('/')}`);
48
46
  // Hack to correct http addresses into https
49
- if ((_a = info.release) === null || _a === void 0 ? void 0 : _a.startsWith('http:')) {
47
+ if (info.release?.startsWith('http:')) {
50
48
  info.release = `https${info.release.substring(4)}`;
51
49
  }
52
50
  return info;
@@ -1,8 +1,6 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
- import * as crypto from 'node:crypto';
3
- function md5(str) {
4
- return crypto.createHash('md5').update(str).digest('hex'); // lgtm [js/insufficient-password-hash]
5
- }
2
+ import sparkMd5 from 'spark-md5';
3
+ const md5 = sparkMd5.hash;
6
4
  export class DigestAuth {
7
5
  /**
8
6
  * RFC 2617: handle both MD5 and MD5-sess algorithms.
@@ -0,0 +1,27 @@
1
+ export type HttpFormData = {
2
+ [key: string]: string;
3
+ };
4
+ export interface IHttpClientOptions {
5
+ baseUrl: string;
6
+ timeout: number;
7
+ userAgent: string;
8
+ }
9
+ export interface IFetchOptions {
10
+ query?: HttpFormData;
11
+ retryLimit?: number;
12
+ body?: string;
13
+ headers?: HeadersInit;
14
+ followRedirects?: boolean;
15
+ }
16
+ export declare class HttpClient {
17
+ private options;
18
+ private cookieJar;
19
+ constructor(options: IHttpClientOptions);
20
+ get(path: string, options?: IFetchOptions): Promise<Response>;
21
+ post(path: string, options?: IFetchOptions): Promise<Response>;
22
+ postForm(path: string, formData: HttpFormData, options?: IFetchOptions): Promise<Response>;
23
+ postJson(path: string, json: Object, options?: IFetchOptions): Promise<Response>;
24
+ private _fetch;
25
+ private registerCookies;
26
+ getCookies(): Promise<string>;
27
+ }
@@ -0,0 +1,55 @@
1
+ import { CookieJar } from "tough-cookie";
2
+ export class HttpClient {
3
+ constructor(options) {
4
+ this.options = options;
5
+ this.cookieJar = new CookieJar();
6
+ }
7
+ get(path, options) {
8
+ return this._fetch('get', path, options);
9
+ }
10
+ post(path, options) {
11
+ return this._fetch('post', path, options);
12
+ }
13
+ postForm(path, formData, options) {
14
+ const encodedFormData = new URLSearchParams(formData).toString();
15
+ return this._fetch('post', path, { ...options, body: encodedFormData, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
16
+ }
17
+ // biome-ignore lint/complexity/noBannedTypes:
18
+ postJson(path, json, options) {
19
+ const encodedJson = JSON.stringify(json);
20
+ return this._fetch('post', path, { ...options, body: encodedJson, headers: { 'Content-Type': 'application/json.' } });
21
+ }
22
+ async _fetch(method, path, options) {
23
+ if (!options)
24
+ options = {};
25
+ let url = `${this.options.baseUrl}/${path}`;
26
+ if (options.query) {
27
+ url += `?${new URLSearchParams(options.query)}`;
28
+ }
29
+ const cookies = await this.getCookies();
30
+ const headers = {
31
+ ...options.headers,
32
+ 'User-Agent': this.options.userAgent,
33
+ 'Cookie': cookies
34
+ };
35
+ const response = await fetch(url, {
36
+ method,
37
+ ...options,
38
+ headers,
39
+ body: options.body,
40
+ redirect: options.followRedirects === false ? 'manual' : 'follow'
41
+ });
42
+ await this.registerCookies(response);
43
+ return response;
44
+ }
45
+ registerCookies(response) {
46
+ const cookie = response.headers.get('set-cookie');
47
+ if (cookie) {
48
+ return this.cookieJar.setCookie(cookie, response.url);
49
+ }
50
+ }
51
+ getCookies() {
52
+ return this.cookieJar.getCookieString(this.options.baseUrl); // Get cookies for the request
53
+ }
54
+ }
55
+ //# sourceMappingURL=httpClient.js.map
@@ -50,7 +50,7 @@ export interface IMusicBrainzConfig {
50
50
  username?: string;
51
51
  password?: string;
52
52
  };
53
- baseUrl?: string;
53
+ baseUrl: string;
54
54
  appName?: string;
55
55
  appVersion?: string;
56
56
  /**
@@ -74,14 +74,13 @@ export interface ISessionInformation {
74
74
  export declare class MusicBrainzApi {
75
75
  readonly config: IMusicBrainzConfig;
76
76
  private rateLimiter;
77
- private options;
77
+ private httpClient;
78
78
  private session?;
79
79
  static fetchCsrf(html: string): ICsrfSession;
80
80
  private static fetchValue;
81
- private getCookies;
82
81
  constructor(_config?: IMusicBrainzConfig);
83
82
  restGet<T>(relUrl: string, query?: {
84
- [key: string]: any;
83
+ [key: string]: string;
85
84
  }): Promise<T>;
86
85
  /**
87
86
  * Lookup entity
@@ -1,4 +1,3 @@
1
- import * as assert from 'node:assert';
2
1
  import { StatusCodes as HttpStatus } from 'http-status-codes';
3
2
  import Debug from 'debug';
4
3
  export { XmlMetadata } from './xml/xml-metadata.js';
@@ -8,10 +7,8 @@ export { XmlRecording } from './xml/xml-recording.js';
8
7
  import { DigestAuth } from './digest-auth.js';
9
8
  import { RateLimitThreshold } from 'rate-limit-threshold';
10
9
  import * as mb from './musicbrainz.types.js';
11
- import got from 'got';
12
- import { CookieJar } from 'tough-cookie';
10
+ import { HttpClient } from "./httpClient.js";
13
11
  export * from './musicbrainz.types.js';
14
- import { promisify } from 'node:util';
15
12
  const debug = Debug('musicbrainz-api');
16
13
  export class MusicBrainzApi {
17
14
  static fetchCsrf(html) {
@@ -36,33 +33,21 @@ export class MusicBrainzApi {
36
33
  baseUrl: 'https://musicbrainz.org'
37
34
  };
38
35
  Object.assign(this.config, _config);
39
- const cookieJar = new CookieJar();
40
- this.getCookies = promisify(cookieJar.getCookies.bind(cookieJar));
41
- // @ts-ignore
42
- this.options = {
43
- prefixUrl: this.config.baseUrl,
44
- timeout: {
45
- read: 20 * 1000
46
- },
47
- headers: {
48
- 'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
49
- },
50
- cookieJar: cookieJar
51
- };
36
+ this.httpClient = new HttpClient({
37
+ baseUrl: this.config.baseUrl,
38
+ timeout: 20 * 1000,
39
+ userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
40
+ });
52
41
  this.rateLimiter = new RateLimitThreshold(15, 18);
53
42
  }
54
43
  async restGet(relUrl, query = {}) {
55
44
  query.fmt = 'json';
56
45
  await this.applyRateLimiter();
57
- const response = await got.get(`ws/2${relUrl}`, {
58
- ...this.options,
59
- searchParams: query,
60
- responseType: 'json',
61
- retry: {
62
- limit: 10
63
- }
46
+ const response = await this.httpClient.get(`ws/2${relUrl}`, {
47
+ query,
48
+ retryLimit: 10
64
49
  });
65
- return response.body;
50
+ return response.json();
66
51
  }
67
52
  lookup(entity, mbid, inc = []) {
68
53
  return this.restGet(`/${entity}/${mbid}`, { inc: inc.join(' ') });
@@ -91,20 +76,18 @@ export class MusicBrainzApi {
91
76
  const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
92
77
  const path = `ws/2/${entity}/`;
93
78
  // Get digest challenge
94
- let digest;
79
+ let digest = '';
95
80
  let n = 1;
96
81
  const postData = xmlMetadata.toXml();
97
82
  do {
98
83
  await this.applyRateLimiter();
99
- const response = await got.post(path, {
100
- ...this.options,
101
- searchParams: { client: clientId },
84
+ const response = await this.httpClient.post(path, {
85
+ query: { client: clientId },
102
86
  headers: {
103
87
  authorization: digest,
104
88
  'Content-Type': 'application/xml'
105
89
  },
106
- body: postData,
107
- throwHttpErrors: false
90
+ body: postData
108
91
  });
109
92
  if (response.statusCode === HttpStatus.UNAUTHORIZED) {
110
93
  // Respond to digest challenge
@@ -119,15 +102,13 @@ export class MusicBrainzApi {
119
102
  } while (n++ < 5);
120
103
  }
121
104
  async login() {
122
- var _a, _b, _c;
123
- assert.ok((_a = this.config.botAccount) === null || _a === void 0 ? void 0 : _a.username, 'bot username should be set');
124
- assert.ok((_b = this.config.botAccount) === null || _b === void 0 ? void 0 : _b.password, 'bot password should be set');
125
- if ((_c = this.session) === null || _c === void 0 ? void 0 : _c.loggedIn) {
126
- for (const cookie of await this.getCookies(this.options.prefixUrl)) {
127
- if (cookie.key === 'remember_login') {
128
- return true;
129
- }
130
- }
105
+ if (!this.config.botAccount?.username)
106
+ throw new Error('bot username should be set');
107
+ if (!this.config.botAccount?.password)
108
+ throw new Error('bot password should be set');
109
+ if (this.session?.loggedIn) {
110
+ const cookies = await this.httpClient.getCookies();
111
+ return cookies.indexOf('musicbrainz_server_session') !== -1;
131
112
  }
132
113
  this.session = await this.getSession();
133
114
  const redirectUri = '/success';
@@ -136,17 +117,15 @@ export class MusicBrainzApi {
136
117
  password: this.config.botAccount.password,
137
118
  csrf_session_key: this.session.csrf.sessionKey,
138
119
  csrf_token: this.session.csrf.token,
139
- remember_me: 1
120
+ remember_me: '1'
140
121
  };
141
- const response = await got.post('login', {
142
- ...this.options,
143
- followRedirect: false,
144
- searchParams: {
122
+ const response = await this.httpClient.postForm('login', formData, {
123
+ query: {
145
124
  returnto: redirectUri
146
125
  },
147
- form: formData
126
+ followRedirects: false
148
127
  });
149
- const success = response.statusCode === HttpStatus.MOVED_TEMPORARILY && response.headers.location === redirectUri;
128
+ const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
150
129
  if (success) {
151
130
  this.session.loggedIn = true;
152
131
  }
@@ -157,14 +136,13 @@ export class MusicBrainzApi {
157
136
  */
158
137
  async logout() {
159
138
  const redirectUri = '/success';
160
- const response = await got.get('logout', {
161
- ...this.options,
162
- followRedirect: false,
163
- searchParams: {
139
+ const response = await this.httpClient.post('logout', {
140
+ followRedirects: false,
141
+ query: {
164
142
  returnto: redirectUri
165
143
  }
166
144
  });
167
- const success = response.statusCode === HttpStatus.MOVED_TEMPORARILY && response.headers.location === redirectUri;
145
+ const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
168
146
  if (success && this.session) {
169
147
  this.session.loggedIn = true;
170
148
  }
@@ -177,24 +155,21 @@ export class MusicBrainzApi {
177
155
  * @param formData
178
156
  */
179
157
  async editEntity(entity, mbid, formData) {
180
- var _a, _b;
181
158
  await this.applyRateLimiter();
182
159
  this.session = await this.getSession();
183
160
  formData.csrf_session_key = this.session.csrf.sessionKey;
184
161
  formData.csrf_token = this.session.csrf.token;
185
- formData.username = (_a = this.config.botAccount) === null || _a === void 0 ? void 0 : _a.username;
186
- formData.password = (_b = this.config.botAccount) === null || _b === void 0 ? void 0 : _b.password;
162
+ formData.username = this.config.botAccount?.username;
163
+ formData.password = this.config.botAccount?.password;
187
164
  formData.remember_me = 1;
188
- const response = await got.post(`${entity}/${mbid}/edit`, {
189
- ...this.options,
190
- form: formData,
191
- followRedirect: false
165
+ const response = await this.httpClient.postForm(`${entity}/${mbid}/edit`, formData, {
166
+ followRedirects: false
192
167
  });
193
- if (response.statusCode === HttpStatus.OK)
168
+ if (response.status === HttpStatus.OK)
194
169
  throw new Error("Failed to submit form data");
195
- if (response.statusCode === HttpStatus.MOVED_TEMPORARILY)
170
+ if (response.status === HttpStatus.MOVED_TEMPORARILY)
196
171
  return;
197
- throw new Error(`Unexpected status code: ${response.statusCode}`);
172
+ throw new Error(`Unexpected status code: ${response.status}`);
198
173
  }
199
174
  /**
200
175
  * Set URL to recording
@@ -203,14 +178,13 @@ export class MusicBrainzApi {
203
178
  * @param editNote Edit note
204
179
  */
205
180
  async addUrlToRecording(recording, url2add, editNote = '') {
206
- var _a;
207
181
  const formData = {};
208
182
  formData['edit-recording.name'] = recording.title; // Required
209
183
  formData['edit-recording.comment'] = recording.disambiguation;
210
184
  formData['edit-recording.make_votable'] = true;
211
185
  formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId;
212
186
  formData['edit-recording.url.0.text'] = url2add.text;
213
- (_a = recording.isrcs) === null || _a === void 0 ? void 0 : _a.forEach((isrcs, i) => {
187
+ recording.isrcs?.forEach((isrcs, i) => {
214
188
  formData[`edit-recording.isrcs.${i}`] = isrcs;
215
189
  });
216
190
  formData['edit-recording.edit_note'] = editNote;
@@ -246,20 +220,20 @@ export class MusicBrainzApi {
246
220
  * @param editNote Comment to add.
247
221
  */
248
222
  addSpotifyIdToRecording(recording, spotifyId, editNote) {
249
- assert.strictEqual(spotifyId.length, 22);
223
+ if (spotifyId.length !== 22) {
224
+ throw new Error('Invalid Spotify ID length');
225
+ }
250
226
  return this.addUrlToRecording(recording, {
251
227
  linkTypeId: mb.LinkType.stream_for_free,
252
228
  text: `https://open.spotify.com/track/${spotifyId}`
253
229
  }, editNote);
254
230
  }
255
231
  async getSession() {
256
- const response = await got.get('login', {
257
- ...this.options,
258
- followRedirect: false, // Disable redirects
259
- responseType: 'text'
232
+ const response = await this.httpClient.get('login', {
233
+ followRedirects: false
260
234
  });
261
235
  return {
262
- csrf: MusicBrainzApi.fetchCsrf(response.body)
236
+ csrf: MusicBrainzApi.fetchCsrf(await response.text())
263
237
  };
264
238
  }
265
239
  async applyRateLimiter() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musicbrainz-api",
3
- "version": "0.17.0",
3
+ "version": "0.18.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,36 +45,36 @@
45
45
  "url": "https://github.com/Borewit/musicbrainz-api/issues"
46
46
  },
47
47
  "dependencies": {
48
- "@biomejs/biome": "1.8.3",
49
- "@types/caseless": "^0.12.1",
50
- "@types/request-promise-native": "^1.0.17",
51
- "@types/uuid": "^10.0.0",
52
- "caseless": "^0.12.0",
53
48
  "debug": "^4.3.4",
54
- "got": "^14.2.1",
55
49
  "http-status-codes": "^2.1.4",
56
50
  "json-stringify-safe": "^5.0.1",
57
51
  "jsontoxml": "^1.0.1",
58
- "rate-limit-threshold": "^0.1.5",
59
- "source-map-support": "^0.5.16",
52
+ "rate-limit-threshold": "^0.2.0",
53
+ "spark-md5": "^3.0.2",
60
54
  "tough-cookie": "^4.1.3",
61
55
  "uuid": "^10.0.0"
62
56
  },
63
57
  "devDependencies": {
58
+ "@biomejs/biome": "^1.8.3",
64
59
  "@types/chai": "^4.3.0",
65
60
  "@types/jsontoxml": "^1.0.5",
66
61
  "@types/mocha": "^10.0.4",
67
- "@types/node": "^22.1.0",
62
+ "@types/node": "^22.5.0",
68
63
  "@types/sinon": "^17.0.3",
64
+ "@types/source-map-support": "^0",
65
+ "@types/spark-md5": "^3",
66
+ "@types/tough-cookie": "^4.0.5",
67
+ "@types/uuid": "^10.0.0",
69
68
  "c8": "^10.1.2",
70
- "chai": "^5.1.0",
71
- "del-cli": "^5.0.0",
72
- "mocha": "^10.1.0",
69
+ "chai": "^5.1.1",
70
+ "del-cli": "^5.1.0",
71
+ "mocha": "^10.7.3",
73
72
  "remark-cli": "^12.0.0",
74
73
  "remark-preset-lint-recommended": "^7.0.0",
75
74
  "sinon": "^18.0.0",
76
- "ts-node": "^10.0.0",
77
- "typescript": "^5.0.2"
75
+ "source-map-support": "^0.5.21",
76
+ "ts-node": "^10.9.2",
77
+ "typescript": "^5.5.4"
78
78
  },
79
79
  "scripts": {
80
80
  "clean": "del-cli 'lib/**/*.js' 'lib/**/*.js.map' 'lib/**/*.d.ts' 'test/**/*.js' 'test/**/*.js.map'",
@@ -88,7 +88,7 @@
88
88
  "build": "yarn run clean && yarn run compile",
89
89
  "start": "yarn run compile && yarn run lint && yarn run cover-test",
90
90
  "test-coverage": "c8 yarn run test",
91
- "send-codacy": "nyc report --reporter=text-lcov | codacy-coverage"
91
+ "send-codacy": "c8 report --reporter=text-lcov | codacy-coverage"
92
92
  },
93
93
  "nyc": {
94
94
  "exclude": [