gerdur-core 2.1.0 → 2.3.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,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.0 - 2026-08-31
4
+
5
+ Phase 2.1 — all the formats, and previews. Additive; the `1 / 3 / 9` path is
6
+ unchanged.
7
+
8
+ ### Added
9
+
10
+ - **`getTrackPreview(track)` / `downloadPreview(track)`** — the 30-second preview
11
+ clip. A plain MP3: no licence, no `arl`, no Blowfish. Accepts a gw track (reads
12
+ `MEDIA`, no extra request), a track id, or a number.
13
+ - **`DEEZER_FORMATS`** — every format `get_url` understands, best → worst
14
+ (`FLAC`, `MP3_320`, `MP3_256`, `MP3_128`, `MP3_64`, `AAC_64`, `MP4_RA3/2/1`).
15
+ - **`resolveDownloadUrls(tracks, qualities)`** now accepts format strings in the
16
+ preference list (`['FLAC', 'MP3_320', 'AAC_64']`), not just `1 | 3 | 9`. Each
17
+ `ResolvedUrl` gains `cipher` (`'BF_CBC_STRIPE'` | `'NONE'`).
18
+ - **`toFormat(quality)`** — normalise a number or format string to the `get_url`
19
+ format string. `formatName` now also accepts format strings (identity).
20
+ - `Quality` / `DeezerFormat` exported types.
21
+
22
+ ### Changed
23
+
24
+ - `isEncrypted` (from `getTrackDownloadUrl` and `resolveDownloadUrls`) is now
25
+ taken from the media API's `cipher` field instead of guessing from the URL
26
+ path — authoritative, and correct for `cipher: NONE` content.
27
+ - `trackType.FILESIZE_*` fields are typed `string` (several were mistyped as the
28
+ literal `'0'`); added `FILESIZE_MP3_MISC` / `FILESIZE_MHM1_RA*`.
29
+
30
+ ## 2.2.0 - 2026-08-31
31
+
32
+ Phase 2.4 — browse & discovery. All additive, all on the public REST API (no
33
+ `arl` needed), all memoised.
34
+
35
+ ### Added
36
+
37
+ - **Charts / editorial**: `getGenres`, `getChart(genreId, limit)` (the five
38
+ ranked lists), `getChartTracks`, `getGenreArtists`, `getEditorialList`,
39
+ `getEditorialReleases`, `getEditorialSelection`, `getEditorialCharts`.
40
+ - **Artist discovery**: `getArtistTopTracks`, `getRelatedArtists`,
41
+ `getArtistAlbums`, `getArtistPlaylists`, `getArtistRadioTracks`.
42
+ - **ISRC / UPC resolution**: `getTrackByISRC(isrc)` and `getAlbumByUPC(upc)` —
43
+ raw public-API track/album (with `bpm`, `gain`, `preview`, embedded `tracks`).
44
+ Complements the converter's `isrc2deezer` / `upc2deezer`, which hydrate a gw
45
+ track instead.
46
+ - New exported types: `chartType`, `chartTrack`/`chartAlbum`/`chartArtist`/
47
+ `chartPlaylist`/`chartPodcast`, `genreType`, `editorialType`,
48
+ `artistAlbumResult`, `publicApiList<T>`.
49
+
3
50
  ## 2.1.0 - 2026-08-31
4
51
 
5
52
  Phase 2.5 — search, properly. All additive.
package/README.md CHANGED
@@ -196,6 +196,41 @@ ignore the operators, so pass a plain string there.
196
196
  "as you type" UIs. `nb` (default 5) caps items per type. Needs an initialised
197
197
  session (`initDeezerApi`).
198
198
 
199
+ ### Browse & discovery
200
+
201
+ Public REST endpoints — no `arl` needed, memoised like the rest. All return a
202
+ `{data, total?, next?}` list unless noted.
203
+
204
+ | Method | Returns |
205
+ | :--- | :--- |
206
+ | `getGenres()` | Deezer's genre list (`id` `0` = "All"). |
207
+ | `getChart(genreId = 0, limit = 10)` | `{tracks, albums, artists, playlists, podcasts}` — the ranked lists for a genre. |
208
+ | `getChartTracks(genreId = 0, limit = 100, index = 0)` | just the track chart, each with a `position`. |
209
+ | `getGenreArtists(genreId)` | artists filed under a genre. |
210
+ | `getEditorialList()` | Deezer's editorial sections. |
211
+ | `getEditorialReleases(editorialId = 0, limit = 25, index = 0)` | new releases for a section. |
212
+ | `getEditorialSelection(editorialId = 0)` | albums the editors are pushing. |
213
+ | `getEditorialCharts(editorialId = 0)` | a section's charts (same 5-list shape as `getChart`). |
214
+ | `getArtistTopTracks(artistId, limit = 50)` | an artist's most popular tracks. |
215
+ | `getRelatedArtists(artistId, limit = 20)` | similar / related artists. |
216
+ | `getArtistAlbums(artistId, limit = 50, index = 0)` | the artist's discography. |
217
+ | `getArtistPlaylists(artistId, limit = 25)` | playlists featuring the artist. |
218
+ | `getArtistRadioTracks(artistId)` | a ready-made radio seeded from the artist. |
219
+ | `getTrackByISRC(isrc)` | the public-API track for an ISRC (`bpm`, `gain`, `preview`, …). |
220
+ | `getAlbumByUPC(upc)` | the public-API album (with its `tracks`) for a UPC/EAN barcode. |
221
+
222
+ ```js
223
+ const {data: genres} = await getGenres();
224
+ const rock = genres.find((g) => g.name === 'Rock');
225
+ const {tracks} = await getChart(rock.id, 20); // this week's rock chart
226
+ const similar = await getRelatedArtists(27); // artists like Daft Punk
227
+ const track = await getTrackByISRC('USUM71311296'); // "Get Lucky"
228
+ ```
229
+
230
+ `getTrackByISRC` / `getAlbumByUPC` return raw public-API objects. To download,
231
+ pass the `id` to `getTrackInfo` / `getAlbumTracks` (or use the converter's
232
+ `isrc2deezer` / `upc2deezer`, which hydrate a gw track for you).
233
+
199
234
  ### `.getTrackDownloadUrl(track, quality);`
