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 +19 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +249 -0
- package/README.md +16 -0
- package/package.json +4 -3
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.
|
|
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": "
|
|
52
|
+
"license": "MIT",
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"browser-id3-writer": "^4.4.0",
|
|
54
55
|
"delay": "^5.0.0",
|