musicbrainz-api 0.25.0 → 0.25.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.
@@ -2,7 +2,7 @@
2
2
  import { HttpClient } from "./http-client.js";
3
3
  export class CoverArtArchiveApi {
4
4
  constructor() {
5
- this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 20000, followRedirects: false });
5
+ this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 2000, followRedirects: false });
6
6
  }
7
7
  async getJson(path) {
8
8
  const response = await this.httpClient.get(path, {
@@ -13,7 +13,7 @@ export class HttpClientNode extends HttpClient {
13
13
  return Promise.resolve(undefined);
14
14
  }
15
15
  getCookies() {
16
- return this.cookieJar.getCookieString(this.options.baseUrl); // Get cookies for the request
16
+ return this.cookieJar.getCookieString(this.httpOptions.baseUrl); // Get cookies for the request
17
17
  }
18
18
  }
19
19
  //# sourceMappingURL=http-client-node.js.map
@@ -10,6 +10,9 @@ export type MultiQueryFormData = {
10
10
  };
11
11
  export interface IHttpClientOptions {
12
12
  baseUrl: string;
13
+ /**
14
+ * Retry time-out, default 500 ms
15
+ */
13
16
  timeout: number;
14
17
  userAgent: string;
15
18
  followRedirects?: boolean;
@@ -22,13 +25,15 @@ export interface IFetchOptions {
22
25
  followRedirects?: boolean;
23
26
  }
24
27
  export declare class HttpClient {
25
- protected options: IHttpClientOptions;
26
- constructor(options: IHttpClientOptions);
28
+ protected httpOptions: IHttpClientOptions;
29
+ constructor(httpOptions: IHttpClientOptions);
27
30
  get(path: string, options?: IFetchOptions): Promise<Response>;
28
31
  post(path: string, options?: IFetchOptions): Promise<Response>;
29
32
  postForm(path: string, formData: HttpFormData, options?: IFetchOptions): Promise<Response>;
30
33
  postJson(path: string, json: Object, options?: IFetchOptions): Promise<Response>;
31
34
  private _fetch;
32
- protected registerCookies(response: Response): Promise<Cookie | undefined>;
35
+ private _buildUrl;
36
+ private _delay;
37
+ protected registerCookies(_response: Response): Promise<Cookie | undefined>;
33
38
  getCookies(): Promise<string | null>;
34
39
  }
@@ -1,6 +1,8 @@
1
+ import Debug from "debug";
2
+ const debug = Debug('musicbrainz-api-node');
1
3
  export class HttpClient {
2
- constructor(options) {
3
- this.options = options;
4
+ constructor(httpOptions) {
5
+ this.httpOptions = httpOptions;
4
6
  }
5
7
  get(path, options) {
6
8
  return this._fetch('get', path, options);
@@ -12,7 +14,6 @@ export class HttpClient {
12
14
  const encodedFormData = new URLSearchParams(formData).toString();
13
15
  return this._fetch('post', path, { ...options, body: encodedFormData, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
14
16
  }
15
- // biome-ignore lint/complexity/noBannedTypes:
16
17
  postJson(path, json, options) {
17
18
  const encodedJson = JSON.stringify(json);
18
19
  return this._fetch('post', path, { ...options, body: encodedJson, headers: { 'Content-Type': 'application/json.' } });
@@ -20,34 +21,58 @@ export class HttpClient {
20
21
  async _fetch(method, path, options) {
21
22
  if (!options)
22
23
  options = {};
23
- let url = path.startsWith('/') ? `${this.options.baseUrl}${path}` : `${this.options.baseUrl}/${path}`;
24
- if (options.query) {
25
- const urlSearchParams = new URLSearchParams();
26
- for (const key of Object.keys(options.query)) {
27
- const value = options.query[key];
28
- (Array.isArray(value) ? value : [value]).forEach(value => {
29
- urlSearchParams.append(key, value);
30
- });
31
- }
32
- url += `?${urlSearchParams.toString()}`;
33
- }
24
+ let retryLimit = options.retryLimit && options.retryLimit > 1 ? options.retryLimit : 1;
25
+ const retryTimeout = this.httpOptions.timeout ? this.httpOptions.timeout : 500;
26
+ const url = this._buildUrl(path, options.query);
34
27
  const cookies = await this.getCookies();
35
28
  const headers = new Headers(options.headers);
36
- headers.set('User-Agent', this.options.userAgent);
29
+ headers.set('User-Agent', this.httpOptions.userAgent);
37
30
  if (cookies !== null) {
38
31
  headers.set('Cookie', cookies);
39
32
  }
40
- const response = await fetch(url, {
41
- method,
42
- ...options,
43
- headers,
44
- body: options.body,
45
- redirect: options.followRedirects === false ? 'manual' : 'follow'
46
- });
47
- await this.registerCookies(response);
48
- return response;
49
- }
50
- registerCookies(response) {
33
+ while (retryLimit > 0) {
34
+ const response = await fetch(url, {
35
+ method,
36
+ ...options,
37
+ headers,
38
+ body: options.body,
39
+ redirect: options.followRedirects === false ? 'manual' : 'follow'
40
+ });
41
+ if (response.status === 429 || response.status === 503) {
42
+ debug(`Received status=${response.status}, assume reached rate limit, retry in ${retryTimeout} ms`);
43
+ retryLimit--;
44
+ if (retryLimit > 0) {
45
+ await this._delay(retryTimeout); // wait 200ms before retry
46
+ continue;
47
+ }
48
+ }
49
+ await this.registerCookies(response);
50
+ return response;
51
+ }
52
+ throw new Error(`Failed to fetch ${url} after retries`);
53
+ }
54
+ // Helper: Builds URL with query string
55
+ _buildUrl(path, query) {
56
+ let url = path.startsWith('/')
57
+ ? `${this.httpOptions.baseUrl}${path}`
58
+ : `${this.httpOptions.baseUrl}/${path}`;
59
+ if (query) {
60
+ const urlSearchParams = new URLSearchParams();
61
+ for (const key of Object.keys(query)) {
62
+ const value = query[key];
63
+ (Array.isArray(value) ? value : [value]).forEach(v => {
64
+ urlSearchParams.append(key, v);
65
+ });
66
+ }
67
+ url += `?${urlSearchParams.toString()}`;
68
+ }
69
+ return url;
70
+ }
71
+ // Helper: Delays execution
72
+ _delay(ms) {
73
+ return new Promise(resolve => setTimeout(resolve, ms));
74
+ }
75
+ registerCookies(_response) {
51
76
  return Promise.resolve(undefined);
52
77
  }
53
78
  async getCookies() {
@@ -1,5 +1,4 @@
1
1
  import { StatusCodes as HttpStatus } from 'http-status-codes';
2
- import Debug from 'debug';
3
2
  export { XmlMetadata } from './xml/xml-metadata.js';
4
3
  export { XmlIsrc } from './xml/xml-isrc.js';
5
4
  export { XmlIsrcList } from './xml/xml-isrc-list.js';
@@ -11,12 +10,11 @@ export * from './http-client.js';
11
10
  /*
12
11
  * https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2#Subqueries
13
12
  */
14
- const debug = Debug('musicbrainz-api-node');
15
13
  export class MusicBrainzApi extends MusicBrainzApiDefault {
16
14
  initHttpClient() {
17
15
  return new HttpClientNode({
18
16
  baseUrl: this.config.baseUrl,
19
- timeout: 20 * 1000,
17
+ timeout: 500,
20
18
  userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
21
19
  });
22
20
  }
@@ -41,7 +41,7 @@ export class MusicBrainzApi {
41
41
  initHttpClient() {
42
42
  return new HttpClient({
43
43
  baseUrl: this.config.baseUrl,
44
- timeout: 20 * 1000,
44
+ timeout: 500,
45
45
  userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
46
46
  });
47
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musicbrainz-api",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "MusicBrainz API client for reading and submitting metadata",
5
5
  "exports": {
6
6
  "node": {
@@ -61,11 +61,11 @@
61
61
  "uuid": "^11.0.3"
62
62
  },
63
63
  "devDependencies": {
64
- "@biomejs/biome": "^1.8.3",
64
+ "@biomejs/biome": "2.1.2",
65
65
  "@types/chai": "^5.0.0",
66
66
  "@types/jsontoxml": "^1.0.5",
67
67
  "@types/mocha": "^10.0.4",
68
- "@types/node": "^22.5.0",
68
+ "@types/node": "^24.0.3",
69
69
  "@types/sinon": "^17.0.3",
70
70
  "@types/source-map-support": "^0",
71
71
  "@types/spark-md5": "^3",
@@ -77,7 +77,7 @@
77
77
  "mocha": "^11.0.1",
78
78
  "remark-cli": "^12.0.0",
79
79
  "remark-preset-lint-recommended": "^7.0.0",
80
- "sinon": "^20.0.0",
80
+ "sinon": "^21.0.0",
81
81
  "source-map-support": "^0.5.21",
82
82
  "ts-node": "^10.9.2",
83
83
  "typescript": "^5.5.4"
@@ -96,7 +96,8 @@
96
96
  "start": "yarn run compile && yarn run lint && yarn run cover-test",
97
97
  "test-coverage": "c8 yarn run test",
98
98
  "send-codacy": "c8 report --reporter=text-lcov | codacy-coverage",
99
- "prepublishOnly": "yarn run build"
99
+ "prepublishOnly": "yarn run build",
100
+ "update-biome": "yarn add -D --exact @biomejs/biome && npx @biomejs/biome migrate --write && npm run lint:fix"
100
101
  },
101
102
  "nyc": {
102
103
  "exclude": [