200
235
 
201
236
  | Parameters | Required | Type | Description |
@@ -203,6 +238,37 @@ session (`initDeezerApi`).
203
238
  | `track` | Yes | `string` | track object |
204
239
  | `quality` | Yes | `1, 3 or 9` | 1 = 128kbps, 3 = 320kbps, 9 = flac |
205
240
 
241
+ Resolves `{trackUrl, isEncrypted, fileSize}`. `isEncrypted` now comes from the
242
+ media API's `cipher` field (authoritative) rather than a URL guess.
243
+
244
+ ### Formats
245
+
246
+ Deezer's `get_url` understands more than `1 / 3 / 9`. `DEEZER_FORMATS` lists them
247
+ best → worst: `FLAC`, `MP3_320`, `MP3_256`, `MP3_128`, `MP3_64`, `AAC_64`,
248
+ `MP4_RA3`, `MP4_RA2`, `MP4_RA1` (the last four are the HE-AAC ladder some
249
+ accounts / regions expose).
250
+
251
+ - **`resolveDownloadUrls(tracks, qualities)`** — `qualities` entries may be the
252
+ `1 | 3 | 9` shorthand **or** any format string, e.g.
253
+ `resolveDownloadUrls(tracks, ['FLAC', 'MP3_320', 'AAC_64'])`. Deezer returns
254
+ the best each track is licensed for. Each result now also carries `format` and
255
+ `cipher` (`'BF_CBC_STRIPE'` or `'NONE'`).
256
+ - **`formatName(quality)`** / **`toFormat(quality)`** — normalise a number or
257
+ format string to the `get_url` format string.
258
+
259
+ ### `.getTrackPreview(track)` / `.downloadPreview(track)`
260
+
261
+ The 30-second preview clip — a plain MP3, **no licence, no `arl`, no
262
+ encryption**. `track` may be a gw track object (reads its `MEDIA`, no extra
263
+ request), a track id, or a number.
264
+
265
+ ```js
266
+ const {url} = await getTrackPreview('3135556'); // {url, duration: 30}
267
+ const clip = await downloadPreview('3135556'); // Buffer (ID3-tagged MP3)
268
+ ```
269
+
270
+ Useful for "audition before download" and for CI that shouldn't pull full tracks.
271
+
206
272
  ### `.decryptDownload(data, song_id);`
207
273
 
208
274
  | Parameters | Required | Type | Description |
