musicbrainz-api 0.21.0 → 0.22.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/LICENSE.txt ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2025 Borewit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -425,26 +425,40 @@ await mbApi.addSpotifyIdToRecording(recording, '2AMysGXOe0zzZJMtH3Nizb');
425
425
  This library also supports the [Cover Art Archive API](https://musicbrainz.org/doc/Cover_Art_Archive/API).
426
426
 
427
427
  ### Fetch Release Cover Art
428
+
429
+ #### Fetch available cover art information
430
+
428
431
  ```js
429
432
  import { CoverArtArchiveApi } from 'musicbrainz-api';
430
433
 
431
434
  const coverArtArchiveApiClient = new CoverArtArchiveApi();
432
435
 
433
- async function getReleaseCoverArt(releaseMbid, coverType = '') {
434
- try {
435
- const coverInfo = await coverArtArchiveApiClient.getReleaseCovers(releaseMbid, coverType);
436
- console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
437
- } catch (error) {
438
- console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
436
+ async function fetchCoverArt(releaseMbid, coverType = '') {
437
+ const coverInfo = await coverArtArchiveApiClient.getReleaseCovers(releaseMbid);
438
+ for(const image of coverInfo.images) {
439
+ console.log(`Cover art front=${image.front} back=${image.back} url=${image.image}`);
439
440
  }
440
441
  }
441
442
 
442
- (async () => {
443
- const releaseMbid = 'your-release-mbid-here'; // Replace with actual MBID
444
- await getReleaseCoverArt(releaseMbid); // Get all covers
445
- await getReleaseCoverArt(releaseMbid, 'front'); // Get best front cover
446
- await getReleaseCoverArt(releaseMbid, 'back'); // Get best back cover
447
- })();
443
+ fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
444
+ console.error(`Failed to fetch cover art: ${error.message}`);
445
+ })
446
+ ```
447
+
448
+ #### Fetch front or back cover for a release
449
+ ```js
450
+ import { CoverArtArchiveApi } from 'musicbrainz-api';
451
+
452
+ const coverArtArchiveApiClient = new CoverArtArchiveApi();
453
+
454
+ async function fetchCoverArt(releaseMbid, coverType = '') {
455
+ const coverInfo = await coverArtArchiveApiClient.getReleaseCover(releaseMbid, 'front');
456
+ console.log(`Cover art url=${coverInfo.url}`);
457
+ }
458
+
459
+ fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
460
+ console.error(`Failed to fetch cover art: ${error.message}`);
461
+ })
448
462
  ```
449
463
 
450
464
  ### Release Group Cover Art
@@ -453,22 +467,32 @@ import { CoverArtArchiveApi } from 'musicbrainz-api';
453
467
 
454
468
  const coverArtArchiveApiClient = new CoverArtArchiveApi();
455
469
 
456
- async function getCoverArt(releaseGroupMbid, coverType = '') {
457
- try {
458
- const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCovers(releaseGroupMbid, coverType);
459
- console.log(`Cover info for ${coverType || 'all covers'}`, coverInfo);
460
- } catch (error) {
461
- console.error(`Failed to fetch ${coverType || 'all covers'}:`, error);
470
+ async function fetchCoverArt(releaseMbid, coverType = '') {
471
+ const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCovers(releaseMbid);
472
+ for(const image of coverInfo.images) {
473
+ console.log(`Cover art front=${image.front} back=${image.back} url=${image.image}`);
462
474
  }
463
475
  }
464
476
 
465
- (async () => {
466
- const releaseGroupMbid = 'your-release-group-mbid-here'; // Replace with actual MBID
467
- await getCoverArt(releaseGroupMbid); // Get all covers
468
- await getCoverArt(releaseGroupMbid, 'front'); // Get best front cover
469
- await getCoverArt(releaseGroupMbid, 'back'); // Get best back cover
470
- })();
477
+ fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
478
+ console.error(`Failed to fetch cover art: ${error.message}`);
479
+ })
480
+ ```
481
+
482
+ #### Fetch front or back cover for a release-group
483
+ ```js
484
+ import { CoverArtArchiveApi } from 'musicbrainz-api';
485
+
486
+ const coverArtArchiveApiClient = new CoverArtArchiveApi();
487
+
488
+ async function fetchCoverArt(releaseMbid, coverType = '') {
489
+ const coverInfo = await coverArtArchiveApiClient.getReleaseGroupCover(releaseMbid, 'front');
490
+ console.log(`Cover art url=${coverInfo.url}`);
491
+ }
471
492
 
493
+ fetchCoverArt('976e0677-a480-4a5e-a177-6a86c1900bbf').catch(error => {
494
+ console.error(`Failed to fetch cover art: ${error.message}`);
495
+ })
472
496
  ```
473
497
 
474
498
  ## CommonJS backward compatibility
@@ -505,3 +529,7 @@ async function run() {
505
529
 
506
530
  run();
507
531
  ```
532
+
533
+ ## Licence
534
+
535
+ This project is licensed under the [MIT License](LICENSE.txt). Feel free to use, modify, and distribute as needed.
@@ -16,27 +16,45 @@ export interface IImage {
16
16
  '1200'?: string;
17
17
  };
18
18
  }
