gerdur-core 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.12.0 - 2026-08-31
4
+
5
+ Phase 4 — an optional, read-only **enrichment** layer against open databases.
6
+ Off by default, never touches `addTrackTags`.
7
+
8
+ ### Added
9
+
10
+ - **MusicBrainz** (`src/enrich/`): `lookupRecordingByISRC(isrc)` — canonical
11
+ recording (title, artist credits, length, every known ISRC, the releases it's
12
+ on); `getMusicBrainzRecording(mbid)` / `getMusicBrainzRelease(mbid, inc?)` —
13
+ direct MBID lookups with label / catalogue number / barcode.
14
+ `configureMusicBrainz({userAgent, minIntervalMs})` — they require a descriptive
15
+ UA and ≤ 1 req/s.
16
+ - **Cover Art Archive**: `getCoverArt(mbid, entity?)` and
17
+ `getBestCoverArtUrl(mbid, {minSize})` — higher-resolution covers than Deezer's
18
+ 1800 px ceiling. `null` when there's no art.
19
+ - **`PoliteJsonClient`** — the serialised, rate-limited, `503`/`429`-retrying,
20
+ `404`→`null` JSON client both use; exported for building your own.
21
+
22
+ ## 2.11.0 - 2026-08-31
23
+
24
+ ### Added
25
+
26
+ - **`searchFacets(result)`** — flatten the per-type hit counts + Deezer's
27
+ relevance `order` out of a `searchMusic` result.
28
+
29
+ ### Internal
30
+
31
+ - The live-API test suite skips more gracefully when an upstream (Deezer public
32
+ REST, Tidal, YouTube) rate-limits or consent-walls the runner — the
33
+ `--> DEEZER` converter tests and YouTube tests now degrade to a skip instead
34
+ of a hard fail.
35
+
3
36
  ## 2.10.0 - 2026-08-31
4
37
 
5
38
  Leftovers — batch token refresh + podcast episodes. Additive.
package/README.md CHANGED
@@ -261,6 +261,16 @@ ignore the operators, so pass a plain string there.
261
261
  "as you type" UIs. `nb` (default 5) caps items per type. Needs an initialised
262
262
  session (`initDeezerApi`).
263
263
 
264
+ ### `.searchFacets(result)`
265
+
266
+ Flattens the per-type hit counts + Deezer's relevance `order` out of a
267
+ `searchMusic` result — for "207 tracks · 99 albums · 17 artists" UIs.
268
+
269
+ ```js
270
+ const r = await searchMusic('daft punk', ['TRACK', 'ALBUM', 'ARTIST']);
271
+ searchFacets(r); // {track: 207, album: 99, artist: 17, …, order: ['TOP_RESULT','TRACK',…]}
272
+ ```
273
+
264
274
  ### Browse & discovery
265
275
 
266
276
  Public REST endpoints — no `arl` needed, memoised like the rest. All return a
@@ -427,6 +437,31 @@ the audio (ID3v2.3 for MP3, Vorbis comments for FLAC). Returns
427
437
  | `track` | Yes | `object` | track object from `getTrackInfo` / `parseInfo` |
428
438
  | `options` | No | `object` | `AddTrackTagsOptions` — `coverSize` (56–1800), pre-fetched `album`/`lyrics`/`cover`, and toggles (`richCredits`, `embedArtistImage`, `deezerIds`, …) |
429
439
 
440
+ ### Enrichment (optional, read-only)
441
+
442
+ Fill gaps Deezer leaves, from open databases — **off by default, never wired into
443
+ `addTrackTags`**. Both services rate-limit and want a descriptive `User-Agent`;
444
+ call `configureMusicBrainz({userAgent})` once at startup.
445
+
446
+ | Method | |
447
+ | :--- | :--- |
448
+ | `lookupRecordingByISRC(isrc)` | canonical MusicBrainz recording — title, artist credits, length, **all** known ISRCs, and the releases it's on (each with a `releaseGroupMbid`). `null` if unknown. |
449
+ | `getMusicBrainzRecording(mbid)` / `getMusicBrainzRelease(mbid, inc?)` | direct MBID lookups — the release adds label, catalogue number, barcode. |
450
+ | `getCoverArt(mbid, entity = 'release-group')` | Cover Art Archive images (`front` / `approved` / `thumbnails`). `null` when there's no art. |
451
+ | `getBestCoverArtUrl(mbid, {entity?, minSize = 1200})` | one URL — the approved front cover at ≥ `minSize` px, else full-res. Deezer caps its own art at 1800 px; this goes bigger. |
452
+
453
+ ```js
454
+ configureMusicBrainz({userAgent: 'myapp/1.0 ( me@example.com )'});
455
+ const rec = await lookupRecordingByISRC(track.isrc);
456
+ if (rec?.releases[0]?.releaseGroupMbid) {
457
+ const cover = await getBestCoverArtUrl(rec.releases[0].releaseGroupMbid, {minSize: 1200});
458
+ }
459
+ ```
460
+
461
+ Errors: a persistent MusicBrainz `503` ("server busy") surfaces as
462
+ `HttpStatusError` after 3 backed-off retries — catch and fall back to Deezer's
463
+ data.
464
+
430
465
  ###