@@ -0,0 +1,49 @@
1
+ import type { albumTypePublicApi, artistAlbumResult, chartType, editorialType, genreType, publicApiList, searchResultArtist, searchResultPlaylist, searchResultTrack, trackTypePublicApi } from '../types';
2
+ /**
3
+ * The five ranked lists Deezer publishes for a genre: `tracks`, `albums`,
4
+ * `artists`, `playlists`, `podcasts`. `genreId` `0` (the default) is "all
5
+ * genres"; other ids come from {@link getGenres}.
6
+ */
7
+ export declare const getChart: (genreId?: number | string, limit?: number) => Promise<chartType>;
8
+ /** Just the track chart for a genre (`0` = all). Handy as a ready-to-play list. */
9
+ export declare const getChartTracks: (genreId?: number | string, limit?: number, index?: number) => Promise<publicApiList<searchResultTrack & {
10
+ position: number;
11
+ }>>;
12
+ /** Deezer's genre list — the `id`s feed {@link getChart}, {@link getGenreArtists}, advanced search. */
13
+ export declare const getGenres: () => Promise<publicApiList<genreType>>;
14
+ /** Artists Deezer files under a genre. */
15
+ export declare const getGenreArtists: (genreId: number | string) => Promise<publicApiList<searchResultArtist>>;
16
+ /** Deezer's editorial sections (the `id`s feed {@link getEditorialReleases} / {@link getEditorialSelection}). */
17
+ export declare const getEditorialList: () => Promise<publicApiList<editorialType>>;
18
+ /** New releases for an editorial section (`0` = the default section). */
19
+ export declare const getEditorialReleases: (editorialId?: number | string, limit?: number, index?: number) => Promise<publicApiList<artistAlbumResult>>;
20
+ /** The albums Deezer's editors are currently pushing for a section. */
21
+ export declare const getEditorialSelection: (editorialId?: number | string) => Promise<publicApiList<artistAlbumResult>>;
22
+ /** The editorial charts for a section — same five-list shape as {@link getChart}. */
23
+ export declare const getEditorialCharts: (editorialId?: number | string) => Promise<chartType>;
24
+ /** An artist's most popular tracks. */
25
+ export declare const getArtistTopTracks: (artistId: number | string, limit?: number) => Promise<publicApiList<searchResultTrack>>;
26
+ /** Artists Deezer considers related / similar. */
27
+ export declare const getRelatedArtists: (artistId: number | string, limit?: number) => Promise<publicApiList<searchResultArtist>>;
28
+ /** An artist's discography (public-API album shape). */
29
+ export declare const getArtistAlbums: (artistId: number | string, limit?: number, index?: number) => Promise<publicApiList<artistAlbumResult>>;
30
+ /** Playlists featuring an artist. */
31
+ export declare const getArtistPlaylists: (artistId: number | string, limit?: number) => Promise<publicApiList<searchResultPlaylist>>;
32
+ /** A ready-made radio (track list) seeded from an artist. */
33
+ export declare const getArtistRadioTracks: (artistId: number | string) => Promise<publicApiList<searchResultTrack>>;
34
+ /**
35
+ * Resolve an ISRC to the Deezer **public-API** track (with `bpm`, `gain`,
36
+ * `isrc`, `preview`, `contributors`). Unlike the converter's `isrc2deezer`, this
37
+ * does not hydrate a gw track — pass `result.id` to `getTrackInfo` for that.
38
+ *
39
+ * @throws when Deezer has no track for the code
40
+ */
41
+ export declare const getTrackByISRC: (isrc: string) => Promise<trackTypePublicApi>;
42
+ /**
43
+ * Resolve a UPC / EAN barcode to the Deezer **public-API** album (with its
44
+ * `tracks`). A 13-digit barcode with a leading `0` is trimmed to 12, matching
45
+ * Deezer's own lookup.
46
+ *
47
+ * @throws when Deezer has no album for the code
48
+ */
49
+ export declare const getAlbumByUPC: (upc: string) => Promise<albumTypePublicApi>;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ 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.getGenres = exports.getChartTracks = exports.getChart = void 0;
4
+ const request_1 = require("./request");
5
+ const withParams = (slug, params) => {
6
+ const search = new URLSearchParams();
7
+ for (const [key, value] of Object.entries(params)) {
8
+ if (value !== undefined && value !== '') {
9
+ search.set(key, String(value));
10
+ }
11
+ }
12
+ const qs = search.toString();
13
+ return qs ? `${slug}?${qs}` : slug;
14
+ };
15
+ // ─── Charts ──────────────────────────────────────────────────────────────────
16
+ /**
17
+ * The five ranked lists Deezer publishes for a genre: `tracks`, `albums`,
18
+ * `artists`, `playlists`, `podcasts`. `genreId` `0` (the default) is "all
19
+ * genres"; other ids come from {@link getGenres}.
20
+ */
21
+ const getChart = (genreId = 0, limit = 10) => (0, request_1.requestPublicApi)(withParams(`/chart/${genreId}`, { limit }));
22
+ exports.getChart = getChart;
23
+ /** Just the track chart for a genre (`0` = all). Handy as a ready-to-play list. */
24
+ const getChartTracks = (genreId = 0, limit = 100, index = 0) => (0, request_1.requestPublicApi)(withParams(`/chart/${genreId}/tracks`, { limit, index }));
25
+ exports.getChartTracks = getChartTracks;
26
+ // ─── Genres & editorial ──────────────────────────────────────────────────────
27
+ /** Deezer's genre list — the `id`s feed {@link getChart}, {@link getGenreArtists}, advanced search. */
28
+ const getGenres = () => (0, request_1.requestPublicApi)('/genre');
29
+ exports.getGenres = getGenres;
30
+ /** Artists Deezer files under a genre. */
31
+ const getGenreArtists = (genreId) => (0, request_1.requestPublicApi)(`/genre/${genreId}/artists`);
32
+ exports.getGenreArtists = getGenreArtists;
33
+ /** Deezer's editorial sections (the `id`s feed {@link getEditorialReleases} / {@link getEditorialSelection}). */
34
+ const getEditorialList = () => (0, request_1.requestPublicApi)('/editorial');
35
+ exports.getEditorialList = getEditorialList;
36
+ /** New releases for an editorial section (`0` = the default section). */
37
+ const getEditorialReleases = (editorialId = 0, limit = 25, index = 0) => (0, request_1.requestPublicApi)(withParams(`/editorial/${editorialId}/releases`, { limit, index }));
38
+ exports.getEditorialReleases = getEditorialReleases;
39
+ /** The albums Deezer's editors are currently pushing for a section. */
40
+ const getEditorialSelection = (editorialId = 0) => (0, request_1.requestPublicApi)(`/editorial/${editorialId}/selection`);
41
+ exports.getEditorialSelection = getEditorialSelection;
42
+ /** The editorial charts for a section — same five-list shape as {@link getChart}. */
43
+ const getEditorialCharts = (editorialId = 0) => (0, request_1.requestPublicApi)(`/editorial/${editorialId}/charts`);
44
+ exports.getEditorialCharts = getEditorialCharts;
45
+ // ─── Artist discovery ────────────────────────────────────────────────────────
46
+ /** An artist's most popular tracks. */
47
+ const getArtistTopTracks = (artistId, limit = 50) => (0, request_1.requestPublicApi)(withParams(`/artist/${artistId}/top`, { limit }));
48
+ exports.getArtistTopTracks = getArtistTopTracks;
49
+ /** Artists Deezer considers related / similar. */
50
+ const getRelatedArtists = (artistId, limit = 20) => (0, request_1.requestPublicApi)(withParams(`/artist/${artistId}/related`, { limit }));
51
+ exports.getRelatedArtists = getRelatedArtists;
52
+ /** An artist's discography (public-API album shape). */
53
+ const getArtistAlbums = (artistId, limit = 50, index = 0) => (0, request_1.requestPublicApi)(withParams(`/artist/${artistId}/albums`, { limit, index }));
54
+ exports.getArtistAlbums = getArtistAlbums;
55
+ /** Playlists featuring an artist. */
56
+ const getArtistPlaylists = (artistId, limit = 25) => (0, request_1.requestPublicApi)(withParams(`/artist/${artistId}/playlists`, { limit }));
57
+ exports.getArtistPlaylists = getArtistPlaylists;
58
+ /** A ready-made radio (track list) seeded from an artist. */
59
+ const getArtistRadioTracks = (artistId) => (0, request_1.requestPublicApi)(`/artist/${artistId}/radio`);
60
+ exports.getArtistRadioTracks = getArtistRadioTracks;
61
+ // ─── ISRC / UPC resolution ───────────────────────────────────────────────────
62
+ /**
63
+ * Resolve an ISRC to the Deezer **public-API** track (with `bpm`, `gain`,
64
+ * `isrc`, `preview`, `contributors`). Unlike the converter's `isrc2deezer`, this
65
+ * does not hydrate a gw track — pass `result.id` to `getTrackInfo` for that.
66
+ *
67
+ * @throws when Deezer has no track for the code
68
+ */
69
+ const getTrackByISRC = (isrc) => (0, request_1.requestPublicApi)(`/track/isrc:${encodeURIComponent(isrc)}`);
70
+ exports.getTrackByISRC = getTrackByISRC;
71
+ /**
72
+ * Resolve a UPC / EAN barcode to the Deezer **public-API** album (with its
73
+ * `tracks`). A 13-digit barcode with a leading `0` is trimmed to 12, matching
74
+ * Deezer's own lookup.
75
+ *
76
+ * @throws when Deezer has no album for the code
77
+ */
78
+ const getAlbumByUPC = (upc) => {
79
+ const code = upc.length > 12 && upc.startsWith('0') ? upc.slice(-12) : upc;
80
+ return (0, request_1.requestPublicApi)(`/album/upc:${encodeURIComponent(code)}`);
81
+ };
82
+ exports.getAlbumByUPC = getAlbumByUPC;
@@ -1,3 +1,5 @@
1
1
  export * from './api';
