gerdur 2.4.0 → 2.6.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 CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.6.0 - 2026-08-31
4
+
5
+ ### Added
6
+
7
+ - **`gerdur-core@^2.6.0`** — re-exports the Flow / radios / library surface:
8
+ `getUserFlow`, `getUserFavoriteTracks` / `Albums` / `Artists`,
9
+ `getUserPlaylists`, `getUserRadios`, `getUserChartTracks`, `getRadios`,
10
+ `getRadioTracks`, `getRadioGenres` + their types.
11
+ - **`Session` methods**: `flow`, `favoriteTracks`, `favoriteAlbums`,
12
+ `favoriteArtists`, `playlists`, `userRadios`, `radios`, `radioTracks` — the
13
+ `userId` argument defaults to the logged-in user.
14
+ - **CLI `flow` / `flow:<userId>` / `radio:<id>`** — pick tracks from your (or
15
+ another user's) Flow, or a radio's current playlist, and download them.
16
+ Headless-friendly.
17
+
18
+ ## 2.5.0 - 2026-08-31
19
+
20
+ ### Changed
21
+
22
+ - **`gerdur-core@^2.5.0`.** Track decryption in the download pipeline is now
23
+ **streamed** to a temp file (`decryptFileToFile`) instead of read fully into
24
+ memory twice — peak memory during decrypt drops to ~one 2048-byte stripe.
25
+ - Gateway failures now surface as `DeezerError` (re-exported) with `code` /
26
+ `keys` / `retryable`, and gateway retries are bounded (no more infinite spin
27
+ on a persistently failing endpoint).
28
+
29
+ ### Added
30
+
31
+ - Re-exports: `streamTrackDownload`, `createDecryptStream`, `getStream`,
32
+ `DeezerError` + the `StreamTrackOptions` / `TrackStream` / `StreamResponse` types.
33
+ - **`Session.streamTrack(track, quality?, options?)`** — download a track as a
34
+ constant-memory stream of decrypted audio (`{stream, size, startedAt}`),
35
+ with `onProgress` and `resumeFrom`.
36
+
3
37
  ## 2.4.0 - 2026-08-31
4
38
 
5
39
  ### Added
package/README.md CHANGED
@@ -159,8 +159,11 @@ When `gerdur` asks for a URL, a search term, or a prefixed query:
159
159
  | `search:artist:"daft punk" bpm_min:120` | advanced track search (see operators below) |
160
160
  | `isrc:GBDUW0000059` | download the exact track for an ISRC |
161
161
  | `upc:0724384960650` | download the album for a UPC / EAN barcode |
162
+ | `flow` | pick tracks from your Deezer **Flow** |
163
+ | `flow:2064440442` | pick tracks from another user's Flow |
164
+ | `radio:38305` | pick tracks from a radio's current playlist |
162
165
 
163
- `isrc:` and `upc:` also work in `--headless` mode (`-u isrc:…` / `-u upc:…`).
166
+ `isrc:` / `upc:` / `flow` / `radio:` also work in `--headless` mode (`-u flow`, ).
164
167
 
165
168
  ### From flags (works headless)
166
169
 
@@ -258,13 +261,20 @@ Session methods:
258
261
  - **browse** — `genres`, `chart`, `chartTracks`, `editorialSections`,
259
262
  `artistTopTracks`, `relatedArtists`, `artistAlbums`, `artistRadio`,
260
263
  `trackByISRC`, `albumByUPC`
261
- - **user / download** — `getUser`, `getTrackBuffer`, `downloadTrack`,
262
- `downloadTracks`, `downloadUrl`, `trackPreview`, `downloadPreview`
264
+ - **flow / library** — `flow`, `favoriteTracks`, `favoriteAlbums`,
265
+ `favoriteArtists`, `playlists`, `userRadios`, `radios`, `radioTracks`
266
+ (the `userId` arg defaults to the logged-in user)
267
+ - **user / download** — `getUser`, `getTrackBuffer`, `streamTrack`,
268
+ `downloadTrack`, `downloadTracks`, `downloadUrl`, `trackPreview`, `downloadPreview`
263
269
 
264
270
  Every download call is silent; `downloadTracks` / `downloadUrl` accept
265
271
  `concurrency` and an `onProgress` callback and return one `{path, written} | null`
266
272
  per track.
267
273
 
274
+ `streamTrack(track, quality?, options?)` returns `{stream, size, startedAt,
275
+ isEncrypted}` — decrypted audio at constant memory, with `onProgress(got, total)`
276
+ and `resumeFrom` (bytes). Pipe `stream` to a file or your own tag muxer.
277
+
268
278
  `searchAdvanced(filters, options?)` builds a Deezer advanced-search query from
269
279
  `{query?, artist?, album?, track?, label?, durMin?, durMax?, bpmMin?, bpmMax?}`
270
280
  and takes `{order?, strict?, limit?, index?, fallback?}`. Deezer's operators are
@@ -315,8 +325,14 @@ or `createSession`):
315
325
  `getEditorialCharts`, `getArtistTopTracks`, `getRelatedArtists`,
