musicbrainz-api 0.17.0 → 0.18.1

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,98 +1,121 @@
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**: Retrieve detailed metadata from the [MusicBrainz database](https://musicbrainz.org/).
18
+ - **Submit metadata**: Easily submit new metadata to [MusicBrainz](https://musicbrainz.org/).
19
+ - **Smart throttling**: Implements intelligent throttling, allowing bursts of requests while adhering to [MusicBrainz rate limits](https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting).
20
+ - **TypeScript Definitions**: Fully typed with built-in [TypeScript](https://www.typescriptlang.org/) definitions for a seamless development experience.
20
21
 
21
- ### Hint
22
+ ## Compatibility
23
+
24
+ Module: version 8 migrated from [CommonJS](https://en.wikipedia.org/wiki/CommonJS) to [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
+ ### Requirements
28
+ - Node.js: Requires [Node.js version 16](https://nodejs.org/en/about/previous-releases) or higher.
29
+ - Browser: Can be used in browser environments when bundled with a module bundler (not actively tested).
30
+
31
+ > [!NOTE]
32
+ > We are looking into making this package usable in the browser as well.
22
33
 
23
- This package is currently only developed for the use in a [node.js environment](http://nodejs.org/).
24
- We are looking into making this package usable in the browser as well.
34
+ ## Support the Project
35
+ If you find this project useful and would like to support its development, consider sponsoring or contributing:
36
+
37
+ - [Become a sponsor to Borewit](https://github.com/sponsors/Borewit)
38
+
39
+ - Buy me a coffee:
40
+
41
+ <a href="https://www.buymeacoffee.com/borewit" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy me A coffee" height="41" width="174"></a>
42
+
43
+ ## Getting Started
25
44
 
26
- ## Before using this library
45
+ ### Identifying Your Application
27
46
 
28
- MusicBrainz asks that you to [identify your application](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#User%20Data) by filling in the ['User-Agent' Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent).
29
- By passing `appName`, `appVersion`, `appMail` musicbrainz-api takes care of that.
47
+ MusicBrainz requires all API clients to [identify their application](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#User%20Data).
48
+ Ensure you set the User-Agent header by providing `appName`, `appVersion`, and `appContactInfo` when configuring the client.
49
+ This library will automatically handle this for you.
30
50
 
31
- ## Submitting metadata
51
+ ### Submitting metadata
32
52
 
33
53
  If you plan to use this module for submitting metadata, please ensure you comply with [the MusicBrainz Code of conduct/Bots](https://wiki.musicbrainz.org/Code_of_Conduct/Bots).
34
54
 
35
- ## Example
55
+ ## Example Usage
56
+
57
+ ### Importing the Library
36
58
 
37
- Example, how to import 'musicbrainz-api:
38
59
  ```js
39
- import {MusicBrainzApi} from 'musicbrainz-api';
60
+ import { MusicBrainzApi } from 'musicbrainz-api';
40
61
 
41
62
  const mbApi = new MusicBrainzApi({
42
- appName: 'my-app',
43
- appVersion: '0.1.0',
44
- appContactInfo: 'user@mail.org'
63
+ appName: 'my-app',
64
+ appVersion: '0.1.0',
65
+ appContactInfo: 'user@mail.org',
45
66
  });
46
67
  ```
47
68
 
48
- The following configuration settings can be passed
49
- ```js
50
- import {MusicBrainzApi} from 'musicbrainz-api';
69
+ ### Configuration Options
51
70
 
71
+ ```js
52
72
  const config = {
53
- // MusicBrainz bot account username & password (optional)
54
- botAccount: {
55
- username: 'myUserName_bot',
56
- password: 'myPassword'
57
- },
58
-
59
- // API base URL, default: 'https://musicbrainz.org' (optional)
60
- baseUrl: 'https://musicbrainz.org',
61
-
62
- appName: 'my-app',
63
- appVersion: '0.1.0',
64
-
65
- // Optional, default: no proxy server
66
- proxy: {
67
- host: 'localhost',
68
- port: 8888
69
- },
70
-
71
- // Your e-mail address, required for submitting ISRCs
72
- appMail: string,
73
+ // Optional: MusicBrainz bot account credentials
74
+ botAccount: {
75
+ username: 'myUserName_bot',
76
+ password: 'myPassword',
77
+ },
78
+
79
+ // Optional: API base URL (default: 'https://musicbrainz.org')
80
+ baseUrl: 'https://musicbrainz.org',
81
+
82
+ // Required: Application details
83
+ appName: 'my-app',
84
+ appVersion: '0.1.0',
85
+ appMail: 'user@mail.org',
86
+
87
+ // Optional: Proxy settings (default: no proxy server)
88
+ proxy: {
89
+ host: 'localhost',
90
+ port: 8888,
91
+ },
92
+
93
+ // Optional: Disable rate limiting (default: false)
94
+ disableRateLimiting: false,
95
+ };
96
+
97
+ const mbApi = new MusicBrainzApi(config);
98
+ ```
73
99
 
74
- // Helpful if you have your own MusicBrainz server, default: false (optional)
75
- disableRateLimiting: false,
76
- }
100
+ ## Accessing MusicBrainz Data
77
101
 
78
- const mbApi = new MusicbrainzApi(config);
79
- ```
102
+ The MusicBrainz API allows you to look up various entities. Here’s how to use the lookup function:
80
103
 
81
104
  ## Lookup MusicBrainz Entities
82
105
 
83
106
  MusicBrainz API documentation: [XML Web Service/Version 2 Lookups](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#Lookups)
84
107
 
85
- ### Generic lookup function
86
-
87
- 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
108
+ ### Lookup Function
91
109
 
92
110
  ```js
93
111
  const artist = await mbApi.lookup('artist', 'ab2528d9-719f-4261-8098-21849222a0f2');
94
112
  ```
95
113
 
114
+ Arguments:
115
+ - entity: `'area'` | `'artist'` | `'collection'` | `'instrument'` | `'label'` | `'place'` | `'release'` | `'release-group'` | `'recording'` | `'series'` | `'work'` | `'url'` | `'event'`
116
+ - MBID [(MusicBrainz identifier)](https://wiki.musicbrainz.org/MusicBrainz_Identifier)
117
+ - query
118
+
96
119
 
97
120
  | Query argument | Query value |
98
121
  |-----------------------|-----------------|
@@ -263,19 +286,19 @@ There are different search fields depending on the entity.
263
286
  Searches can be performed using the generic search function: `query(entity: mb.EntityType, query: string | IFormData, offset?: number, limit?: number): Promise<entity>`
264
287
 
265
288
  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.
289
+ - Entity type, which can be one of:
290
+ - `artist`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Artist)
291
+ - `label`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Label)
292
+ - `recording`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Recording)
293
+ - `release`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release)
294
+ - `release-group`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release_Group)
295
+ - `work`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Work)
296
+ - `area`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Area)
297
+ - `url`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#URL)
298
+ - `query {query: string, offset: number, limit: number}`
299
+ - `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.
300
+ - `query.offset`: optional, return search results starting at a given offset. Used for paging through more than one page of results.
301
+ - `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
302
 
280
303
  For example, to find any recordings of _'We Will Rock You'_ by Queen:
281
304
  ```js
@@ -348,8 +371,7 @@ For all of the following function you need to use a dedicated bot account.
348
371
 
349
372
  ## Submitting ISRC via post user form-data
350
373
 
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.
374
+ Use with caution, and only on a test server, it may clear existing metadata has side effect.
353
375
 
354
376
  ```js
355
377
 
@@ -392,46 +414,52 @@ await mbApi.addSpotifyIdToRecording(recording, '2AMysGXOe0zzZJMtH3Nizb');
392
414
 
393
415
  ## Cover Art Archive API
394
416
 
395
- Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
417
+ This library also supports the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
396
418
 
397
- ### Release Cover Art
419
+ ### Fetch Release Cover Art
398
420
  ```js
399
- import {CoverArtArchiveApi} from 'musicbrainz-api';
421
+ import { CoverArtArchiveApi } from 'musicbrainz-api';
400
422
 
401
- coverArtArchiveApiClient.getReleaseCovers(releaseMbid).then(releaseCoverInfo => {
402
- console.log('Release cover info', releaseCoverInfo);
403
- });
404
-
405
- coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'front').then(releaseCoverInfo => {
406
- console.log('Get best front cover', releaseCoverInfo);
407
- });
423
+ const coverArtArchiveApiClient = new CoverArtArchiveApi();
408
424
 
409
- coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'back').then(releaseCoverInfo => {
410
- console.log('Get best back cover', releaseCoverInfo);
411
- });
425
+ async function getReleaseCoverArt(releaseMbid, coverType = '') {
426
+ try {
427
+ const coverInfo = await coverArtArchiveApiClient.getReleaseCovers(releaseMbid, coverType);
428
+ console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
429
+ } catch (error) {
430
+ console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
431
+ }
432
+ }
412
433
 
434
+ (async () => {
435
+ const releaseMbid = 'your-release-mbid-here'; // Replace with actual MBID
436
+ await getReleaseCoverArt(releaseMbid); // Get all covers
437
+ await getReleaseCoverArt(releaseMbid, 'front'); // Get best front cover
438
+ await getReleaseCoverArt(releaseMbid, 'back'); // Get best back cover
439
+ })();
413
440
  ```
414
441
 
415
442
  ### Release Group Cover Art
416
443
  ```js
417
- import {CoverArtArchiveApi} from 'musicbrainz-api';
444
+ import { CoverArtArchiveApi } from 'musicbrainz-api';
418
445
 
419
- coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid).then(releaseGroupCoverInfo => {
420
- console.log('Release cover info', releaseGroupCoverInfo);
421
- });
446
+ const coverArtArchiveApiClient = new CoverArtArchiveApi();
422
447
 
423
- coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'front').then(releaseGroupCoverInfo => {
424
- console.log('Get best front cover', releaseGroupCoverInfo);
425
- });
448
+ async function getCoverArt(releaseGroupMbid, coverType = '') {
449
+ try {
450
+ const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, coverType);
451
+ console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
452
+ } catch (error) {
453
+ console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
454
+ }
455
+ }
426
456
 
427
- coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'back').then(releaseGroupCoverInfo => {
428
- console.log('Get best back cover', releaseGroupCoverInfo);
429
- });
457
+ (async () => {
458
+ const releaseGroupMbid = 'your-release-group-mbid-here'; // Replace with actual MBID
459
+ await getCoverArt(releaseGroupMbid); // Get all covers
460
+ await getCoverArt(releaseGroupMbid, 'front'); // Get best front cover
461
+ await getCoverArt(releaseGroupMbid, 'back'); // Get best back cover
462
+ })();
430
463
 
431
464
  ```
432
465
 
433
-
434
- ## Compatibility
435
-
436
- The JavaScript in runtime is compliant with [ECMAScript 2017 (ES8)](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017).
437
- Requires [Node.js®](https://nodejs.org/) version 6 or higher.
@@ -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.1",
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": [