gerdur-core 2.13.1 → 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,24 @@
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
+
15
+ ## 2.13.2 - 2026-08-31
16
+
17
+ ### Changed
18
+
19
+ - **Licensed under MIT.** The `LICENSE` file was empty and `package.json` said
20
+ `"SEE LICENSE IN LICENSE"`; both now state MIT.
21
+
3
22
  ## 2.13.1 - 2026-08-31
4
23
 
5
24
  ### Docs
package/LICENSE CHANGED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 soulwax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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.1",
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"
@@ -48,7 +49,7 @@
48
49
  "access": "public"
49
50
  },
50
51
  "author": "Christian",
51
- "license": "SEE LICENSE IN LICENSE",
52
+ "license": "MIT",
52
53
  "dependencies": {
53
54
  "browser-id3-writer": "^4.4.0",
54
55
  "delay": "^5.0.0",