316
326
  `getArtistAlbums`, `getArtistPlaylists`, `getArtistRadioTracks`,
317
327
  `getTrackByISRC`, `getAlbumByUPC`
318
- - **Download** — `getTrackDownloadUrl`, `resolveDownloadUrls`, `getTrackPreview`,
319
- `downloadPreview`, `formatName`, `toFormat`, `DEEZER_FORMATS`, `GeoBlocked`
328
+ - **Flow / library** — `getUserFlow`, `getUserFavoriteTracks`,
329
+ `getUserFavoriteAlbums`, `getUserFavoriteArtists`, `getUserPlaylists`,
330
+ `getUserRadios`, `getUserChartTracks`, `getRadios`, `getRadioTracks`,
331
+ `getRadioGenres`
332
+ - **Download** — `getTrackDownloadUrl`, `resolveDownloadUrls`,
333
+ `streamTrackDownload`, `createDecryptStream`, `getStream`, `getTrackPreview`,
334
+ `downloadPreview`, `formatName`, `toFormat`, `DEEZER_FORMATS`
335
+ - **Errors** — `DeezerError` (`code` / `keys` / `retryable`), `GeoBlocked`
320
336
 
321
337
  ### Auth & config helpers
322
338
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "The crucial streaming module for dabox systems.",
5
5
  "author": "Christian",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -48,7 +48,7 @@
48
48
  "adm-zip": "^0.5.16",
49
49
  "chalk": "^4.1.2",
50
50
  "commander": "^9.5.0",
51
- "gerdur-core": "^2.3.0",
51
+ "gerdur-core": "^2.6.0",
52
52
  "dot-prop": "^6.0.1",
53
53
  "got": "^11.8.6",
54
54
  "gradient-string": "^2.0.2",