2
2
  export * from './request';
3
3
  export * from './search';
4
+ export * from './browse';
5
+ export * from './preview';
package/dist/api/index.js CHANGED
@@ -17,3 +17,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./api"), exports);
18
18
  __exportStar(require("./request"), exports);
19
19
  __exportStar(require("./search"), exports);
20
+ __exportStar(require("./browse"), exports);
21
+ __exportStar(require("./preview"), exports);
@@ -0,0 +1,21 @@
1
+ /// <reference types="node" />
2
+ import type { trackType } from '../types';
3
+ export interface TrackPreview {
4
+ /** direct URL to a ~30 s MP3 clip — no licence, no encryption, no `arl` */
5
+ url: string;
6
+ /** clip length in seconds (Deezer previews are 30 s) */
7
+ duration: number;
8
+ }
9
+ /**
10
+ * The 30-second preview clip for a track. Deezer exposes it two ways:
11
+ * `song.getData` carries it in `MEDIA` (`{TYPE: 'preview', HREF}`), and the
12
+ * public `/track/` endpoint carries it as `preview`. Pass a gw `track` object
13
+ * (uses `MEDIA`, no extra request) or a track id (one public-API lookup).
14
+ *
15
+ * The clip is a plain MP3 — never Blowfish-encrypted — so it needs no
16
+ * `decryptDownload` and is safe to use in tests and "audition before download"
17
+ * flows.
18
+ */
19
+ export declare const getTrackPreview: (track: trackType | string | number) => Promise<TrackPreview | null>;
20
+ /** Fetch the 30-second preview clip as a `Buffer` (plain MP3, no decryption needed). */
21
+ export declare const downloadPreview: (track: trackType | string | number) => Promise<Buffer | null>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.downloadPreview = exports.getTrackPreview = void 0;
4
+ const http_1 = require("../lib/http");
5
+ const api_1 = require("./api");
6
+ /**
7
+ * The 30-second preview clip for a track. Deezer exposes it two ways:
8
+ * `song.getData` carries it in `MEDIA` (`{TYPE: 'preview', HREF}`), and the
9
+ * public `/track/` endpoint carries it as `preview`. Pass a gw `track` object
10
+ * (uses `MEDIA`, no extra request) or a track id (one public-API lookup).
11
+ *
12
+ * The clip is a plain MP3 — never Blowfish-encrypted — so it needs no
13
+ * `decryptDownload` and is safe to use in tests and "audition before download"
14
+ * flows.
15
+ */
16
+ const getTrackPreview = async (track) => {
17
+ var _a;
18
+ if (typeof track === 'object') {
19
+ const media = (_a = track.MEDIA) === null || _a === void 0 ? void 0 : _a[0];
20
+ if ((media === null || media === void 0 ? void 0 : media.HREF) && (!media.TYPE || media.TYPE === 'preview')) {
21
+ return { url: media.HREF, duration: 30 };
22
+ }
23
+ const pub = await (0, api_1.getTrackInfoPublicApi)(track.SNG_ID);
24
+ return pub.preview ? { url: pub.preview, duration: 30 } : null;
25
+ }
26
+ const pub = await (0, api_1.getTrackInfoPublicApi)(String(track));
27
+ return pub.preview ? { url: pub.preview, duration: 30 } : null;
28
+ };
29
+ exports.getTrackPreview = getTrackPreview;
30
+ /** Fetch the 30-second preview clip as a `Buffer` (plain MP3, no decryption needed). */
31
+ const downloadPreview = async (track) => {
32
+ const preview = await (0, exports.getTrackPreview)(track);
33
+ if (!preview)
34
+ return null;
35
+ return (0, http_1.getBuffer)(preview.url);
36
+ };
37
+ exports.downloadPreview = downloadPreview;
@@ -13,8 +13,24 @@ export declare class ExpiredTrackToken extends Error {
13
13
  readonly sngId: string;
14
14
  constructor(sngId: string);
15
15
  }
