gerdur-core 2.9.0 → 2.11.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 +28 -0
- package/README.md +30 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/podcast.d.ts +16 -0
- package/dist/api/podcast.js +25 -0
- package/dist/api/search.d.ts +19 -1
- package/dist/api/search.js +20 -1
- package/dist/lib/get-url.d.ts +18 -0
- package/dist/lib/get-url.js +33 -1
- package/dist/lib/session-augment.d.ts +2 -0
- package/dist/lib/session-augment.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.11.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`searchFacets(result)`** — flatten the per-type hit counts + Deezer's
|
|
8
|
+
relevance `order` out of a `searchMusic` result.
|
|
9
|
+
|
|
10
|
+
### Internal
|
|
11
|
+
|
|
12
|
+
- The live-API test suite skips more gracefully when an upstream (Deezer public
|
|
13
|
+
REST, Tidal, YouTube) rate-limits or consent-walls the runner — the
|
|
14
|
+
`--> DEEZER` converter tests and YouTube tests now degrade to a skip instead
|
|
15
|
+
of a hard fail.
|
|
16
|
+
|
|
17
|
+
## 2.10.0 - 2026-08-31
|
|
18
|
+
|
|
19
|
+
Leftovers — batch token refresh + podcast episodes. Additive.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`refreshTrackTokens(tracks, {graceSeconds?, session?})`** — one
|
|
24
|
+
`song.getListData` request refreshes every `TRACK_TOKEN` that has expired (or
|
|
25
|
+
is about to). Run a long playlist through this before `resolveDownloadUrls` so
|
|
26
|
+
it doesn't die on stale tokens at track 40. Also `session.refreshTrackTokens`.
|
|
27
|
+
- **`getEpisode(episodeId)`** (`episode.getData`) and **`getShowEpisodes(showId,
|
|
28
|
+
nb?, start?)`** — podcast episodes; `EPISODE_DIRECT_STREAM_URL` is a plain MP3
|
|
29
|
+
(no licence, no decryption).
|
|
30
|
+
|
|
3
31
|
## 2.9.0 - 2026-08-31
|
|
4
32
|
|
|
5
33
|
Phase 3.2 (cont.) — downloads are session-aware. Additive.
|
package/README.md
CHANGED
|
@@ -261,6 +261,16 @@ ignore the operators, so pass a plain string there.
|
|
|
261
261
|
"as you type" UIs. `nb` (default 5) caps items per type. Needs an initialised
|
|
262
262
|
session (`initDeezerApi`).
|
|
263
263
|
|
|
264
|
+
### `.searchFacets(result)`
|
|
265
|
+
|
|
266
|
+
Flattens the per-type hit counts + Deezer's relevance `order` out of a
|
|
267
|
+
`searchMusic` result — for "207 tracks · 99 albums · 17 artists" UIs.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const r = await searchMusic('daft punk', ['TRACK', 'ALBUM', 'ARTIST']);
|
|
271
|
+
searchFacets(r); // {track: 207, album: 99, artist: 17, …, order: ['TOP_RESULT','TRACK',…]}
|
|
272
|
+
```
|
|
273
|
+
|
|
264
274
|
### Browse & discovery
|
|
265
275
|
|
|
266
276
|
Public REST endpoints — no `arl` needed, memoised like the rest. All return a
|
|
@@ -321,6 +331,13 @@ const {data: loved} = await getUserFavoriteTracks(me.USER_ID);
|
|
|
321
331
|
const {data: eighties} = await getRadioTracks(38305); // "The '80s"
|
|
322
332
|
```
|
|
323
333
|
|
|
334
|
+
### Podcasts
|
|
335
|
+
|
|
336
|
+
| Method | |
|
|
337
|
+
| :--- | :--- |
|
|
338
|
+
| `getShowEpisodes(showId, nb = 25, start = 0)` | a page of a show's episodes, newest first |
|
|
339
|
+
| `getEpisode(episodeId)` | one episode — `EPISODE_DIRECT_STREAM_URL` is a plain MP3, no licence / decryption |
|
|
340
|
+
|
|
324
341
|
### `.getTrackDownloadUrl(track, quality);`
|
|
325
342
|
|
|
326
343
|
| Parameters | Required | Type | Description |
|
|
@@ -331,6 +348,19 @@ const {data: eighties} = await getRadioTracks(38305); // "The '80s"
|
|
|
331
348
|
Resolves `{trackUrl, isEncrypted, fileSize}`. `isEncrypted` now comes from the
|
|
332
349
|
media API's `cipher` field (authoritative) rather than a URL guess.
|
|
333
350
|
|
|
351
|
+
### `.refreshTrackTokens(tracks, options?);`
|
|
352
|
+
|
|
353
|
+
`TRACK_TOKEN`s live ~1 hour, so a token fetched at the start of a long playlist
|
|
354
|
+
download is dead by track 40 (surfacing as an opaque CDN 403). Run the selection
|
|
355
|
+
through this first — **one** `song.getListData` request refreshes every token
|
|
356
|
+
that has expired (or expires within `options.graceSeconds`, default 300).
|
|
357
|
+
Tracks with a still-valid token come back untouched. Also `session.refreshTrackTokens(tracks, graceSeconds?)`.
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
const fresh = await refreshTrackTokens(playlist.tracks);
|
|
361
|
+
const urls = await resolveDownloadUrls(fresh, [9, 3, 1]);
|
|
362
|
+
```
|
|
363
|
+
|
|
334
364
|
### Formats
|
|
335
365
|
|
|
336
366
|
Deezer's `get_url` understands more than `1 / 3 / 9`. `DEEZER_FORMATS` lists them
|
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { publicApiList, showEpisodeType } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* One podcast episode (`episode.getData`). Carries `EPISODE_DIRECT_STREAM_URL`
|
|
4
|
+
* (a plain MP3 — no licence, no decryption) plus `MD5_ORIGIN` / `FILESIZE_MP3_*`
|
|
5
|
+
* / `TRACK_TOKEN` for the licensed stream.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getEpisode: (episodeId: string) => Promise<showEpisodeType>;
|
|
8
|
+
/**
|
|
9
|
+
* A page of a show's episodes, newest first — a thin view over `getShowInfo`'s
|
|
10
|
+
* `EPISODES` block.
|
|
11
|
+
*
|
|
12
|
+
* @param showId `SHOW_ID`
|
|
13
|
+
* @param nb page size (default 25)
|
|
14
|
+
* @param start offset
|
|
15
|
+
*/
|
|
16
|
+
export declare const getShowEpisodes: (showId: string, nb?: number, start?: number) => Promise<publicApiList<showEpisodeType>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getShowEpisodes = exports.getEpisode = void 0;
|
|
4
|
+
const request_1 = require("./request");
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
/**
|
|
7
|
+
* One podcast episode (`episode.getData`). Carries `EPISODE_DIRECT_STREAM_URL`
|
|
8
|
+
* (a plain MP3 — no licence, no decryption) plus `MD5_ORIGIN` / `FILESIZE_MP3_*`
|
|
9
|
+
* / `TRACK_TOKEN` for the licensed stream.
|
|
10
|
+
*/
|
|
11
|
+
const getEpisode = (episodeId) => (0, request_1.request)({ episode_id: episodeId }, 'episode.getData');
|
|
12
|
+
exports.getEpisode = getEpisode;
|
|
13
|
+
/**
|
|
14
|
+
* A page of a show's episodes, newest first — a thin view over `getShowInfo`'s
|
|
15
|
+
* `EPISODES` block.
|
|
16
|
+
*
|
|
17
|
+
* @param showId `SHOW_ID`
|
|
18
|
+
* @param nb page size (default 25)
|
|
19
|
+
* @param start offset
|
|
20
|
+
*/
|
|
21
|
+
const getShowEpisodes = async (showId, nb = 25, start = 0) => {
|
|
22
|
+
const { EPISODES } = await (0, api_1.getShowInfo)(showId, nb, start);
|
|
23
|
+
return { data: EPISODES.data, total: EPISODES.total };
|
|
24
|
+
};
|
|
25
|
+
exports.getShowEpisodes = getShowEpisodes;
|
package/dist/api/search.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultAlbum, searchResultArtist, searchResultPlaylist, searchResultTrack, suggestResult } from '../types';
|
|
1
|
+
import type { advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultAlbum, searchResultArtist, searchResultPlaylist, searchResultTrack, searchType, suggestResult } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Compose Deezer's advanced search operators into one query string.
|
|
4
4
|
*
|
|
@@ -47,3 +47,21 @@ export declare const searchPlaylists: (query: string, options?: Omit<publicApiSe
|
|
|
47
47
|
* @param nb max items per type (default 5)
|
|
48
48
|
*/
|
|
49
49
|
export declare const suggest: (query: string, nb?: number) => Promise<suggestResult>;
|
|
50
|
+
/** Per-type total hit counts + Deezer's own relevance ordering, from a `searchMusic` result. */
|
|
51
|
+
export interface SearchFacets {
|
|
52
|
+
track: number;
|
|
53
|
+
album: number;
|
|
54
|
+
artist: number;
|
|
55
|
+
playlist: number;
|
|
56
|
+
radio: number;
|
|
57
|
+
show: number;
|
|
58
|
+
user: number;
|
|
59
|
+
/** the type order Deezer considers most relevant for this query, e.g. `['TOP_RESULT','ARTIST','TRACK',…]` */
|
|
60
|
+
order: string[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pull the facet counts out of a `searchMusic` (`deezer.pageSearch`) result —
|
|
64
|
+
* `pageSearch` already returns `TRACK.total`, `ALBUM.total`, … and an `ORDER`;
|
|
65
|
+
* this just surfaces them in one flat object for "N tracks · M albums · …" UIs.
|
|
66
|
+
*/
|
|
67
|
+
export declare const searchFacets: (result: searchType) => SearchFacets;
|
package/dist/api/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.suggest = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.buildAdvancedQuery = void 0;
|
|
3
|
+
exports.searchFacets = exports.suggest = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.buildAdvancedQuery = void 0;
|
|
4
4
|
const request_1 = require("./request");
|
|
5
5
|
/**
|
|
6
6
|
* Compose Deezer's advanced search operators into one query string.
|
|
@@ -108,3 +108,22 @@ const suggest = (query, nb = 5) => (0, request_1.requestLight)({
|
|
|
108
108
|
TYPES: { ALBUM: true, ARTIST: true, TRACK: true, PLAYLIST: true, RADIO: true, SHOW: true },
|
|
109
109
|
}, 'deezer.suggest');
|
|
110
110
|
exports.suggest = suggest;
|
|
111
|
+
/**
|
|
112
|
+
* Pull the facet counts out of a `searchMusic` (`deezer.pageSearch`) result —
|
|
113
|
+
* `pageSearch` already returns `TRACK.total`, `ALBUM.total`, … and an `ORDER`;
|
|
114
|
+
* this just surfaces them in one flat object for "N tracks · M albums · …" UIs.
|
|
115
|
+
*/
|
|
116
|
+
const searchFacets = (result) => {
|
|
117
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
118
|
+
return ({
|
|
119
|
+
track: (_b = (_a = result.TRACK) === null || _a === void 0 ? void 0 : _a.total) !== null && _b !== void 0 ? _b : 0,
|
|
120
|
+
album: (_d = (_c = result.ALBUM) === null || _c === void 0 ? void 0 : _c.total) !== null && _d !== void 0 ? _d : 0,
|
|
121
|
+
artist: (_f = (_e = result.ARTIST) === null || _e === void 0 ? void 0 : _e.total) !== null && _f !== void 0 ? _f : 0,
|
|
122
|
+
playlist: (_h = (_g = result.PLAYLIST) === null || _g === void 0 ? void 0 : _g.total) !== null && _h !== void 0 ? _h : 0,
|
|
123
|
+
radio: (_k = (_j = result.RADIO) === null || _j === void 0 ? void 0 : _j.total) !== null && _k !== void 0 ? _k : 0,
|
|
124
|
+
show: (_m = (_l = result.SHOW) === null || _l === void 0 ? void 0 : _l.total) !== null && _m !== void 0 ? _m : 0,
|
|
125
|
+
user: (_p = (_o = result.USER) === null || _o === void 0 ? void 0 : _o.total) !== null && _p !== void 0 ? _p : 0,
|
|
126
|
+
order: (_q = result.ORDER) !== null && _q !== void 0 ? _q : [],
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
exports.searchFacets = searchFacets;
|
package/dist/lib/get-url.d.ts
CHANGED
|
@@ -65,3 +65,21 @@ export interface ResolvedUrl {
|
|
|
65
65
|
* @param session which account to resolve for — defaults to the process default
|
|
66
66
|
*/
|
|
67
67
|
export declare const resolveDownloadUrls: (tracks: trackType[], qualities?: Quality[], session?: Session) => Promise<(ResolvedUrl | null)[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Batch-refresh `TRACK_TOKEN`s that have (or are about to) expire — one
|
|
70
|
+
* `song.getListData` request for the lot, instead of a `getTrackInfo` per track.
|
|
71
|
+
* `TRACK_TOKEN`s live ~1 hour, so a token fetched at the start of a long
|
|
72
|
+
* playlist download is dead by track 40 and surfaces as an opaque CDN 403; run
|
|
73
|
+
* the selection through this first.
|
|
74
|
+
*
|
|
75
|
+
* Returns the input tracks with fresh `TRACK_TOKEN` / `TRACK_TOKEN_EXPIRE`
|
|
76
|
+
* merged in; tracks whose token is still comfortably valid are returned as-is.
|
|
77
|
+
*
|
|
78
|
+
* @param tracks from `getTrackInfo` / `getAlbumTracks` / `parseInfo`
|
|
79
|
+
* @param options `graceSeconds` — treat a token expiring within this window as
|
|
80
|
+
* stale (default 300); `session` — which account (default: process default)
|
|
81
|
+
*/
|
|
82
|
+
export declare const refreshTrackTokens: (tracks: trackType[], options?: {
|
|
83
|
+
graceSeconds?: number;
|
|
84
|
+
session?: Session;
|
|
85
|
+
}) => Promise<trackType[]>;
|
package/dist/lib/get-url.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.formatName = exports.toFormat = exports.DEEZER_FORMATS = exports.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
6
|
+
exports.refreshTrackTokens = exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.formatName = exports.toFormat = exports.DEEZER_FORMATS = exports.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
7
7
|
const delay_1 = __importDefault(require("delay"));
|
|
8
8
|
const decrypt_1 = require("../lib/decrypt");
|
|
9
9
|
const errors_1 = require("../lib/errors");
|
|
@@ -265,3 +265,35 @@ const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1], session = (0,
|
|
|
265
265
|
});
|
|
266
266
|
};
|
|
267
267
|
exports.resolveDownloadUrls = resolveDownloadUrls;
|
|
268
|
+
/**
|
|
269
|
+
* Batch-refresh `TRACK_TOKEN`s that have (or are about to) expire — one
|
|
270
|
+
* `song.getListData` request for the lot, instead of a `getTrackInfo` per track.
|
|
271
|
+
* `TRACK_TOKEN`s live ~1 hour, so a token fetched at the start of a long
|
|
272
|
+
* playlist download is dead by track 40 and surfaces as an opaque CDN 403; run
|
|
273
|
+
* the selection through this first.
|
|
274
|
+
*
|
|
275
|
+
* Returns the input tracks with fresh `TRACK_TOKEN` / `TRACK_TOKEN_EXPIRE`
|
|
276
|
+
* merged in; tracks whose token is still comfortably valid are returned as-is.
|
|
277
|
+
*
|
|
278
|
+
* @param tracks from `getTrackInfo` / `getAlbumTracks` / `parseInfo`
|
|
279
|
+
* @param options `graceSeconds` — treat a token expiring within this window as
|
|
280
|
+
* stale (default 300); `session` — which account (default: process default)
|
|
281
|
+
*/
|
|
282
|
+
const refreshTrackTokens = async (tracks, options = {}) => {
|
|
283
|
+
var _a, _b;
|
|
284
|
+
const session = (_a = options.session) !== null && _a !== void 0 ? _a : (0, session_1.defaultSession)();
|
|
285
|
+
const graceMs = ((_b = options.graceSeconds) !== null && _b !== void 0 ? _b : 300) * 1000;
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const staleIds = new Set(tracks
|
|
288
|
+
.filter((t) => !t.TRACK_TOKEN || !t.TRACK_TOKEN_EXPIRE || t.TRACK_TOKEN_EXPIRE * 1000 - now < graceMs)
|
|
289
|
+
.map((t) => t.SNG_ID));
|
|
290
|
+
if (!staleIds.size)
|
|
291
|
+
return tracks;
|
|
292
|
+
const { data } = await session.gw({ sng_ids: [...staleIds] }, 'song.getListData');
|
|
293
|
+
const fresh = new Map(data.map((d) => [d.SNG_ID, d]));
|
|
294
|
+
return tracks.map((track) => {
|
|
295
|
+
const f = fresh.get(track.SNG_ID);
|
|
296
|
+
return f ? { ...track, TRACK_TOKEN: f.TRACK_TOKEN, TRACK_TOKEN_EXPIRE: Number(f.TRACK_TOKEN_EXPIRE) } : track;
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
exports.refreshTrackTokens = refreshTrackTokens;
|
|
@@ -16,5 +16,7 @@ declare module './session' {
|
|
|
16
16
|
streamTrack(track: trackType, quality: number, options?: Omit<StreamTrackOptions, 'session'>): Promise<TrackStream>;
|
|
17
17
|
/** Download + decrypt a track fully into a `Buffer`, as this account. */
|
|
18
18
|
getTrackBuffer(track: trackType, quality: number, options?: Omit<StreamTrackOptions, 'session' | 'resumeFrom'>): Promise<Buffer | null>;
|
|
19
|
+
/** Batch-refresh the `TRACK_TOKEN`s on these tracks (as this account) before a long download. */
|
|
20
|
+
refreshTrackTokens(tracks: trackType[], graceSeconds?: number): Promise<trackType[]>;
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -21,3 +21,6 @@ session_1.Session.prototype.streamTrack = function (track, quality, options = {}
|
|
|
21
21
|
session_1.Session.prototype.getTrackBuffer = function (track, quality, options = {}) {
|
|
22
22
|
return (0, stream_download_1.downloadTrackBuffer)(track, quality, { ...options, session: this });
|
|
23
23
|
};
|
|
24
|
+
session_1.Session.prototype.refreshTrackTokens = function (tracks, graceSeconds) {
|
|
25
|
+
return (0, get_url_1.refreshTrackTokens)(tracks, { graceSeconds, session: this });
|
|
26
|
+
};
|