@@ -111,18 +111,14 @@ const stampVersion = (t) => {
111
111
  * picks into download-ready gw tracks via `getTrackInfo`. Returns `null` when
112
112
  * nothing was matched or selected.
113
113
  */
114
- const resolveAdvancedSearch = async (filters) => {
115
- const limit = toFiniteNumber(options.searchLimit) ?? 50;
116
- const { data: results, query, usedFallback } = await (0, search_1.searchAdvancedTracks)(filters, { limit });
117
- if (!query) {
118
- return null;
119
- }
120
- console.log(signale_1.default.info(`Searching Deezer for: ${chalk_1.default.cyan(query)}`));
121
- if (usedFallback) {
122
- console.log(signale_1.default.note('No matches for the operators — retried as a plain-text search.'));
123
- }
114
+ /**
115
+ * Given a list of public-API track objects (from search / flow / a radio), let
116
+ * the user pick (unless headless) and hydrate the picks into download-ready gw
117
+ * tracks via `getTrackInfo`.
118
+ */
119
+ const pickAndHydrate = async (results, id) => {
124
120
  if (!results.length) {
125
- console.log(signale_1.default.warn('No tracks matched that search.'));
121
+ console.log(signale_1.default.warn('Nothing to download.'));
126
122
  return null;
127
123
  }
128
124
  let picks = results;
@@ -131,7 +127,7 @@ const resolveAdvancedSearch = async (filters) => {
131
127
  {
132
128
  type: 'multiselect',
133
129
  name: 'items',
134
- message: `Select tracks to download. ${results.length} matched.`,
130
+ message: `Select tracks to download. ${results.length} available.`,
135
131
  choices: results.map((r) => ({
136
132
  title: `${r.title} — ${r.artist?.name ?? 'Unknown'}`,
137
133
  value: r,
@@ -151,7 +147,19 @@ const resolveAdvancedSearch = async (filters) => {
151
147
  console.log(signale_1.default.warn('None of the selected tracks could be resolved.'));
152
148
  return null;
153
149
  }
154
- return { info: { type: 'track', id: query }, linktype: 'track', linkinfo: {}, tracks };
150
+ return { info: { type: 'track', id }, linktype: 'track', linkinfo: {}, tracks };
151
+ };
152
+ const resolveAdvancedSearch = async (filters) => {
153
+ const limit = toFiniteNumber(options.searchLimit) ?? 50;
154
+ const { data: results, query, usedFallback } = await (0, search_1.searchAdvancedTracks)(filters, { limit });
155
+ if (!query) {
156
+ return null;
157
+ }
158
+ console.log(signale_1.default.info(`Searching Deezer for: ${chalk_1.default.cyan(query)}`));
159
+ if (usedFallback) {
160
+ console.log(signale_1.default.note('No matches for the operators — retried as a plain-text search.'));
161
+ }
162
+ return pickAndHydrate(results, query);
155
163
  };
156
164
  /**
157
165
  * `--preview`: fetch the 30-second clip for each selected track and write it as
@@ -228,7 +236,7 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
228
236
  {
229
237
  type: 'text',
230
238
  name: 'query',
231
- message: 'Enter a URL, a search term, or a `search:` / `isrc:` / `upc:` query:',
239
+ message: 'Enter a URL, a search term, or `search:` / `isrc:` / `upc:` / `flow` / `radio:<id>`:',
232
240
  validate: (value) => (value ? true : false),
233
241
  },
234
242
  ], { onCancel });
@@ -260,6 +268,26 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
260
268
  console.log(signale_1.default.info(`UPC ${code} → ${found.title} (${found.nb_tracks} tracks)`));
261
269
  url = `https://www.deezer.com/album/${found.id}`;
262
270
  }
271
+ // `flow:` / `flow:<userId>` — download from Deezer Flow.
272
+ if (!searchData && url && (url === 'flow' || url.startsWith('flow:'))) {
273
+ const uid = url.includes(':') ? url.slice('flow:'.length).trim() : (await (0, gerdur_core_1.getUser)()).USER_ID;
274
+ console.log(signale_1.default.info(`Fetching Flow for user ${uid}…`));
275
+ const { data } = await (0, gerdur_core_1.getUserFlow)(uid);
276
+ searchData = await pickAndHydrate(data, `flow:${uid}`);
277
+ if (!searchData) {
278
+ throw new Error('Flow returned no tracks (is the profile public?).');
279
+ }
280
+ }
281
+ // `radio:<id>` — download a radio's current tracklist.
282
+ if (!searchData && url && url.startsWith('radio:')) {
283
+ const rid = url.slice('radio:'.length).trim();
284
+ console.log(signale_1.default.info(`Fetching radio ${rid}…`));
285
+ const { data } = await (0, gerdur_core_1.getRadioTracks)(rid);
286
+ searchData = await pickAndHydrate(data, `radio:${rid}`);
287
+ if (!searchData) {
288
+ throw new Error(`Radio ${rid} returned no tracks.`);
289
+ }
290
+ }
263
291
  if (!searchData && !url.match(urlRegex)) {
264
292
  if (options.headless) {
265
293
  throw new Error('Please provide a valid URL. Unknown URL: ' + url);
@@ -59,7 +59,7 @@ export type { SessionOptions, DownloadTracksOptions, SearchType } from './lib/se
59
59
  export { getTrackBuffer, getTaggedTrack, downloadTrackToFile } from './lib/api-download';
60
60
  export type { Quality, GetTrackBufferOptions, DownloadTrackOptions, DownloadResult } from './lib/api-download';
61
61
  export { default as Config, globalConfigPath, resolveConfigFile } from './lib/config';
62
- export { initDeezerApi, parseInfo, searchMusic, searchPublicApi, searchTracks, searchAlbums, searchArtists, searchPlaylists, buildAdvancedQuery, suggest, getGenres, getChart, getChartTracks, getGenreArtists, getEditorialList, getEditorialReleases, getEditorialSelection, getEditorialCharts, getArtistTopTracks, getRelatedArtists, getArtistAlbums, getArtistPlaylists, getArtistRadioTracks, getTrackByISRC, getAlbumByUPC, getTrackPreview, downloadPreview, formatName, toFormat, DEEZER_FORMATS, getUser, getTrackInfo, getAlbumInfo, getAlbumTracks, getPlaylistInfo, getPlaylistTracks, getArtistInfo, getDiscography, getLyrics, getTrackDownloadUrl, resolveDownloadUrls, addTrackTags, getRichAlbum, normalizeContributors, toLrc, GeoBlocked, } from 'gerdur-core';
62
+ export { initDeezerApi, parseInfo, searchMusic, searchPublicApi, searchTracks, searchAlbums, searchArtists, searchPlaylists, buildAdvancedQuery, suggest, getGenres, getChart, getChartTracks, getGenreArtists, getEditorialList, getEditorialReleases, getEditorialSelection, getEditorialCharts, getArtistTopTracks, getRelatedArtists, getArtistAlbums, getArtistPlaylists, getArtistRadioTracks, getUserFlow, getUserFavoriteTracks, getUserFavoriteAlbums, getUserFavoriteArtists, getUserPlaylists, getUserRadios, getUserChartTracks, getRadios, getRadioTracks, getRadioGenres, getTrackByISRC, getAlbumByUPC, getTrackPreview, downloadPreview, formatName, toFormat, DEEZER_FORMATS, getUser, getTrackInfo, getAlbumInfo, getAlbumTracks, getPlaylistInfo, getPlaylistTracks, getArtistInfo, getDiscography, getLyrics, getTrackDownloadUrl, resolveDownloadUrls, streamTrackDownload, createDecryptStream, getStream, addTrackTags, getRichAlbum, normalizeContributors, toLrc, GeoBlocked, DeezerError, } from 'gerdur-core';
63
63
  export type { AddTrackTagsOptions, TaggedTrack, TrackTagModel, RichAlbum, ResolvedUrl, NormalizedContributors, } from 'gerdur-core';
64
- export type { advancedSearchFilters, searchOrder, searchEntity, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack, searchResultAlbum, searchResultArtist, searchResultPlaylist, suggestResult, publicApiList, chartType, chartTrack, chartAlbum, chartArtist, chartPlaylist, chartPodcast, genreType, editorialType, artistAlbumResult, } from 'gerdur-core/types';
65
- export type { Quality as MediaFormat, DeezerFormat, TrackPreview } from 'gerdur-core';
64
+ export type { advancedSearchFilters, searchOrder, searchEntity, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack, searchResultAlbum, searchResultArtist, searchResultPlaylist, suggestResult, publicApiList, chartType, chartTrack, chartAlbum, chartArtist, chartPlaylist, chartPodcast, genreType, editorialType, artistAlbumResult, userFavoriteTrack, userFavoriteAlbum, userFavoriteArtist, userPlaylistResult, radioResult, radioGenre, } from 'gerdur-core/types';
65
+ export type { Quality as MediaFormat, DeezerFormat, TrackPreview, StreamTrackOptions, TrackStream, StreamResponse, DeezerErrorPayload, } from 'gerdur-core';
package/dist/src/index.js CHANGED
@@ -33,8 +33,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
33
33
  return (mod && mod.__esModule) ? mod : { "default": mod };
34
34
  };
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.getLyrics = exports.getDiscography = exports.getArtistInfo = exports.getPlaylistTracks = exports.getPlaylistInfo = exports.getAlbumTracks = exports.getAlbumInfo = exports.getTrackInfo = exports.getUser = exports.DEEZER_FORMATS = exports.toFormat = exports.formatName = exports.downloadPreview = exports.getTrackPreview = exports.getAlbumByUPC = exports.getTrackByISRC = exports.getArtistRadioTracks = exports.getArtistPlaylists = exports.getArtistAlbums = exports.getRelatedArtists = exports.getArtistTopTracks = exports.getEditorialCharts = exports.getEditorialSelection = exports.getEditorialReleases = exports.getEditorialList = exports.getGenreArtists = exports.getChartTracks = exports.getChart = exports.getGenres = exports.suggest = exports.buildAdvancedQuery = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.searchMusic = exports.parseInfo = exports.initDeezerApi = exports.resolveConfigFile = exports.globalConfigPath = exports.Config = exports.downloadTrackToFile = exports.getTaggedTrack = exports.getTrackBuffer = exports.Session = exports.createSession = exports.getArl = exports.LoginError = exports.loginWithEmail = void 0;
37
- exports.GeoBlocked = exports.toLrc = exports.normalizeContributors = exports.getRichAlbum = exports.addTrackTags = exports.resolveDownloadUrls = exports.getTrackDownloadUrl = void 0;
36
+ exports.toFormat = exports.formatName = exports.downloadPreview = exports.getTrackPreview = exports.getAlbumByUPC = exports.getTrackByISRC = exports.getRadioGenres = exports.getRadioTracks = exports.getRadios = exports.getUserChartTracks = exports.getUserRadios = exports.getUserPlaylists = exports.getUserFavoriteArtists = exports.getUserFavoriteAlbums = exports.getUserFavoriteTracks = exports.getUserFlow = exports.getArtistRadioTracks = exports.getArtistPlaylists = exports.getArtistAlbums = exports.getRelatedArtists = exports.getArtistTopTracks = exports.getEditorialCharts = exports.getEditorialSelection = exports.getEditorialReleases = exports.getEditorialList = exports.getGenreArtists = exports.getChartTracks = exports.getChart = exports.getGenres = exports.suggest = exports.buildAdvancedQuery = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.searchMusic = exports.parseInfo = exports.initDeezerApi = exports.resolveConfigFile = exports.globalConfigPath = exports.Config = exports.downloadTrackToFile = exports.getTaggedTrack = exports.getTrackBuffer = exports.Session = exports.createSession = exports.getArl = exports.LoginError = exports.loginWithEmail = void 0;
37
+ exports.DeezerError = exports.GeoBlocked = exports.toLrc = exports.normalizeContributors = exports.getRichAlbum = exports.addTrackTags = exports.getStream = exports.createDecryptStream = exports.streamTrackDownload = exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.getLyrics = exports.getDiscography = exports.getArtistInfo = exports.getPlaylistTracks = exports.getPlaylistInfo = exports.getAlbumTracks = exports.getAlbumInfo = exports.getTrackInfo = exports.getUser = exports.DEEZER_FORMATS = void 0;
38
38
  // ─── Authentication ────────────────────────────────────────────────────────
39
39
  const email_login_1 = require("./lib/email-login");
40
40
  Object.defineProperty(exports, "loginWithEmail", { enumerable: true, get: function () { return email_login_1.loginWithEmail; } });
@@ -110,6 +110,16 @@ Object.defineProperty(exports, "getRelatedArtists", { enumerable: true, get: fun
110
110
  Object.defineProperty(exports, "getArtistAlbums", { enumerable: true, get: function () { return gerdur_core_1.getArtistAlbums; } });
111
111
  Object.defineProperty(exports, "getArtistPlaylists", { enumerable: true, get: function () { return gerdur_core_1.getArtistPlaylists; } });
112
112
  Object.defineProperty(exports, "getArtistRadioTracks", { enumerable: true, get: function () { return gerdur_core_1.getArtistRadioTracks; } });
113
+ Object.defineProperty(exports, "getUserFlow", { enumerable: true, get: function () { return gerdur_core_1.getUserFlow; } });
114
+ Object.defineProperty(exports, "getUserFavoriteTracks", { enumerable: true, get: function () { return gerdur_core_1.getUserFavoriteTracks; } });
115
+ Object.defineProperty(exports, "getUserFavoriteAlbums", { enumerable: true, get: function () { return gerdur_core_1.getUserFavoriteAlbums; } });
116
+ Object.defineProperty(exports, "getUserFavoriteArtists", { enumerable: true, get: function () { return gerdur_core_1.getUserFavoriteArtists; } });
117
+ Object.defineProperty(exports, "getUserPlaylists", { enumerable: true, get: function () { return gerdur_core_1.getUserPlaylists; } });
118
+ Object.defineProperty(exports, "getUserRadios", { enumerable: true, get: function () { return gerdur_core_1.getUserRadios; } });
119
+ Object.defineProperty(exports, "getUserChartTracks", { enumerable: true, get: function () { return gerdur_core_1.getUserChartTracks; } });
120
+ Object.defineProperty(exports, "getRadios", { enumerable: true, get: function () { return gerdur_core_1.getRadios; } });
121
+ Object.defineProperty(exports, "getRadioTracks", { enumerable: true, get: function () { return gerdur_core_1.getRadioTracks; } });
122
+ Object.defineProperty(exports, "getRadioGenres", { enumerable: true, get: function () { return gerdur_core_1.getRadioGenres; } });
113
123
  Object.defineProperty(exports, "getTrackByISRC", { enumerable: true, get: function () { return gerdur_core_1.getTrackByISRC; } });
114
124
  Object.defineProperty(exports, "getAlbumByUPC", { enumerable: true, get: function () { return gerdur_core_1.getAlbumByUPC; } });
115
125
  Object.defineProperty(exports, "getTrackPreview", { enumerable: true, get: function () { return gerdur_core_1.getTrackPreview; } });
@@ -128,8 +138,12 @@ Object.defineProperty(exports, "getDiscography", { enumerable: true, get: functi
128
138
  Object.defineProperty(exports, "getLyrics", { enumerable: true, get: function () { return gerdur_core_1.getLyrics; } });
129
139
  Object.defineProperty(exports, "getTrackDownloadUrl", { enumerable: true, get: function () { return gerdur_core_1.getTrackDownloadUrl; } });
130
140
  Object.defineProperty(exports, "resolveDownloadUrls", { enumerable: true, get: function () { return gerdur_core_1.resolveDownloadUrls; } });
141
+ Object.defineProperty(exports, "streamTrackDownload", { enumerable: true, get: function () { return gerdur_core_1.streamTrackDownload; } });
142
+ Object.defineProperty(exports, "createDecryptStream", { enumerable: true, get: function () { return gerdur_core_1.createDecryptStream; } });
143
+ Object.defineProperty(exports, "getStream", { enumerable: true, get: function () { return gerdur_core_1.getStream; } });
131
144
  Object.defineProperty(exports, "addTrackTags", { enumerable: true, get: function () { return gerdur_core_1.addTrackTags; } });
132
145
  Object.defineProperty(exports, "getRichAlbum", { enumerable: true, get: function () { return gerdur_core_1.getRichAlbum; } });
133
146
  Object.defineProperty(exports, "normalizeContributors", { enumerable: true, get: function () { return gerdur_core_1.normalizeContributors; } });
134
147
  Object.defineProperty(exports, "toLrc", { enumerable: true, get: function () { return gerdur_core_1.toLrc; } });
135
148
  Object.defineProperty(exports, "GeoBlocked", { enumerable: true, get: function () { return gerdur_core_1.GeoBlocked; } });
149
+ Object.defineProperty(exports, "DeezerError", { enumerable: true, get: function () { return gerdur_core_1.DeezerError; } });
@@ -1,4 +1,9 @@
1
1
  /// <reference types="node" />
2
- export { decryptDownload, TrackDecryptStream } from 'gerdur-core';
3
- /** Decrypt a track that was streamed to a temp file. */
2
+ export { decryptDownload, createDecryptStream, TrackDecryptStream } from 'gerdur-core';
3
+ /** Decrypt a track that was streamed to a temp file (buffered — reads the file into memory). */
4
4
  export declare const decryptDownloadFile: (tmpfile: string, trackId: string) => Buffer;
5
+ /**
6
+ * Decrypt `src` → `dest` as a stream — peak memory is ~one 2048-byte stripe,
7
+ * not the whole file twice. Use this over `decryptDownloadFile` for large FLACs.
8
+ */
9
+ export declare const decryptFileToFile: (src: string, dest: string, trackId: string) => Promise<void>;
@@ -1,17 +1,30 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.decryptDownloadFile = exports.TrackDecryptStream = exports.decryptDownload = void 0;
6
+ exports.decryptFileToFile = exports.decryptDownloadFile = exports.TrackDecryptStream = exports.createDecryptStream = exports.decryptDownload = void 0;
4
7
  /**
5
8
  * Track decryption. As of gerdur-core@1.0.2 the core `decryptDownload` is a
6
9
  * correct, dependency-free Blowfish that works on every Node (it no longer
7
10
  * touches OpenSSL's removed `bf-cbc`), at ~290 MiB/s — so there is nothing left
8
11
  * to wrap or fall back to here.
9
12
  */
13
+ const stream_1 = __importDefault(require("stream"));
14
+ const util_1 = require("util");
10
15
  const fs_1 = require("fs");
11
16
  const gerdur_core_1 = require("gerdur-core");
12
17
  var gerdur_core_2 = require("gerdur-core");
13
18
  Object.defineProperty(exports, "decryptDownload", { enumerable: true, get: function () { return gerdur_core_2.decryptDownload; } });
19
+ Object.defineProperty(exports, "createDecryptStream", { enumerable: true, get: function () { return gerdur_core_2.createDecryptStream; } });
14
20
  Object.defineProperty(exports, "TrackDecryptStream", { enumerable: true, get: function () { return gerdur_core_2.TrackDecryptStream; } });
15
- /** Decrypt a track that was streamed to a temp file. */
21
+ const pipeline = (0, util_1.promisify)(stream_1.default.pipeline);
22
+ /** Decrypt a track that was streamed to a temp file (buffered — reads the file into memory). */
16
23
  const decryptDownloadFile = (tmpfile, trackId) => (0, gerdur_core_1.decryptDownload)((0, fs_1.readFileSync)(tmpfile), trackId);
17
24
  exports.decryptDownloadFile = decryptDownloadFile;
25
+ /**
26
+ * Decrypt `src` → `dest` as a stream — peak memory is ~one 2048-byte stripe,
27
+ * not the whole file twice. Use this over `decryptDownloadFile` for large FLACs.
28
+ */
29
+ const decryptFileToFile = (src, dest, trackId) => pipeline((0, fs_1.createReadStream)(src), (0, gerdur_core_1.createDecryptStream)(trackId), (0, fs_1.createWriteStream)(dest));
30
+ exports.decryptFileToFile = decryptFileToFile;
@@ -145,7 +145,10 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
145
145
  let outFile;
146
146
  if (trackData.isEncrypted) {
147
147
  (0, log_update_1.default)(signale_1.default.pending('Decrypting ' + track.SNG_TITLE + ' by ' + track.ART_NAME));
148
- outFile = await (0, decrypt_1.decryptDownloadFile)(tmpfile, track.SNG_ID);
148
+ const decfile = tmpfile + '.dec';
149
+ await (0, decrypt_1.decryptFileToFile)(tmpfile, decfile, track.SNG_ID); // streamed — ~one stripe in memory
150
+ outFile = (0, fs_1.readFileSync)(decfile);
151
+ (0, fs_1.unlinkSync)(decfile);
149
152
  }
150
153
  else {
151
154
  outFile = (0, fs_1.readFileSync)(tmpfile);
@@ -2,8 +2,8 @@
2
2
  import { parseInfo, searchMusic } from 'gerdur-core';
3
3
  import type { AdvancedSearchOptions } from './search';
4
4
  import type { Quality, DownloadTrackOptions, DownloadResult } from './api-download';
5
- import type { trackType, userType, advancedSearchFilters, publicApiSearchResponse, searchResultTrack, suggestResult, chartType, genreType, editorialType, artistAlbumResult, searchResultArtist, publicApiList, trackTypePublicApi, albumTypePublicApi } from 'gerdur-core/types';
6
- import type { TrackPreview } from 'gerdur-core';
5
+ import type { trackType, userType, advancedSearchFilters, publicApiSearchResponse, searchResultTrack, suggestResult, chartType, genreType, editorialType, artistAlbumResult, searchResultArtist, publicApiList, trackTypePublicApi, albumTypePublicApi, userFavoriteTrack, userFavoriteAlbum, userFavoriteArtist, userPlaylistResult, radioResult } from 'gerdur-core/types';
6
+ import type { TrackPreview, StreamTrackOptions, TrackStream } from 'gerdur-core';
7
7
  /** Search result categories accepted by {@link Session.search}. */
8
8
  export type SearchType = 'ALBUM' | 'ARTIST' | 'TRACK' | 'PLAYLIST' | 'RADIO' | 'SHOW' | 'USER' | 'LIVESTREAM' | 'CHANNEL';
9
9
  export interface SessionOptions {
@@ -39,7 +39,10 @@ export declare class Session {
39
39
  /** The arl in use for this session. */
40
40
  readonly arl: string;
41
41
  private concurrency;
42
+ private userId?;
42
43
  private constructor();
44
+ /** The logged-in user's numeric id (cached). Used by the no-arg `flow` / `favorite*` methods. */
45
+ private resolveUserId;
43
46
  /**
44
47
  * Log in and return a ready-to-use session. Accepts either an `arl` or
45
48
  * `email`+`password` (which are exchanged for an arl via Deezer's OAuth flow).
@@ -86,12 +89,35 @@ export declare class Session {
86
89
  trackByISRC(isrc: string): Promise<trackTypePublicApi>;
87
90
  /** Resolve a UPC/EAN barcode to the public-API album (with its `tracks`). */
88
91
  albumByUPC(upc: string): Promise<albumTypePublicApi>;
92
+ /** Deezer **Flow** — the endless personalised mix, as tracks. */
93
+ flow(userId?: string | number, limit?: number): Promise<publicApiList<searchResultTrack>>;
94
+ /** A user's favourite (loved) tracks, newest first (each with `time_add`). */
95
+ favoriteTracks(userId?: string | number, limit?: number): Promise<publicApiList<userFavoriteTrack>>;
96
+ /** A user's favourite albums. */
97
+ favoriteAlbums(userId?: string | number, limit?: number): Promise<publicApiList<userFavoriteAlbum>>;
98
+ /** A user's favourite artists. */
99
+ favoriteArtists(userId?: string | number, limit?: number): Promise<publicApiList<userFavoriteArtist>>;
100
+ /** A user's own + followed playlists. */
101
+ playlists(userId?: string | number, limit?: number): Promise<publicApiList<userPlaylistResult>>;
102
+ /** The radios a user has favourited. */
103
+ userRadios(userId?: string | number): Promise<publicApiList<radioResult>>;
104
+ /** Deezer's curated radio list. */
105
+ radios(): Promise<publicApiList<radioResult>>;
106
+ /** A radio's current tracklist — a ready-to-play (public-API) source. */
107
+ radioTracks(radioId: string | number): Promise<publicApiList<searchResultTrack>>;
89
108
  /** The 30-second preview clip URL for a track (plain MP3 — no licence, no decryption). */
90
109
  trackPreview(track: trackType | string | number): Promise<TrackPreview | null>;
91
110
  /** Fetch a track's 30-second preview clip as a `Buffer` (plain MP3). */
92
111
  downloadPreview(track: trackType | string | number): Promise<Buffer | null>;
93
112
  /** Return a fully tagged audio Buffer for a track (nothing written to disk). */
94
113
  getTrackBuffer(track: trackType, quality?: Quality, options?: {}): Promise<Buffer | null>;
114
+ /**
115
+ * Download a track as a **stream** of decrypted audio — constant memory,
116
+ * regardless of file size or concurrency. `{stream, size, startedAt,
117
+ * isEncrypted}`; pipe `stream` to a file or your own tag muxer.
118
+ * `options.onProgress(received, total)`, `options.resumeFrom` (bytes).
119
+ */
120
+ streamTrack(track: trackType, quality?: 1 | 3 | 9, options?: StreamTrackOptions): Promise<TrackStream>;
95
121
  /** Download a single track to disk. */
96
122
  downloadTrack(track: trackType, quality?: Quality, options?: DownloadTrackOptions): Promise<DownloadResult | null>;
97
123
  /**
@@ -18,10 +18,21 @@ class Session {
18
18
  /** The arl in use for this session. */
19
19
  arl;
20
20
  concurrency;
21
+ userId;
21
22
  constructor(arl, concurrency) {
22
23
  this.arl = arl;
23
24
  this.concurrency = concurrency;
24
25
  }
26
+ /** The logged-in user's numeric id (cached). Used by the no-arg `flow` / `favorite*` methods. */
27
+ async resolveUserId(explicit) {
28
+ if (explicit !== undefined) {
29
+ return String(explicit);
30
+ }
31
+ if (!this.userId) {
32
+ this.userId = (await (0, gerdur_core_1.getUser)()).USER_ID;
33
+ }
34
+ return this.userId;
35
+ }
25
36
  /**
26
37
  * Log in and return a ready-to-use session. Accepts either an `arl` or
27
38
  * `email`+`password` (which are exchanged for an arl via Deezer's OAuth flow).
@@ -116,6 +127,41 @@ class Session {
116
127
  albumByUPC(upc) {
117
128
  return (0, gerdur_core_1.getAlbumByUPC)(upc);
118
129
  }
130
+ // ─── Flow, radios & library (public REST) ──────────────────────────────────
131
+ // `userId` defaults to the logged-in user (via `getUser`); their profile must
132
+ // be public for these to see it.
133
+ /** Deezer **Flow** — the endless personalised mix, as tracks. */
134
+ async flow(userId, limit) {
135
+ return (0, gerdur_core_1.getUserFlow)(await this.resolveUserId(userId), limit);
136
+ }
137
+ /** A user's favourite (loved) tracks, newest first (each with `time_add`). */
138
+ async favoriteTracks(userId, limit) {
139
+ return (0, gerdur_core_1.getUserFavoriteTracks)(await this.resolveUserId(userId), limit);
140
+ }
141
+ /** A user's favourite albums. */
142
+ async favoriteAlbums(userId, limit) {
143
+ return (0, gerdur_core_1.getUserFavoriteAlbums)(await this.resolveUserId(userId), limit);
144
+ }
145
+ /** A user's favourite artists. */
146
+ async favoriteArtists(userId, limit) {
147
+ return (0, gerdur_core_1.getUserFavoriteArtists)(await this.resolveUserId(userId), limit);
148
+ }
149
+ /** A user's own + followed playlists. */
150
+ async playlists(userId, limit) {
151
+ return (0, gerdur_core_1.getUserPlaylists)(await this.resolveUserId(userId), limit);
152
+ }
153
+ /** The radios a user has favourited. */
154
+ async userRadios(userId) {
155
+ return (0, gerdur_core_1.getUserRadios)(await this.resolveUserId(userId));
156
+ }
157
+ /** Deezer's curated radio list. */
158
+ radios() {
159
+ return (0, gerdur_core_1.getRadios)();
160
+ }
161
+ /** A radio's current tracklist — a ready-to-play (public-API) source. */
162
+ radioTracks(radioId) {
163
+ return (0, gerdur_core_1.getRadioTracks)(radioId);
164
+ }
119
165
  /** The 30-second preview clip URL for a track (plain MP3 — no licence, no decryption). */
120
166
  trackPreview(track) {
121
167
  return (0, gerdur_core_1.getTrackPreview)(track);
@@ -128,6 +174,15 @@ class Session {
128
174
  getTrackBuffer(track, quality = '320', options = {}) {
129
175
  return (0, api_download_1.getTrackBuffer)(track, quality, options);
130
176
  }
177
+ /**
178
+ * Download a track as a **stream** of decrypted audio — constant memory,
179
+ * regardless of file size or concurrency. `{stream, size, startedAt,
180
+ * isEncrypted}`; pipe `stream` to a file or your own tag muxer.
181
+ * `options.onProgress(received, total)`, `options.resumeFrom` (bytes).
182
+ */
183
+ streamTrack(track, quality = 3, options = {}) {
184
+ return (0, gerdur_core_1.streamTrackDownload)(track, quality, options);
185
+ }
131
186
  /** Download a single track to disk. */
132
187
  downloadTrack(track, quality = '320', options = {}) {
133
188
  return (0, api_download_1.downloadTrackToFile)(track, quality, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "The crucial streaming module for dabox systems.",
5
5
  "author": "Christian",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -48,7 +48,7 @@
48
48
  "adm-zip": "^0.5.16",
49
49
  "chalk": "^4.1.2",
50
50
  "commander": "^9.5.0",
51
- "gerdur-core": "^2.3.0",
51
+ "gerdur-core": "^2.6.0",
52
52
  "dot-prop": "^6.0.1",
53
53
  "got": "^11.8.6",
54
54
  "gradient-string": "^2.0.2",