gerdur-core 2.11.0 → 2.13.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 +30 -0
- package/README.md +27 -0
- package/dist/enrich/client.d.ts +25 -0
- package/dist/enrich/client.js +59 -0
- package/dist/enrich/coverart.d.ts +50 -0
- package/dist/enrich/coverart.js +102 -0
- package/dist/enrich/index.d.ts +14 -0
- package/dist/enrich/index.js +24 -0
- package/dist/enrich/musicbrainz.d.ts +61 -0
- package/dist/enrich/musicbrainz.js +109 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.13.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`getCoverArtByISRC(isrc, {minSize?, maxTries?})`** and
|
|
8
|
+
**`getRecordingCoverArt(recording, …)`** — the full ISRC → cover chain with a
|
|
9
|
+
proper release ranking: `getBestCoverArtUrl` on the *first* release a recording
|
|
10
|
+
lists often hits a promo comp / bootleg with no art. These walk the
|
|
11
|
+
release-groups **canonical-first** (Official > Album > earliest date) and
|
|
12
|
+
return the first real front cover, bounded to `maxTries` (default 4) lookups.
|
|
13
|
+
|
|
14
|
+
## 2.12.0 - 2026-08-31
|
|
15
|
+
|
|
16
|
+
Phase 4 — an optional, read-only **enrichment** layer against open databases.
|
|
17
|
+
Off by default, never touches `addTrackTags`.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **MusicBrainz** (`src/enrich/`): `lookupRecordingByISRC(isrc)` — canonical
|
|
22
|
+
recording (title, artist credits, length, every known ISRC, the releases it's
|
|
23
|
+
on); `getMusicBrainzRecording(mbid)` / `getMusicBrainzRelease(mbid, inc?)` —
|
|
24
|
+
direct MBID lookups with label / catalogue number / barcode.
|
|
25
|
+
`configureMusicBrainz({userAgent, minIntervalMs})` — they require a descriptive
|
|
26
|
+
UA and ≤ 1 req/s.
|
|
27
|
+
- **Cover Art Archive**: `getCoverArt(mbid, entity?)` and
|
|
28
|
+
`getBestCoverArtUrl(mbid, {minSize})` — higher-resolution covers than Deezer's
|
|
29
|
+
1800 px ceiling. `null` when there's no art.
|
|
30
|
+
- **`PoliteJsonClient`** — the serialised, rate-limited, `503`/`429`-retrying,
|
|
31
|
+
`404`→`null` JSON client both use; exported for building your own.
|
|
32
|
+
|
|
3
33
|
## 2.11.0 - 2026-08-31
|
|
4
34
|
|
|
5
35
|
### Added
|
package/README.md
CHANGED
|
@@ -437,6 +437,33 @@ the audio (ID3v2.3 for MP3, Vorbis comments for FLAC). Returns
|
|
|
437
437
|
| `track` | Yes | `object` | track object from `getTrackInfo` / `parseInfo` |
|
|
438
438
|
| `options` | No | `object` | `AddTrackTagsOptions` — `coverSize` (56–1800), pre-fetched `album`/`lyrics`/`cover`, and toggles (`richCredits`, `embedArtistImage`, `deezerIds`, …) |
|
|
439
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
|
+
| `getCoverArtByISRC(isrc, {minSize?, maxTries?})` | the whole chain — ISRC → recording → **canonical-first** release walk → first real front cover. Use this, not `getBestCoverArtUrl` on `releases[0]` (often a promo comp with no art). |
|
|
453
|
+
| `getRecordingCoverArt(recording, …)` | same, from an already-fetched `MBRecording`. |
|
|
454
|
+
|
|
455
|
+
```js
|
|
456
|
+
configureMusicBrainz({userAgent: 'myapp/1.0 ( me@example.com )'});
|
|
457
|
+
const rec = await lookupRecordingByISRC(track.isrc);
|
|
458
|
+
if (rec?.releases[0]?.releaseGroupMbid) {
|
|
459
|
+
const cover = await getBestCoverArtUrl(rec.releases[0].releaseGroupMbid, {minSize: 1200});
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Errors: a persistent MusicBrainz `503` ("server busy") surfaces as
|
|
464
|
+
`HttpStatusError` after 3 backed-off retries — catch and fall back to Deezer's
|
|
465
|
+
data.
|
|
466
|
+
|
|
440
467
|
###
|
|
441
468
|
|
|
442
469
|
> 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.
|
|
@@ -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,50 @@
|
|
|
1
|
+
import type { MBRecording } from './musicbrainz';
|
|
2
|
+
export interface CoverArtImage {
|
|
3
|
+
id: string;
|
|
4
|
+
front: boolean;
|
|
5
|
+
back: boolean;
|
|
6
|
+
approved: boolean;
|
|
7
|
+
/** full-resolution image URL (redirects to archive.org) */
|
|
8
|
+
image: string;
|
|
9
|
+
/** `{small, large, '250', '500', '1200'}` — not every size is present */
|
|
10
|
+
thumbnails: Record<string, string>;
|
|
11
|
+
types: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface CoverArt {
|
|
14
|
+
images: CoverArtImage[];
|
|
15
|
+
/** the MusicBrainz release the art belongs to */
|
|
16
|
+
release?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Cover Art Archive images for a MusicBrainz release-group (default) or release.
|
|
20
|
+
* `null` when there's no art — very common, so always handle it. Feed the MBID
|
|
21
|
+
* from `lookupRecordingByISRC(...).releases[i].releaseGroupMbid`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const getCoverArt: (mbid: string, entity?: 'release' | 'release-group') => Promise<CoverArt | null>;
|
|
24
|
+
/**
|
|
25
|
+
* The single best front-cover URL — the approved front image, preferring a
|
|
26
|
+
* thumbnail at least `minSize` px wide, else the full-resolution original.
|
|
27
|
+
* Deezer caps its own art at 1800 px, so this is how you go bigger.
|
|
28
|
+
*/
|
|
29
|
+
export declare const getBestCoverArtUrl: (mbid: string, { entity, minSize }?: {
|
|
30
|
+
entity?: "release-group" | "release" | undefined;
|
|
31
|
+
minSize?: number | undefined;
|
|
32
|
+
}) => Promise<string | null>;
|
|
33
|
+
/**
|
|
34
|
+
* The best cover for a MusicBrainz recording — walks its release-groups
|
|
35
|
+
* canonical-first (Official Album, earliest) and returns the first Cover Art
|
|
36
|
+
* Archive front cover it finds. `null` when none of the top few have art.
|
|
37
|
+
* Bounded to `maxTries` release-groups (default 4) to keep the request count low.
|
|
38
|
+
*/
|
|
39
|
+
export declare const getRecordingCoverArt: (rec: MBRecording, { minSize, maxTries }?: {
|
|
40
|
+
minSize?: number | undefined;
|
|
41
|
+
maxTries?: number | undefined;
|
|
42
|
+
}) => Promise<string | null>;
|
|
43
|
+
/**
|
|
44
|
+
* One call: ISRC → MusicBrainz recording → best Cover Art Archive front cover
|
|
45
|
+
* (larger than Deezer's 1800 px cap). `null` when there's no match or no art.
|
|
46
|
+
*/
|
|
47
|
+
export declare const getCoverArtByISRC: (isrc: string, options?: {
|
|
48
|
+
minSize?: number;
|
|
49
|
+
maxTries?: number;
|
|
50
|
+
}) => Promise<string | null>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCoverArtByISRC = exports.getRecordingCoverArt = exports.getBestCoverArtUrl = exports.getCoverArt = void 0;
|
|
4
|
+
const client_1 = require("./client");
|
|
5
|
+
const musicbrainz_1 = require("./musicbrainz");
|
|
6
|
+
const client = new client_1.PoliteJsonClient({
|
|
7
|
+
userAgent: 'gerdur-core (+https://github.com/soulwax/gerdur-core)',
|
|
8
|
+
minIntervalMs: 200,
|
|
9
|
+
});
|
|
10
|
+
const BASE = 'https://coverartarchive.org';
|
|
11
|
+
/**
|
|
12
|
+
* Cover Art Archive images for a MusicBrainz release-group (default) or release.
|
|
13
|
+
* `null` when there's no art — very common, so always handle it. Feed the MBID
|
|
14
|
+
* from `lookupRecordingByISRC(...).releases[i].releaseGroupMbid`.
|
|
15
|
+
*/
|
|
16
|
+
const getCoverArt = async (mbid, entity = 'release-group') => {
|
|
17
|
+
const data = await client.get(`${BASE}/${entity}/${encodeURIComponent(mbid)}`);
|
|
18
|
+
if (!(data === null || data === void 0 ? void 0 : data.images)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
release: data.release,
|
|
23
|
+
images: data.images.map((i) => {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
return ({
|
|
26
|
+
id: String(i.id),
|
|
27
|
+
front: Boolean(i.front),
|
|
28
|
+
back: Boolean(i.back),
|
|
29
|
+
approved: Boolean(i.approved),
|
|
30
|
+
image: i.image,
|
|
31
|
+
thumbnails: (_a = i.thumbnails) !== null && _a !== void 0 ? _a : {},
|
|
32
|
+
types: (_b = i.types) !== null && _b !== void 0 ? _b : [],
|
|
33
|
+
});
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
exports.getCoverArt = getCoverArt;
|
|
38
|
+
/**
|
|
39
|
+
* The single best front-cover URL — the approved front image, preferring a
|
|
40
|
+
* thumbnail at least `minSize` px wide, else the full-resolution original.
|
|
41
|
+
* Deezer caps its own art at 1800 px, so this is how you go bigger.
|
|
42
|
+
*/
|
|
43
|
+
const getBestCoverArtUrl = async (mbid, { entity = 'release-group', minSize = 1200 } = {}) => {
|
|
44
|
+
var _a, _b;
|
|
45
|
+
const art = await (0, exports.getCoverArt)(mbid, entity);
|
|
46
|
+
if (!art) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
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];
|
|
50
|
+
if (!front) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const sized = Object.entries(front.thumbnails)
|
|
54
|
+
.map(([k, url]) => [Number(k), url])
|
|
55
|
+
.filter(([n]) => Number.isFinite(n) && n >= minSize)
|
|
56
|
+
.sort((a, b) => a[0] - b[0])[0];
|
|
57
|
+
return sized ? sized[1] : front.image;
|
|
58
|
+
};
|
|
59
|
+
exports.getBestCoverArtUrl = getBestCoverArtUrl;
|
|
60
|
+
/** Rank a recording's releases so the canonical original album comes first. */
|
|
61
|
+
const rankReleases = (rec) => {
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
return rec.releases
|
|
64
|
+
.filter((r) => r.releaseGroupMbid)
|
|
65
|
+
.map((r) => {
|
|
66
|
+
var _a;
|
|
67
|
+
return ({
|
|
68
|
+
rgid: r.releaseGroupMbid,
|
|
69
|
+
// Official beats Promotion/Bootleg/Pseudo-Release; Album beats Single/EP/Compilation;
|
|
70
|
+
// then earliest date wins.
|
|
71
|
+
score: (r.status === 'Official' ? 0 : r.status ? 10 : 5) + (r.primaryType === 'Album' ? 0 : r.primaryType ? 2 : 1),
|
|
72
|
+
date: (_a = r.date) !== null && _a !== void 0 ? _a : '9999',
|
|
73
|
+
});
|
|
74
|
+
})
|
|
75
|
+
.sort((a, b) => a.score - b.score || a.date.localeCompare(b.date))
|
|
76
|
+
.map((r) => r.rgid)
|
|
77
|
+
.filter((rgid) => (seen.has(rgid) ? false : seen.add(rgid)));
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The best cover for a MusicBrainz recording — walks its release-groups
|
|
81
|
+
* canonical-first (Official Album, earliest) and returns the first Cover Art
|
|
82
|
+
* Archive front cover it finds. `null` when none of the top few have art.
|
|
83
|
+
* Bounded to `maxTries` release-groups (default 4) to keep the request count low.
|
|
84
|
+
*/
|
|
85
|
+
const getRecordingCoverArt = async (rec, { minSize = 1200, maxTries = 4 } = {}) => {
|
|
86
|
+
for (const rgid of rankReleases(rec).slice(0, maxTries)) {
|
|
87
|
+
const url = await (0, exports.getBestCoverArtUrl)(rgid, { entity: 'release-group', minSize });
|
|
88
|
+
if (url)
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
exports.getRecordingCoverArt = getRecordingCoverArt;
|
|
94
|
+
/**
|
|
95
|
+
* One call: ISRC → MusicBrainz recording → best Cover Art Archive front cover
|
|
96
|
+
* (larger than Deezer's 1800 px cap). `null` when there's no match or no art.
|
|
97
|
+
*/
|
|
98
|
+
const getCoverArtByISRC = async (isrc, options = {}) => {
|
|
99
|
+
const rec = await (0, musicbrainz_1.lookupRecordingByISRC)(isrc);
|
|
100
|
+
return rec ? (0, exports.getRecordingCoverArt)(rec, options) : null;
|
|
101
|
+
};
|
|
102
|
+
exports.getCoverArtByISRC = getCoverArtByISRC;
|
|
@@ -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, getRecordingCoverArt, getCoverArtByISRC } from './coverart';
|
|
14
|
+
export type { CoverArt, CoverArtImage } from './coverart';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCoverArtByISRC = exports.getRecordingCoverArt = 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; } });
|
|
23
|
+
Object.defineProperty(exports, "getRecordingCoverArt", { enumerable: true, get: function () { return coverart_1.getRecordingCoverArt; } });
|
|
24
|
+
Object.defineProperty(exports, "getCoverArtByISRC", { enumerable: true, get: function () { return coverart_1.getCoverArtByISRC; } });
|
|
@@ -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; } });
|