musicbrainz-api 0.10.3 → 0.12.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 +40 -1
- package/lib/coverartarchive-api.d.ts +31 -0
- package/lib/coverartarchive-api.js +37 -0
- package/lib/digest-auth.d.ts +2 -2
- package/lib/digest-auth.js +6 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +19 -0
- package/lib/musicbrainz-api.d.ts +17 -14
- package/lib/musicbrainz-api.js +35 -46
- package/lib/musicbrainz.types.d.ts +8 -1
- package/lib/rate-limiter.d.ts +2 -2
- package/lib/rate-limiter.js +5 -3
- package/lib/xml/xml-isrc-list.d.ts +1 -1
- package/lib/xml/xml-recording.d.ts +2 -2
- package/package.json +18 -7
- package/.idea/checkstyle-idea.xml +0 -16
- package/.idea/inspectionProfiles/Project_Default.xml +0 -7
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/lib/digest-auth.ts +0 -101
- package/lib/musicbrainz-api.ts +0 -794
- package/lib/musicbrainz.types.ts +0 -689
- package/lib/rate-limiter.ts +0 -34
- package/lib/xml/xml-isrc-list.ts +0 -20
- package/lib/xml/xml-isrc.ts +0 -15
- package/lib/xml/xml-metadata.ts +0 -30
- package/lib/xml/xml-recording.ts +0 -19
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ We are looking into making this package usable in the browser as well.
|
|
|
25
25
|
|
|
26
26
|
## Before using this library
|
|
27
27
|
|
|
28
|
-
MusicBrainz asks that you [
|
|
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
29
|
By passing `appName`, `appVersion`, `appMail` musicbrainz-api takes care of that.
|
|
30
30
|
|
|
31
31
|
## Submitting metadata
|
|
@@ -115,6 +115,20 @@ Lookup an `artist` and include their `releases`, `release-groups` and `aliases`
|
|
|
115
115
|
const artist = await mbApi.lookupArtist('ab2528d9-719f-4261-8098-21849222a0f2');
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
### Lookup collection
|
|
119
|
+
|
|
120
|
+
Lookup an instrument
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
const collection = await mbApi.lookupCollection('de4fdfc4-53aa-458a-b463-8761cc7f5af8');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Lookup an event
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
const event = await mbApi.lookupEvent('6d32c658-151e-45ec-88c4-fb8787524d61');
|
|
130
|
+
```
|
|
131
|
+
|
|
118
132
|
### Lookup instrument
|
|
119
133
|
|
|
120
134
|
Lookup an instrument
|
|
@@ -137,6 +151,10 @@ const label = await mbApi.lookupLabel('25dda9f9-f069-4898-82f0-59330a106c7f');
|
|
|
137
151
|
const place = await mbApi.lookupPlace('e6cfb74d-d69b-44c3-b890-1b3f509816e4');
|
|
138
152
|
```
|
|
139
153
|
|
|
154
|
+
```js
|
|
155
|
+
const place = await mbApi.lookupSeries('1ae6c9bc-2931-4d75-bee4-3dc53dfd246a');
|
|
156
|
+
```
|
|
157
|
+
|
|
140
158
|
The second argument can be used to pass [subqueries](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#Subqueries), which will return more (nested) information:
|
|
141
159
|
```js
|
|
142
160
|
const artist = await mbApi.lookupArtist('ab2528d9-719f-4261-8098-21849222a0f2', ['releases', 'recordings', 'url-rels']);
|
|
@@ -475,6 +493,27 @@ assert.isTrue(succeed, 'Login successful');
|
|
|
475
493
|
await mbApi.addSpotifyIdToRecording(recording, '2AMysGXOe0zzZJMtH3Nizb');
|
|
476
494
|
```
|
|
477
495
|
|
|
496
|
+
## Cover Art Archive API
|
|
497
|
+
|
|
498
|
+
Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
502
|
+
|
|
503
|
+
coverArtArchiveApiClient.getReleaseCovers(releaseMbid).then(releaseCoverInfo => {
|
|
504
|
+
console.log('Release cover info', releaseCoverInfo);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'front').then(releaseCoverInfo => {
|
|
508
|
+
console.log('Get best front cover', releaseCoverInfo);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'back').then(releaseCoverInfo => {
|
|
512
|
+
console.log('Get best back cover', releaseCoverInfo);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
|
|
478
517
|
## Compatibility
|
|
479
518
|
|
|
480
519
|
The JavaScript in runtime is compliant with [ECMAScript 2017 (ES8)](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CovertType = 'Front' | 'Back' | 'Booklet' | 'Medium' | 'Obi' | 'Spine' | 'Track' | 'Tray' | 'Sticker' | 'Poster' | 'Liner' | 'Watermark' | 'Raw/Unedited' | 'Matrix/Runout' | 'Top' | 'Bottom' | 'Other';
|
|
2
|
+
export interface IImage {
|
|
3
|
+
types: CovertType[];
|
|
4
|
+
front: boolean;
|
|
5
|
+
back: boolean;
|
|
6
|
+
edit: number;
|
|
7
|
+
image: string;
|
|
8
|
+
comment: string;
|
|
9
|
+
approved: boolean;
|
|
10
|
+
id: string;
|
|
11
|
+
thumbnails: {
|
|
12
|
+
large: string;
|
|
13
|
+
small: string;
|
|
14
|
+
'250': string;
|
|
15
|
+
'500'?: string;
|
|
16
|
+
'1200'?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface ICoverInfo {
|
|
20
|
+
images: IImage[];
|
|
21
|
+
release: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class CoverArtArchiveApi {
|
|
24
|
+
private host;
|
|
25
|
+
private getJson;
|
|
26
|
+
/**
|
|
27
|
+
*
|
|
28
|
+
* @param releaseId MusicBrainz Release MBID
|
|
29
|
+
*/
|
|
30
|
+
getReleaseCovers(releaseId: string, coverType?: 'front' | 'back'): Promise<ICoverInfo>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CoverArtArchiveApi = void 0;
|
|
4
|
+
/* eslint-disable-next-line */
|
|
5
|
+
const got_1 = require("got");
|
|
6
|
+
class CoverArtArchiveApi {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.host = 'coverartarchive.org';
|
|
9
|
+
}
|
|
10
|
+
async getJson(path) {
|
|
11
|
+
const response = await got_1.default.get('https://' + this.host + path, {
|
|
12
|
+
headers: {
|
|
13
|
+
Accept: `application/json`
|
|
14
|
+
},
|
|
15
|
+
responseType: 'json'
|
|
16
|
+
});
|
|
17
|
+
return response.body;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param releaseId MusicBrainz Release MBID
|
|
22
|
+
*/
|
|
23
|
+
async getReleaseCovers(releaseId, coverType) {
|
|
24
|
+
const path = ['release', releaseId];
|
|
25
|
+
if (coverType) {
|
|
26
|
+
path.push(coverType);
|
|
27
|
+
}
|
|
28
|
+
const info = await this.getJson('/' + path.join('/'));
|
|
29
|
+
// Hack to correct http addresses into https
|
|
30
|
+
if (info.release && info.release.startsWith('http:')) {
|
|
31
|
+
info.release = 'https' + info.release.substring(4);
|
|
32
|
+
}
|
|
33
|
+
return info;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.CoverArtArchiveApi = CoverArtArchiveApi;
|
|
37
|
+
//# sourceMappingURL=coverartarchive-api.js.map
|
package/lib/digest-auth.d.ts
CHANGED
|
@@ -12,10 +12,10 @@ export declare class DigestAuth {
|
|
|
12
12
|
* If the algorithm directive's value is "MD5-sess", then HA1 is
|
|
13
13
|
* HA1=MD5(MD5(username:realm:password):nonce:cnonce)
|
|
14
14
|
*/
|
|
15
|
-
static ha1Compute(algorithm:
|
|
15
|
+
static ha1Compute(algorithm: string, user: string, realm: string, pass: string, nonce: string, cnonce: string): string;
|
|
16
16
|
hasAuth: boolean;
|
|
17
17
|
sentAuth: boolean;
|
|
18
|
-
bearerToken: string;
|
|
18
|
+
bearerToken: string | null;
|
|
19
19
|
constructor(credentials: ICredentials);
|
|
20
20
|
digest(method: string, path: string, authHeader: string): string;
|
|
21
21
|
}
|
package/lib/digest-auth.js
CHANGED
|
@@ -64,16 +64,16 @@ class DigestAuth {
|
|
|
64
64
|
opaque: challenge.opaque
|
|
65
65
|
};
|
|
66
66
|
const parts = [];
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
if (
|
|
70
|
-
parts.push(
|
|
67
|
+
Object.entries(authValues).forEach(([key, value]) => {
|
|
68
|
+
if (value) {
|
|
69
|
+
if (key === 'qop' || key === 'nc' || key === 'algorithm') {
|
|
70
|
+
parts.push(key + '=' + value);
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
|
-
parts.push(
|
|
73
|
+
parts.push(key + '="' + value + '"');
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
}
|
|
76
|
+
});
|
|
77
77
|
authHeader = 'Digest ' + parts.join(', ');
|
|
78
78
|
this.sentAuth = true;
|
|
79
79
|
return authHeader;
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./coverartarchive-api"), exports);
|
|
18
|
+
__exportStar(require("./musicbrainz-api"), exports);
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
package/lib/musicbrainz-api.d.ts
CHANGED
|
@@ -42,13 +42,13 @@ export type ReleaseGroupIncludes = MiscIncludes | SubQueryIncludes | RelationsIn
|
|
|
42
42
|
export type SeriesIncludes = MiscIncludes | RelationsIncludes;
|
|
43
43
|
export type WorkIncludes = MiscIncludes | RelationsIncludes;
|
|
44
44
|
export type UrlIncludes = RelationsIncludes;
|
|
45
|
-
export
|
|
45
|
+
export type IFormData = {
|
|
46
46
|
[key: string]: string | number;
|
|
47
|
-
}
|
|
47
|
+
};
|
|
48
48
|
export interface IMusicBrainzConfig {
|
|
49
|
-
botAccount
|
|
50
|
-
username
|
|
51
|
-
password
|
|
49
|
+
botAccount: {
|
|
50
|
+
username?: string;
|
|
51
|
+
password?: string;
|
|
52
52
|
};
|
|
53
53
|
baseUrl?: string;
|
|
54
54
|
appName?: string;
|
|
@@ -62,11 +62,12 @@ export interface IMusicBrainzConfig {
|
|
|
62
62
|
*/
|
|
63
63
|
appContactInfo?: string;
|
|
64
64
|
}
|
|
65
|
+
export interface ICsrfSession {
|
|
66
|
+
sessionKey: string;
|
|
67
|
+
token: string;
|
|
68
|
+
}
|
|
65
69
|
export interface ISessionInformation {
|
|
66
|
-
csrf:
|
|
67
|
-
sessionKey: string;
|
|
68
|
-
token: string;
|
|
69
|
-
};
|
|
70
|
+
csrf: ICsrfSession;
|
|
70
71
|
loggedIn?: boolean;
|
|
71
72
|
}
|
|
72
73
|
export declare class MusicBrainzApi {
|
|
@@ -74,11 +75,8 @@ export declare class MusicBrainzApi {
|
|
|
74
75
|
readonly config: IMusicBrainzConfig;
|
|
75
76
|
private rateLimiter;
|
|
76
77
|
private options;
|
|
77
|
-
private session
|
|
78
|
-
static fetchCsrf(html: string):
|
|
79
|
-
sessionKey: string;
|
|
80
|
-
token: string;
|
|
81
|
-
};
|
|
78
|
+
private session?;
|
|
79
|
+
static fetchCsrf(html: string): ICsrfSession;
|
|
82
80
|
private static fetchValue;
|
|
83
81
|
private getCookies;
|
|
84
82
|
constructor(_config?: IMusicBrainzConfig);
|
|
@@ -147,6 +145,11 @@ export declare class MusicBrainzApi {
|
|
|
147
145
|
* @param inc Include: artist-credits, isrcs
|
|
148
146
|
*/
|
|
149
147
|
lookupRecording(recordingId: string, inc?: RecordingIncludes[]): Promise<mb.IRecording>;
|
|
148
|
+
/**
|
|
149
|
+
* Lookup series
|
|
150
|
+
* @param seriesId Series MBID
|
|
151
|
+
*/
|
|
152
|
+
lookupSeries(seriesId: string): Promise<mb.ISeries>;
|
|
150
153
|
/**
|
|
151
154
|
* Lookup work
|
|
152
155
|
* @param workId Work MBID
|
package/lib/musicbrainz-api.js
CHANGED
|
@@ -30,11 +30,11 @@ Object.defineProperty(exports, "XmlRecording", { enumerable: true, get: function
|
|
|
30
30
|
const digest_auth_1 = require("./digest-auth");
|
|
31
31
|
const rate_limiter_1 = require("./rate-limiter");
|
|
32
32
|
const mb = require("./musicbrainz.types");
|
|
33
|
+
/* eslint-disable-next-line */
|
|
33
34
|
const got_1 = require("got");
|
|
34
|
-
const
|
|
35
|
+
const tough_cookie_1 = require("tough-cookie");
|
|
35
36
|
__exportStar(require("./musicbrainz.types"), exports);
|
|
36
37
|
const util_1 = require("util");
|
|
37
|
-
const retries = 3;
|
|
38
38
|
const debug = Debug('musicbrainz-api');
|
|
39
39
|
class MusicBrainzApi {
|
|
40
40
|
static escapeText(text) {
|
|
@@ -86,10 +86,11 @@ class MusicBrainzApi {
|
|
|
86
86
|
}
|
|
87
87
|
constructor(_config) {
|
|
88
88
|
this.config = {
|
|
89
|
-
baseUrl: 'https://musicbrainz.org'
|
|
89
|
+
baseUrl: 'https://musicbrainz.org',
|
|
90
|
+
botAccount: {}
|
|
90
91
|
};
|
|
91
92
|
Object.assign(this.config, _config);
|
|
92
|
-
const cookieJar = new
|
|
93
|
+
const cookieJar = new tough_cookie_1.CookieJar();
|
|
93
94
|
this.getCookies = (0, util_1.promisify)(cookieJar.getCookies.bind(cookieJar));
|
|
94
95
|
this.options = {
|
|
95
96
|
prefixUrl: this.config.baseUrl,
|
|
@@ -97,37 +98,17 @@ class MusicBrainzApi {
|
|
|
97
98
|
headers: {
|
|
98
99
|
'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
99
100
|
},
|
|
100
|
-
cookieJar
|
|
101
|
+
cookieJar: cookieJar
|
|
101
102
|
};
|
|
102
|
-
this.rateLimiter = new rate_limiter_1.RateLimiter(
|
|
103
|
+
this.rateLimiter = new rate_limiter_1.RateLimiter(15, 18);
|
|
103
104
|
}
|
|
104
105
|
async restGet(relUrl, query = {}, attempt = 1) {
|
|
105
106
|
query.fmt = 'json';
|
|
106
|
-
let response;
|
|
107
107
|
await this.rateLimiter.limit();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
debug('Rate limiter kicked in, slowing down...');
|
|
113
|
-
await rate_limiter_1.RateLimiter.sleep(500);
|
|
114
|
-
} while (true);
|
|
115
|
-
switch (response.statusCode) {
|
|
116
|
-
case http_status_codes_1.StatusCodes.OK:
|
|
117
|
-
return response.body;
|
|
118
|
-
case http_status_codes_1.StatusCodes.BAD_REQUEST:
|
|
119
|
-
case http_status_codes_1.StatusCodes.NOT_FOUND:
|
|
120
|
-
throw new Error(`Got response status ${response.statusCode}: ${(0, http_status_codes_1.getReasonPhrase)(response.status)}`);
|
|
121
|
-
case http_status_codes_1.StatusCodes.SERVICE_UNAVAILABLE: // 503
|
|
122
|
-
default:
|
|
123
|
-
const msg = `Got response status ${response.statusCode} on attempt #${attempt} (${(0, http_status_codes_1.getReasonPhrase)(response.status)})`;
|
|
124
|
-
debug(msg);
|
|
125
|
-
if (attempt < retries) {
|
|
126
|
-
return this.restGet(relUrl, query, attempt + 1);
|
|
127
|
-
}
|
|
128
|
-
else
|
|
129
|
-
throw new Error(msg);
|
|
130
|
-
}
|
|
108
|
+
const response = await got_1.default.get('ws/2' + relUrl, Object.assign(Object.assign({}, this.options), { searchParams: query, responseType: 'json', retry: {
|
|
109
|
+
limit: 10
|
|
110
|
+
} }));
|
|
111
|
+
return response.body;
|
|
131
112
|
}
|
|
132
113
|
// -----------------------------------------------------------------------------------------------------------------
|
|
133
114
|
// Lookup functions
|
|
@@ -214,6 +195,13 @@ class MusicBrainzApi {
|
|
|
214
195
|
lookupRecording(recordingId, inc = []) {
|
|
215
196
|
return this.lookupEntity('recording', recordingId, inc);
|
|
216
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Lookup series
|
|
200
|
+
* @param seriesId Series MBID
|
|
201
|
+
*/
|
|
202
|
+
lookupSeries(seriesId) {
|
|
203
|
+
return this.lookupEntity('series', seriesId);
|
|
204
|
+
}
|
|
217
205
|
/**
|
|
218
206
|
* Lookup work
|
|
219
207
|
* @param workId Work MBID
|
|
@@ -353,15 +341,15 @@ class MusicBrainzApi {
|
|
|
353
341
|
const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
|
|
354
342
|
const path = `ws/2/${entity}/`;
|
|
355
343
|
// Get digest challenge
|
|
356
|
-
let digest
|
|
344
|
+
let digest;
|
|
357
345
|
let n = 1;
|
|
358
346
|
const postData = xmlMetadata.toXml();
|
|
359
347
|
do {
|
|
360
348
|
await this.rateLimiter.limit();
|
|
361
|
-
const response = await got_1.default.post(path, Object.assign({ searchParams: { client: clientId }, headers: {
|
|
349
|
+
const response = await got_1.default.post(path, Object.assign(Object.assign({}, this.options), { searchParams: { client: clientId }, headers: {
|
|
362
350
|
authorization: digest,
|
|
363
351
|
'Content-Type': 'application/xml'
|
|
364
|
-
}, body: postData, throwHttpErrors: false }
|
|
352
|
+
}, body: postData, throwHttpErrors: false }));
|
|
365
353
|
if (response.statusCode === http_status_codes_1.StatusCodes.UNAUTHORIZED) {
|
|
366
354
|
// Respond to digest challenge
|
|
367
355
|
const auth = new digest_auth_1.DigestAuth(this.config.botAccount);
|
|
@@ -384,7 +372,7 @@ class MusicBrainzApi {
|
|
|
384
372
|
}
|
|
385
373
|
}
|
|
386
374
|
}
|
|
387
|
-
this.session = await this.getSession(
|
|
375
|
+
this.session = await this.getSession();
|
|
388
376
|
const redirectUri = '/success';
|
|
389
377
|
const formData = {
|
|
390
378
|
username: this.config.botAccount.username,
|
|
@@ -393,9 +381,9 @@ class MusicBrainzApi {
|
|
|
393
381
|
csrf_token: this.session.csrf.token,
|
|
394
382
|
remember_me: 1
|
|
395
383
|
};
|
|
396
|
-
const response = await got_1.default.post('login', Object.assign({ followRedirect: false, searchParams: {
|
|
384
|
+
const response = await got_1.default.post('login', Object.assign(Object.assign({}, this.options), { followRedirect: false, searchParams: {
|
|
397
385
|
returnto: redirectUri
|
|
398
|
-
}, form: formData }
|
|
386
|
+
}, form: formData }));
|
|
399
387
|
const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri;
|
|
400
388
|
if (success) {
|
|
401
389
|
this.session.loggedIn = true;
|
|
@@ -407,11 +395,11 @@ class MusicBrainzApi {
|
|
|
407
395
|
*/
|
|
408
396
|
async logout() {
|
|
409
397
|
const redirectUri = '/success';
|
|
410
|
-
const response = await got_1.default.get('logout', Object.assign({ followRedirect: false, searchParams: {
|
|
398
|
+
const response = await got_1.default.get('logout', Object.assign(Object.assign({}, this.options), { followRedirect: false, searchParams: {
|
|
411
399
|
returnto: redirectUri
|
|
412
|
-
} }
|
|
400
|
+
} }));
|
|
413
401
|
const success = response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY && response.headers.location === redirectUri;
|
|
414
|
-
if (success) {
|
|
402
|
+
if (success && this.session) {
|
|
415
403
|
this.session.loggedIn = true;
|
|
416
404
|
}
|
|
417
405
|
return success;
|
|
@@ -424,13 +412,13 @@ class MusicBrainzApi {
|
|
|
424
412
|
*/
|
|
425
413
|
async editEntity(entity, mbid, formData) {
|
|
426
414
|
await this.rateLimiter.limit();
|
|
427
|
-
this.session = await this.getSession(
|
|
415
|
+
this.session = await this.getSession();
|
|
428
416
|
formData.csrf_session_key = this.session.csrf.sessionKey;
|
|
429
417
|
formData.csrf_token = this.session.csrf.token;
|
|
430
418
|
formData.username = this.config.botAccount.username;
|
|
431
419
|
formData.password = this.config.botAccount.password;
|
|
432
420
|
formData.remember_me = 1;
|
|
433
|
-
const response = await got_1.default.post(`${entity}/${mbid}/edit`, Object.assign({ form: formData, followRedirect: false }
|
|
421
|
+
const response = await got_1.default.post(`${entity}/${mbid}/edit`, Object.assign(Object.assign({}, this.options), { form: formData, followRedirect: false }));
|
|
434
422
|
if (response.statusCode === http_status_codes_1.StatusCodes.OK)
|
|
435
423
|
throw new Error(`Failed to submit form data`);
|
|
436
424
|
if (response.statusCode === http_status_codes_1.StatusCodes.MOVED_TEMPORARILY)
|
|
@@ -444,15 +432,16 @@ class MusicBrainzApi {
|
|
|
444
432
|
* @param editNote Edit note
|
|
445
433
|
*/
|
|
446
434
|
async addUrlToRecording(recording, url2add, editNote = '') {
|
|
435
|
+
var _a;
|
|
447
436
|
const formData = {};
|
|
448
437
|
formData['edit-recording.name'] = recording.title; // Required
|
|
449
438
|
formData['edit-recording.comment'] = recording.disambiguation;
|
|
450
439
|
formData['edit-recording.make_votable'] = true;
|
|
451
440
|
formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId;
|
|
452
441
|
formData['edit-recording.url.0.text'] = url2add.text;
|
|
453
|
-
|
|
454
|
-
formData[`edit-recording.isrcs.${i}`] =
|
|
455
|
-
}
|
|
442
|
+
(_a = recording.isrcs) === null || _a === void 0 ? void 0 : _a.forEach((isrcs, i) => {
|
|
443
|
+
formData[`edit-recording.isrcs.${i}`] = isrcs;
|
|
444
|
+
});
|
|
456
445
|
formData['edit-recording.edit_note'] = editNote;
|
|
457
446
|
return this.editEntity('recording', recording.id, formData);
|
|
458
447
|
}
|
|
@@ -527,8 +516,8 @@ class MusicBrainzApi {
|
|
|
527
516
|
searchUrl(query) {
|
|
528
517
|
return this.search('url', query);
|
|
529
518
|
}
|
|
530
|
-
async getSession(
|
|
531
|
-
const response = await got_1.default.get('login', Object.assign({ followRedirect: false, responseType: 'text' }
|
|
519
|
+
async getSession() {
|
|
520
|
+
const response = await got_1.default.get('login', Object.assign(Object.assign({}, this.options), { followRedirect: false, responseType: 'text' }));
|
|
532
521
|
return {
|
|
533
522
|
csrf: MusicBrainzApi.fetchCsrf(response.body)
|
|
534
523
|
};
|
|
@@ -33,7 +33,7 @@ export interface IArtist extends IEntity {
|
|
|
33
33
|
disambiguation: string;
|
|
34
34
|
'sort-name': string;
|
|
35
35
|
'type-id'?: string;
|
|
36
|
-
'gender-id'?:
|
|
36
|
+
'gender-id'?: string;
|
|
37
37
|
'life-span'?: IPeriod;
|
|
38
38
|
country?: string;
|
|
39
39
|
ipis?: any[];
|
|
@@ -183,6 +183,7 @@ export interface IAreaList extends ISearchResult {
|
|
|
183
183
|
}
|
|
184
184
|
export interface IReleaseList extends ISearchResult {
|
|
185
185
|
releases: IReleaseMatch[];
|
|
186
|
+
'release-count': number;
|
|
186
187
|
}
|
|
187
188
|
export interface IReleaseGroupList extends ISearchResult {
|
|
188
189
|
'release-groups': IReleaseGroupMatch[];
|
|
@@ -219,6 +220,12 @@ export interface ILabel extends IEntity {
|
|
|
219
220
|
export interface IPlace extends IEntity {
|
|
220
221
|
name: string;
|
|
221
222
|
}
|
|
223
|
+
export interface ISeries extends IEntity {
|
|
224
|
+
name: string;
|
|
225
|
+
type: string;
|
|
226
|
+
disambiguation: string;
|
|
227
|
+
'type-id': string;
|
|
228
|
+
}
|
|
222
229
|
export interface IUrl extends IEntity {
|
|
223
230
|
id: string;
|
|
224
231
|
resource: string;
|
package/lib/rate-limiter.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export declare class RateLimiter {
|
|
2
2
|
private maxCalls;
|
|
3
|
-
static sleep(ms:
|
|
3
|
+
static sleep(ms: number): Promise<void>;
|
|
4
4
|
queue: number[];
|
|
5
5
|
private readonly period;
|
|
6
|
-
constructor(
|
|
6
|
+
constructor(maxCalls: number, period: number);
|
|
7
7
|
limit(): Promise<void>;
|
|
8
8
|
}
|
package/lib/rate-limiter.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RateLimiter = void 0;
|
|
4
|
-
const
|
|
5
|
-
const debug =
|
|
4
|
+
const debug_1 = require("debug");
|
|
5
|
+
const debug = (0, debug_1.default)('musicbrainz-api:rate-limiter');
|
|
6
6
|
class RateLimiter {
|
|
7
7
|
static sleep(ms) {
|
|
8
8
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
9
9
|
}
|
|
10
|
-
constructor(
|
|
10
|
+
constructor(maxCalls, period) {
|
|
11
11
|
this.maxCalls = maxCalls;
|
|
12
12
|
this.queue = [];
|
|
13
|
+
debug(`Rate limiter initialized with max ${maxCalls} calls in ${period} seconds.`);
|
|
13
14
|
this.period = 1000 * period;
|
|
14
15
|
}
|
|
15
16
|
async limit() {
|
|
@@ -18,6 +19,7 @@ class RateLimiter {
|
|
|
18
19
|
while (this.queue.length > 0 && this.queue[0] < t0) {
|
|
19
20
|
this.queue.shift();
|
|
20
21
|
}
|
|
22
|
+
// debug(`Current rate is ${this.queue.length} per ${this.period / 1000} sec`);
|
|
21
23
|
if (this.queue.length >= this.maxCalls) {
|
|
22
24
|
const delay = this.queue[0] + this.period - now;
|
|
23
25
|
debug(`Client side rate limiter activated: cool down for ${delay / 1000} s...`);
|
|
@@ -8,7 +8,7 @@ export declare class XmlRecording {
|
|
|
8
8
|
attrs: {
|
|
9
9
|
id: string;
|
|
10
10
|
};
|
|
11
|
-
children: {
|
|
11
|
+
children: ({
|
|
12
12
|
name: string;
|
|
13
13
|
attrs: {
|
|
14
14
|
count: number;
|
|
@@ -19,6 +19,6 @@ export declare class XmlRecording {
|
|
|
19
19
|
id: string;
|
|
20
20
|
};
|
|
21
21
|
}[];
|
|
22
|
-
}[];
|
|
22
|
+
} | null)[];
|
|
23
23
|
};
|
|
24
24
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musicbrainz-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "MusicBrainz API client for reading and submitting metadata",
|
|
5
|
-
"main": "lib/
|
|
6
|
-
"types": "lib/
|
|
5
|
+
"main": "lib/index",
|
|
6
|
+
"types": "lib/index",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Borewit",
|
|
9
9
|
"url": "https://github.com/Borewit"
|
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
"web",
|
|
21
21
|
"service",
|
|
22
22
|
"submit",
|
|
23
|
-
"metabrainz"
|
|
23
|
+
"metabrainz",
|
|
24
|
+
"Cover Art Archive",
|
|
25
|
+
"coverartarchive",
|
|
26
|
+
"coverartarchive.org",
|
|
27
|
+
"album art",
|
|
28
|
+
"covers",
|
|
29
|
+
"download covers"
|
|
24
30
|
],
|
|
25
31
|
"license": "MIT",
|
|
26
32
|
"private": false,
|
|
@@ -45,13 +51,14 @@
|
|
|
45
51
|
"json-stringify-safe": "^5.0.1",
|
|
46
52
|
"jsontoxml": "^1.0.1",
|
|
47
53
|
"source-map-support": "^0.5.16",
|
|
48
|
-
"tough-cookie": "^4.
|
|
54
|
+
"tough-cookie": "^4.1.3",
|
|
49
55
|
"uuid": "^9.0.0"
|
|
50
56
|
},
|
|
51
57
|
"devDependencies": {
|
|
52
58
|
"@types/chai": "^4.3.0",
|
|
59
|
+
"@types/jsontoxml": "^1.0.5",
|
|
53
60
|
"@types/mocha": "^9.0.0",
|
|
54
|
-
"@types/node": "^
|
|
61
|
+
"@types/node": "^20.8.10",
|
|
55
62
|
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
|
56
63
|
"@typescript-eslint/parser": "^5.13.0",
|
|
57
64
|
"chai": "^4.2.0",
|
|
@@ -60,7 +67,7 @@
|
|
|
60
67
|
"eslint-config-prettier": "^8.4.0",
|
|
61
68
|
"eslint-import-resolver-typescript": "^3.3.0",
|
|
62
69
|
"eslint-plugin-import": "^2.25.4",
|
|
63
|
-
"eslint-plugin-jsdoc": "^
|
|
70
|
+
"eslint-plugin-jsdoc": "^46.8.2",
|
|
64
71
|
"eslint-plugin-node": "^11.1.0",
|
|
65
72
|
"eslint-plugin-unicorn": "^46.0.0",
|
|
66
73
|
"mocha": "^9.0.1",
|
|
@@ -71,6 +78,10 @@
|
|
|
71
78
|
"tslint": "^6.1.1",
|
|
72
79
|
"typescript": "^5.0.2"
|
|
73
80
|
},
|
|
81
|
+
"files": [
|
|
82
|
+
"lib/**/*.js",
|
|
83
|
+
"lib/**/*.d.ts"
|
|
84
|
+
],
|
|
74
85
|
"scripts": {
|
|
75
86
|
"clean": "del-cli lib/**/*.js lib/**/*.js.map lib/**/*.d.ts test/**/*.js test/**/*.js.map",
|
|
76
87
|
"compile-lib": "tsc -p lib",
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="CheckStyle-IDEA" serialisationVersion="2">
|
|
4
|
-
<checkstyleVersion>10.5.0</checkstyleVersion>
|
|
5
|
-
<scanScope>JavaOnly</scanScope>
|
|
6
|
-
<copyLibs>true</copyLibs>
|
|
7
|
-
<option name="thirdPartyClasspath" />
|
|
8
|
-
<option name="activeLocationIds" />
|
|
9
|
-
<option name="locations">
|
|
10
|
-
<list>
|
|
11
|
-
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
|
|
12
|
-
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
|
|
13
|
-
</list>
|
|
14
|
-
</option>
|
|
15
|
-
</component>
|
|
16
|
-
</project>
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
<component name="InspectionProjectProfileManager">
|
|
2
|
-
<profile version="1.0">
|
|
3
|
-
<option name="myName" value="Project Default" />
|
|
4
|
-
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
5
|
-
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
6
|
-
</profile>
|
|
7
|
-
</component>
|
package/.idea/misc.xml
DELETED
package/.idea/modules.xml
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<project version="4">
|
|
3
|
-
<component name="ProjectModuleManager">
|
|
4
|
-
<modules>
|
|
5
|
-
<module fileurl="file://$PROJECT_DIR$/.idea/musicbrainz-api.iml" filepath="$PROJECT_DIR$/.idea/musicbrainz-api.iml" />
|
|
6
|
-
</modules>
|
|
7
|
-
</component>
|
|
8
|
-
</project>
|