gerdur-core 2.12.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 CHANGED
@@ -1,5 +1,16 @@
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
+
3
14
  ## 2.12.0 - 2026-08-31
4
15
 
5
16
  Phase 4 — an optional, read-only **enrichment** layer against open databases.
package/README.md CHANGED
@@ -449,6 +449,8 @@ call `configureMusicBrainz({userAgent})` once at startup.
449
449
  | `getMusicBrainzRecording(mbid)` / `getMusicBrainzRelease(mbid, inc?)` | direct MBID lookups — the release adds label, catalogue number, barcode. |
450
450
  | `getCoverArt(mbid, entity = 'release-group')` | Cover Art Archive images (`front` / `approved` / `thumbnails`). `null` when there's no art. |
451
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`. |
452
454
 
453
455
  ```js
454
456
  configureMusicBrainz({userAgent: 'myapp/1.0 ( me@example.com )'});
@@ -1,3 +1,4 @@
1
+ import type { MBRecording } from './musicbrainz';
1
2
  export interface CoverArtImage {
2
3
  id: string;
3
4
  front: boolean;
@@ -29,3 +30,21 @@ export declare const getBestCoverArtUrl: (mbid: string, { entity, minSize }?: {
29
30
  entity?: "release-group" | "release" | undefined;
30
31
  minSize?: number | undefined;
31
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>;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getBestCoverArtUrl = exports.getCoverArt = void 0;
3
+ exports.getCoverArtByISRC = exports.getRecordingCoverArt = exports.getBestCoverArtUrl = exports.getCoverArt = void 0;
4
4
  const client_1 = require("./client");
5
+ const musicbrainz_1 = require("./musicbrainz");
5
6
  const client = new client_1.PoliteJsonClient({
6
7
  userAgent: 'gerdur-core (+https://github.com/soulwax/gerdur-core)',
7
8
  minIntervalMs: 200,
@@ -56,3 +57,46 @@ const getBestCoverArtUrl = async (mbid, { entity = 'release-group', minSize = 12
56
57
  return sized ? sized[1] : front.image;
57
58
  };
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;
@@ -10,5 +10,5 @@
10
10
  export { PoliteJsonClient } from './client';
11
11
  export { configureMusicBrainz, lookupRecordingByISRC, getMusicBrainzRecording, getMusicBrainzRelease, } from './musicbrainz';
12
12
  export type { MBRecording, MBRelease, MBArtistCredit } from './musicbrainz';
13
- export { getCoverArt, getBestCoverArtUrl } from './coverart';
13
+ export { getCoverArt, getBestCoverArtUrl, getRecordingCoverArt, getCoverArtByISRC } from './coverart';
14
14
  export type { CoverArt, CoverArtImage } from './coverart';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getBestCoverArtUrl = exports.getCoverArt = exports.getMusicBrainzRelease = exports.getMusicBrainzRecording = exports.lookupRecordingByISRC = exports.configureMusicBrainz = exports.PoliteJsonClient = void 0;
3
+ exports.getCoverArtByISRC = exports.getRecordingCoverArt = exports.getBestCoverArtUrl = exports.getCoverArt = exports.getMusicBrainzRelease = exports.getMusicBrainzRecording = exports.lookupRecordingByISRC = exports.configureMusicBrainz = exports.PoliteJsonClient = void 0;
4
4
  /**
5
5
  * Optional, pluggable **enrichment** against third-party open databases —
6
6
  * strictly read-only, off by default, never wired into `addTrackTags`. Use it to
@@ -20,3 +20,5 @@ Object.defineProperty(exports, "getMusicBrainzRelease", { enumerable: true, get:
20
20
  var coverart_1 = require("./coverart");
21
21
  Object.defineProperty(exports, "getCoverArt", { enumerable: true, get: function () { return coverart_1.getCoverArt; } });
22
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; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur-core",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "Core module for gerdur.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",