musicbrainz-api 0.25.0 → 0.26.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/lib/coverartarchive-api.js +1 -1
- package/lib/http-client-node.js +1 -1
- package/lib/http-client.d.ts +8 -3
- package/lib/http-client.js +68 -26
- package/lib/musicbrainz-api-node.js +1 -3
- package/lib/musicbrainz-api.js +7 -1
- package/lib/musicbrainz.types.d.ts +22 -11
- package/package.json +10 -9
|
@@ -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:
|
|
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, {
|
package/lib/http-client-node.js
CHANGED
|
@@ -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.
|
|
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
|
package/lib/http-client.d.ts
CHANGED
|
@@ -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
|
|
26
|
-
constructor(
|
|
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
|
-
|
|
35
|
+
private _buildUrl;
|
|
36
|
+
private _delay;
|
|
37
|
+
protected registerCookies(_response: Response): Promise<Cookie | undefined>;
|
|
33
38
|
getCookies(): Promise<string | null>;
|
|
34
39
|
}
|
package/lib/http-client.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import Debug from "debug";
|
|
2
|
+
const debug = Debug('musicbrainz-api-node');
|
|
3
|
+
function isConnectionReset(err) {
|
|
4
|
+
// Undici puts the OS error on .cause, with .code like 'ECONNRESET'
|
|
5
|
+
const code = err?.cause?.code ?? err?.code;
|
|
6
|
+
// Add other transient codes you consider safe to retry:
|
|
7
|
+
return typeof code === "string" && code === 'ECONNRESET';
|
|
8
|
+
}
|
|
1
9
|
export class HttpClient {
|
|
2
|
-
constructor(
|
|
3
|
-
this.
|
|
10
|
+
constructor(httpOptions) {
|
|
11
|
+
this.httpOptions = httpOptions;
|
|
4
12
|
}
|
|
5
13
|
get(path, options) {
|
|
6
14
|
return this._fetch('get', path, options);
|
|
@@ -12,7 +20,6 @@ export class HttpClient {
|
|
|
12
20
|
const encodedFormData = new URLSearchParams(formData).toString();
|
|
13
21
|
return this._fetch('post', path, { ...options, body: encodedFormData, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
|
14
22
|
}
|
|
15
|
-
// biome-ignore lint/complexity/noBannedTypes:
|
|
16
23
|
postJson(path, json, options) {
|
|
17
24
|
const encodedJson = JSON.stringify(json);
|
|
18
25
|
return this._fetch('post', path, { ...options, body: encodedJson, headers: { 'Content-Type': 'application/json.' } });
|
|
@@ -20,34 +27,69 @@ export class HttpClient {
|
|
|
20
27
|
async _fetch(method, path, options) {
|
|
21
28
|
if (!options)
|
|
22
29
|
options = {};
|
|
23
|
-
let
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
}
|
|
30
|
+
let retryLimit = options.retryLimit && options.retryLimit > 1 ? options.retryLimit : 1;
|
|
31
|
+
const retryTimeout = this.httpOptions.timeout ? this.httpOptions.timeout : 500;
|
|
32
|
+
const url = this._buildUrl(path, options.query);
|
|
34
33
|
const cookies = await this.getCookies();
|
|
35
34
|
const headers = new Headers(options.headers);
|
|
36
|
-
headers.set('User-Agent', this.
|
|
35
|
+
headers.set('User-Agent', this.httpOptions.userAgent);
|
|
37
36
|
if (cookies !== null) {
|
|
38
37
|
headers.set('Cookie', cookies);
|
|
39
38
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
while (retryLimit > 0) {
|
|
40
|
+
let response;
|
|
41
|
+
try {
|
|
42
|
+
response = await fetch(url, {
|
|
43
|
+
method,
|
|
44
|
+
...options,
|
|
45
|
+
headers,
|
|
46
|
+
body: options.body,
|
|
47
|
+
redirect: options.followRedirects === false ? 'manual' : 'follow'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (isConnectionReset(err)) {
|
|
52
|
+
// Retry on TCP connection resets
|
|
53
|
+
await this._delay(retryTimeout); // wait 200ms before retry
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
if (response.status === 429 || response.status === 503) {
|
|
59
|
+
debug(`Received status=${response.status}, assume reached rate limit, retry in ${retryTimeout} ms`);
|
|
60
|
+
retryLimit--;
|
|
61
|
+
if (retryLimit > 0) {
|
|
62
|
+
await this._delay(retryTimeout); // wait 200ms before retry
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await this.registerCookies(response);
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Failed to fetch ${url} after retries`);
|
|
70
|
+
}
|
|
71
|
+
// Helper: Builds URL with query string
|
|
72
|
+
_buildUrl(path, query) {
|
|
73
|
+
let url = path.startsWith('/')
|
|
74
|
+
? `${this.httpOptions.baseUrl}${path}`
|
|
75
|
+
: `${this.httpOptions.baseUrl}/${path}`;
|
|
76
|
+
if (query) {
|
|
77
|
+
const urlSearchParams = new URLSearchParams();
|
|
78
|
+
for (const key of Object.keys(query)) {
|
|
79
|
+
const value = query[key];
|
|
80
|
+
(Array.isArray(value) ? value : [value]).forEach(v => {
|
|
81
|
+
urlSearchParams.append(key, v);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
url += `?${urlSearchParams.toString()}`;
|
|
85
|
+
}
|
|
86
|
+
return url;
|
|
87
|
+
}
|
|
88
|
+
// Helper: Delays execution
|
|
89
|
+
_delay(ms) {
|
|
90
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
91
|
+
}
|
|
92
|
+
registerCookies(_response) {
|
|
51
93
|
return Promise.resolve(undefined);
|
|
52
94
|
}
|
|
53
95
|
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:
|
|
17
|
+
timeout: 500,
|
|
20
18
|
userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
21
19
|
});
|
|
22
20
|
}
|
package/lib/musicbrainz-api.js
CHANGED
|
@@ -41,7 +41,7 @@ export class MusicBrainzApi {
|
|
|
41
41
|
initHttpClient() {
|
|
42
42
|
return new HttpClient({
|
|
43
43
|
baseUrl: this.config.baseUrl,
|
|
44
|
-
timeout:
|
|
44
|
+
timeout: 500,
|
|
45
45
|
userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -74,6 +74,12 @@ export class MusicBrainzApi {
|
|
|
74
74
|
// Serialize include parameter
|
|
75
75
|
query.inc = inc.join(' ');
|
|
76
76
|
}
|
|
77
|
+
for (const pipedFilter of ['type', 'status']) {
|
|
78
|
+
if (query[pipedFilter]) {
|
|
79
|
+
// Serialize type parameter
|
|
80
|
+
query[pipedFilter] = query[pipedFilter].join('|');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
77
83
|
return this.restGet(`/${entity}`, query);
|
|
78
84
|
}
|
|
79
85
|
search(entity, query) {
|
|
@@ -46,7 +46,7 @@ export interface IMatch {
|
|
|
46
46
|
score: number;
|
|
47
47
|
}
|
|
48
48
|
export type Gender = 'male' | 'female' | 'other' | 'not applicable';
|
|
49
|
-
export interface IArtist extends ITypedEntity {
|
|
49
|
+
export interface IArtist extends ITypedEntity, IMayHaveRelations {
|
|
50
50
|
name: string;
|
|
51
51
|
disambiguation: string;
|
|
52
52
|
'sort-name': string;
|
|
@@ -60,7 +60,6 @@ export interface IArtist extends ITypedEntity {
|
|
|
60
60
|
area?: IArea;
|
|
61
61
|
begin_area?: IArea;
|
|
62
62
|
end_area?: IArea;
|
|
63
|
-
relations?: IRelation[];
|
|
64
63
|
/**
|
|
65
64
|
* Only defined if 'releases' are includes
|
|
66
65
|
*/
|
|
@@ -104,7 +103,7 @@ export interface IInstrument extends ITypedEntity {
|
|
|
104
103
|
export type ReleaseQuality = 'normal' | 'high';
|
|
105
104
|
export type ReleaseStatus = 'Official' | 'Promotion' | 'Bootleg' | 'Pseudo-release' | 'Withdrawn' | 'Expunged' | 'Cancelled';
|
|
106
105
|
export type ReleasePackaging = 'Book' | 'Box' | 'Cardboard/Paper Sleeve' | 'Cassette Case' | 'Clamshell Case' | 'Digibook' | 'Digifile' | 'Digipak' | 'Discbox Slider' | 'Fatbox' | 'Gatefold Cover' | 'Jewel case' | 'Keep Case' | 'Longbox' | 'Metal Tin' | 'Plastic sleeve' | 'Slidepack' | 'Slim Jewel Case' | 'Snap Case' | 'SnapPack' | 'Super Jewel Box' | 'Other' | 'None';
|
|
107
|
-
export interface IRelease extends IEntity {
|
|
106
|
+
export interface IRelease extends IEntity, IMayHaveRelations {
|
|
108
107
|
title: string;
|
|
109
108
|
'text-representation': {
|
|
110
109
|
'language': string;
|
|
@@ -123,7 +122,6 @@ export interface IRelease extends IEntity {
|
|
|
123
122
|
country: string;
|
|
124
123
|
quality: ReleaseQuality;
|
|
125
124
|
barcode: string;
|
|
126
|
-
relations?: IRelation[];
|
|
127
125
|
'artist-credit'?: IArtistCredit[];
|
|
128
126
|
'release-group'?: IReleaseGroup;
|
|
129
127
|
collections?: ICollection[];
|
|
@@ -135,14 +133,13 @@ export interface IReleaseEvent {
|
|
|
135
133
|
date?: string;
|
|
136
134
|
}
|
|
137
135
|
export type MediaFormatType = 'Digital Media';
|
|
138
|
-
export interface IRecording extends IEntity {
|
|
136
|
+
export interface IRecording extends IEntity, IMayHaveRelations {
|
|
139
137
|
video: boolean;
|
|
140
138
|
length: number;
|
|
141
139
|
title: string;
|
|
142
140
|
disambiguation: string;
|
|
143
141
|
isrcs?: string[];
|
|
144
142
|
releases?: IRelease[];
|
|
145
|
-
relations?: IRelation[];
|
|
146
143
|
'artist-credit'?: IArtistCredit[];
|
|
147
144
|
aliases?: IAlias[];
|
|
148
145
|
'first-release-date': string;
|
|
@@ -273,7 +270,7 @@ export interface IRelation {
|
|
|
273
270
|
url?: IUrl;
|
|
274
271
|
release?: IRelease;
|
|
275
272
|
}
|
|
276
|
-
export interface
|
|
273
|
+
export interface IMayHaveRelations {
|
|
277
274
|
relations: IRelation[];
|
|
278
275
|
}
|
|
279
276
|
export interface IWork extends IEntity {
|
|
@@ -301,10 +298,9 @@ export interface ISeries extends ITypedEntity {
|
|
|
301
298
|
export interface ITag {
|
|
302
299
|
name: string;
|
|
303
300
|
}
|
|
304
|
-
export interface IUrl extends IEntity {
|
|
301
|
+
export interface IUrl extends IEntity, IMayHaveRelations {
|
|
305
302
|
id: string;
|
|
306
303
|
resource: string;
|
|
307
|
-
relations?: IRelationList[];
|
|
308
304
|
}
|
|
309
305
|
export interface IExernalIds {
|
|
310
306
|
[type: string]: string;
|
|
@@ -321,6 +317,14 @@ export type OtherEntityTypes = 'annotation' | 'cdstub' | 'tag';
|
|
|
321
317
|
*/
|
|
322
318
|
export type EntityType = 'annotation' | 'area' | 'artist' | 'collection' | 'event' | 'instrument' | 'label' | 'place' | 'recording' | 'release' | 'release-group' | 'series' | 'work' | 'url';
|
|
323
319
|
export type Relationships = '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';
|
|
320
|
+
/**
|
|
321
|
+
* Ref: https://musicbrainz.org/doc/MusicBrainz_API#Release_.28Group.29_Type_and_Status
|
|
322
|
+
*/
|
|
323
|
+
export type ReleaseStatusQuery = 'official' | 'promotion' | 'bootleg' | 'pseudo-release' | 'withdrawn' | 'cancelled';
|
|
324
|
+
/**
|
|
325
|
+
* Ref: https://musicbrainz.org/doc/MusicBrainz_API#Release_.28Group.29_Type_and_Status
|
|
326
|
+
*/
|
|
327
|
+
export type ReleaseTypeQuery = 'album' | 'single' | 'ep' | 'broadcast' | 'other' | 'audiobook' | 'compilation' | 'demo' | 'dj-mix' | 'field recording' | 'interview' | 'live' | 'mixtape/street' | 'remix' | 'soundtrack' | 'spokenword';
|
|
324
328
|
export declare enum LinkType {
|
|
325
329
|
license = 302,
|
|
326
330
|
production = 256,
|
|
@@ -346,6 +350,13 @@ export interface IPagination {
|
|
|
346
350
|
*/
|
|
347
351
|
limit?: number;
|
|
348
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Release and release-group types
|
|
355
|
+
*/
|
|
356
|
+
export interface IReleaseTypeAndStatus {
|
|
357
|
+
status?: ReleaseStatusQuery[];
|
|
358
|
+
type?: ReleaseTypeQuery[];
|
|
359
|
+
}
|
|
349
360
|
/**
|
|
350
361
|
* https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Artist
|
|
351
362
|
*/
|
|
@@ -510,7 +521,7 @@ interface BrowseReleasesEntityParams {
|
|
|
510
521
|
track_artist: string;
|
|
511
522
|
work: string;
|
|
512
523
|
}
|
|
513
|
-
export type IBrowseReleasesQuery = IPagination & OneOf<BrowseReleasesEntityParams>;
|
|
524
|
+
export type IBrowseReleasesQuery = IPagination & IReleaseTypeAndStatus & OneOf<BrowseReleasesEntityParams>;
|
|
514
525
|
/**
|
|
515
526
|
* List of entity names allowed for browsing artists by a single MBID.
|
|
516
527
|
* Used as a key set for constructing exclusive query types.
|
|
@@ -591,7 +602,7 @@ interface BrowseReleaseGroupsEntityParams {
|
|
|
591
602
|
collection: string;
|
|
592
603
|
release: string;
|
|
593
604
|
}
|
|
594
|
-
export type IBrowseReleaseGroupsQuery = IPagination & OneOf<BrowseReleaseGroupsEntityParams>;
|
|
605
|
+
export type IBrowseReleaseGroupsQuery = IPagination & IReleaseTypeAndStatus & OneOf<BrowseReleaseGroupsEntityParams>;
|
|
595
606
|
/**
|
|
596
607
|
* List of entity names allowed for browsing works by a single MBID.
|
|
597
608
|
* Used as a key set for constructing exclusive query types.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musicbrainz-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "MusicBrainz API client for reading and submitting metadata",
|
|
5
5
|
"exports": {
|
|
6
6
|
"node": {
|
|
@@ -58,26 +58,26 @@
|
|
|
58
58
|
"rate-limit-threshold": "^0.2.0",
|
|
59
59
|
"spark-md5": "^3.0.2",
|
|
60
60
|
"tough-cookie": "^5.0.0",
|
|
61
|
-
"uuid": "^
|
|
61
|
+
"uuid": "^13.0.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@biomejs/biome": "
|
|
64
|
+
"@biomejs/biome": "2.3.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": "^
|
|
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",
|
|
72
72
|
"@types/tough-cookie": "^4.0.5",
|
|
73
73
|
"@types/uuid": "^10.0.0",
|
|
74
74
|
"c8": "^10.1.2",
|
|
75
|
-
"chai": "^
|
|
76
|
-
"del-cli": "^
|
|
75
|
+
"chai": "^6.2.0",
|
|
76
|
+
"del-cli": "^7.0.0",
|
|
77
77
|
"mocha": "^11.0.1",
|
|
78
78
|
"remark-cli": "^12.0.0",
|
|
79
79
|
"remark-preset-lint-recommended": "^7.0.0",
|
|
80
|
-
"sinon": "^
|
|
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"
|
|
@@ -89,14 +89,15 @@
|
|
|
89
89
|
"compile": "yarn run compile-lib && yarn run compile-test",
|
|
90
90
|
"lint:md": "remark -u preset-lint-recommended .",
|
|
91
91
|
"lint:ts": "biome check",
|
|
92
|
-
"lint:fix": "biome check --
|
|
92
|
+
"lint:fix": "biome check --write",
|
|
93
93
|
"lint": "yarn run lint:md && yarn run lint:ts",
|
|
94
94
|
"test": "mocha",
|
|
95
95
|
"build": "yarn run clean && yarn run compile",
|
|
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": [
|