musicbrainz-api 0.17.0 → 0.18.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 +69 -46
- package/lib/coverartarchive-api.d.ts +1 -1
- package/lib/coverartarchive-api.js +6 -8
- package/lib/digest-auth.js +2 -4
- package/lib/httpClient.d.ts +27 -0
- package/lib/httpClient.js +55 -0
- package/lib/musicbrainz-api.d.ts +3 -4
- package/lib/musicbrainz-api.js +44 -70
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
[](https://github.com/Borewit/musicbrainz-api/actions/workflows/nodejs-ci.yml)
|
|
2
|
+
[](https://github.com/Borewit/musicbrainz-api/actions/workflows/github-code-scanning/codeql)
|
|
2
3
|
[](https://npmjs.org/package/musicbrainz-api)
|
|
3
4
|
[](https://npmcharts.com/compare/musicbrainz-api?interval=30&start=365)
|
|
4
5
|
[](https://coveralls.io/github/Borewit/musicbrainz-api?branch=master)
|
|
5
6
|
[](https://app.codacy.com/gh/Borewit/musicbrainz-api/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
6
|
-
[](https://github.com/Borewit/musicbrainz-api/actions/workflows/codeql.yml)
|
|
7
7
|
[](https://snyk.io/test/github/Borewit/musicbrainz-api?targetFile=package.json)
|
|
8
8
|
[](https://deepscan.io/dashboard#view=project&tid=5165&pid=6991&bid=63373)
|
|
9
9
|
[](https://discord.gg/958xT5X)
|
|
10
|
+
[](https://bundlejs.com/?q=musicbrainz-api)
|
|
10
11
|
|
|
11
12
|
# musicbrainz-api
|
|
12
13
|
|
|
13
14
|
A MusicBrainz-API-client for reading and submitting metadata
|
|
14
15
|
|
|
15
16
|
## Features
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
- Access metadata from MusicBrainz
|
|
18
|
+
- Submit metadata
|
|
19
|
+
- Smart and adjustable throttling, like MusicBrainz, it allows a bursts of requests
|
|
20
|
+
- Build in TypeScript definitions
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
## Compatibility
|
|
23
|
+
|
|
24
|
+
Module: [Pure ECMAScript Module (ESM)](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).
|
|
25
|
+
The distributed JavaScript codebase is compliant with the [ECMAScript 2020 (11th Edition)](https://en.wikipedia.org/wiki/ECMAScript_version_history#11th_Edition_%E2%80%93_ECMAScript_2020) standard.
|
|
26
|
+
|
|
27
|
+
This module requires a [Node.js ≥ 20](https://nodejs.org/en/about/previous-releases) engine.
|
|
28
|
+
|
|
29
|
+
> [!NOTE]
|
|
30
|
+
> We are looking into making this package usable in the browser as well.
|
|
31
|
+
|
|
32
|
+
## Sponsor
|
|
33
|
+
If you appreciate my work and want to support the development of open-source projects like [musicbrainz-api](https://github.com/Borewit/musicbrainz-api), [music-metadata](https://github.com/Borewit/music-metadata), [file-type](https://github.com/sindresorhus/file-type), [listFix()](https://github.com/Borewit/listFix), [lizzy](https://github.com/Borewit/lizzy), [strtok3](https://github.com/Borewit/strtok3), or [tokenizer-s3](https://github.com/Borewit/tokenizer-s3), please consider becoming a sponsor or making a small contribution.
|
|
34
|
+
Your support helps sustain ongoing development and improvements.
|
|
35
|
+
[Become a sponsor to Borewit](https://github.com/sponsors/Borewit)
|
|
22
36
|
|
|
23
37
|
This package is currently only developed for the use in a [node.js environment](http://nodejs.org/).
|
|
24
38
|
We are looking into making this package usable in the browser as well.
|
|
@@ -85,15 +99,14 @@ MusicBrainz API documentation: [XML Web Service/Version 2 Lookups](https://wiki.
|
|
|
85
99
|
### Generic lookup function
|
|
86
100
|
|
|
87
101
|
Arguments:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
- entity: `'area'` | `'artist'` | `'collection'` | `'instrument'` | `'label'` | `'place'` | `'release'` | `'release-group'` | `'recording'` | `'series'` | `'work'` | `'url'` | `'event'`
|
|
103
|
+
- MBID [(MusicBrainz identifier)](https://wiki.musicbrainz.org/MusicBrainz_Identifier)
|
|
104
|
+
- query
|
|
91
105
|
|
|
92
106
|
```js
|
|
93
107
|
const artist = await mbApi.lookup('artist', 'ab2528d9-719f-4261-8098-21849222a0f2');
|
|
94
108
|
```
|
|
95
109
|
|
|
96
|
-
|
|
97
110
|
| Query argument | Query value |
|
|
98
111
|
|-----------------------|-----------------|
|
|
99
112
|
| `query.collection` | Collection MBID |
|
|
@@ -263,19 +276,19 @@ There are different search fields depending on the entity.
|
|
|
263
276
|
Searches can be performed using the generic search function: `query(entity: mb.EntityType, query: string | IFormData, offset?: number, limit?: number): Promise<entity>`
|
|
264
277
|
|
|
265
278
|
Arguments:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
+
- Entity type, which can be one of:
|
|
280
|
+
- `artist`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Artist)
|
|
281
|
+
- `label`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Label)
|
|
282
|
+
- `recording`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Recording)
|
|
283
|
+
- `release`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release)
|
|
284
|
+
- `release-group`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release_Group)
|
|
285
|
+
- `work`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Work)
|
|
286
|
+
- `area`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Area)
|
|
287
|
+
- `url`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#URL)
|
|
288
|
+
- `query {query: string, offset: number, limit: number}`
|
|
289
|
+
- `query.query`: supports the full Lucene Search syntax; you can find a detailed guide at [Lucene Search Syntax](https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description). For example, you can set conditions while searching for a name with the AND operator.
|
|
290
|
+
- `query.offset`: optional, return search results starting at a given offset. Used for paging through more than one page of results.
|
|
291
|
+
- `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.
|
|
279
292
|
|
|
280
293
|
For example, to find any recordings of _'We Will Rock You'_ by Queen:
|
|
281
294
|
```js
|
|
@@ -348,8 +361,7 @@ For all of the following function you need to use a dedicated bot account.
|
|
|
348
361
|
|
|
349
362
|
## Submitting ISRC via post user form-data
|
|
350
363
|
|
|
351
|
-
|
|
352
|
-
Use with caution, and only on a test server, it may clear existing metadata as side effect.
|
|
364
|
+
Use with caution, and only on a test server, it may clear existing metadata has side effect.
|
|
353
365
|
|
|
354
366
|
```js
|
|
355
367
|
|
|
@@ -396,37 +408,48 @@ Implementation of the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_
|
|
|
396
408
|
|
|
397
409
|
### Release Cover Art
|
|
398
410
|
```js
|
|
399
|
-
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
411
|
+
import { CoverArtArchiveApi } from 'musicbrainz-api';
|
|
400
412
|
|
|
401
|
-
coverArtArchiveApiClient
|
|
402
|
-
console.log('Release cover info', releaseCoverInfo);
|
|
403
|
-
});
|
|
413
|
+
const coverArtArchiveApiClient = new CoverArtArchiveApi();
|
|
404
414
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
415
|
+
async function getReleaseCoverArt(releaseMbid, coverType = '') {
|
|
416
|
+
try {
|
|
417
|
+
const coverInfo = await coverArtArchiveApiClient.getReleaseCovers(releaseMbid, coverType);
|
|
418
|
+
console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
412
423
|
|
|
424
|
+
(async () => {
|
|
425
|
+
const releaseMbid = 'your-release-mbid-here'; // Replace with actual MBID
|
|
426
|
+
await getReleaseCoverArt(releaseMbid); // Get all covers
|
|
427
|
+
await getReleaseCoverArt(releaseMbid, 'front'); // Get best front cover
|
|
428
|
+
await getReleaseCoverArt(releaseMbid, 'back'); // Get best back cover
|
|
429
|
+
})();
|
|
413
430
|
```
|
|
414
431
|
|
|
415
432
|
### Release Group Cover Art
|
|
416
433
|
```js
|
|
417
|
-
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
434
|
+
import { CoverArtArchiveApi } from 'musicbrainz-api';
|
|
418
435
|
|
|
419
|
-
coverArtArchiveApiClient
|
|
420
|
-
console.log('Release cover info', releaseGroupCoverInfo);
|
|
421
|
-
});
|
|
436
|
+
const coverArtArchiveApiClient = new CoverArtArchiveApi();
|
|
422
437
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
438
|
+
async function getCoverArt(releaseGroupMbid, coverType = '') {
|
|
439
|
+
try {
|
|
440
|
+
const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, coverType);
|
|
441
|
+
console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
426
446
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
447
|
+
(async () => {
|
|
448
|
+
const releaseGroupMbid = 'your-release-group-mbid-here'; // Replace with actual MBID
|
|
449
|
+
await getCoverArt(releaseGroupMbid); // Get all covers
|
|
450
|
+
await getCoverArt(releaseGroupMbid, 'front'); // Get best front cover
|
|
451
|
+
await getCoverArt(releaseGroupMbid, 'back'); // Get best back cover
|
|
452
|
+
})();
|
|
430
453
|
|
|
431
454
|
```
|
|
432
455
|
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/* eslint-disable-next-line */
|
|
2
|
-
import
|
|
2
|
+
import { HttpClient } from "./httpClient.js";
|
|
3
3
|
export class CoverArtArchiveApi {
|
|
4
4
|
constructor() {
|
|
5
|
-
this.
|
|
5
|
+
this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 20000 });
|
|
6
6
|
}
|
|
7
7
|
async getJson(path) {
|
|
8
|
-
const response = await
|
|
8
|
+
const response = await this.httpClient.get(path, {
|
|
9
9
|
headers: {
|
|
10
10
|
Accept: "application/json"
|
|
11
|
-
}
|
|
12
|
-
responseType: 'json'
|
|
11
|
+
}
|
|
13
12
|
});
|
|
14
|
-
return response.
|
|
13
|
+
return response.json();
|
|
15
14
|
}
|
|
16
15
|
/**
|
|
17
16
|
* Fetch release
|
|
@@ -39,14 +38,13 @@ export class CoverArtArchiveApi {
|
|
|
39
38
|
* @param coverType Cover type
|
|
40
39
|
*/
|
|
41
40
|
async getCovers(releaseId, releaseType = 'release', coverType) {
|
|
42
|
-
var _a;
|
|
43
41
|
const path = [releaseType, releaseId];
|
|
44
42
|
if (coverType) {
|
|
45
43
|
path.push(coverType);
|
|
46
44
|
}
|
|
47
45
|
const info = await this.getJson(`/${path.join('/')}`);
|
|
48
46
|
// Hack to correct http addresses into https
|
|
49
|
-
if (
|
|
47
|
+
if (info.release?.startsWith('http:')) {
|
|
50
48
|
info.release = `https${info.release.substring(4)}`;
|
|
51
49
|
}
|
|
52
50
|
return info;
|
package/lib/digest-auth.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
return crypto.createHash('md5').update(str).digest('hex'); // lgtm [js/insufficient-password-hash]
|
|
5
|
-
}
|
|
2
|
+
import sparkMd5 from 'spark-md5';
|
|
3
|
+
const md5 = sparkMd5.hash;
|
|
6
4
|
export class DigestAuth {
|
|
7
5
|
/**
|
|
8
6
|
* RFC 2617: handle both MD5 and MD5-sess algorithms.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type HttpFormData = {
|
|
2
|
+
[key: string]: string;
|
|
3
|
+
};
|
|
4
|
+
export interface IHttpClientOptions {
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
timeout: number;
|
|
7
|
+
userAgent: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IFetchOptions {
|
|
10
|
+
query?: HttpFormData;
|
|
11
|
+
retryLimit?: number;
|
|
12
|
+
body?: string;
|
|
13
|
+
headers?: HeadersInit;
|
|
14
|
+
followRedirects?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class HttpClient {
|
|
17
|
+
private options;
|
|
18
|
+
private cookieJar;
|
|
19
|
+
constructor(options: IHttpClientOptions);
|
|
20
|
+
get(path: string, options?: IFetchOptions): Promise<Response>;
|
|
21
|
+
post(path: string, options?: IFetchOptions): Promise<Response>;
|
|
22
|
+
postForm(path: string, formData: HttpFormData, options?: IFetchOptions): Promise<Response>;
|
|
23
|
+
postJson(path: string, json: Object, options?: IFetchOptions): Promise<Response>;
|
|
24
|
+
private _fetch;
|
|
25
|
+
private registerCookies;
|
|
26
|
+
getCookies(): Promise<string>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CookieJar } from "tough-cookie";
|
|
2
|
+
export class HttpClient {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this.options = options;
|
|
5
|
+
this.cookieJar = new CookieJar();
|
|
6
|
+
}
|
|
7
|
+
get(path, options) {
|
|
8
|
+
return this._fetch('get', path, options);
|
|
9
|
+
}
|
|
10
|
+
post(path, options) {
|
|
11
|
+
return this._fetch('post', path, options);
|
|
12
|
+
}
|
|
13
|
+
postForm(path, formData, options) {
|
|
14
|
+
const encodedFormData = new URLSearchParams(formData).toString();
|
|
15
|
+
return this._fetch('post', path, { ...options, body: encodedFormData, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
|
16
|
+
}
|
|
17
|
+
// biome-ignore lint/complexity/noBannedTypes:
|
|
18
|
+
postJson(path, json, options) {
|
|
19
|
+
const encodedJson = JSON.stringify(json);
|
|
20
|
+
return this._fetch('post', path, { ...options, body: encodedJson, headers: { 'Content-Type': 'application/json.' } });
|
|
21
|
+
}
|
|
22
|
+
async _fetch(method, path, options) {
|
|
23
|
+
if (!options)
|
|
24
|
+
options = {};
|
|
25
|
+
let url = `${this.options.baseUrl}/${path}`;
|
|
26
|
+
if (options.query) {
|
|
27
|
+
url += `?${new URLSearchParams(options.query)}`;
|
|
28
|
+
}
|
|
29
|
+
const cookies = await this.getCookies();
|
|
30
|
+
const headers = {
|
|
31
|
+
...options.headers,
|
|
32
|
+
'User-Agent': this.options.userAgent,
|
|
33
|
+
'Cookie': cookies
|
|
34
|
+
};
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method,
|
|
37
|
+
...options,
|
|
38
|
+
headers,
|
|
39
|
+
body: options.body,
|
|
40
|
+
redirect: options.followRedirects === false ? 'manual' : 'follow'
|
|
41
|
+
});
|
|
42
|
+
await this.registerCookies(response);
|
|
43
|
+
return response;
|
|
44
|
+
}
|
|
45
|
+
registerCookies(response) {
|
|
46
|
+
const cookie = response.headers.get('set-cookie');
|
|
47
|
+
if (cookie) {
|
|
48
|
+
return this.cookieJar.setCookie(cookie, response.url);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
getCookies() {
|
|
52
|
+
return this.cookieJar.getCookieString(this.options.baseUrl); // Get cookies for the request
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=httpClient.js.map
|
package/lib/musicbrainz-api.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface IMusicBrainzConfig {
|
|
|
50
50
|
username?: string;
|
|
51
51
|
password?: string;
|
|
52
52
|
};
|
|
53
|
-
baseUrl
|
|
53
|
+
baseUrl: string;
|
|
54
54
|
appName?: string;
|
|
55
55
|
appVersion?: string;
|
|
56
56
|
/**
|
|
@@ -74,14 +74,13 @@ export interface ISessionInformation {
|
|
|
74
74
|
export declare class MusicBrainzApi {
|
|
75
75
|
readonly config: IMusicBrainzConfig;
|
|
76
76
|
private rateLimiter;
|
|
77
|
-
private
|
|
77
|
+
private httpClient;
|
|
78
78
|
private session?;
|
|
79
79
|
static fetchCsrf(html: string): ICsrfSession;
|
|
80
80
|
private static fetchValue;
|
|
81
|
-
private getCookies;
|
|
82
81
|
constructor(_config?: IMusicBrainzConfig);
|
|
83
82
|
restGet<T>(relUrl: string, query?: {
|
|
84
|
-
[key: string]:
|
|
83
|
+
[key: string]: string;
|
|
85
84
|
}): Promise<T>;
|
|
86
85
|
/**
|
|
87
86
|
* Lookup entity
|
package/lib/musicbrainz-api.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as assert from 'node:assert';
|
|
2
1
|
import { StatusCodes as HttpStatus } from 'http-status-codes';
|
|
3
2
|
import Debug from 'debug';
|
|
4
3
|
export { XmlMetadata } from './xml/xml-metadata.js';
|
|
@@ -8,10 +7,8 @@ export { XmlRecording } from './xml/xml-recording.js';
|
|
|
8
7
|
import { DigestAuth } from './digest-auth.js';
|
|
9
8
|
import { RateLimitThreshold } from 'rate-limit-threshold';
|
|
10
9
|
import * as mb from './musicbrainz.types.js';
|
|
11
|
-
import
|
|
12
|
-
import { CookieJar } from 'tough-cookie';
|
|
10
|
+
import { HttpClient } from "./httpClient.js";
|
|
13
11
|
export * from './musicbrainz.types.js';
|
|
14
|
-
import { promisify } from 'node:util';
|
|
15
12
|
const debug = Debug('musicbrainz-api');
|
|
16
13
|
export class MusicBrainzApi {
|
|
17
14
|
static fetchCsrf(html) {
|
|
@@ -36,33 +33,21 @@ export class MusicBrainzApi {
|
|
|
36
33
|
baseUrl: 'https://musicbrainz.org'
|
|
37
34
|
};
|
|
38
35
|
Object.assign(this.config, _config);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
timeout: {
|
|
45
|
-
read: 20 * 1000
|
|
46
|
-
},
|
|
47
|
-
headers: {
|
|
48
|
-
'User-Agent': `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
49
|
-
},
|
|
50
|
-
cookieJar: cookieJar
|
|
51
|
-
};
|
|
36
|
+
this.httpClient = new HttpClient({
|
|
37
|
+
baseUrl: this.config.baseUrl,
|
|
38
|
+
timeout: 20 * 1000,
|
|
39
|
+
userAgent: `${this.config.appName}/${this.config.appVersion} ( ${this.config.appContactInfo} )`
|
|
40
|
+
});
|
|
52
41
|
this.rateLimiter = new RateLimitThreshold(15, 18);
|
|
53
42
|
}
|
|
54
43
|
async restGet(relUrl, query = {}) {
|
|
55
44
|
query.fmt = 'json';
|
|
56
45
|
await this.applyRateLimiter();
|
|
57
|
-
const response = await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
responseType: 'json',
|
|
61
|
-
retry: {
|
|
62
|
-
limit: 10
|
|
63
|
-
}
|
|
46
|
+
const response = await this.httpClient.get(`ws/2${relUrl}`, {
|
|
47
|
+
query,
|
|
48
|
+
retryLimit: 10
|
|
64
49
|
});
|
|
65
|
-
return response.
|
|
50
|
+
return response.json();
|
|
66
51
|
}
|
|
67
52
|
lookup(entity, mbid, inc = []) {
|
|
68
53
|
return this.restGet(`/${entity}/${mbid}`, { inc: inc.join(' ') });
|
|
@@ -91,20 +76,18 @@ export class MusicBrainzApi {
|
|
|
91
76
|
const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
|
|
92
77
|
const path = `ws/2/${entity}/`;
|
|
93
78
|
// Get digest challenge
|
|
94
|
-
let digest;
|
|
79
|
+
let digest = '';
|
|
95
80
|
let n = 1;
|
|
96
81
|
const postData = xmlMetadata.toXml();
|
|
97
82
|
do {
|
|
98
83
|
await this.applyRateLimiter();
|
|
99
|
-
const response = await
|
|
100
|
-
|
|
101
|
-
searchParams: { client: clientId },
|
|
84
|
+
const response = await this.httpClient.post(path, {
|
|
85
|
+
query: { client: clientId },
|
|
102
86
|
headers: {
|
|
103
87
|
authorization: digest,
|
|
104
88
|
'Content-Type': 'application/xml'
|
|
105
89
|
},
|
|
106
|
-
body: postData
|
|
107
|
-
throwHttpErrors: false
|
|
90
|
+
body: postData
|
|
108
91
|
});
|
|
109
92
|
if (response.statusCode === HttpStatus.UNAUTHORIZED) {
|
|
110
93
|
// Respond to digest challenge
|
|
@@ -119,15 +102,13 @@ export class MusicBrainzApi {
|
|
|
119
102
|
} while (n++ < 5);
|
|
120
103
|
}
|
|
121
104
|
async login() {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
}
|
|
105
|
+
if (!this.config.botAccount?.username)
|
|
106
|
+
throw new Error('bot username should be set');
|
|
107
|
+
if (!this.config.botAccount?.password)
|
|
108
|
+
throw new Error('bot password should be set');
|
|
109
|
+
if (this.session?.loggedIn) {
|
|
110
|
+
const cookies = await this.httpClient.getCookies();
|
|
111
|
+
return cookies.indexOf('musicbrainz_server_session') !== -1;
|
|
131
112
|
}
|
|
132
113
|
this.session = await this.getSession();
|
|
133
114
|
const redirectUri = '/success';
|
|
@@ -136,17 +117,15 @@ export class MusicBrainzApi {
|
|
|
136
117
|
password: this.config.botAccount.password,
|
|
137
118
|
csrf_session_key: this.session.csrf.sessionKey,
|
|
138
119
|
csrf_token: this.session.csrf.token,
|
|
139
|
-
remember_me: 1
|
|
120
|
+
remember_me: '1'
|
|
140
121
|
};
|
|
141
|
-
const response = await
|
|
142
|
-
|
|
143
|
-
followRedirect: false,
|
|
144
|
-
searchParams: {
|
|
122
|
+
const response = await this.httpClient.postForm('login', formData, {
|
|
123
|
+
query: {
|
|
145
124
|
returnto: redirectUri
|
|
146
125
|
},
|
|
147
|
-
|
|
126
|
+
followRedirects: false
|
|
148
127
|
});
|
|
149
|
-
const success = response.
|
|
128
|
+
const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
|
|
150
129
|
if (success) {
|
|
151
130
|
this.session.loggedIn = true;
|
|
152
131
|
}
|
|
@@ -157,14 +136,13 @@ export class MusicBrainzApi {
|
|
|
157
136
|
*/
|
|
158
137
|
async logout() {
|
|
159
138
|
const redirectUri = '/success';
|
|
160
|
-
const response = await
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
searchParams: {
|
|
139
|
+
const response = await this.httpClient.post('logout', {
|
|
140
|
+
followRedirects: false,
|
|
141
|
+
query: {
|
|
164
142
|
returnto: redirectUri
|
|
165
143
|
}
|
|
166
144
|
});
|
|
167
|
-
const success = response.
|
|
145
|
+
const success = response.status === HttpStatus.MOVED_TEMPORARILY && response.headers.get('location') === redirectUri;
|
|
168
146
|
if (success && this.session) {
|
|
169
147
|
this.session.loggedIn = true;
|
|
170
148
|
}
|
|
@@ -177,24 +155,21 @@ export class MusicBrainzApi {
|
|
|
177
155
|
* @param formData
|
|
178
156
|
*/
|
|
179
157
|
async editEntity(entity, mbid, formData) {
|
|
180
|
-
var _a, _b;
|
|
181
158
|
await this.applyRateLimiter();
|
|
182
159
|
this.session = await this.getSession();
|
|
183
160
|
formData.csrf_session_key = this.session.csrf.sessionKey;
|
|
184
161
|
formData.csrf_token = this.session.csrf.token;
|
|
185
|
-
formData.username =
|
|
186
|
-
formData.password =
|
|
162
|
+
formData.username = this.config.botAccount?.username;
|
|
163
|
+
formData.password = this.config.botAccount?.password;
|
|
187
164
|
formData.remember_me = 1;
|
|
188
|
-
const response = await
|
|
189
|
-
|
|
190
|
-
form: formData,
|
|
191
|
-
followRedirect: false
|
|
165
|
+
const response = await this.httpClient.postForm(`${entity}/${mbid}/edit`, formData, {
|
|
166
|
+
followRedirects: false
|
|
192
167
|
});
|
|
193
|
-
if (response.
|
|
168
|
+
if (response.status === HttpStatus.OK)
|
|
194
169
|
throw new Error("Failed to submit form data");
|
|
195
|
-
if (response.
|
|
170
|
+
if (response.status === HttpStatus.MOVED_TEMPORARILY)
|
|
196
171
|
return;
|
|
197
|
-
throw new Error(`Unexpected status code: ${response.
|
|
172
|
+
throw new Error(`Unexpected status code: ${response.status}`);
|
|
198
173
|
}
|
|
199
174
|
/**
|
|
200
175
|
* Set URL to recording
|
|
@@ -203,14 +178,13 @@ export class MusicBrainzApi {
|
|
|
203
178
|
* @param editNote Edit note
|
|
204
179
|
*/
|
|
205
180
|
async addUrlToRecording(recording, url2add, editNote = '') {
|
|
206
|
-
var _a;
|
|
207
181
|
const formData = {};
|
|
208
182
|
formData['edit-recording.name'] = recording.title; // Required
|
|
209
183
|
formData['edit-recording.comment'] = recording.disambiguation;
|
|
210
184
|
formData['edit-recording.make_votable'] = true;
|
|
211
185
|
formData['edit-recording.url.0.link_type_id'] = url2add.linkTypeId;
|
|
212
186
|
formData['edit-recording.url.0.text'] = url2add.text;
|
|
213
|
-
|
|
187
|
+
recording.isrcs?.forEach((isrcs, i) => {
|
|
214
188
|
formData[`edit-recording.isrcs.${i}`] = isrcs;
|
|
215
189
|
});
|
|
216
190
|
formData['edit-recording.edit_note'] = editNote;
|
|
@@ -246,20 +220,20 @@ export class MusicBrainzApi {
|
|
|
246
220
|
* @param editNote Comment to add.
|
|
247
221
|
*/
|
|
248
222
|
addSpotifyIdToRecording(recording, spotifyId, editNote) {
|
|
249
|
-
|
|
223
|
+
if (spotifyId.length !== 22) {
|
|
224
|
+
throw new Error('Invalid Spotify ID length');
|
|
225
|
+
}
|
|
250
226
|
return this.addUrlToRecording(recording, {
|
|
251
227
|
linkTypeId: mb.LinkType.stream_for_free,
|
|
252
228
|
text: `https://open.spotify.com/track/${spotifyId}`
|
|
253
229
|
}, editNote);
|
|
254
230
|
}
|
|
255
231
|
async getSession() {
|
|
256
|
-
const response = await
|
|
257
|
-
|
|
258
|
-
followRedirect: false, // Disable redirects
|
|
259
|
-
responseType: 'text'
|
|
232
|
+
const response = await this.httpClient.get('login', {
|
|
233
|
+
followRedirects: false
|
|
260
234
|
});
|
|
261
235
|
return {
|
|
262
|
-
csrf: MusicBrainzApi.fetchCsrf(response.
|
|
236
|
+
csrf: MusicBrainzApi.fetchCsrf(await response.text())
|
|
263
237
|
};
|
|
264
238
|
}
|
|
265
239
|
async applyRateLimiter() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musicbrainz-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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,36 +45,36 @@
|
|
|
45
45
|
"url": "https://github.com/Borewit/musicbrainz-api/issues"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@biomejs/biome": "1.8.3",
|
|
49
|
-
"@types/caseless": "^0.12.1",
|
|
50
|
-
"@types/request-promise-native": "^1.0.17",
|
|
51
|
-
"@types/uuid": "^10.0.0",
|
|
52
|
-
"caseless": "^0.12.0",
|
|
53
48
|
"debug": "^4.3.4",
|
|
54
|
-
"got": "^14.2.1",
|
|
55
49
|
"http-status-codes": "^2.1.4",
|
|
56
50
|
"json-stringify-safe": "^5.0.1",
|
|
57
51
|
"jsontoxml": "^1.0.1",
|
|
58
|
-
"rate-limit-threshold": "^0.
|
|
59
|
-
"
|
|
52
|
+
"rate-limit-threshold": "^0.2.0",
|
|
53
|
+
"spark-md5": "^3.0.2",
|
|
60
54
|
"tough-cookie": "^4.1.3",
|
|
61
55
|
"uuid": "^10.0.0"
|
|
62
56
|
},
|
|
63
57
|
"devDependencies": {
|
|
58
|
+
"@biomejs/biome": "^1.8.3",
|
|
64
59
|
"@types/chai": "^4.3.0",
|
|
65
60
|
"@types/jsontoxml": "^1.0.5",
|
|
66
61
|
"@types/mocha": "^10.0.4",
|
|
67
|
-
"@types/node": "^22.
|
|
62
|
+
"@types/node": "^22.5.0",
|
|
68
63
|
"@types/sinon": "^17.0.3",
|
|
64
|
+
"@types/source-map-support": "^0",
|
|
65
|
+
"@types/spark-md5": "^3",
|
|
66
|
+
"@types/tough-cookie": "^4.0.5",
|
|
67
|
+
"@types/uuid": "^10.0.0",
|
|
69
68
|
"c8": "^10.1.2",
|
|
70
|
-
"chai": "^5.1.
|
|
71
|
-
"del-cli": "^5.
|
|
72
|
-
"mocha": "^10.
|
|
69
|
+
"chai": "^5.1.1",
|
|
70
|
+
"del-cli": "^5.1.0",
|
|
71
|
+
"mocha": "^10.7.3",
|
|
73
72
|
"remark-cli": "^12.0.0",
|
|
74
73
|
"remark-preset-lint-recommended": "^7.0.0",
|
|
75
74
|
"sinon": "^18.0.0",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"source-map-support": "^0.5.21",
|
|
76
|
+
"ts-node": "^10.9.2",
|
|
77
|
+
"typescript": "^5.5.4"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"clean": "del-cli 'lib/**/*.js' 'lib/**/*.js.map' 'lib/**/*.d.ts' 'test/**/*.js' 'test/**/*.js.map'",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"build": "yarn run clean && yarn run compile",
|
|
89
89
|
"start": "yarn run compile && yarn run lint && yarn run cover-test",
|
|
90
90
|
"test-coverage": "c8 yarn run test",
|
|
91
|
-
"send-codacy": "
|
|
91
|
+
"send-codacy": "c8 report --reporter=text-lcov | codacy-coverage"
|
|
92
92
|
},
|
|
93
93
|
"nyc": {
|
|
94
94
|
"exclude": [
|