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 +12 -0
- package/MIGRATING.md +249 -0
- package/README.md +16 -0
- package/package.json +3 -2
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.
|
|
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"
|