16
- /** quality code -> the `format` string the media API expects */
17
- export declare const formatName: (quality: number) => string;
16
+ /**
17
+ * Every audio format Deezer's `get_url` understands, best → worst. `FLAC`,
18
+ * `MP3_320` and `MP3_128` are the classic `9 / 3 / 1` qualities; the rest are
19
+ * additional tiers (`MP3_256`, `MP3_64`, the `AAC_64` / `MP4_RA*` HE-AAC ladder)
20
+ * that some accounts and regions expose.
21
+ */
22
+ export declare const DEEZER_FORMATS: readonly ["FLAC", "MP3_320", "MP3_256", "MP3_128", "MP3_64", "AAC_64", "MP4_RA3", "MP4_RA2", "MP4_RA1"];
23
+ export type DeezerFormat = (typeof DEEZER_FORMATS)[number];
24
+ /** A quality request: the numeric `1 | 3 | 9` shorthand, or any format string. */
25
+ export type Quality = number | DeezerFormat | string;
26
+ /** Normalise a {@link Quality} (number shorthand or format string) to a format string. */
27
+ export declare const toFormat: (quality: Quality) => string;
28
+ /**
29
+ * Quality code (`1 | 3 | 9`) or format string → the `format` string the media
30
+ * API expects. Format strings pass straight through, so this doubles as a
31
+ * validator-free identity for `'AAC_64'`, `'MP3_256'`, …
32
+ */
33
+ export declare const formatName: (quality: Quality) => string;
18
34
  /**
19
35
  * @param track Track info json returned from `getTrackInfo`
20
36
  * @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
@@ -28,18 +44,21 @@ export interface ResolvedUrl {
28
44
  trackUrl: string;
29
45
  isEncrypted: boolean;
30
46
  fileSize: number;
31
- /** the format Deezer actually returned, e.g. `'FLAC'` */
47
+ /** the format Deezer actually returned, e.g. `'FLAC'`, `'MP3_128'`, `'AAC_64'` */
32
48
  format: string;
49
+ /** the cipher Deezer applied — `'BF_CBC_STRIPE'` (stripe-encrypted) or `'NONE'` */
50
+ cipher: string;
33
51
  }
34
52
  /**
35
53
  * Resolve download URLs for many tracks in a **single** `get_url` request.
36
54
  *
37
- * `qualities` is an ordered preference list (e.g. `[9, 3, 1]`): Deezer returns
38
- * the best each track is licensed for, so there is no per-quality retry. The
39
- * result has one entry per input track, in order; `null` for a track that is
40
- * geo-blocked, unavailable, or errored.
55
+ * `qualities` is an ordered preference list: Deezer returns the best each track
56
+ * is licensed for, so there is no per-quality retry. Entries may be the numeric
57
+ * `1 | 3 | 9` shorthand **or** any format string from {@link DEEZER_FORMATS}
58
+ * (e.g. `['FLAC', 'MP3_320', 'AAC_64']`). The result has one entry per input
59
+ * track, in order; `null` for a track that is geo-blocked, unavailable, or errored.
41
60
  *
42
61
  * @param tracks from `getTrackInfo` / `parseInfo` (needs `TRACK_TOKEN`, `SNG_ID`, `FILESIZE_*`)
43
- * @param qualities preference order — 9 = FLAC, 3 = MP3 320, 1 = MP3 128
62
+ * @param qualities preference order — default `[9, 3, 1]` (FLAC MP3 320 MP3 128)
44
63
  */