431
466
 
432
467
  > We are not responsible for any misuse of this library by any third party. Please make sure to respect the artists and the music industry when using this library.
@@ -1,4 +1,4 @@
1
- import type { advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultAlbum, searchResultArtist, searchResultPlaylist, searchResultTrack, suggestResult } from '../types';
1
+ import type { advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultAlbum, searchResultArtist, searchResultPlaylist, searchResultTrack, searchType, suggestResult } from '../types';
2
2
  /**
3
3
  * Compose Deezer's advanced search operators into one query string.
4
4
  *
@@ -47,3 +47,21 @@ export declare const searchPlaylists: (query: string, options?: Omit<publicApiSe
47
47
  * @param nb max items per type (default 5)
48
48
  */
49
49
  export declare const suggest: (query: string, nb?: number) => Promise<suggestResult>;
50
+ /** Per-type total hit counts + Deezer's own relevance ordering, from a `searchMusic` result. */
51
+ export interface SearchFacets {
52
+ track: number;
53
+ album: number;
54
+ artist: number;
55
+ playlist: number;
56
+ radio: number;
57
+ show: number;
58
+ user: number;
59
+ /** the type order Deezer considers most relevant for this query, e.g. `['TOP_RESULT','ARTIST','TRACK',…]` */
60
+ order: string[];
61
+ }
62
+ /**
63
+ * Pull the facet counts out of a `searchMusic` (`deezer.pageSearch`) result —
64
+ * `pageSearch` already returns `TRACK.total`, `ALBUM.total`, … and an `ORDER`;
65
+ * this just surfaces them in one flat object for "N tracks · M albums · …" UIs.
66
+ */
67
+ export declare const searchFacets: (result: searchType) => SearchFacets;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.suggest = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.buildAdvancedQuery = void 0;
3
+ exports.searchFacets = exports.suggest = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.buildAdvancedQuery = void 0;
4
4
  const request_1 = require("./request");
