gerdur-core 2.13.2 → 2.13.3

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.13.3 - 2026-08-31
4
+
5
+ ### Docs
6
+
7
+ - **`MIGRATING.md`** — a migration guide from `@soulwax/d-fi-core`: dependency
8
+ swap, import rename, the one breaking `addTrackTags` change, the local
9
+ workarounds that can now be deleted (OpenSSL-3 Blowfish fallback, per-track
10
+ URL-retry loops, manual album-info caching, `code === 4` spin guards), and the
11
+ faster primitives to adopt (`downloadTrackBuffer`, batch `resolveDownloadUrls`,
12
+ `streamTrackDownload`, `Session` / `createSession`, `DeezerError`). Linked from
13
+ the README and shipped in the package.
14
+
3
15
  ## 2.13.2 - 2026-08-31
4
16
 
5
17
  ### Changed
package/MIGRATING.md ADDED
@@ -0,0 +1,249 @@
1
+ # Migrating from `@soulwax/d-fi-core`
2
+
3
+ `gerdur-core` is the continuation of `@soulwax/d-fi-core`. It was relaunched
4
+ clean-slate on 2026-08-30 — new repo, squashed history, published under a new
5
+ name — so **`gerdur-core@1.0.1` is the last state of `@soulwax/d-fi-core` with
6
+ every identifier renamed**. Everything since (`1.0.1 → 2.13.x`) is additive
7
+ except for one call.
8
+
9
+ **TL;DR:** rename the dependency and the imports, fix one `addTrackTags` call,
10
+ and you're done. Then delete the workarounds you no longer need and adopt the
11
+ faster primitives below.
12
+
13
+ ---
14
+
15
+ ## 1. Swap the dependency
16
+
17
+ ```bash
18
+ npm rm @soulwax/d-fi-core
19
+ npm i gerdur-core
20
+ ```
21
+
22
+ ```jsonc
23
+ // package.json
24
+ - "@soulwax/d-fi-core": "^x.y.z"
25
+ + "gerdur-core": "^2.13.2"
26
+ ```
27
+
28
+ ## 2. Rename imports
29
+
30
+ Mechanical, whole codebase:
31
+
32
+ ```ts
33
+ - import { … } from '@soulwax/d-fi-core';
34
+ + import { … } from 'gerdur-core';
35
+
36
+ - import type { trackType } from '@soulwax/d-fi-core/types';
37
+ + import type { trackType } from 'gerdur-core/types'; // the /types subpath name is unchanged
38
+ ```
39
+
40
+ ## 3. Fix the one breaking change — `addTrackTags`
41
+
42
+ The third argument changed from a cover-size `number` to an options object, and
43
+ the return changed from `Buffer` to `{buffer, model}`.
44
+
45
+ ```ts
46
+ // before
47
+ const out: Buffer = await addTrackTags(audio, track, 500);
48
+
49
+ // after
50
+ const {buffer: out, model} = await addTrackTags(audio, track, {coverSize: 500});
51
+ ```
52
+
53
+ - `coverSize` now defaults to **1000** (was 1000 in code but often passed
54
+ explicitly) and is clamped to Deezer's real ceiling of **1800**.
55
+ - `model` is the full [`TrackTagModel`](README.md#types) — every resolved field,
56
+ plus `model.lyricsSynced` (an LRC document, ready for a `.lrc` sidecar).
57
+
58
+ If you called the lower-level writers directly, they now take a `TrackTagModel`
59
+ instead of `(buffer, track, album, cover)`:
60
+
61
+ ```ts
62
+ // before
63
+ writeMetadataMp3(buffer, track, album, cover);
64
+ writeMetadataFlac(buffer, track, album, size, cover);
65
+
66
+ // after — build the model, or just call addTrackTags
67
+ import {buildTagModel} from 'gerdur-core';
68
+ const model = buildTagModel({track, album, publicTrack, lyrics, cover, coverSize: 1000, deezerIds: true, includeRank: true});
69
+ writeMetadataMp3(buffer, model);
70
+ writeMetadataFlac(buffer, model, {embedSyncedLyrics: true});
71
+ ```
72
+
73
+ ## 4. Everything else compiles unchanged
74
+
75
+ Same names, same signatures, same return shapes:
76
+
77
+ - `initDeezerApi(arl)` — still `Promise<string>` (the gateway `SESSION` id); the
78
+ `arl` must still be exactly 192 characters.
79
+ - `getTrackInfo` · `getTrackInfoPublicApi` · `getLyrics` · `getAlbumInfo` ·
80
+ `getAlbumInfoPublicApi` · `getAlbumTracks` · `getPlaylistInfo` ·
81
+ `getPlaylistTracks` · `getArtistInfo` · `getDiscography` · `getProfile` ·
82
+ `getUser` · `getChannelList` · `getShowInfo` · `getPlaylistChannel`
83
+ - `searchMusic` · `searchAlternative`
84
+ - `getTrackDownloadUrl(track, 1 | 3 | 9)` → `{trackUrl, isEncrypted, fileSize} | null`
85
+ - `decryptDownload(buffer, sngId)` · `getSongFileName(track, quality)`
86
+ - `WrongLicense` · `GeoBlocked`
87
+ - `parseInfo` · `getUrlParts` · `isrc2deezer` · `upc2deezer`
88
+ - the `spotify` · `tidal` · `youtube` converter namespaces
89
+
90
+ ## 5. Delete your workarounds
91
+
92
+ If your integration carried any of these, remove them — `gerdur-core` handles it
93
+ now:
94
+
95
+ | Workaround you probably wrote | Now handled by |
96
+ | :--- | :--- |
97
+ | `egoroof-blowfish` / manual `bf-cbc` fallback for `ERR_OSSL_EVP_UNSUPPORTED` on Node 17+ / OpenSSL 3 | native, dependency-free Blowfish since `1.0.2` (~290 MiB/s), verified against the canonical test vectors |
98
+ | per-track `for (const q of [9,3,1]) try { getTrackDownloadUrl(track, q) }` retry loops | `getTrackDownloadUrl` / `resolveDownloadUrls` — media-API 403/429/5xx triggers re-auth + backoff, then a token-free legacy-CDN fallback instead of throwing |
99
+ | a guard against `requestWithRetry` spinning forever on `error.code === 4` | `RETRY_POLICY` — per-error-class attempt caps + a 30 s wall-clock deadline; throws `DeezerError` on exhaustion |
100
+ | a `Map` / object cache of album info while tagging a whole album | LRU + in-flight coalescing in the API layer — each metadata endpoint is hit once per album automatically |
101
+ | `isEncrypted = url.includes('/mobile/')` heuristics | `resolved.isEncrypted` now comes from the media API's `cipher` field — authoritative |
102
+ | clearing a cache yourself when switching `arl` | `initDeezerApi(newArl)` clears the default session cache |
103
+ | buffering the whole file twice (download, then decrypt) | `streamTrackDownload` — constant memory, ~one 2048-byte stripe |
104
+
105
+ ## 6. Adopt the faster primitives
106
+
107
+ ### One-call download
108
+
109
+ The `getTrackDownloadUrl` → fetch → `decryptDownload` sequence collapses to:
110
+
111
+ ```ts
112
+ import {downloadTrackBuffer} from 'gerdur-core';
113
+ const audio = await downloadTrackBuffer(track, 3); // Buffer | null — URL resolve, retries and decrypt inside
114
+ ```
115
+
116
+ ### Batch URL resolution — one request per album, not N
117
+
118
+ ```ts
119
+ import {refreshTrackTokens, resolveDownloadUrls} from 'gerdur-core';
120
+
121
+ const fresh = await refreshTrackTokens(tracks); // one request; refreshes tokens older than ~1 h
122
+ const urls = await resolveDownloadUrls(fresh, ['FLAC', 'MP3_320', 'MP3_128']);
123
+ // urls[i] = {trackUrl, isEncrypted, fileSize, format, cipher} | null — Deezer returns the best each is licensed for
124
+ ```
125
+
126
+ `resolveDownloadUrls` is a single `media.deezer.com/v1/get_url` POST for the
127
+ whole list — roughly **19× faster** than the old per-track-per-quality loop for a
128
+ 14-track album. `refreshTrackTokens` first prevents the classic "long playlist
129
+ starts 403-ing partway through" (tokens expire after ~1 h).
130
+
131
+ ### Streaming — constant memory + resume
132
+
133
+ ```ts
134
+ import {pipeline} from 'stream/promises';
135
+ import {createWriteStream, existsSync, statSync} from 'fs';
136
+ import {streamTrackDownload} from 'gerdur-core';
137
+
138
+ const {stream, startedAt} = await streamTrackDownload(track, 9, {
139
+ resumeFrom: existsSync(f) ? statSync(f).size : 0, // snapped to a 2048-byte stripe boundary
140
+ onProgress: (received, total) => …,
141
+ });
142
+ await pipeline(stream, createWriteStream(f, {flags: startedAt ? 'a' : 'w'}));
143
+ ```
144
+
145
+ Or drop `createDecryptStream(sngId, startChunk)` into a pipeline you already own.
146
+
147
+ ### Multi-account without global state
148
+
149
+ The module-level `arl` / session / `license_token` are gone into a `Session`:
150
+
151
+ ```ts
152
+ import {createSession} from 'gerdur-core';
153
+
154
+ const a = await createSession(arlOne);
155
+ const b = await createSession(arlTwo);
156
+ const track = await a.getTrackInfo('3135556');
157
+ const audio = await a.getTrackBuffer(track, 9); // isolated arl, tokens, and response cache
158
+ ```
159
+
160
+ `initDeezerApi` still works — it is now a thin shim over a process-wide default
161
+ session.
162
+
163
+ ### Typed errors
164
+
165
+ ```ts
166
+ import {DeezerError, GeoBlocked, WrongLicense, ExpiredTrackToken} from 'gerdur-core';
167
+
168
+ try {
169
+ await downloadTrackBuffer(track, 9);
170
+ } catch (err) {
171
+ if (err instanceof DeezerError) console.error(err.code, err.keys, err.retryable);
172
+ if (err instanceof ExpiredTrackToken) { /* re-fetch the track and retry */ }
173
+ }
174
+ ```
175
+
176
+ ### Richer tags, fed once
177
+
178
+ ```ts
179
+ const {buffer, model} = await addTrackTags(audio, track, {album, lyrics, cover}); // skip the refetch
180
+ if (model.lyricsSynced) writeFileSync(f.replace(/\.\w+$/, '.lrc'), model.lyricsSynced);
181
+ ```
182
+
183
+ `addTrackTags` now also writes ReplayGain, BPM, real `©` / `℗` lines, the
184
+ original (vs reissue) release date, full engineer / producer / performer credits,
185
+ featured artists, ISRC / UPC, a proper explicit-status enum, Deezer ids, and the
186
+ artist photo as a second embedded image.
187
+
188
+ ### New read surfaces
189
+
190
+ Mostly no auth required:
191
+
192
+ - **Search** — `searchPublicApi` / `searchTracks` / `searchAlbums` / … ,
193
+ `buildAdvancedQuery`, `suggest`, `searchFacets`
194
+ - **Browse** — `getChart`, `getGenres`, `getRelatedArtists`, `getArtistTopTracks`,
195
+ `getEditorialReleases`, …
196
+ - **Resolve codes** — `getTrackByISRC`, `getAlbumByUPC`
197
+ - **Flow / library / radios** — `getUserFlow`, `getUserFavoriteTracks`,
198
+ `getRadioTracks`, …
199
+ - **Podcasts** — `getEpisode`, `getShowEpisodes`
200
+ - **Previews** — `getTrackPreview`, `downloadPreview` (licence-free 30 s MP3, no
201
+ `arl`, no decryption — good for tests / CI)
202
+
203
+ ### Enrichment — covers larger than Deezer's 1800 px cap
204
+
205
+ ```ts
206
+ import {configureMusicBrainz, getCoverArtByISRC, getBuffer, addTrackTags} from 'gerdur-core';
207
+
208
+ configureMusicBrainz({userAgent: 'my-app/1.0 ( me@example.com )'});
209
+ const coverUrl = await getCoverArtByISRC(track.ISRC, {minSize: 1200});
210
+ if (coverUrl) await addTrackTags(audio, track, {cover: await getBuffer(coverUrl)});
211
+ ```
212
+
213
+ ### Shared socket pool
214
+
215
+ ```ts
216
+ import {httpAgent, httpsAgent, getBuffer, getJson, getStream} from 'gerdur-core';
217
+ // hand httpAgent / httpsAgent to your own got/undici calls to reuse the same keep-alive sockets
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Before / after
223
+
224
+ ```ts
225
+ // ── before: @soulwax/d-fi-core ───────────────────────────────────────────────
226
+ import got from 'got';
227
+ import {initDeezerApi, getTrackInfo, getTrackDownloadUrl, decryptDownload, addTrackTags} from '@soulwax/d-fi-core';
228
+
229
+ await initDeezerApi(arl);
230
+ const track = await getTrackInfo('3135556');
231
+ const {trackUrl, isEncrypted} = (await getTrackDownloadUrl(track, 3))!;
232
+ const body = (await got(trackUrl, {responseType: 'buffer'})).body;
233
+ const audio = isEncrypted ? decryptDownload(body, track.SNG_ID) : body;
234
+ const tagged = await addTrackTags(audio, track, 500); // Buffer
235
+ fs.writeFileSync('out.mp3', tagged);
236
+
237
+ // ── after: gerdur-core ──────────────────────────────────────────────────────
238
+ import {initDeezerApi, getTrackInfo, downloadTrackBuffer, addTrackTags} from 'gerdur-core';
239
+
240
+ await initDeezerApi(arl);
241
+ const track = await getTrackInfo('3135556');
242
+ const audio = await downloadTrackBuffer(track, 3); // fetch + decrypt, retries handled
243
+ const {buffer, model} = await addTrackTags(audio!, track, {coverSize: 500});
244
+ fs.writeFileSync('out.mp3', buffer);
245
+ if (model.lyricsSynced) fs.writeFileSync('out.lrc', model.lyricsSynced);
246
+ ```
247
+
248
+ See [README.md](README.md) for the full guide and the
249
+ [CHANGELOG](CHANGELOG.md) for the version-by-version history.
package/README.md CHANGED
@@ -27,6 +27,10 @@ It has **no CLI and does no disk I/O** — every function returns data or a
27
27
  `Buffer`/stream. The [`gerdur`](https://www.npmjs.com/package/gerdur) package is
28
28
  the CLI and the file-writing layer on top.
29
29
 
30
+ > **Coming from `@soulwax/d-fi-core`?** `gerdur-core` is its continuation — a
31
+ > near drop-in rename plus one `addTrackTags` change. See
32
+ > **[MIGRATING.md](MIGRATING.md)**.
33
+
30
34
  ---
31
35
 
32
36
  ## Contents
@@ -55,6 +59,7 @@ the CLI and the file-writing layer on top.
55
59
  - [Errors](#errors)
56
60
  - [Types](#types)
57
61
  - [API reference](#api-reference)
62
+ - [Migrating from @soulwax/d-fi-core](#migrating)
58
63
  - [The name](#the-name)
59
64
  - [Legal](#legal)
60
65
 
@@ -762,6 +767,17 @@ import type {
762
767
  See the [FAQ](docs/faq.md) and the [`gerdur` CLI](https://www.npmjs.com/package/gerdur)
763
768
  for end-to-end usage.
764
769
 
770
+ <a id="migrating"></a>
771
+
772
+ ## Migrating from @soulwax/d-fi-core
773
+
774
+ `gerdur-core` is the continuation of `@soulwax/d-fi-core`. `gerdur-core@1.0.1` is
775
+ that codebase renamed; everything since is additive except a single `addTrackTags`
776
+ signature change. **[MIGRATING.md](MIGRATING.md)** has the exact steps —
777
+ dependency swap, import rename, the one fix, the workarounds you can now delete,
778
+ and the faster primitives (`downloadTrackBuffer`, batch `resolveDownloadUrls`,
779
+ `streamTrackDownload`, `Session`, `DeezerError`) to adopt.
780
+
765
781
  ## The name
766
782
 
767
783
  **Gerðr** is the jötunn Freyr sends Skírnir riding through a wall of fire to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur-core",
3
- "version": "2.13.2",
3
+ "version": "2.13.3",
4
4
  "description": "Deezer API client, cross-service URL resolution, Blowfish track decryption and MP3/FLAC metadata tagging — the engine behind the gerdur CLI.",
5
5
  "keywords": [
6
6
  "deezer",
@@ -31,7 +31,8 @@
31
31
  "files": [
32
32
  "dist/**/*",
33
33
  "types/**/*",
34
- "CHANGELOG.md"
34
+ "CHANGELOG.md",
35
+ "MIGRATING.md"
35
36
  ],
36
37
  "engines": {
37
38
  "node": ">=12"