musicbrainz-api 0.19.1 → 0.20.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 +2 -2
- package/lib/coverartarchive-api.js +1 -1
- package/lib/entry-default.cjs +5 -0
- package/lib/{index.js → entry-default.js} +1 -1
- package/lib/entry-node.cjs +5 -0
- package/lib/entry-node.d.ts +2 -0
- package/lib/entry-node.js +3 -0
- package/lib/http-client-node.d.ts +11 -0
- package/lib/http-client-node.js +19 -0
- package/lib/{httpClient.d.ts → http-client.d.ts} +4 -4
- package/lib/{httpClient.js → http-client.js} +9 -14
- package/lib/musicbrainz-api-node.d.ts +16 -0
- package/lib/musicbrainz-api-node.js +71 -0
- package/lib/musicbrainz-api.d.ts +8 -10
- package/lib/musicbrainz-api.js +6 -50
- package/package.json +16 -9
- package/lib/default.cjs +0 -5
- /package/lib/{index.d.ts → entry-default.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -308,9 +308,9 @@ Arguments:
|
|
|
308
308
|
- `query.offset`: optional, return search results starting at a given offset. Used for paging through more than one page of results.
|
|
309
309
|
- `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.
|
|
310
310
|
|
|
311
|
-
For example, to
|
|
311
|
+
For example, to search for _release-group_: _"We Will Rock You"_ by _Queen_:
|
|
312
312
|
```js
|
|
313
|
-
const query = 'query="We Will Rock You"
|
|
313
|
+
const query = 'query=artist:"Queen" AND release:"We Will Rock You"';
|
|
314
314
|
const result = await mbApi.search('release-group', {query});
|
|
315
315
|
```
|
|
316
316
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable-next-line */
|
|
2
|
-
import { HttpClient } from "./
|
|
2
|
+
import { HttpClient } from "./http-client.js";
|
|
3
3
|
export class CoverArtArchiveApi {
|
|
4
4
|
constructor() {
|
|
5
5
|
this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 20000 });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Cookie } from "tough-cookie";
|
|
2
|
+
import { HttpClient, type IHttpClientOptions } from "./http-client.js";
|
|
3
|
+
export type HttpFormData = {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class HttpClientNode extends HttpClient {
|
|
7
|
+
private cookieJar;
|
|
8
|
+
constructor(options: IHttpClientOptions);
|
|
9
|
+
protected registerCookies(response: Response): Promise<Cookie | undefined>;
|
|
10
|
+
getCookies(): Promise<string | null>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CookieJar } from "tough-cookie";
|
|
2
|
+
import { HttpClient } from "./http-client.js";
|
|
3
|
+
export class HttpClientNode extends HttpClient {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super(options);
|
|
6
|
+
this.cookieJar = new CookieJar();
|
|
7
|
+
}
|
|
8
|
+
registerCookies(response) {
|
|
9
|
+
const cookie = response.headers.get('set-cookie');
|
|
10
|
+
if (cookie) {
|
|
11
|
+
return this.cookieJar.setCookie(cookie, response.url);
|
|
12
|
+
}
|
|
13
|
+
return Promise.resolve(undefined);
|
|
14
|
+
}
|
|
15
|
+
getCookies() {
|
|
16
|
+
return this.cookieJar.getCookieString(this.options.baseUrl); // Get cookies for the request
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=http-client-node.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Cookie } from "tough-cookie";
|
|
1
2
|
export type HttpFormData = {
|
|
2
3
|
[key: string]: string;
|
|
3
4
|
};
|
|
@@ -14,14 +15,13 @@ export interface IFetchOptions {
|
|
|
14
15
|
followRedirects?: boolean;
|
|
15
16
|
}
|
|
16
17
|
export declare class HttpClient {
|
|
17
|
-
|
|
18
|
-
private cookieJar;
|
|
18
|
+
protected options: IHttpClientOptions;
|
|
19
19
|
constructor(options: IHttpClientOptions);
|
|
20
20
|
get(path: string, options?: IFetchOptions): Promise<Response>;
|
|
21
21
|
post(path: string, options?: IFetchOptions): Promise<Response>;
|
|
22
22
|
postForm(path: string, formData: HttpFormData, options?: IFetchOptions): Promise<Response>;
|
|
23
23
|
postJson(path: string, json: Object, options?: IFetchOptions): Promise<Response>;
|
|
24
24
|
private _fetch;
|
|
25
|
-
|
|
26
|
-
getCookies(): Promise<string>;
|
|
25
|
+
protected registerCookies(response: Response): Promise<Cookie | undefined>;
|
|
26
|
+
getCookies(): Promise<string | null>;
|
|
27
27
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { CookieJar } from "tough-cookie";
|
|
2
1
|
export class HttpClient {
|
|
3
2
|
constructor(options) {
|
|
4
3
|
this.options = options;
|
|
5
|
-
this.cookieJar = new CookieJar();
|
|
6
4
|
}
|
|
7
5
|
get(path, options) {
|
|
8
6
|
return this._fetch('get', path, options);
|
|
@@ -27,11 +25,11 @@ export class HttpClient {
|
|
|
27
25
|
url += `?${new URLSearchParams(options.query)}`;
|
|
28
26
|
}
|
|
29
27
|
const cookies = await this.getCookies();
|
|
30
|
-
const headers =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'Cookie'
|
|
34
|
-
}
|
|
28
|
+
const headers = new Headers(options.headers);
|
|
29
|
+
headers.set('User-Agent', this.options.userAgent);
|
|
30
|
+
if (cookies !== null) {
|
|
31
|
+
headers.set('Cookie', cookies);
|
|
32
|
+
}
|
|
35
33
|
const response = await fetch(url, {
|
|
36
34
|
method,
|
|
37
35
|
...options,
|
|
@@ -43,13 +41,10 @@ export class HttpClient {
|
|
|
43
41
|
return response;
|
|
44
42
|
}
|
|
45
43
|
registerCookies(response) {
|
|
46
|
-
|
|
47
|
-
if (cookie) {
|
|
48
|
-
return this.cookieJar.setCookie(cookie, response.url);
|
|
49
|
-
}
|
|
44
|
+
return Promise.resolve(undefined);
|
|
50
45
|
}
|
|
51
|
-
getCookies() {
|
|
52
|
-
return
|
|
46
|
+
async getCookies() {
|
|
47
|
+
return Promise.resolve(null);
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
|
-
//# sourceMappingURL=
|
|
50
|
+
//# sourceMappingURL=http-client.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { XmlMetadata } from './xml/xml-metadata.js';
|
|
2
|
+
export { XmlIsrc } from './xml/xml-isrc.js';
|
|
3
|
+
export { XmlIsrcList } from './xml/xml-isrc-list.js';
|
|
4
|
+
export { XmlRecording } from './xml/xml-recording.js';
|
|
5
|
+
import { HttpClientNode } from "./http-client-node.js";
|
|
6
|
+
import { MusicBrainzApi as MusicBrainzApiDefault } from "./musicbrainz-api.js";
|
|
7
|
+
export * from './musicbrainz.types.js';
|
|
8
|
+
export * from './http-client.js';
|
|
9
|
+
export declare class MusicBrainzApi extends MusicBrainzApiDefault {
|
|
10
|
+
protected initHttpClient(): HttpClientNode;
|
|
11
|
+
login(): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Logout
|
|
14
|
+
*/
|
|
15
|
+
logout(): Promise<boolean>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { StatusCodes as HttpStatus } from 'http-status-codes';
|
|
2
|
+
import Debug from 'debug';
|
|
3
|
+
export { XmlMetadata } from './xml/xml-metadata.js';
|
|
4
|
+
export { XmlIsrc } from './xml/xml-isrc.js';
|
|
5
|
+
export { XmlIsrcList } from './xml/xml-isrc-list.js';
|
|
6
|
+
export { XmlRecording } from './xml/xml-recording.js';
|
|
7
|
+
import { HttpClientNode } from "./http-client-node.js";
|
|
8
|
+
import { MusicBrainzApi as MusicBrainzApiDefault } from "./musicbrainz-api.js";
|
|
9
|
+
export * from './musicbrainz.types.js';
|
|
10
|
+
export * from './http-client.js';
|
|
11
|
+
/*
|
|
12
|
+
* https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2#Subqueries
|
|
13
|
+
*/
|
|
14
|
+
const debug = Debug('musicbrainz-api-node');
|
|
15
|
+
export class MusicBrainzApi extends MusicBrainzApiDefault {
|
|
16
|
+
initHttpClient() {
|
|
17
|
+
return new HttpClientNode({
|
|
18
|
+
baseUrl: this.config.baseUrl,
|
|
19
|
+
timeout: 20 * 1000,
|
|
20
|
+
userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async login() {
|
|
24
|
+
if (!this.config.botAccount?.username)
|
|
25
|
+
throw new Error('bot username should be set');
|
|
26
|
+
if (!this.config.botAccount?.password)
|
|
27
|
+
throw new Error('bot password should be set');
|
|
28
|
+
if (this.session?.loggedIn) {
|
|
29
|
+
const cookies = await this.httpClient.getCookies();
|
|
30
|
+
return cookies.indexOf('musicbrainz_server_session') !== -1;
|
|
31
|
+
}
|
|
32
|
+
this.session = await this.getSession();
|
|
33
|
+
const redirectUri = '/success';
|
|
34
|
+
const formData = {
|
|
35
|
+
username: this.config.botAccount.username,
|
|
36
|
+
password: this.config.botAccount.password,
|
|
37
|
+
csrf_session_key: this.session.csrf.sessionKey,
|
|
38
|
+
csrf_token: this.session.csrf.token,
|
|
39
|
+
remember_me: '1'
|
|
40
|
+
};
|
|
41
|
+
const response = await this.httpClient.postForm('login', formData, {
|
|
42
|
+
query: {
|
|
43
|
+
returnto: redirectUri
|
|
44
|
+
},
|
|
45
|
+
followRedirects: false
|
|
46
|
+
});
|
|
47
|
+
const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
|
|
48
|
+
if (success) {
|
|
49
|
+
this.session.loggedIn = true;
|
|
50
|
+
}
|
|
51
|
+
return success;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Logout
|
|
55
|
+
*/
|
|
56
|
+
async logout() {
|
|
57
|
+
const redirectUri = '/success';
|
|
58
|
+
const response = await this.httpClient.post('logout', {
|
|
59
|
+
followRedirects: false,
|
|
60
|
+
query: {
|
|
61
|
+
returnto: redirectUri
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
|
|
65
|
+
if (success && this.session) {
|
|
66
|
+
this.session.loggedIn = true;
|
|
67
|
+
}
|
|
68
|
+
return success;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=musicbrainz-api-node.js.map
|
package/lib/musicbrainz-api.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ 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
5
|
import type { XmlMetadata } from './xml/xml-metadata.js';
|
|
6
|
+
import { RateLimitThreshold } from 'rate-limit-threshold';
|
|
6
7
|
import * as mb from './musicbrainz.types.js';
|
|
8
|
+
import { HttpClient } from "./http-client.js";
|
|
7
9
|
export * from './musicbrainz.types.js';
|
|
8
10
|
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';
|
|
9
11
|
export type SubQueryIncludes =
|
|
@@ -76,12 +78,13 @@ export interface ISessionInformation {
|
|
|
76
78
|
}
|
|
77
79
|
export declare class MusicBrainzApi {
|
|
78
80
|
readonly config: IInternalConfig;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
protected rateLimiter: RateLimitThreshold;
|
|
82
|
+
protected httpClient: HttpClient;
|
|
83
|
+
protected session?: ISessionInformation;
|
|
82
84
|
static fetchCsrf(html: string): ICsrfSession;
|
|
83
85
|
private static fetchValue;
|
|
84
86
|
constructor(_config?: IMusicBrainzConfig);
|
|
87
|
+
protected initHttpClient(): HttpClient;
|
|
85
88
|
restGet<T>(relUrl: string, query?: {
|
|
86
89
|
[key: string]: string;
|
|
87
90
|
}): Promise<T>;
|
|
@@ -140,11 +143,6 @@ export declare class MusicBrainzApi {
|
|
|
140
143
|
search(artist: 'url', query: mb.ISearchQuery<UrlIncludes> & mb.ILinkedEntitiesArea): Promise<mb.IUrlList>;
|
|
141
144
|
postRecording(xmlMetadata: XmlMetadata): Promise<void>;
|
|
142
145
|
post(entity: mb.EntityType, xmlMetadata: XmlMetadata): Promise<void>;
|
|
143
|
-
login(): Promise<boolean>;
|
|
144
|
-
/**
|
|
145
|
-
* Logout
|
|
146
|
-
*/
|
|
147
|
-
logout(): Promise<boolean>;
|
|
148
146
|
/**
|
|
149
147
|
* Submit entity
|
|
150
148
|
* @param entity Entity type e.g. 'recording'
|
|
@@ -176,7 +174,7 @@ export declare class MusicBrainzApi {
|
|
|
176
174
|
* @param editNote Comment to add.
|
|
177
175
|
*/
|
|
178
176
|
addSpotifyIdToRecording(recording: mb.IRecording, spotifyId: string, editNote: string): Promise<void>;
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
protected getSession(): Promise<ISessionInformation>;
|
|
178
|
+
protected applyRateLimiter(): Promise<void>;
|
|
181
179
|
}
|
|
182
180
|
export declare function makeAndQueryString(keyValuePairs: IFormData): string;
|
package/lib/musicbrainz-api.js
CHANGED
|
@@ -7,7 +7,7 @@ export { XmlRecording } from './xml/xml-recording.js';
|
|
|
7
7
|
import { DigestAuth } from './digest-auth.js';
|
|
8
8
|
import { RateLimitThreshold } from 'rate-limit-threshold';
|
|
9
9
|
import * as mb from './musicbrainz.types.js';
|
|
10
|
-
import { HttpClient } from "./
|
|
10
|
+
import { HttpClient } from "./http-client.js";
|
|
11
11
|
export * from './musicbrainz.types.js';
|
|
12
12
|
const debug = Debug('musicbrainz-api');
|
|
13
13
|
export class MusicBrainzApi {
|
|
@@ -35,12 +35,15 @@ export class MusicBrainzApi {
|
|
|
35
35
|
},
|
|
36
36
|
..._config
|
|
37
37
|
};
|
|
38
|
-
this.httpClient =
|
|
38
|
+
this.httpClient = this.initHttpClient();
|
|
39
|
+
this.rateLimiter = new RateLimitThreshold(15, 18);
|
|
40
|
+
}
|
|
41
|
+
initHttpClient() {
|
|
42
|
+
return new HttpClient({
|
|
39
43
|
baseUrl: this.config.baseUrl,
|
|
40
44
|
timeout: 20 * 1000,
|
|
41
45
|
userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
42
46
|
});
|
|
43
|
-
this.rateLimiter = new RateLimitThreshold(15, 18);
|
|
44
47
|
}
|
|
45
48
|
async restGet(relUrl, query = {}) {
|
|
46
49
|
query.fmt = 'json';
|
|
@@ -103,53 +106,6 @@ export class MusicBrainzApi {
|
|
|
103
106
|
}
|
|
104
107
|
} while (n++ < 5);
|
|
105
108
|
}
|
|
106
|
-
async login() {
|
|
107
|
-
if (!this.config.botAccount?.username)
|
|
108
|
-
throw new Error('bot username should be set');
|
|
109
|
-
if (!this.config.botAccount?.password)
|
|
110
|
-
throw new Error('bot password should be set');
|
|
111
|
-
if (this.session?.loggedIn) {
|
|
112
|
-
const cookies = await this.httpClient.getCookies();
|
|
113
|
-
return cookies.indexOf('musicbrainz_server_session') !== -1;
|
|
114
|
-
}
|
|
115
|
-
this.session = await this.getSession();
|
|
116
|
-
const redirectUri = '/success';
|
|
117
|
-
const formData = {
|
|
118
|
-
username: this.config.botAccount.username,
|
|
119
|
-
password: this.config.botAccount.password,
|
|
120
|
-
csrf_session_key: this.session.csrf.sessionKey,
|
|
121
|
-
csrf_token: this.session.csrf.token,
|
|
122
|
-
remember_me: '1'
|
|
123
|
-
};
|
|
124
|
-
const response = await this.httpClient.postForm('login', formData, {
|
|
125
|
-
query: {
|
|
126
|
-
returnto: redirectUri
|
|
127
|
-
},
|
|
128
|
-
followRedirects: false
|
|
129
|
-
});
|
|
130
|
-
const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
|
|
131
|
-
if (success) {
|
|
132
|
-
this.session.loggedIn = true;
|
|
133
|
-
}
|
|
134
|
-
return success;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Logout
|
|
138
|
-
*/
|
|
139
|
-
async logout() {
|
|
140
|
-
const redirectUri = '/success';
|
|
141
|
-
const response = await this.httpClient.post('logout', {
|
|
142
|
-
followRedirects: false,
|
|
143
|
-
query: {
|
|
144
|
-
returnto: redirectUri
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
|
|
148
|
-
if (success && this.session) {
|
|
149
|
-
this.session.loggedIn = true;
|
|
150
|
-
}
|
|
151
|
-
return success;
|
|
152
|
-
}
|
|
153
109
|
/**
|
|
154
110
|
* Submit entity
|
|
155
111
|
* @param entity Entity type e.g. 'recording'
|
package/package.json
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musicbrainz-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "MusicBrainz API client for reading and submitting metadata",
|
|
5
5
|
"exports": {
|
|
6
|
-
"
|
|
7
|
-
|
|
6
|
+
"node": {
|
|
7
|
+
"import": "./lib/entry-node.js",
|
|
8
|
+
"require": "./lib/entry-node.cjs"
|
|
9
|
+
},
|
|
10
|
+
"default": {
|
|
11
|
+
"import": "./lib/entry-default.js",
|
|
12
|
+
"require": "./lib/entry-default.cjs"
|
|
13
|
+
}
|
|
8
14
|
},
|
|
9
15
|
"types": "lib/index.d.ts",
|
|
10
16
|
"files": [
|
|
11
17
|
"lib/**/*.js",
|
|
12
18
|
"lib/**/*.d.ts",
|
|
13
|
-
"lib/
|
|
19
|
+
"lib/entry-node.cjs",
|
|
20
|
+
"lib/entry-default.cjs"
|
|
14
21
|
],
|
|
15
22
|
"type": "module",
|
|
16
23
|
"author": {
|
|
@@ -56,11 +63,11 @@
|
|
|
56
63
|
"rate-limit-threshold": "^0.2.0",
|
|
57
64
|
"spark-md5": "^3.0.2",
|
|
58
65
|
"tough-cookie": "^5.0.0",
|
|
59
|
-
"uuid": "^
|
|
66
|
+
"uuid": "^11.0.3"
|
|
60
67
|
},
|
|
61
68
|
"devDependencies": {
|
|
62
69
|
"@biomejs/biome": "^1.8.3",
|
|
63
|
-
"@types/chai": "^
|
|
70
|
+
"@types/chai": "^5.0.0",
|
|
64
71
|
"@types/jsontoxml": "^1.0.5",
|
|
65
72
|
"@types/mocha": "^10.0.4",
|
|
66
73
|
"@types/node": "^22.5.0",
|
|
@@ -71,8 +78,8 @@
|
|
|
71
78
|
"@types/uuid": "^10.0.0",
|
|
72
79
|
"c8": "^10.1.2",
|
|
73
80
|
"chai": "^5.1.1",
|
|
74
|
-
"del-cli": "^
|
|
75
|
-
"mocha": "^
|
|
81
|
+
"del-cli": "^6.0.0",
|
|
82
|
+
"mocha": "^11.0.1",
|
|
76
83
|
"remark-cli": "^12.0.0",
|
|
77
84
|
"remark-preset-lint-recommended": "^7.0.0",
|
|
78
85
|
"sinon": "^19.0.2",
|
|
@@ -109,5 +116,5 @@
|
|
|
109
116
|
],
|
|
110
117
|
"report-dir": "coverage"
|
|
111
118
|
},
|
|
112
|
-
"packageManager": "yarn@4.
|
|
119
|
+
"packageManager": "yarn@4.6.0"
|
|
113
120
|
}
|
package/lib/default.cjs
DELETED
|
File without changes
|