5
5
  /**
6
6
  * Compose Deezer's advanced search operators into one query string.
@@ -108,3 +108,22 @@ const suggest = (query, nb = 5) => (0, request_1.requestLight)({
108
108
  TYPES: { ALBUM: true, ARTIST: true, TRACK: true, PLAYLIST: true, RADIO: true, SHOW: true },
109
109
  }, 'deezer.suggest');
110
110
  exports.suggest = suggest;
111
+ /**
112
+ * Pull the facet counts out of a `searchMusic` (`deezer.pageSearch`) result —
113
+ * `pageSearch` already returns `TRACK.total`, `ALBUM.total`, … and an `ORDER`;
114
+ * this just surfaces them in one flat object for "N tracks · M albums · …" UIs.
115
+ */
116
+ const searchFacets = (result) => {
117
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
118
+ return ({
119
+ track: (_b = (_a = result.TRACK) === null || _a === void 0 ? void 0 : _a.total) !== null && _b !== void 0 ? _b : 0,
120
+ album: (_d = (_c = result.ALBUM) === null || _c === void 0 ? void 0 : _c.total) !== null && _d !== void 0 ? _d : 0,
121
+ artist: (_f = (_e = result.ARTIST) === null || _e === void 0 ? void 0 : _e.total) !== null && _f !== void 0 ? _f : 0,
122
+ playlist: (_h = (_g = result.PLAYLIST) === null || _g === void 0 ? void 0 : _g.total) !== null && _h !== void 0 ? _h : 0,
123
+ radio: (_k = (_j = result.RADIO) === null || _j === void 0 ? void 0 : _j.total) !== null && _k !== void 0 ? _k : 0,
124
+ show: (_m = (_l = result.SHOW) === null || _l === void 0 ? void 0 : _l.total) !== null && _m !== void 0 ? _m : 0,
125
+ user: (_p = (_o = result.USER) === null || _o === void 0 ? void 0 : _o.total) !== null && _p !== void 0 ? _p : 0,
126
+ order: (_q = result.ORDER) !== null && _q !== void 0 ? _q : [],
127
+ });
128
+ };
129
+ exports.searchFacets = searchFacets;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Small polite JSON client for the third-party enrichment services (MusicBrainz,
3
+ * Cover Art Archive). Each service gets its own instance so it can enforce its
4
+ * own minimum request interval and identify itself with a `User-Agent`.
5
+ *
6
+ * - serialises requests and spaces them by `minIntervalMs` (MusicBrainz asks for
7
+ * ≤ 1 req/s per IP)
8
+ * - retries `503` (their "server busy") and `429` with exponential backoff
9
+ * - returns `null` on `404` rather than throwing (no data for that id)
10
+ */
11
+ export declare class PoliteJsonClient {
12
+ userAgent: string;
13
+ minIntervalMs: number;
14
+ private queue;
15
+ private lastAt;
16
+ constructor(opts: {
17
+ userAgent: string;
18
+ minIntervalMs?: number;
19
+ });
20
+ /** GET `url` as JSON. Resolves `null` on 404. */
21
+ get<T>(url: string, { retries }?: {
22
+ retries?: number;
23
+ }): Promise<T | null>;
24
+ private fetch;
25
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PoliteJsonClient = void 0;
7
+ const delay_1 = __importDefault(require("delay"));
8
+ const http_1 = require("../lib/http");
9
+ /**
10
+ * Small polite JSON client for the third-party enrichment services (MusicBrainz,
11
+ * Cover Art Archive). Each service gets its own instance so it can enforce its
12
+ * own minimum request interval and identify itself with a `User-Agent`.
13
+ *
14
+ * - serialises requests and spaces them by `minIntervalMs` (MusicBrainz asks for
15
+ * ≤ 1 req/s per IP)
16
+ * - retries `503` (their "server busy") and `429` with exponential backoff
17
+ * - returns `null` on `404` rather than throwing (no data for that id)
18
+ */
19
+ class PoliteJsonClient {
20
+ constructor(opts) {
21
+ var _a;
22
+ this.queue = Promise.resolve();
23
+ this.lastAt = 0;
24
+ this.userAgent = opts.userAgent;
25
+ this.minIntervalMs = (_a = opts.minIntervalMs) !== null && _a !== void 0 ? _a : 1100;
26
+ }
27
+ /** GET `url` as JSON. Resolves `null` on 404. */
28
+ get(url, { retries = 3 } = {}) {
29
+ const run = this.queue.then(() => this.fetch(url, retries));
30
+ // keep the chain alive even if this call rejects
31
+ this.queue = run.catch(() => undefined);
32
+ return run;
33
+ }
34
+ async fetch(url, retries) {
35
+ const wait = this.minIntervalMs - (Date.now() - this.lastAt);
36
+ if (wait > 0)
37
+ await (0, delay_1.default)(wait);
38
+ for (let attempt = 0;; attempt++) {
39
+ try {
40
+ const data = await (0, http_1.getJson)(url, { headers: { 'User-Agent': this.userAgent, Accept: 'application/json' } });
41
+ this.lastAt = Date.now();
42
+ return data;
43
+ }
44
+ catch (err) {
45
+ this.lastAt = Date.now();
46
+ const status = err instanceof http_1.HttpStatusError ? err.statusCode : 0;
47
+ if (status === 404) {
48
+ return null;
49
+ }
50
+ if ((status === 503 || status === 429 || status >= 500) && attempt < retries) {
51
+ await (0, delay_1.default)(Math.min(1000 * 2 ** attempt, 8000) + Math.random() * 400);
52
+ continue;
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ exports.PoliteJsonClient = PoliteJsonClient;
@@ -0,0 +1,31 @@
1
+ export interface CoverArtImage {
2
+ id: string;
3
+ front: boolean;
4
+ back: boolean;
5
+ approved: boolean;
6
+ /** full-resolution image URL (redirects to archive.org) */
7
+ image: string;
8
+ /** `{small, large, '250', '500', '1200'}` — not every size is present */
9
+ thumbnails: Record<string, string>;
10
+ types: string[];
11
+ }
12
+ export interface CoverArt {
13
+ images: CoverArtImage[];
14
+ /** the MusicBrainz release the art belongs to */
15
+ release?: string;
16
+ }
17
+ /**
18
+ * Cover Art Archive images for a MusicBrainz release-group (default) or release.
19
+ * `null` when there's no art — very common, so always handle it. Feed the MBID
20
+ * from `lookupRecordingByISRC(...).releases[i].releaseGroupMbid`.
21
+ */
22
+ export declare const getCoverArt: (mbid: string, entity?: 'release' | 'release-group') => Promise<CoverArt | null>;
23
+ /**
24
+ * The single best front-cover URL — the approved front image, preferring a
25
+ * thumbnail at least `minSize` px wide, else the full-resolution original.
26
+ * Deezer caps its own art at 1800 px, so this is how you go bigger.
27
+ */
28
+ export declare const getBestCoverArtUrl: (mbid: string, { entity, minSize }?: {
29
+ entity?: "release-group" | "release" | undefined;
30
+ minSize?: number | undefined;
31
+ }) => Promise<string | null>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getBestCoverArtUrl = exports.getCoverArt = void 0;
4
+ const client_1 = require("./client");
5
+ const client = new client_1.PoliteJsonClient({
6
+ userAgent: 'gerdur-core (+https://github.com/soulwax/gerdur-core)',
7
+ minIntervalMs: 200,
8
+ });
9
+ const BASE = 'https://coverartarchive.org';
10
+ /**
11
+ * Cover Art Archive images for a MusicBrainz release-group (default) or release.
12
+ * `null` when there's no art — very common, so always handle it. Feed the MBID
13
+ * from `lookupRecordingByISRC(...).releases[i].releaseGroupMbid`.
14
+ */
15
+ const getCoverArt = async (mbid, entity = 'release-group') => {
16
+ const data = await client.get(`${BASE}/${entity}/${encodeURIComponent(mbid)}`);
17
+ if (!(data === null || data === void 0 ? void 0 : data.images)) {
18
+ return null;
19
+ }
20
+ return {
21
+ release: data.release,
22
+ images: data.images.map((i) => {
23
+ var _a, _b;
24
+ return ({
25
+ id: String(i.id),
26
+ front: Boolean(i.front),
27
+ back: Boolean(i.back),
28
+ approved: Boolean(i.approved),
29
+ image: i.image,
30
+ thumbnails: (_a = i.thumbnails) !== null && _a !== void 0 ? _a : {},
31
+ types: (_b = i.types) !== null && _b !== void 0 ? _b : [],
32
+ });
33
+ }),
34
+ };
35
+ };
36
+ exports.getCoverArt = getCoverArt;
37
+ /**
38
+ * The single best front-cover URL — the approved front image, preferring a
39
+ * thumbnail at least `minSize` px wide, else the full-resolution original.
40
+ * Deezer caps its own art at 1800 px, so this is how you go bigger.
41
+ */
42
+ const getBestCoverArtUrl = async (mbid, { entity = 'release-group', minSize = 1200 } = {}) => {
43
+ var _a, _b;
44
+ const art = await (0, exports.getCoverArt)(mbid, entity);
45
+ if (!art) {
46
+ return null;
47
+ }
48
+ const front = (_b = (_a = art.images.find((i) => i.front && i.approved)) !== null && _a !== void 0 ? _a : art.images.find((i) => i.front)) !== null && _b !== void 0 ? _b : art.images[0];
49
+ if (!front) {
50
+ return null;
51
+ }
52
+ const sized = Object.entries(front.thumbnails)
53
+ .map(([k, url]) => [Number(k), url])
54
+ .filter(([n]) => Number.isFinite(n) && n >= minSize)
55
+ .sort((a, b) => a[0] - b[0])[0];
56
+ return sized ? sized[1] : front.image;
57
+ };
58
+ exports.getBestCoverArtUrl = getBestCoverArtUrl;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Optional, pluggable **enrichment** against third-party open databases —
3
+ * strictly read-only, off by default, never wired into `addTrackTags`. Use it to
4
+ * fill gaps Deezer leaves: canonical release/label data (MusicBrainz, by ISRC)
5
+ * and higher-resolution cover art (Cover Art Archive).
6
+ *
7
+ * Both services are rate-limited and want a descriptive `User-Agent` —
8
+ * `configureMusicBrainz({userAgent})` before first use.
9
+ */
10
+ export { PoliteJsonClient } from './client';
11
+ export { configureMusicBrainz, lookupRecordingByISRC, getMusicBrainzRecording, getMusicBrainzRelease, } from './musicbrainz';
12
+ export type { MBRecording, MBRelease, MBArtistCredit } from './musicbrainz';
13
+ export { getCoverArt, getBestCoverArtUrl } from './coverart';
14
+ export type { CoverArt, CoverArtImage } from './coverart';
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getBestCoverArtUrl = exports.getCoverArt = exports.getMusicBrainzRelease = exports.getMusicBrainzRecording = exports.lookupRecordingByISRC = exports.configureMusicBrainz = exports.PoliteJsonClient = void 0;
4
+ /**
5
+ * Optional, pluggable **enrichment** against third-party open databases —
6
+ * strictly read-only, off by default, never wired into `addTrackTags`. Use it to
7
+ * fill gaps Deezer leaves: canonical release/label data (MusicBrainz, by ISRC)
8
+ * and higher-resolution cover art (Cover Art Archive).
9
+ *
10
+ * Both services are rate-limited and want a descriptive `User-Agent` —
11
+ * `configureMusicBrainz({userAgent})` before first use.
12
+ */
13
+ var client_1 = require("./client");
14
+ Object.defineProperty(exports, "PoliteJsonClient", { enumerable: true, get: function () { return client_1.PoliteJsonClient; } });
15
+ var musicbrainz_1 = require("./musicbrainz");
16
+ Object.defineProperty(exports, "configureMusicBrainz", { enumerable: true, get: function () { return musicbrainz_1.configureMusicBrainz; } });
17
+ Object.defineProperty(exports, "lookupRecordingByISRC", { enumerable: true, get: function () { return musicbrainz_1.lookupRecordingByISRC; } });
18
+ Object.defineProperty(exports, "getMusicBrainzRecording", { enumerable: true, get: function () { return musicbrainz_1.getMusicBrainzRecording; } });
19
+ Object.defineProperty(exports, "getMusicBrainzRelease", { enumerable: true, get: function () { return musicbrainz_1.getMusicBrainzRelease; } });
20
+ var coverart_1 = require("./coverart");
21
+ Object.defineProperty(exports, "getCoverArt", { enumerable: true, get: function () { return coverart_1.getCoverArt; } });
22
+ Object.defineProperty(exports, "getBestCoverArtUrl", { enumerable: true, get: function () { return coverart_1.getBestCoverArtUrl; } });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Set the `User-Agent` (MusicBrainz requires a descriptive one that identifies
3
+ * your application and a contact URL/email) and the minimum request interval
4
+ * (they ask for ≤ 1 req/s per IP; default 1100 ms). Call once at startup.
5
+ */
6
+ export declare const configureMusicBrainz: (opts: {
7
+ userAgent?: string;
8
+ minIntervalMs?: number;
9
+ }) => void;
10
+ export interface MBArtistCredit {
11
+ name: string;
12
+ mbid?: string;
13
+ /** text that joins this credit to the next, e.g. `' feat. '` */
14
+ joinPhrase?: string;
15
+ }
16
+ export interface MBRelease {
17
+ mbid: string;
18
+ title: string;
19
+ /** YYYY / YYYY-MM / YYYY-MM-DD */
20
+ date?: string;
21
+ /** ISO 3166 country code */
22
+ country?: string;
23
+ status?: string;
24
+ barcode?: string;
25
+ releaseGroupMbid?: string;
26
+ primaryType?: string;
27
+ /** first label + catalogue number, when a full release lookup was done */
28
+ label?: string;
29
+ catalogNumber?: string;
30
+ }
31
+ export interface MBRecording {
32
+ mbid: string;
33
+ title: string;
34
+ disambiguation?: string;
35
+ /** track length in milliseconds */
36
+ lengthMs?: number;
37
+ /** every ISRC MusicBrainz has for this recording */
38
+ isrcs: string[];
39
+ /** search relevance 0–100 */
40
+ score?: number;
41
+ artistCredit: MBArtistCredit[];
42
+ /** the artist credit rendered as one display string */
43
+ artist: string;
44
+ /** earliest release date across the releases MB returned */
45
+ firstReleaseDate?: string;
46
+ releases: MBRelease[];
47
+ }
48
+ /**
49
+ * The best-matching MusicBrainz recording for an ISRC — canonical title, artist
50
+ * credits, length, every known ISRC, and the releases it appears on. `null` when
51
+ * MusicBrainz has nothing for the code.
52
+ */
53
+ export declare const lookupRecordingByISRC: (isrc: string) => Promise<MBRecording | null>;
54
+ /**
55
+ * A full MusicBrainz release — with `inc` (default `labels`, `release-groups`)
56
+ * you get the label, catalogue number, barcode and release-group MBID (which
57
+ * feeds `getCoverArt`). `null` when the MBID is unknown.
58
+ */
59
+ export declare const getMusicBrainzRelease: (releaseMbid: string, inc?: string[]) => Promise<MBRelease | null>;
60
+ /** Look up a recording by its own MBID (rather than by ISRC). */
61
+ export declare const getMusicBrainzRecording: (recordingMbid: string) => Promise<MBRecording | null>;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMusicBrainzRecording = exports.getMusicBrainzRelease = exports.lookupRecordingByISRC = exports.configureMusicBrainz = void 0;
4
+ const client_1 = require("./client");
5
+ const DEFAULT_UA = 'gerdur-core (+https://github.com/soulwax/gerdur-core)';
6
+ let client = new client_1.PoliteJsonClient({ userAgent: DEFAULT_UA, minIntervalMs: 1100 });
7
+ /**
8
+ * Set the `User-Agent` (MusicBrainz requires a descriptive one that identifies
9
+ * your application and a contact URL/email) and the minimum request interval
10
+ * (they ask for ≤ 1 req/s per IP; default 1100 ms). Call once at startup.
11
+ */
12
+ const configureMusicBrainz = (opts) => {
13
+ var _a, _b;
14
+ client = new client_1.PoliteJsonClient({
15
+ userAgent: (_a = opts.userAgent) !== null && _a !== void 0 ? _a : client.userAgent,
16
+ minIntervalMs: (_b = opts.minIntervalMs) !== null && _b !== void 0 ? _b : client.minIntervalMs,
17
+ });
18
+ };
19
+ exports.configureMusicBrainz = configureMusicBrainz;
20
+ const BASE = 'https://musicbrainz.org/ws/2';
21
+ const renderCredit = (credits) => credits.map((c, i) => { var _a; return c.name + (i < credits.length - 1 ? (_a = c.joinPhrase) !== null && _a !== void 0 ? _a : ', ' : ''); }).join('');
22
+ const mapCredit = (raw) => (raw !== null && raw !== void 0 ? raw : []).map((c) => {
23
+ var _a, _b, _c;
24
+ return ({
25
+ name: (_a = c.name) !== null && _a !== void 0 ? _a : (_b = c.artist) === null || _b === void 0 ? void 0 : _b.name,
26
+ mbid: (_c = c.artist) === null || _c === void 0 ? void 0 : _c.id,
27
+ joinPhrase: c.joinphrase || undefined,
28
+ });
29
+ });
30
+ const mapRelease = (r) => {
31
+ var _a, _b, _c, _d, _e, _f, _g;
32
+ return ({
33
+ mbid: r.id,
34
+ title: r.title,
35
+ date: r.date || undefined,
36
+ country: r.country || undefined,
37
+ status: r.status || undefined,
38
+ barcode: r.barcode || undefined,
39
+ releaseGroupMbid: (_a = r['release-group']) === null || _a === void 0 ? void 0 : _a.id,
40
+ primaryType: ((_b = r['release-group']) === null || _b === void 0 ? void 0 : _b['primary-type']) || undefined,
41
+ label: (_e = (_d = (_c = r['label-info']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.label) === null || _e === void 0 ? void 0 : _e.name,
42
+ catalogNumber: ((_g = (_f = r['label-info']) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g['catalog-number']) || undefined,
43
+ });
44
+ };
45
+ /**
46
+ * The best-matching MusicBrainz recording for an ISRC — canonical title, artist
47
+ * credits, length, every known ISRC, and the releases it appears on. `null` when
48
+ * MusicBrainz has nothing for the code.
49
+ */
50
+ const lookupRecordingByISRC = async (isrc) => {
51
+ var _a, _b, _c;
52
+ const data = await client.get(`${BASE}/recording?query=isrc:${encodeURIComponent(isrc)}&fmt=json&limit=5&inc=releases`);
53
+ const raw = (_a = data === null || data === void 0 ? void 0 : data.recordings) === null || _a === void 0 ? void 0 : _a[0];
54
+ if (!raw) {
55
+ return null;
56
+ }
57
+ const releases = ((_b = raw.releases) !== null && _b !== void 0 ? _b : []).map(mapRelease);
58
+ const firstReleaseDate = releases
59
+ .map((r) => r.date)
60
+ .filter(Boolean)
61
+ .sort()[0];
62
+ return {
63
+ mbid: raw.id,
64
+ title: raw.title,
65
+ disambiguation: raw.disambiguation || undefined,
66
+ lengthMs: typeof raw.length === 'number' ? raw.length : undefined,
67
+ isrcs: (_c = raw.isrcs) !== null && _c !== void 0 ? _c : [isrc],
68
+ score: typeof raw.score === 'number' ? raw.score : undefined,
69
+ artistCredit: mapCredit(raw['artist-credit']),
70
+ artist: renderCredit(mapCredit(raw['artist-credit'])),
71
+ firstReleaseDate,
72
+ releases,
73
+ };
74
+ };
75
+ exports.lookupRecordingByISRC = lookupRecordingByISRC;
76
+ /**
77
+ * A full MusicBrainz release — with `inc` (default `labels`, `release-groups`)
78
+ * you get the label, catalogue number, barcode and release-group MBID (which
79
+ * feeds `getCoverArt`). `null` when the MBID is unknown.
80
+ */
81
+ const getMusicBrainzRelease = async (releaseMbid, inc = ['labels', 'release-groups']) => {
82
+ const data = await client.get(`${BASE}/release/${encodeURIComponent(releaseMbid)}?fmt=json&inc=${inc.join('+')}`);
83
+ return data ? mapRelease(data) : null;
84
+ };
85
+ exports.getMusicBrainzRelease = getMusicBrainzRelease;
86
+ /** Look up a recording by its own MBID (rather than by ISRC). */
87
+ const getMusicBrainzRecording = async (recordingMbid) => {
88
+ var _a, _b;
89
+ const raw = await client.get(`${BASE}/recording/${encodeURIComponent(recordingMbid)}?fmt=json&inc=artist-credits+isrcs+releases`);
90
+ if (!raw) {
91
+ return null;
92
+ }
93
+ const releases = ((_a = raw.releases) !== null && _a !== void 0 ? _a : []).map(mapRelease);
94
+ return {
95
+ mbid: raw.id,
96
+ title: raw.title,
97
+ disambiguation: raw.disambiguation || undefined,
98
+ lengthMs: typeof raw.length === 'number' ? raw.length : undefined,
99
+ isrcs: (_b = raw.isrcs) !== null && _b !== void 0 ? _b : [],
100
+ artistCredit: mapCredit(raw['artist-credit']),
101
+ artist: renderCredit(mapCredit(raw['artist-credit'])),
102
+ firstReleaseDate: releases
103
+ .map((r) => r.date)
104
+ .filter(Boolean)
105
+ .sort()[0],
106
+ releases,
107
+ };
108
+ };
109
+ exports.getMusicBrainzRecording = getMusicBrainzRecording;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from './converter';
8
8
  export * from './lib/decrypt';
9
9
  export * from './lib/get-url';
10
10
  export * from './lib/stream-download';
11
+ export * from './enrich';
11
12
  export { httpAgent, httpsAgent, getBuffer, getJson, getText, getStream } from './lib/http';
12
13
  export type { StreamResponse } from './lib/http';
13
14
  export * from './metadata-writer';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ __exportStar(require("./converter"), exports);
30
30
  __exportStar(require("./lib/decrypt"), exports);
31
31
  __exportStar(require("./lib/get-url"), exports);
32
32
  __exportStar(require("./lib/stream-download"), exports);
33
+ __exportStar(require("./enrich"), exports);
33
34
  var http_1 = require("./lib/http");
34
35
  Object.defineProperty(exports, "httpAgent", { enumerable: true, get: function () { return http_1.httpAgent; } });
35
36
  Object.defineProperty(exports, "httpsAgent", { enumerable: true, get: function () { return http_1.httpsAgent; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur-core",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "Core module for gerdur.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",