musicbrainz-api 0.15.0 → 0.17.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 +24 -1
- package/lib/coverartarchive-api.js +6 -5
- package/lib/digest-auth.js +10 -10
- package/lib/musicbrainz-api.d.ts +3 -1
- package/lib/musicbrainz-api.js +19 -14
- package/lib/musicbrainz.types.d.ts +3 -3
- package/package.json +16 -22
package/README.md
CHANGED
|
@@ -69,7 +69,10 @@ const config = {
|
|
|
69
69
|
},
|
|
70
70
|
|
|
71
71
|
// Your e-mail address, required for submitting ISRCs
|
|
72
|
-
appMail: string
|
|
72
|
+
appMail: string,
|
|
73
|
+
|
|
74
|
+
// Helpful if you have your own MusicBrainz server, default: false (optional)
|
|
75
|
+
disableRateLimiting: false,
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
const mbApi = new MusicbrainzApi(config);
|
|
@@ -391,6 +394,7 @@ await mbApi.addSpotifyIdToRecording(recording, '2AMysGXOe0zzZJMtH3Nizb');
|
|
|
391
394
|
|
|
392
395
|
Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
|
|
393
396
|
|
|
397
|
+
### Release Cover Art
|
|
394
398
|
```js
|
|
395
399
|
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
396
400
|
|
|
@@ -408,6 +412,25 @@ coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'back').then(releaseCover
|
|
|
408
412
|
|
|
409
413
|
```
|
|
410
414
|
|
|
415
|
+
### Release Group Cover Art
|
|
416
|
+
```js
|
|
417
|
+
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
418
|
+
|
|
419
|
+
coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid).then(releaseGroupCoverInfo => {
|
|
420
|
+
console.log('Release cover info', releaseGroupCoverInfo);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'front').then(releaseGroupCoverInfo => {
|
|
424
|
+
console.log('Get best front cover', releaseGroupCoverInfo);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, 'back').then(releaseGroupCoverInfo => {
|
|
428
|
+
console.log('Get best back cover', releaseGroupCoverInfo);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
|
|
411
434
|
## Compatibility
|
|
412
435
|
|
|
413
436
|
The JavaScript in runtime is compliant with [ECMAScript 2017 (ES8)](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017).
|
|
@@ -5,9 +5,9 @@ export class CoverArtArchiveApi {
|
|
|
5
5
|
this.host = 'coverartarchive.org';
|
|
6
6
|
}
|
|
7
7
|
async getJson(path) {
|
|
8
|
-
const response = await got.get(
|
|
8
|
+
const response = await got.get(`https://${this.host}${path}`, {
|
|
9
9
|
headers: {
|
|
10
|
-
Accept:
|
|
10
|
+
Accept: "application/json"
|
|
11
11
|
},
|
|
12
12
|
responseType: 'json'
|
|
13
13
|
});
|
|
@@ -39,14 +39,15 @@ export class CoverArtArchiveApi {
|
|
|
39
39
|
* @param coverType Cover type
|
|
40
40
|
*/
|
|
41
41
|
async getCovers(releaseId, releaseType = 'release', coverType) {
|
|
42
|
+
var _a;
|
|
42
43
|
const path = [releaseType, releaseId];
|
|
43
44
|
if (coverType) {
|
|
44
45
|
path.push(coverType);
|
|
45
46
|
}
|
|
46
|
-
const info = await this.getJson(
|
|
47
|
+
const info = await this.getJson(`/${path.join('/')}`);
|
|
47
48
|
// Hack to correct http addresses into https
|
|
48
|
-
if (info.release
|
|
49
|
-
info.release =
|
|
49
|
+
if ((_a = info.release) === null || _a === void 0 ? void 0 : _a.startsWith('http:')) {
|
|
50
|
+
info.release = `https${info.release.substring(4)}`;
|
|
50
51
|
}
|
|
51
52
|
return info;
|
|
52
53
|
}
|
package/lib/digest-auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import * as crypto from 'crypto';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
3
|
function md5(str) {
|
|
4
4
|
return crypto.createHash('md5').update(str).digest('hex'); // lgtm [js/insufficient-password-hash]
|
|
5
5
|
}
|
|
@@ -13,8 +13,8 @@ export class DigestAuth {
|
|
|
13
13
|
* HA1=MD5(MD5(username:realm:password):nonce:cnonce)
|
|
14
14
|
*/
|
|
15
15
|
static ha1Compute(algorithm, user, realm, pass, nonce, cnonce) {
|
|
16
|
-
const ha1 = md5(user
|
|
17
|
-
return algorithm && algorithm.toLowerCase() === 'md5-sess' ? md5(ha1
|
|
16
|
+
const ha1 = md5(`${user}:${realm}:${pass}`); // lgtm [js/insufficient-password-hash]
|
|
17
|
+
return algorithm && algorithm.toLowerCase() === 'md5-sess' ? md5(`${ha1}:${nonce}:${cnonce}`) : ha1;
|
|
18
18
|
}
|
|
19
19
|
constructor(credentials) {
|
|
20
20
|
this.credentials = credentials;
|
|
@@ -44,10 +44,10 @@ export class DigestAuth {
|
|
|
44
44
|
const nc = qop && '00000001';
|
|
45
45
|
const cnonce = qop && uuidv4().replace(/-/g, '');
|
|
46
46
|
const ha1 = DigestAuth.ha1Compute(challenge.algorithm, this.credentials.username, challenge.realm, this.credentials.password, challenge.nonce, cnonce);
|
|
47
|
-
const ha2 = md5(method
|
|
47
|
+
const ha2 = md5(`${method}:${path}`); // lgtm [js/insufficient-password-hash]
|
|
48
48
|
const digestResponse = qop
|
|
49
|
-
? md5(ha1
|
|
50
|
-
: md5(ha1
|
|
49
|
+
? md5(`${ha1}:${challenge.nonce}:${nc}:${cnonce}:${qop}:${ha2}`) // lgtm [js/insufficient-password-hash]
|
|
50
|
+
: md5(`${ha1}:${challenge.nonce}:${ha2}`); // lgtm [js/insufficient-password-hash]
|
|
51
51
|
const authValues = {
|
|
52
52
|
username: this.credentials.username,
|
|
53
53
|
realm: challenge.realm,
|
|
@@ -64,16 +64,16 @@ export class DigestAuth {
|
|
|
64
64
|
Object.entries(authValues).forEach(([key, value]) => {
|
|
65
65
|
if (value) {
|
|
66
66
|
if (key === 'qop' || key === 'nc' || key === 'algorithm') {
|
|
67
|
-
parts.push(key
|
|
67
|
+
parts.push(`${key}=${value}`);
|
|
68
68
|
}
|
|
69
69
|
else {
|
|
70
|
-
parts.push(key
|
|
70
|
+
parts.push(`${key}="${value}"`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
-
|
|
74
|
+
const digest = `Digest ${parts.join(', ')}`;
|
|
75
75
|
this.sentAuth = true;
|
|
76
|
-
return
|
|
76
|
+
return digest;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
//# sourceMappingURL=digest-auth.js.map
|
package/lib/musicbrainz-api.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { XmlMetadata } from './xml/xml-metadata.js';
|
|
|
2
2
|
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
|
-
import { XmlMetadata } from './xml/xml-metadata.js';
|
|
5
|
+
import type { XmlMetadata } from './xml/xml-metadata.js';
|
|
6
6
|
import * as mb from './musicbrainz.types.js';
|
|
7
7
|
export * from './musicbrainz.types.js';
|
|
8
8
|
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';
|
|
@@ -61,6 +61,7 @@ export interface IMusicBrainzConfig {
|
|
|
61
61
|
* User e-mail address or application URL
|
|
62
62
|
*/
|
|
63
63
|
appContactInfo?: string;
|
|
64
|
+
disableRateLimiting?: boolean;
|
|
64
65
|
}
|
|
65
66
|
export interface ICsrfSession {
|
|
66
67
|
sessionKey: string;
|
|
@@ -174,5 +175,6 @@ export declare class MusicBrainzApi {
|
|
|
174
175
|
*/
|
|
175
176
|
addSpotifyIdToRecording(recording: mb.IRecording, spotifyId: string, editNote: string): Promise<void>;
|
|
176
177
|
private getSession;
|
|
178
|
+
private applyRateLimiter;
|
|
177
179
|
}
|
|
178
180
|
export declare function makeAndQueryString(keyValuePairs: IFormData): string;
|
package/lib/musicbrainz-api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as assert from 'assert';
|
|
1
|
+
import * as assert from 'node:assert';
|
|
2
2
|
import { StatusCodes as HttpStatus } from 'http-status-codes';
|
|
3
3
|
import Debug from 'debug';
|
|
4
4
|
export { XmlMetadata } from './xml/xml-metadata.js';
|
|
@@ -11,7 +11,7 @@ import * as mb from './musicbrainz.types.js';
|
|
|
11
11
|
import got from 'got';
|
|
12
12
|
import { CookieJar } from 'tough-cookie';
|
|
13
13
|
export * from './musicbrainz.types.js';
|
|
14
|
-
import { promisify } from 'util';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
15
|
const debug = Debug('musicbrainz-api');
|
|
16
16
|
export class MusicBrainzApi {
|
|
17
17
|
static fetchCsrf(html) {
|
|
@@ -53,9 +53,8 @@ export class MusicBrainzApi {
|
|
|
53
53
|
}
|
|
54
54
|
async restGet(relUrl, query = {}) {
|
|
55
55
|
query.fmt = 'json';
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const response = await got.get('ws/2' + relUrl, {
|
|
56
|
+
await this.applyRateLimiter();
|
|
57
|
+
const response = await got.get(`ws/2${relUrl}`, {
|
|
59
58
|
...this.options,
|
|
60
59
|
searchParams: query,
|
|
61
60
|
responseType: 'json',
|
|
@@ -79,7 +78,7 @@ export class MusicBrainzApi {
|
|
|
79
78
|
if (Array.isArray(query.inc)) {
|
|
80
79
|
urlQuery.inc = urlQuery.inc.join(' ');
|
|
81
80
|
}
|
|
82
|
-
return this.restGet(
|
|
81
|
+
return this.restGet(`/${entity}/`, urlQuery);
|
|
83
82
|
}
|
|
84
83
|
// ---------------------------------------------------------------------------
|
|
85
84
|
async postRecording(xmlMetadata) {
|
|
@@ -87,7 +86,7 @@ export class MusicBrainzApi {
|
|
|
87
86
|
}
|
|
88
87
|
async post(entity, xmlMetadata) {
|
|
89
88
|
if (!this.config.appName || !this.config.appVersion) {
|
|
90
|
-
throw new Error(
|
|
89
|
+
throw new Error("XML-Post requires the appName & appVersion to be defined");
|
|
91
90
|
}
|
|
92
91
|
const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
|
|
93
92
|
const path = `ws/2/${entity}/`;
|
|
@@ -96,7 +95,7 @@ export class MusicBrainzApi {
|
|
|
96
95
|
let n = 1;
|
|
97
96
|
const postData = xmlMetadata.toXml();
|
|
98
97
|
do {
|
|
99
|
-
await this.
|
|
98
|
+
await this.applyRateLimiter();
|
|
100
99
|
const response = await got.post(path, {
|
|
101
100
|
...this.options,
|
|
102
101
|
searchParams: { client: clientId },
|
|
@@ -120,10 +119,10 @@ export class MusicBrainzApi {
|
|
|
120
119
|
} while (n++ < 5);
|
|
121
120
|
}
|
|
122
121
|
async login() {
|
|
123
|
-
var _a, _b;
|
|
122
|
+
var _a, _b, _c;
|
|
124
123
|
assert.ok((_a = this.config.botAccount) === null || _a === void 0 ? void 0 : _a.username, 'bot username should be set');
|
|
125
124
|
assert.ok((_b = this.config.botAccount) === null || _b === void 0 ? void 0 : _b.password, 'bot password should be set');
|
|
126
|
-
if (this.session
|
|
125
|
+
if ((_c = this.session) === null || _c === void 0 ? void 0 : _c.loggedIn) {
|
|
127
126
|
for (const cookie of await this.getCookies(this.options.prefixUrl)) {
|
|
128
127
|
if (cookie.key === 'remember_login') {
|
|
129
128
|
return true;
|
|
@@ -179,7 +178,7 @@ export class MusicBrainzApi {
|
|
|
179
178
|
*/
|
|
180
179
|
async editEntity(entity, mbid, formData) {
|
|
181
180
|
var _a, _b;
|
|
182
|
-
await this.
|
|
181
|
+
await this.applyRateLimiter();
|
|
183
182
|
this.session = await this.getSession();
|
|
184
183
|
formData.csrf_session_key = this.session.csrf.sessionKey;
|
|
185
184
|
formData.csrf_token = this.session.csrf.token;
|
|
@@ -192,7 +191,7 @@ export class MusicBrainzApi {
|
|
|
192
191
|
followRedirect: false
|
|
193
192
|
});
|
|
194
193
|
if (response.statusCode === HttpStatus.OK)
|
|
195
|
-
throw new Error(
|
|
194
|
+
throw new Error("Failed to submit form data");
|
|
196
195
|
if (response.statusCode === HttpStatus.MOVED_TEMPORARILY)
|
|
197
196
|
return;
|
|
198
197
|
throw new Error(`Unexpected status code: ${response.statusCode}`);
|
|
@@ -224,7 +223,7 @@ export class MusicBrainzApi {
|
|
|
224
223
|
*/
|
|
225
224
|
async addIsrc(recording, isrc) {
|
|
226
225
|
const formData = {};
|
|
227
|
-
formData[
|
|
226
|
+
formData["edit-recording.name"] = recording.title; // Required
|
|
228
227
|
if (!recording.isrcs) {
|
|
229
228
|
throw new Error('You must retrieve recording with existing ISRC values');
|
|
230
229
|
}
|
|
@@ -250,7 +249,7 @@ export class MusicBrainzApi {
|
|
|
250
249
|
assert.strictEqual(spotifyId.length, 22);
|
|
251
250
|
return this.addUrlToRecording(recording, {
|
|
252
251
|
linkTypeId: mb.LinkType.stream_for_free,
|
|
253
|
-
text:
|
|
252
|
+
text: `https://open.spotify.com/track/${spotifyId}`
|
|
254
253
|
}, editNote);
|
|
255
254
|
}
|
|
256
255
|
async getSession() {
|
|
@@ -263,6 +262,12 @@ export class MusicBrainzApi {
|
|
|
263
262
|
csrf: MusicBrainzApi.fetchCsrf(response.body)
|
|
264
263
|
};
|
|
265
264
|
}
|
|
265
|
+
async applyRateLimiter() {
|
|
266
|
+
if (!this.config.disableRateLimiting) {
|
|
267
|
+
const delay = await this.rateLimiter.limit();
|
|
268
|
+
debug(`Client side rate limiter activated: cool down for ${Math.round(delay / 100) / 10} s...`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
266
271
|
}
|
|
267
272
|
export function makeAndQueryString(keyValuePairs) {
|
|
268
273
|
return Object.keys(keyValuePairs).map(key => `${key}:"${keyValuePairs[key]}"`).join(' AND ');
|
|
@@ -36,7 +36,7 @@ export interface IArtist extends IEntity {
|
|
|
36
36
|
'gender-id'?: string;
|
|
37
37
|
'life-span'?: IPeriod;
|
|
38
38
|
country?: string;
|
|
39
|
-
ipis?:
|
|
39
|
+
ipis?: string[];
|
|
40
40
|
isnis?: string[];
|
|
41
41
|
aliases?: IAlias[];
|
|
42
42
|
gender?: string;
|
|
@@ -200,13 +200,13 @@ export interface IUrlList extends ISearchResult {
|
|
|
200
200
|
}
|
|
201
201
|
export type RelationDirection = 'backward' | 'forward';
|
|
202
202
|
export interface IRelation {
|
|
203
|
-
'attribute-ids':
|
|
203
|
+
'attribute-ids': unknown[];
|
|
204
204
|
direction: RelationDirection;
|
|
205
205
|
'target-credit': string;
|
|
206
206
|
end: null | unknown;
|
|
207
207
|
'source-credit': string;
|
|
208
208
|
ended: boolean;
|
|
209
|
-
'attribute-values': unknown;
|
|
209
|
+
'attribute-values': unknown[];
|
|
210
210
|
attributes?: any[];
|
|
211
211
|
type: string;
|
|
212
212
|
begin?: null | unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musicbrainz-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
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,9 +45,10 @@
|
|
|
45
45
|
"url": "https://github.com/Borewit/musicbrainz-api/issues"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@biomejs/biome": "1.8.3",
|
|
48
49
|
"@types/caseless": "^0.12.1",
|
|
49
50
|
"@types/request-promise-native": "^1.0.17",
|
|
50
|
-
"@types/uuid": "^
|
|
51
|
+
"@types/uuid": "^10.0.0",
|
|
51
52
|
"caseless": "^0.12.0",
|
|
52
53
|
"debug": "^4.3.4",
|
|
53
54
|
"got": "^14.2.1",
|
|
@@ -57,43 +58,36 @@
|
|
|
57
58
|
"rate-limit-threshold": "^0.1.5",
|
|
58
59
|
"source-map-support": "^0.5.16",
|
|
59
60
|
"tough-cookie": "^4.1.3",
|
|
60
|
-
"uuid": "^
|
|
61
|
+
"uuid": "^10.0.0"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@types/chai": "^4.3.0",
|
|
64
65
|
"@types/jsontoxml": "^1.0.5",
|
|
65
66
|
"@types/mocha": "^10.0.4",
|
|
66
|
-
"@types/node": "^
|
|
67
|
-
"@
|
|
68
|
-
"
|
|
69
|
-
"c8": "^9.1.0",
|
|
67
|
+
"@types/node": "^22.1.0",
|
|
68
|
+
"@types/sinon": "^17.0.3",
|
|
69
|
+
"c8": "^10.1.2",
|
|
70
70
|
"chai": "^5.1.0",
|
|
71
71
|
"del-cli": "^5.0.0",
|
|
72
|
-
"eslint": "^8.10.0",
|
|
73
|
-
"eslint-config-prettier": "^9.0.0",
|
|
74
|
-
"eslint-import-resolver-typescript": "^3.3.0",
|
|
75
|
-
"eslint-plugin-import": "^2.25.4",
|
|
76
|
-
"eslint-plugin-jsdoc": "^48.2.2",
|
|
77
|
-
"eslint-plugin-node": "^11.1.0",
|
|
78
|
-
"eslint-plugin-unicorn": "^49.0.0",
|
|
79
72
|
"mocha": "^10.1.0",
|
|
80
73
|
"remark-cli": "^12.0.0",
|
|
81
|
-
"remark-preset-lint-recommended": "^
|
|
74
|
+
"remark-preset-lint-recommended": "^7.0.0",
|
|
75
|
+
"sinon": "^18.0.0",
|
|
82
76
|
"ts-node": "^10.0.0",
|
|
83
77
|
"typescript": "^5.0.2"
|
|
84
78
|
},
|
|
85
79
|
"scripts": {
|
|
86
|
-
"clean": "del-cli lib/**/*.js lib/**/*.js.map lib/**/*.d.ts test/**/*.js test/**/*.js.map",
|
|
80
|
+
"clean": "del-cli 'lib/**/*.js' 'lib/**/*.js.map' 'lib/**/*.d.ts' 'test/**/*.js' 'test/**/*.js.map'",
|
|
87
81
|
"compile-lib": "tsc -p lib",
|
|
88
82
|
"compile-test": "tsc -p test",
|
|
89
|
-
"compile": "
|
|
90
|
-
"eslint": "eslint lib/**/*.ts --ignore-pattern lib/**/*.d.ts test/**/*.ts",
|
|
83
|
+
"compile": "yarn run compile-lib && yarn run compile-test",
|
|
91
84
|
"lint-md": "remark -u preset-lint-recommended .",
|
|
92
|
-
"lint": "
|
|
85
|
+
"lint-ts": "biome check",
|
|
86
|
+
"lint": "yarn run lint-md && yarn run lint-ts",
|
|
93
87
|
"test": "mocha",
|
|
94
|
-
"build": "
|
|
95
|
-
"start": "
|
|
96
|
-
"test-coverage": "c8
|
|
88
|
+
"build": "yarn run clean && yarn run compile",
|
|
89
|
+
"start": "yarn run compile && yarn run lint && yarn run cover-test",
|
|
90
|
+
"test-coverage": "c8 yarn run test",
|
|
97
91
|
"send-codacy": "nyc report --reporter=text-lcov | codacy-coverage"
|
|
98
92
|
},
|
|
99
93
|
"nyc": {
|