19
- export interface ICoverInfo {
19
+ export type CoverType = 'front' | 'back';
20
+ export interface ICoversInfo {
20
21
  images: IImage[];
21
22
  release: string;
22
23
  }
24
+ export interface ICoverInfo {
25
+ url: string | null;
26
+ }
23
27
  export declare class CoverArtArchiveApi {
24
28
  private httpClient;
25
29
  private getJson;
30
+ private getCoverRedirect;
26
31
  /**
27
32
  * Fetch release
28
33
  * @releaseId Release MBID
29
34
  * @param releaseId MusicBrainz Release MBID
30
- * @param coverType Cover type
31
35
  */
32
- getReleaseCovers(releaseId: string, coverType?: 'front' | 'back'): Promise<ICoverInfo>;
36
+ getReleaseCovers(releaseId: string): Promise<ICoversInfo>;
33
37
  /**
34
38
  * Fetch release-group
35
39
  * @releaseGroupId Release-group MBID
36
40
  * @param releaseGroupId MusicBrainz Release Group MBID
37
- * @param coverType Cover type
38
41
  */
39
- getReleaseGroupCovers(releaseGroupId: string, coverType?: 'front' | 'back'): Promise<ICoverInfo>;
42
+ getReleaseGroupCovers(releaseGroupId: string): Promise<ICoversInfo>;
43
+ /**
44
+ * Fetch release cover
45
+ * @releaseId Release MBID
46
+ * @param releaseId MusicBrainz Release MBID
47
+ * @param coverType Front or back cover
48
+ */
49
+ getReleaseCover(releaseId: string, coverType: CoverType): Promise<ICoverInfo>;
50
+ /**
51
+ * Fetch release-group cover
52
+ * @releaseId Release-group MBID
53
+ * @param releaseGroupId MusicBrainz Release-group MBID
54
+ * @param coverType Front or back cover
55
+ */
56
+ getReleaseGroupCover(releaseGroupId: string, coverType: CoverType): Promise<ICoverInfo>;
57
+ private static makePath;
40
58
  /**
41
59
  * Fetch covers
42
60
  * @releaseId MBID
@@ -45,4 +63,12 @@ export declare class CoverArtArchiveApi {
45
63
  * @param coverType Cover type
46
64
  */
47
65
  private getCovers;
66
+ /**
67
+ * Fetch covers
68
+ * @releaseId MBID
69
+ * @param releaseId MusicBrainz Release Group MBID
70
+ * @param releaseType Fetch covers for specific release or release-group
71
+ * @param coverType Cover type
72
+ */
73
+ private getCover;
48
74
  }
@@ -2,7 +2,7 @@
2
2
  import { HttpClient } from "./http-client.js";
3
3
  export class CoverArtArchiveApi {
4
4
  constructor() {
5
- this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 20000 });
5
+ this.httpClient = new HttpClient({ baseUrl: 'https://coverartarchive.org', userAgent: 'Node.js musicbrains-api', timeout: 20000, followRedirects: false });
6
6
  }