45
- export declare const resolveDownloadUrls: (tracks: trackType[], qualities?: number[]) => Promise<(ResolvedUrl | null)[]>;
64
+ export declare const resolveDownloadUrls: (tracks: trackType[], qualities?: Quality[]) => Promise<(ResolvedUrl | null)[]>;
@@ -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.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
6
+ 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 http_1 = require("../lib/http");
@@ -37,17 +37,50 @@ class ExpiredTrackToken extends Error {
37
37
  }
38
38
  exports.ExpiredTrackToken = ExpiredTrackToken;
39
39
  let user_data = null;
40
- const getTrackFileSize = (track, quality) => {
41
- switch (quality) {
42
- case 9:
43
- return Number(track.FILESIZE_FLAC);
44
- case 3:
45
- return Number(track.FILESIZE_MP3_320);
46
- case 1:
47
- return Number(track.FILESIZE_MP3_128);
48
- default:
49
- return 0;
40
+ /**
41
+ * Every audio format Deezer's `get_url` understands, best → worst. `FLAC`,
42
+ * `MP3_320` and `MP3_128` are the classic `9 / 3 / 1` qualities; the rest are
43
+ * additional tiers (`MP3_256`, `MP3_64`, the `AAC_64` / `MP4_RA*` HE-AAC ladder)
44
+ * that some accounts and regions expose.
45
+ */
46
+ exports.DEEZER_FORMATS = [
47
+ 'FLAC',
48
+ 'MP3_320',
49
+ 'MP3_256',
50
+ 'MP3_128',
51
+ 'MP3_64',
52
+ 'AAC_64',
53
+ 'MP4_RA3',
54
+ 'MP4_RA2',
55
+ 'MP4_RA1',
56
+ ];
57
+ const FORMAT_FILESIZE_KEY = {
58
+ FLAC: 'FILESIZE_FLAC',
59
+ MP3_320: 'FILESIZE_MP3_320',
60
+ MP3_256: 'FILESIZE_MP3_256',
61
+ MP3_128: 'FILESIZE_MP3_128',
62
+ MP3_64: 'FILESIZE_MP3_64',
63
+ MP3_MISC: 'FILESIZE_MP3_MISC',
64
+ AAC_64: 'FILESIZE_AAC_64',
65
+ MP4_RA1: 'FILESIZE_MP4_RA1',
66
+ MP4_RA2: 'FILESIZE_MP4_RA2',
67
+ MP4_RA3: 'FILESIZE_MP4_RA3',
68
+ };
69
+ const NUMERIC_FORMAT = { 9: 'FLAC', 3: 'MP3_320', 1: 'MP3_128' };
70
+ /** Normalise a {@link Quality} (number shorthand or format string) to a format string. */
71
+ const toFormat = (quality) => {
72
+ if (typeof quality === 'number') {
73
+ const f = NUMERIC_FORMAT[quality];
74
+ if (!f)
75
+ throw new Error(`Unknown quality ${quality}`);
76
+ return f;
50
77
  }
78
+ return quality;
79
+ };
80
+ exports.toFormat = toFormat;
81
+ const getTrackFileSize = (track, quality) => {
82
+ const key = FORMAT_FILESIZE_KEY[(0, exports.toFormat)(quality)];
83
+ return key ? Number(track[key]) || 0 : 0;
51
84
  };
52
85
  const dzAuthenticate = async () => {
53
86
  const { data } = await request_1.default.get('https://www.deezer.com/ajax/gw-light.php', {
@@ -66,19 +99,12 @@ const dzAuthenticate = async () => {
66
99
  return user_data;
67
100
  };
68
101
  const MEDIA_MAX_RETRIES = 3;
69
- /** quality code -> the `format` string the media API expects */
70
- const formatName = (quality) => {
71
- switch (quality) {
72
- case 9:
73
- return 'FLAC';
74
- case 3:
75
- return 'MP3_320';
76
- case 1:
77
- return 'MP3_128';
78
- default:
79
- throw new Error(`Unknown quality ${quality}`);
80
- }
81
- };
102
+ /**
103
+ * Quality code (`1 | 3 | 9`) or format string → the `format` string the media
104
+ * API expects. Format strings pass straight through, so this doubles as a
105
+ * validator-free identity for `'AAC_64'`, `'MP3_256'`, …
106
+ */
107
+ const formatName = (quality) => (0, exports.toFormat)(quality);
82
108
  exports.formatName = formatName;
83
109
  /** POST media.deezer.com/v1/get_url with re-auth + exponential-backoff retry on 403/429/5xx. */
84
110
  const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
@@ -104,7 +130,7 @@ const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
104
130
  };
105
131
  /** Parse one `data[i]` entry from a get_url response into a source URL (or null / throw). */
106
132
  const parseMediaEntry = (entry, token, country) => {
107
- var _a, _b;
133
+ var _a, _b, _c, _d;
108
134
  if (entry === null || entry === void 0 ? void 0 : entry.errors) {
109
135
  const { code } = entry.errors[0];
110
136
  if (code === 2002)
@@ -116,10 +142,11 @@ const parseMediaEntry = (entry, token, country) => {
116
142
  const media = (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a[0];
117
143
  if (!((_b = media === null || media === void 0 ? void 0 : media.sources) === null || _b === void 0 ? void 0 : _b.length))
118
144
  return null;
119
- return { url: media.sources[0].url, format: media.format };
145
+ return { url: media.sources[0].url, format: media.format, cipher: (_d = (_c = media.cipher) === null || _c === void 0 ? void 0 : _c.type) !== null && _d !== void 0 ? _d : 'BF_CBC_STRIPE' };
120
146
  };
147
+ /** Whether a track fetched with the given cipher needs Blowfish stripe decryption. */
148
+ const cipherIsEncrypted = (cipher, url) => cipher ? cipher !== 'NONE' : url.includes('/mobile/') || url.includes('/media/');
121
149
  const getTrackUrlFromServer = async (track_token, format) => {
122
- var _a, _b;
123
150
  const user = user_data ? user_data : await dzAuthenticate();
124
151
  if ((format === 'FLAC' && !user.can_stream_lossless) || (format === 'MP3_320' && !user.can_stream_hq)) {
125
152
  throw new WrongLicense(format);
@@ -127,7 +154,7 @@ const getTrackUrlFromServer = async (track_token, format) => {
127
154
  const { data, country } = await mediaGetUrl([track_token], [{ format, cipher: 'BF_CBC_STRIPE' }]);
128
155
  if (!data.length)
129
156
  return null;
130
- return (_b = (_a = parseMediaEntry(data[0], track_token, country)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : null;
157
+ return parseMediaEntry(data[0], track_token, country);
131
158
  };
132
159
  /**
133
160
  * @param track Track info json returned from `getTrackInfo`
@@ -148,11 +175,11 @@ const getTrackDownloadUrl = async (track, quality) => {
148
175
  else {
149
176
  // Get URL with the official API
150
177
  try {
151
- const url = await getTrackUrlFromServer(track.TRACK_TOKEN, format);
152
- if (url) {
178
+ const resolved = await getTrackUrlFromServer(track.TRACK_TOKEN, format);
179
+ if (resolved) {
153
180
  return {
154
- trackUrl: url,
155
- isEncrypted: url.includes('/mobile/') || url.includes('/media/'),
181
+ trackUrl: resolved.url,
182
+ isEncrypted: cipherIsEncrypted(resolved.cipher, resolved.url),
156
183
  fileSize: getTrackFileSize(track, quality),
157
184
  };
158
185
  }
@@ -213,17 +240,17 @@ const testUrl = async (url) => {
213
240
  return 0;
214
241
  }
215
242
  };
216
- const QUALITY_OF_FORMAT = { FLAC: 9, MP3_320: 3, MP3_128: 1, MP3_256: 3, MP3_64: 1 };
217
243
  /**
218
244
  * Resolve download URLs for many tracks in a **single** `get_url` request.
219
245
  *
220
- * `qualities` is an ordered preference list (e.g. `[9, 3, 1]`): Deezer returns
221
- * the best each track is licensed for, so there is no per-quality retry. The
222
- * result has one entry per input track, in order; `null` for a track that is
223
- * geo-blocked, unavailable, or errored.
246
+ * `qualities` is an ordered preference list: Deezer returns the best each track
247
+ * is licensed for, so there is no per-quality retry. Entries may be the numeric
248
+ * `1 | 3 | 9` shorthand **or** any format string from {@link DEEZER_FORMATS}
249
+ * (e.g. `['FLAC', 'MP3_320', 'AAC_64']`). The result has one entry per input
250
+ * track, in order; `null` for a track that is geo-blocked, unavailable, or errored.
224
251
  *
225
252
  * @param tracks from `getTrackInfo` / `parseInfo` (needs `TRACK_TOKEN`, `SNG_ID`, `FILESIZE_*`)
226
- * @param qualities preference order — 9 = FLAC, 3 = MP3 320, 1 = MP3 128
253
+ * @param qualities preference order — default `[9, 3, 1]` (FLAC MP3 320 MP3 128)
227
254
  */
228
255
  const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
229
256
  if (!tracks.length)
@@ -231,7 +258,6 @@ const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
231
258
  const formats = qualities.map((q) => ({ format: (0, exports.formatName)(q), cipher: 'BF_CBC_STRIPE' }));
232
259
  const { data, country } = await mediaGetUrl(tracks.map((t) => t.TRACK_TOKEN), formats);
233
260
  return tracks.map((track, i) => {
234
- var _a;
235
261
  let parsed;
236
262
  try {
237
263
  parsed = parseMediaEntry(data[i], track.TRACK_TOKEN, country);
@@ -242,12 +268,12 @@ const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
242
268
  }
243
269
  if (!parsed)
244
270
  return null;
245
- const q = (_a = QUALITY_OF_FORMAT[parsed.format]) !== null && _a !== void 0 ? _a : qualities[0];
246
271
  return {
247
272
  trackUrl: parsed.url,
248
- isEncrypted: parsed.url.includes('/mobile/') || parsed.url.includes('/media/'),
249
- fileSize: getTrackFileSize(track, q),
273
+ isEncrypted: cipherIsEncrypted(parsed.cipher, parsed.url),
274
+ fileSize: getTrackFileSize(track, parsed.format),
250
275
  format: parsed.format,
276
+ cipher: parsed.cipher,
251
277
  };
252
278
  });
253
279
  };
@@ -0,0 +1,85 @@
1
+ import type { searchResultAlbum, searchResultArtist, searchResultPlaylist, searchResultTrack } from './search';
2
+ /** The `{data, total, next?, prev?}` envelope every paginated public-API list uses. */
3
+ export interface publicApiList<T> {
4
+ data: T[];
5
+ total?: number;
6
+ /** absolute URL of the next page, when there is one */
7
+ next?: string;
8
+ prev?: string;
9
+ }
10
+ export interface chartTrack extends searchResultTrack {
11
+ /** 1-based position in the chart */
12
+ position: number;
13
+ }
14
+ export interface chartArtist extends searchResultArtist {
15
+ position: number;
16
+ }
17
+ export interface chartAlbum extends searchResultAlbum {
18
+ position: number;
19
+ }
20
+ export interface chartPlaylist extends searchResultPlaylist {
21
+ position: number;
22
+ }
23
+ export interface chartPodcast {
24
+ id: number;
25
+ title: string;
26
+ description?: string;
27
+ available?: boolean;
28
+ fans?: number;
29
+ link?: string;
30
+ share?: string;
31
+ picture: string;
32
+ picture_small?: string;
33
+ picture_medium?: string;
34
+ picture_big?: string;
35
+ picture_xl?: string;
36
+ position: number;
37
+ type: 'podcast';
38
+ }
39
+ /** `/chart/{genreId}` — the five ranked lists Deezer publishes per genre (`0` = all genres). */
40
+ export interface chartType {
41
+ tracks: publicApiList<chartTrack>;
42
+ albums: publicApiList<chartAlbum>;
43
+ artists: publicApiList<chartArtist>;
44
+ playlists: publicApiList<chartPlaylist>;
45
+ podcasts: publicApiList<chartPodcast>;
46
+ }
47
+ export interface genreType {
48
+ id: number;
49
+ name: string;
50
+ picture: string;
51
+ picture_small?: string;
52
+ picture_medium?: string;
53
+ picture_big?: string;
54
+ picture_xl?: string;
55
+ type: 'genre';
56
+ }
57
+ export interface editorialType {
58
+ id: number;
59
+ name: string;
60
+ picture: string;
61
+ picture_small?: string;
62
+ picture_medium?: string;
63
+ picture_big?: string;
64
+ picture_xl?: string;
65
+ type: 'editorial';
66
+ }
67
+ /** An album from `/artist/{id}/albums` — the artist's own discography, public-API shape. */
68
+ export interface artistAlbumResult {
69
+ id: number;
70
+ title: string;
71
+ link: string;
72
+ cover: string;
73
+ cover_small?: string;
74
+ cover_medium?: string;
75
+ cover_big?: string;
76
+ cover_xl?: string;
77
+ md5_image: string;
78
+ genre_id: number;
79
+ fans?: number;
80
+ release_date: string;
81
+ record_type: string;
82
+ tracklist: string;
83
+ explicit_lyrics: boolean;
84
+ type: 'album';
85
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,6 @@
1
1
  export * from './album';
2
2
  export * from './artist';
3
+ export * from './browse';
3
4
  export * from './show';
4
5
  export * from './playlist';
5
6
  export * from './playlist-channel';
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./album"), exports);
18
18
  __exportStar(require("./artist"), exports);
19
+ __exportStar(require("./browse"), exports);
19
20
  __exportStar(require("./show"), exports);
20
21
  __exportStar(require("./playlist"), exports);
21
22
  __exportStar(require("./playlist-channel"), exports);
@@ -1,6 +1,6 @@
1
1
  import type { artistType } from './artist';
2
2
  interface mediaType {
3
- TYPE: 'preview';
3
+ TYPE: 'preview' | string;
4
4
  HREF: string;
5
5
  }
6
6
  export interface lyricsSync {
@@ -59,15 +59,19 @@ interface songType {
59
59
  URL_REWRITING: string;
60
60
  VERSION?: string;
61
61
  MD5_ORIGIN?: string;
62
- FILESIZE_AAC_64: '0';
62
+ FILESIZE_AAC_64: string;
63
63
  FILESIZE_MP3_64: string;
64
64
  FILESIZE_MP3_128: string;
65
- FILESIZE_MP3_256: '0';
66
- FILESIZE_MP3_320: '0';
67
- FILESIZE_MP4_RA1: '0';
68
- FILESIZE_MP4_RA2: '0';
69
- FILESIZE_MP4_RA3: '0';
70
- FILESIZE_FLAC: '0';
65
+ FILESIZE_MP3_256: string;
66
+ FILESIZE_MP3_320: string;
67
+ FILESIZE_MP3_MISC?: string;
68
+ FILESIZE_MP4_RA1: string;
69
+ FILESIZE_MP4_RA2: string;
70
+ FILESIZE_MP4_RA3: string;
71
+ FILESIZE_MHM1_RA1?: string;
72
+ FILESIZE_MHM1_RA2?: string;
73
+ FILESIZE_MHM1_RA3?: string;
74
+ FILESIZE_FLAC: string;
71
75
  FILESIZE: string;
72
76
  GAIN: string;
73
77
  MEDIA_VERSION: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur-core",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Core module for gerdur.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",