musicbrainz-api 0.17.0 → 0.18.1
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 +123 -95
- 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,98 +1,121 @@
|
|
|
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**: Retrieve detailed metadata from the [MusicBrainz database](https://musicbrainz.org/).
|
|
18
|
+
- **Submit metadata**: Easily submit new metadata to [MusicBrainz](https://musicbrainz.org/).
|
|
19
|
+
- **Smart throttling**: Implements intelligent throttling, allowing bursts of requests while adhering to [MusicBrainz rate limits](https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting).
|
|
20
|
+
- **TypeScript Definitions**: Fully typed with built-in [TypeScript](https://www.typescriptlang.org/) definitions for a seamless development experience.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
## Compatibility
|
|
23
|
+
|
|
24
|
+
Module: version 8 migrated from [CommonJS](https://en.wikipedia.org/wiki/CommonJS) to [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
|
+
### Requirements
|
|
28
|
+
- Node.js: Requires [Node.js version 16](https://nodejs.org/en/about/previous-releases) or higher.
|
|
29
|
+
- Browser: Can be used in browser environments when bundled with a module bundler (not actively tested).
|
|
30
|
+
|
|
31
|
+
> [!NOTE]
|
|
32
|
+
> We are looking into making this package usable in the browser as well.
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
## Support the Project
|
|
35
|
+
If you find this project useful and would like to support its development, consider sponsoring or contributing:
|
|
36
|
+
|
|
37
|
+
- [Become a sponsor to Borewit](https://github.com/sponsors/Borewit)
|
|
38
|
+
|
|
39
|
+
- Buy me a coffee:
|
|
40
|
+
|
|
41
|
+
<a href="https://www.buymeacoffee.com/borewit" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy me A coffee" height="41" width="174"></a>
|
|
42
|
+
|
|
43
|
+
## Getting Started
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
### Identifying Your Application
|
|
27
46
|
|
|
28
|
-
MusicBrainz
|
|
29
|
-
|
|
47
|
+
MusicBrainz requires all API clients to [identify their application](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#User%20Data).
|
|
48
|
+
Ensure you set the User-Agent header by providing `appName`, `appVersion`, and `appContactInfo` when configuring the client.
|
|
49
|
+
This library will automatically handle this for you.
|
|
30
50
|
|
|
31
|
-
|
|
51
|
+
### Submitting metadata
|
|
32
52
|
|
|
33
53
|
If you plan to use this module for submitting metadata, please ensure you comply with [the MusicBrainz Code of conduct/Bots](https://wiki.musicbrainz.org/Code_of_Conduct/Bots).
|
|
34
54
|
|
|
35
|
-
## Example
|
|
55
|
+
## Example Usage
|
|
56
|
+
|
|
57
|
+
### Importing the Library
|
|
36
58
|
|
|
37
|
-
Example, how to import 'musicbrainz-api:
|
|
38
59
|
```js
|
|
39
|
-
import {MusicBrainzApi} from 'musicbrainz-api';
|
|
60
|
+
import { MusicBrainzApi } from 'musicbrainz-api';
|
|
40
61
|
|
|
41
62
|
const mbApi = new MusicBrainzApi({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
appName: 'my-app',
|
|
64
|
+
appVersion: '0.1.0',
|
|
65
|
+
appContactInfo: 'user@mail.org',
|
|
45
66
|
});
|
|
46
67
|
```
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
```js
|
|
50
|
-
import {MusicBrainzApi} from 'musicbrainz-api';
|
|
69
|
+
### Configuration Options
|
|
51
70
|
|
|
71
|
+
```js
|
|
52
72
|
const config = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
// Optional: MusicBrainz bot account credentials
|
|
74
|
+
botAccount: {
|
|
75
|
+
username: 'myUserName_bot',
|
|
76
|
+
password: 'myPassword',
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Optional: API base URL (default: 'https://musicbrainz.org')
|
|
80
|
+
baseUrl: 'https://musicbrainz.org',
|
|
81
|
+
|
|
82
|
+
// Required: Application details
|
|
83
|
+
appName: 'my-app',
|
|
84
|
+
appVersion: '0.1.0',
|
|
85
|
+
appMail: 'user@mail.org',
|
|
86
|
+
|
|
87
|
+
// Optional: Proxy settings (default: no proxy server)
|
|
88
|
+
proxy: {
|
|
89
|
+
host: 'localhost',
|
|
90
|
+
port: 8888,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Optional: Disable rate limiting (default: false)
|
|
94
|
+
disableRateLimiting: false,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const mbApi = new MusicBrainzApi(config);
|
|
98
|
+
```
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
disableRateLimiting: false,
|
|
76
|
-
}
|
|
100
|
+
## Accessing MusicBrainz Data
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
```
|
|
102
|
+
The MusicBrainz API allows you to look up various entities. Here’s how to use the lookup function:
|
|
80
103
|
|
|
81
104
|
## Lookup MusicBrainz Entities
|
|
82
105
|
|
|
83
106
|
MusicBrainz API documentation: [XML Web Service/Version 2 Lookups](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2#Lookups)
|
|
84
107
|
|
|
85
|
-
###
|
|
86
|
-
|
|
87
|
-
Arguments:
|
|
88
|
-
* entity: `'area'` | `'artist'` | `'collection'` | `'instrument'` | `'label'` | `'place'` | `'release'` | `'release-group'` | `'recording'` | `'series'` | `'work'` | `'url'` | `'event'`
|
|
89
|
-
* MBID [(MusicBrainz identifier)](https://wiki.musicbrainz.org/MusicBrainz_Identifier)
|
|
90
|
-
* query
|
|
108
|
+
### Lookup Function
|
|
91
109
|
|
|
92
110
|
```js
|
|
93
111
|
const artist = await mbApi.lookup('artist', 'ab2528d9-719f-4261-8098-21849222a0f2');
|
|
94
112
|
```
|
|
95
113
|
|
|
114
|
+
Arguments:
|
|
115
|
+
- entity: `'area'` | `'artist'` | `'collection'` | `'instrument'` | `'label'` | `'place'` | `'release'` | `'release-group'` | `'recording'` | `'series'` | `'work'` | `'url'` | `'event'`
|
|
116
|
+
- MBID [(MusicBrainz identifier)](https://wiki.musicbrainz.org/MusicBrainz_Identifier)
|
|
117
|
+
- query
|
|
118
|
+
|
|
96
119
|
|
|
97
120
|
| Query argument | Query value |
|
|
98
121
|
|-----------------------|-----------------|
|
|
@@ -263,19 +286,19 @@ There are different search fields depending on the entity.
|
|
|
263
286
|
Searches can be performed using the generic search function: `query(entity: mb.EntityType, query: string | IFormData, offset?: number, limit?: number): Promise<entity>`
|
|
264
287
|
|
|
265
288
|
Arguments:
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
289
|
+
- Entity type, which can be one of:
|
|
290
|
+
- `artist`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Artist)
|
|
291
|
+
- `label`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Label)
|
|
292
|
+
- `recording`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Recording)
|
|
293
|
+
- `release`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release)
|
|
294
|
+
- `release-group`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Release_Group)
|
|
295
|
+
- `work`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Work)
|
|
296
|
+
- `area`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#Area)
|
|
297
|
+
- `url`: [search fields](https://wiki.musicbrainz.org/Development/XML_Web_Service/Version_2/Search#URL)
|
|
298
|
+
- `query {query: string, offset: number, limit: number}`
|
|
299
|
+
- `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.
|
|
300
|
+
- `query.offset`: optional, return search results starting at a given offset. Used for paging through more than one page of results.
|
|
301
|
+
- `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
302
|
|
|
280
303
|
For example, to find any recordings of _'We Will Rock You'_ by Queen:
|
|
281
304
|
```js
|
|
@@ -348,8 +371,7 @@ For all of the following function you need to use a dedicated bot account.
|
|
|
348
371
|
|
|
349
372
|
## Submitting ISRC via post user form-data
|
|
350
373
|
|
|
351
|
-
|
|
352
|
-
Use with caution, and only on a test server, it may clear existing metadata as side effect.
|
|
374
|
+
Use with caution, and only on a test server, it may clear existing metadata has side effect.
|
|
353
375
|
|
|
354
376
|
```js
|
|
355
377
|
|
|
@@ -392,46 +414,52 @@ await mbApi.addSpotifyIdToRecording(recording, '2AMysGXOe0zzZJMtH3Nizb');
|
|
|
392
414
|
|
|
393
415
|
## Cover Art Archive API
|
|
394
416
|
|
|
395
|
-
|
|
417
|
+
This library also supports the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
|
|
396
418
|
|
|
397
|
-
### Release Cover Art
|
|
419
|
+
### Fetch Release Cover Art
|
|
398
420
|
```js
|
|
399
|
-
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
421
|
+
import { CoverArtArchiveApi } from 'musicbrainz-api';
|
|
400
422
|
|
|
401
|
-
coverArtArchiveApiClient
|
|
402
|
-
console.log('Release cover info', releaseCoverInfo);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
coverArtArchiveApiClient.getReleaseCovers(releaseMbid, 'front').then(releaseCoverInfo => {
|
|
406
|
-
console.log('Get best front cover', releaseCoverInfo);
|
|
407
|
-
});
|
|
423
|
+
const coverArtArchiveApiClient = new CoverArtArchiveApi();
|
|
408
424
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
425
|
+
async function getReleaseCoverArt(releaseMbid, coverType = '') {
|
|
426
|
+
try {
|
|
427
|
+
const coverInfo = await coverArtArchiveApiClient.getReleaseCovers(releaseMbid, coverType);
|
|
428
|
+
console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
412
433
|
|
|
434
|
+
(async () => {
|
|
435
|
+
const releaseMbid = 'your-release-mbid-here'; // Replace with actual MBID
|
|
436
|
+
await getReleaseCoverArt(releaseMbid); // Get all covers
|
|
437
|
+
await getReleaseCoverArt(releaseMbid, 'front'); // Get best front cover
|
|
438
|
+
await getReleaseCoverArt(releaseMbid, 'back'); // Get best back cover
|
|
439
|
+
})();
|
|
413
440
|
```
|
|
414
441
|
|
|
415
442
|
### Release Group Cover Art
|
|
416
443
|
```js
|
|
417
|
-
import {CoverArtArchiveApi} from 'musicbrainz-api';
|
|
444
|
+
import { CoverArtArchiveApi } from 'musicbrainz-api';
|
|
418
445
|
|
|
419
|
-
coverArtArchiveApiClient
|
|
420
|
-
console.log('Release cover info', releaseGroupCoverInfo);
|
|
421
|
-
});
|
|
446
|
+
const coverArtArchiveApiClient = new CoverArtArchiveApi();
|
|
422
447
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
448
|
+
async function getCoverArt(releaseGroupMbid, coverType = '') {
|
|
449
|
+
try {
|
|
450
|
+
const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, coverType);
|
|
451
|
+
console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
426
456
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
457
|
+
(async () => {
|
|
458
|
+
const releaseGroupMbid = 'your-release-group-mbid-here'; // Replace with actual MBID
|
|
459
|
+
await getCoverArt(releaseGroupMbid); // Get all covers
|
|
460
|
+
await getCoverArt(releaseGroupMbid, 'front'); // Get best front cover
|
|
461
|
+
await getCoverArt(releaseGroupMbid, 'back'); // Get best back cover
|
|
462
|
+
})();
|
|
430
463
|
|
|
431
464
|
```
|
|
432
465
|
|
|
433
|
-
|
|
434
|
-
## Compatibility
|
|
435
|
-
|
|
436
|
-
The JavaScript in runtime is compliant with [ECMAScript 2017 (ES8)](https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017).
|
|
437
|
-
Requires [Node.js®](https://nodejs.org/) version 6 or higher.
|
|
@@ -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.1",
|
|
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": [
|