7
7
  async getJson(path) {
8
8
  const response = await this.httpClient.get(path, {
@@ -19,23 +19,66 @@ export class CoverArtArchiveApi {
19
19
  }
20
20
  return response.json();
21
21
  }
22
+ async getCoverRedirect(path) {
23
+ const response = await this.httpClient.get(path, {
24
+ followRedirects: false
25
+ });
26
+ switch (response.status) {
27
+ case 307:
28
+ return response.headers.get('LOCATION');
29
+ case 400:
30
+ throw new Error('Invalid UUID');
31
+ case 404:
32
+ // No release with this MBID
33
+ return null;
34
+ case 405:
35
+ throw new Error('Invalid HTTP method');
36
+ case 503:
37
+ return null;
38
+ default:
39
+ throw new Error(`Unexpected HTTP-status response: ${response.status}`);
40
+ }
41
+ }
22
42
  /**
23
43
  * Fetch release
24
44
  * @releaseId Release MBID
25
45
  * @param releaseId MusicBrainz Release MBID
26
- * @param coverType Cover type
27
46
  */
28
- getReleaseCovers(releaseId, coverType) {
29
- return this.getCovers(releaseId, 'release', coverType);
47
+ getReleaseCovers(releaseId) {
48
+ return this.getCovers(releaseId, 'release');
30
49
  }
31
50
  /**
32
51
  * Fetch release-group
33
52
  * @releaseGroupId Release-group MBID
34
53
  * @param releaseGroupId MusicBrainz Release Group MBID
35
- * @param coverType Cover type
36
54
  */
37
- getReleaseGroupCovers(releaseGroupId, coverType) {
38
- return this.getCovers(releaseGroupId, 'release-group', coverType);
55
+ getReleaseGroupCovers(releaseGroupId) {
56
+ return this.getCovers(releaseGroupId, 'release-group');
57
+ }
58
+ /**
59
+ * Fetch release cover
60
+ * @releaseId Release MBID
61
+ * @param releaseId MusicBrainz Release MBID
62
+ * @param coverType Front or back cover
63
+ */
64
+ getReleaseCover(releaseId, coverType) {
65
+ return this.getCover(releaseId, 'release', coverType);
66
+ }
67
+ /**
68
+ * Fetch release-group cover
69
+ * @releaseId Release-group MBID
70
+ * @param releaseGroupId MusicBrainz Release-group MBID
71
+ * @param coverType Front or back cover
72
+ */
73
+ getReleaseGroupCover(releaseGroupId, coverType) {
74
+ return this.getCover(releaseGroupId, 'release-group', coverType);
75
+ }
76
+ static makePath(releaseId, releaseType = 'release', coverType) {
77
+ const path = [releaseType, releaseId];
78
+ if (coverType) {
79
+ path.push(coverType);
80
+ }
81
+ return `/${path.join('/')}`;
39
82
  }
40
83
  /**
41
84
  * Fetch covers
@@ -44,17 +87,24 @@ export class CoverArtArchiveApi {
44
87
  * @param releaseType Fetch covers for specific release or release-group
45
88
  * @param coverType Cover type
46
89
  */
47
- async getCovers(releaseId, releaseType = 'release', coverType) {
48
- const path = [releaseType, releaseId];
49
- if (coverType) {
50
- path.push(coverType);
51
- }
52
- const info = await this.getJson(`/${path.join('/')}`);
90
+ async getCovers(releaseId, releaseType = 'release') {
91
+ const info = await this.getJson(CoverArtArchiveApi.makePath(releaseId, releaseType));
53
92
  // Hack to correct http addresses into https
54
93
  if (info.release?.startsWith('http:')) {
55
94
  info.release = `https${info.release.substring(4)}`;
56
95
  }
57
96
  return info;
58
97
  }
98
+ /**
99
+ * Fetch covers
100
+ * @releaseId MBID
101
+ * @param releaseId MusicBrainz Release Group MBID
102
+ * @param releaseType Fetch covers for specific release or release-group
103
+ * @param coverType Cover type
104
+ */
105
+ async getCover(releaseId, releaseType = 'release', coverType) {
106
+ const url = await this.getCoverRedirect(CoverArtArchiveApi.makePath(releaseId, releaseType, coverType));
107
+ return { url: url };
108
+ }
59
109
  }
60
110
  //# sourceMappingURL=coverartarchive-api.js.map
@@ -6,6 +6,7 @@ export interface IHttpClientOptions {
6
6
  baseUrl: string;
7
7
  timeout: number;
8
8
  userAgent: string;
9
+ followRedirects?: boolean;
9
10
  }
10
11
  export interface IFetchOptions {
11
12
  query?: HttpFormData;
@@ -20,7 +20,7 @@ export class HttpClient {
20
20
  async _fetch(method, path, options) {
21
21
  if (!options)
22
22
  options = {};
23
- let url = `${this.options.baseUrl}/${path}`;
23
+ let url = path.startsWith('/') ? `${this.options.baseUrl}${path}` : `${this.options.baseUrl}/${path}`;
24
24
  if (options.query) {
25
25
  url += `?${new URLSearchParams(options.query)}`;
26
26
  }
@@ -48,7 +48,7 @@ export class MusicBrainzApi {
48
48
  async restGet(relUrl, query = {}) {
49
49
  query.fmt = 'json';
50
50
  await this.applyRateLimiter();
51
- const response = await this.httpClient.get(`ws/2${relUrl}`, {
51
+ const response = await this.httpClient.get(`/ws/2${relUrl}`, {
52
52
  query,
53
53
  retryLimit: 10
54
54
  });
@@ -79,7 +79,7 @@ export class MusicBrainzApi {
79
79
  throw new Error("XML-Post requires the appName & appVersion to be defined");
80
80
  }
81
81
  const clientId = `${this.config.appName.replace(/-/g, '.')}-${this.config.appVersion}`;
82
- const path = `ws/2/${entity}/`;
82
+ const path = `/ws/2/${entity}/`;
83
83
  // Get digest challenge
84
84
  let digest = '';
85
85
  let n = 1;
@@ -120,7 +120,7 @@ export class MusicBrainzApi {
120
120
  formData.username = this.config.botAccount?.username;
121
121
  formData.password = this.config.botAccount?.password;
122
122
  formData.remember_me = 1;
123
- const response = await this.httpClient.postForm(`${entity}/${mbid}/edit`, formData, {
123
+ const response = await this.httpClient.postForm(`/${entity}/${mbid}/edit`, formData, {
124
124
  followRedirects: false
125
125
  });
126
126
  if (response.status === HttpStatus.OK)
@@ -5,21 +5,32 @@ export interface IPeriod {
5
5
  'ended': boolean;
6
6
  'end': string;
7
7
  }
8
+ export interface ITypedEntity extends IEntity {
9
+ 'type-id': string;
10
+ type: string;
11
+ id: string;
12
+ }
8
13
  export interface IEntity {
9
14
  id: string;
10
15
  }
11
- export interface IArea extends IEntity {
12
- 'iso-3166-1-codes': string[];
16
+ export interface LifeSpan {
17
+ ended: boolean;
18
+ begin: null | string;
19
+ end: null | string;
20
+ }
21
+ export interface IArea extends ITypedEntity {
22
+ type: 'Country' | 'Subdivision' | 'Municipality' | 'City' | 'District' | 'Island';
23
+ 'iso-3166-1-codes'?: string[];
24
+ primary: boolean;
13
25
  name: string;
14
26
  'sort-name': string;
15
27
  disambiguation: string;
28
+ 'life-span': LifeSpan;
16
29
  }
17
- export interface IAlias extends IEntity {
30
+ export interface IAlias extends ITypedEntity {
18
31
  name: string;
19
32
  'sort-name': string;
20
33
  ended: boolean;
21
- 'type-id': string;
22
- type: string;
23
34
  locale: string;
24
35
  primary: string;
25
36
  begin: string;
@@ -28,19 +39,18 @@ export interface IAlias extends IEntity {
28
39
  export interface IMatch {
29
40
  score: number;
30
41
  }
31
- export interface IArtist extends IEntity {
42
+ export type Gender = 'male' | 'female' | 'other' | 'not applicable';
43
+ export interface IArtist extends ITypedEntity {
32
44
  name: string;
33
45
  disambiguation: string;
34
46
  'sort-name': string;
35
- 'type-id'?: string;
36
47
  'gender-id'?: string;
37
48
  'life-span'?: IPeriod;
38
49
  country?: string;
39
50
  ipis?: string[];
40
51
  isnis?: string[];
41
52
  aliases?: IAlias[];
42
- gender?: string;
43
- type?: string;
53
+ gender?: Gender;
44
54
  area?: IArea;
45
55
  begin_area?: IArea;
46
56
  end_area?: IArea;
@@ -56,32 +66,31 @@ export interface IArtistCredit {
56
66
  joinphrase: string;
57
67
  name: string;
58
68
  }
59
- export interface ICollection extends IEntity {
60
- type: string;
69
+ export interface ICollection extends ITypedEntity {
70
+ type: 'Recording collection';
61
71
  name: string;
62
- 'type-id': string;
63
72
  'recording-count': number;
64
73
  editor: string;
65
74
  'entity-type': string;
66
75
  }
67
- export interface IEvent extends IEntity {
76
+ export interface IEvent extends ITypedEntity {
68
77
  cancelled: boolean;
69
- type: string;
70
78
  'life-span': IPeriod;
71
79
  disambiguation: string;
72
- 'type-id': string;
73
80
  time: string;
74
81
  setlist: string;
75
82
  name: string;
76
83
  }
77
- export interface IInstrument extends IEntity {
84
+ export type InstrumentType = 'Wind instrument' | 'String instrument' | 'Percussion instrument' | 'Electronic instrument' | 'Family' | 'Ensemble' | 'Other instrument';
85
+ export interface IInstrument extends ITypedEntity {
78
86
  disambiguation: string;
79
87
  name: string;
80
- 'type-id': string;
81
- type: string;
88
+ type: InstrumentType;
82
89
  description: string;
83
90
  }
84
- export type ReleaseQuality = 'normal';
91
+ export type ReleaseQuality = 'normal' | 'high';
92
+ export type ReleaseStatus = 'Official' | 'Promotion' | 'Bootleg' | 'Pseudo-release' | 'Withdrawn' | 'Expunged' | 'Cancelled';
93
+ export type ReleasePackaging = 'Book' | 'Box' | 'Cardboard/Paper Sleeve' | 'Cassette Case' | 'Clamshell Case' | 'Digibook' | 'Digifile' | 'Digipak' | 'Discbox Slider' | 'Fatbox' | 'Gatefold Cover' | 'Jewel case' | 'Keep Case' | 'Longbox' | 'Metal Tin' | 'Plastic sleeve' | 'Slidepack' | 'Slim Jewel Case' | 'Snap Case' | 'SnapPack' | 'Super Jewel Box' | 'Other' | 'None';
85
94
  export interface IRelease extends IEntity {
86
95
  title: string;
87
96
  'text-representation': {
@@ -89,22 +98,23 @@ export interface IRelease extends IEntity {
89
98
  'script': string;
90
99
  };
91
100
  disambiguation: string;
92
- asin: string;
101
+ asin: null | string;
102
+ status: ReleaseStatus;
93
103
  'status-id': string;
94
- packaging?: string;
95
- status: string;
104
+ packaging?: ReleasePackaging;
96
105
  'packaging-id'?: string;
97
106
  'release-events'?: IReleaseEvent[];
98
107
  date: string;
99
108
  media: IMedium[];
100
109
  'cover-art-archive': ICoverArtArchive;
101
110
  country: string;
102
- quality: string;
111
+ quality: ReleaseQuality;
103
112
  barcode: string;
104
113
  relations?: IRelation[];
105
114
  'artist-credit'?: IArtistCredit[];
106
115
  'release-group'?: IReleaseGroup;
107
116
  collections?: ICollection[];
117
+ 'track-count'?: number;
108
118
  }
109
119
  export interface IReleaseEvent {
110
120
  area?: IArea;
@@ -121,6 +131,7 @@ export interface IRecording extends IEntity {
121
131
  relations?: IRelation[];
122
132
  'artist-credit'?: IArtistCredit[];
123
133
  aliases?: IAlias[];
134
+ 'first-release-date': string;
124
135
  }
125
136
  export interface ITrack extends IEntity {
126
137
  position: number;
@@ -172,6 +183,7 @@ export interface IRecordingMatch extends IRecording, IMatch {
172
183
  export interface IReleaseGroupMatch extends IReleaseGroup, IMatch {
173
184
  }
174
185
  export interface IReleaseMatch extends IRelease, IMatch {
186
+ count: number;
175
187
  }
176
188
  export interface ISearchResult {
177
189
  created: DateTimeFormat;
@@ -223,16 +235,23 @@ export interface IWork extends IEntity {
223
235
  title: string;
224
236
  }
225
237
  export interface ILabel extends IEntity {
238
+ asin: null | string;
239
+ barcode: null | string;
240
+ country: null | string;
226
241
  name: string;
242
+ 'sort-name': string;
243
+ 'life-span': LifeSpan;
244
+ disambiguation?: string;
245
+ 'label-code': null | string;
246
+ ipis: string[];
247
+ area: IArea;
227
248
  }
228
249
  export interface IPlace extends IEntity {
229
250
  name: string;
230
251
  }
231
- export interface ISeries extends IEntity {
252
+ export interface ISeries extends ITypedEntity {
232
253
  name: string;
233
- type: string;
234
254
  disambiguation: string;
235
- 'type-id': string;
236
255
  }
237
256
  export interface IUrl extends IEntity {
238
257
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musicbrainz-api",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "MusicBrainz API client for reading and submitting metadata",
5
5
  "exports": {
6
6
  "node": {
@@ -90,9 +90,10 @@
90
90
  "compile-lib": "tsc -p lib",
91
91
  "compile-test": "tsc -p test",
92
92
  "compile": "yarn run compile-lib && yarn run compile-test",
93
- "lint-md": "remark -u preset-lint-recommended .",
94
- "lint-ts": "biome check",
95
- "lint": "yarn run lint-md && yarn run lint-ts",
93
+ "lint:md": "remark -u preset-lint-recommended .",
94
+ "lint:ts": "biome check",
95
+ "lint:fix": "biome check --fix",
96
+ "lint": "yarn run lint:md && yarn run lint:ts",
96
97
  "test": "mocha",
97
98
  "build": "yarn run clean && yarn run compile",
98
99
  "start": "yarn run compile && yarn run lint && yarn run cover-test",