gerdur-core 2.2.0 → 2.4.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 +53 -0
- package/README.md +45 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/preview.d.ts +21 -0
- package/dist/api/preview.js +37 -0
- package/dist/api/request.js +8 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/lib/errors.d.ts +27 -0
- package/dist/lib/errors.js +44 -0
- package/dist/lib/get-url.d.ts +28 -9
- package/dist/lib/get-url.js +70 -43
- package/dist/lib/request.d.ts +20 -0
- package/dist/lib/request.js +59 -22
- package/dist/types/tracks.d.ts +12 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
Phase 3.3 — deterministic retries and typed errors.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **The gateway retry loop had no cap** on `error.code === 4`,
|
|
10
|
+
`NEED_API_AUTH_REQUIRED`, or `GATEWAY_ERROR` — a failing endpoint could spin
|
|
11
|
+
forever. `requestWithRetry` is now a bounded loop: per-class attempt caps
|
|
12
|
+
(`code 4` ×6, re-auth ×3, token refresh ×15), full-jittered exponential
|
|
13
|
+
backoff, and a 30 s wall-clock deadline. On exhaustion it throws.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`DeezerError`** (`extends Error`) — thrown for gateway / media-API failures
|
|
18
|
+
instead of `new Error(Object.entries(error).join(', '))`. Carries `code`,
|
|
19
|
+
`keys`, `retryable`, and the raw `payload`. `message` stays human-readable.
|
|
20
|
+
- **`RETRY_POLICY`** — the exported, inspectable retry configuration.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Gateway helpers (`request`, `requestLight`, `requestGet`, `requestPublicApi`)
|
|
25
|
+
and `resolveDownloadUrls` now throw `DeezerError`. `message` text changed shape
|
|
26
|
+
(`"KEY: value"` rather than `"KEY,value"`); read `err.code` / `err.keys`
|
|
27
|
+
instead of matching the string.
|
|
28
|
+
|
|
29
|
+
## 2.3.0 - 2026-08-31
|
|
30
|
+
|
|
31
|
+
Phase 2.1 — all the formats, and previews. Additive; the `1 / 3 / 9` path is
|
|
32
|
+
unchanged.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`getTrackPreview(track)` / `downloadPreview(track)`** — the 30-second preview
|
|
37
|
+
clip. A plain MP3: no licence, no `arl`, no Blowfish. Accepts a gw track (reads
|
|
38
|
+
`MEDIA`, no extra request), a track id, or a number.
|
|
39
|
+
- **`DEEZER_FORMATS`** — every format `get_url` understands, best → worst
|
|
40
|
+
(`FLAC`, `MP3_320`, `MP3_256`, `MP3_128`, `MP3_64`, `AAC_64`, `MP4_RA3/2/1`).
|
|
41
|
+
- **`resolveDownloadUrls(tracks, qualities)`** now accepts format strings in the
|
|
42
|
+
preference list (`['FLAC', 'MP3_320', 'AAC_64']`), not just `1 | 3 | 9`. Each
|
|
43
|
+
`ResolvedUrl` gains `cipher` (`'BF_CBC_STRIPE'` | `'NONE'`).
|
|
44
|
+
- **`toFormat(quality)`** — normalise a number or format string to the `get_url`
|
|
45
|
+
format string. `formatName` now also accepts format strings (identity).
|
|
46
|
+
- `Quality` / `DeezerFormat` exported types.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- `isEncrypted` (from `getTrackDownloadUrl` and `resolveDownloadUrls`) is now
|
|
51
|
+
taken from the media API's `cipher` field instead of guessing from the URL
|
|
52
|
+
path — authoritative, and correct for `cipher: NONE` content.
|
|
53
|
+
- `trackType.FILESIZE_*` fields are typed `string` (several were mistyped as the
|
|
54
|
+
literal `'0'`); added `FILESIZE_MP3_MISC` / `FILESIZE_MHM1_RA*`.
|
|
55
|
+
|
|
3
56
|
## 2.2.0 - 2026-08-31
|
|
4
57
|
|
|
5
58
|
Phase 2.4 — browse & discovery. All additive, all on the public REST API (no
|
package/README.md
CHANGED
|
@@ -82,7 +82,20 @@ if (model.lyricsSynced) fs.writeFileSync(track.SNG_TITLE + '.lrc', model.lyricsS
|
|
|
82
82
|
|
|
83
83
|
## Methods
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Every method returns an `Object` or throws. Gateway / media-API failures throw a
|
|
86
|
+
**`DeezerError`** (`extends Error`) with:
|
|
87
|
+
|
|
88
|
+
| Field | |
|
|
89
|
+
| :--- | :--- |
|
|
90
|
+
| `code` | Deezer's numeric error code, when it sent one (e.g. `4`, `800`) |
|
|
91
|
+
| `keys` | the gateway error keys, e.g. `['VALID_TOKEN_REQUIRED']`, `['DATA_ERROR']` |
|
|
92
|
+
| `retryable` | whether a retry could plausibly succeed |
|
|
93
|
+
| `payload` | the raw error object |
|
|
94
|
+
|
|
95
|
+
Retries are bounded — `RETRY_POLICY` (exported) sets per-class attempt caps and a
|
|
96
|
+
30 s wall-clock deadline, so a persistently failing endpoint surfaces a
|
|
97
|
+
`DeezerError` instead of spinning. `GeoBlocked`, `WrongLicense` and
|
|
98
|
+
`ExpiredTrackToken` are still thrown as their own types from the download path.
|
|
86
99
|
|
|
87
100
|
### `.initDeezerApi(arl_cookie);`
|
|
88
101
|
|
|
@@ -238,6 +251,37 @@ pass the `id` to `getTrackInfo` / `getAlbumTracks` (or use the converter's
|
|
|
238
251
|
| `track` | Yes | `string` | track object |
|
|
239
252
|
| `quality` | Yes | `1, 3 or 9` | 1 = 128kbps, 3 = 320kbps, 9 = flac |
|
|
240
253
|
|
|
254
|
+
Resolves `{trackUrl, isEncrypted, fileSize}`. `isEncrypted` now comes from the
|
|
255
|
+
media API's `cipher` field (authoritative) rather than a URL guess.
|
|
256
|
+
|
|
257
|
+
### Formats
|
|
258
|
+
|
|
259
|
+
Deezer's `get_url` understands more than `1 / 3 / 9`. `DEEZER_FORMATS` lists them
|
|
260
|
+
best → worst: `FLAC`, `MP3_320`, `MP3_256`, `MP3_128`, `MP3_64`, `AAC_64`,
|
|
261
|
+
`MP4_RA3`, `MP4_RA2`, `MP4_RA1` (the last four are the HE-AAC ladder some
|
|
262
|
+
accounts / regions expose).
|
|
263
|
+
|
|
264
|
+
- **`resolveDownloadUrls(tracks, qualities)`** — `qualities` entries may be the
|
|
265
|
+
`1 | 3 | 9` shorthand **or** any format string, e.g.
|
|
266
|
+
`resolveDownloadUrls(tracks, ['FLAC', 'MP3_320', 'AAC_64'])`. Deezer returns
|
|
267
|
+
the best each track is licensed for. Each result now also carries `format` and
|
|
268
|
+
`cipher` (`'BF_CBC_STRIPE'` or `'NONE'`).
|
|
269
|
+
- **`formatName(quality)`** / **`toFormat(quality)`** — normalise a number or
|
|
270
|
+
format string to the `get_url` format string.
|
|
271
|
+
|
|
272
|
+
### `.getTrackPreview(track)` / `.downloadPreview(track)`
|
|
273
|
+
|
|
274
|
+
The 30-second preview clip — a plain MP3, **no licence, no `arl`, no
|
|
275
|
+
encryption**. `track` may be a gw track object (reads its `MEDIA`, no extra
|
|
276
|
+
request), a track id, or a number.
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
const {url} = await getTrackPreview('3135556'); // {url, duration: 30}
|
|
280
|
+
const clip = await downloadPreview('3135556'); // Buffer (ID3-tagged MP3)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Useful for "audition before download" and for CI that shouldn't pull full tracks.
|
|
284
|
+
|
|
241
285
|
### `.decryptDownload(data, song_id);`
|
|
242
286
|
|
|
243
287
|
| Parameters | Required | Type | Description |
|
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.js
CHANGED
|
@@ -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;
|
package/dist/api/request.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.requestPublicApi = exports.requestGet = exports.requestLight = exports.request = void 0;
|
|
7
7
|
const request_1 = __importDefault(require("../lib/request"));
|
|
8
|
+
const errors_1 = require("../lib/errors");
|
|
8
9
|
const cache_1 = __importDefault(require("./cache"));
|
|
9
10
|
/**
|
|
10
11
|
* In-flight request coalescing (single-flight).
|
|
@@ -47,11 +48,11 @@ const request = async (body, method) => {
|
|
|
47
48
|
const cacheKey = method + ':' + Object.entries(body).join(':');
|
|
48
49
|
return coalesce(cacheKey, async () => {
|
|
49
50
|
const { data: { error, results }, } = await request_1.default.post('/gateway.php', body, { params: { method } });
|
|
50
|
-
if (Object.keys(results).length > 0) {
|
|
51
|
+
if (results && Object.keys(results).length > 0) {
|
|
51
52
|
cache_1.default.set(cacheKey, results);
|
|
52
53
|
return results;
|
|
53
54
|
}
|
|
54
|
-
throw new
|
|
55
|
+
throw new errors_1.DeezerError(error);
|
|
55
56
|
});
|
|
56
57
|
};
|
|
57
58
|
exports.request = request;
|
|
@@ -66,11 +67,11 @@ const requestLight = async (body, method) => {
|
|
|
66
67
|
const { data: { error, results }, } = await request_1.default.post('https://www.deezer.com/ajax/gw-light.php', body, {
|
|
67
68
|
params: { method, api_version: '1.0' },
|
|
68
69
|
});
|
|
69
|
-
if (Object.keys(results).length > 0) {
|
|
70
|
+
if (results && Object.keys(results).length > 0) {
|
|
70
71
|
cache_1.default.set(cacheKey, results);
|
|
71
72
|
return results;
|
|
72
73
|
}
|
|
73
|
-
throw new
|
|
74
|
+
throw new errors_1.DeezerError(error);
|
|
74
75
|
});
|
|
75
76
|
};
|
|
76
77
|
exports.requestLight = requestLight;
|
|
@@ -83,11 +84,11 @@ const requestGet = async (method, params = {}, key = 'get_request') => {
|
|
|
83
84
|
const cacheKey = method + key;
|
|
84
85
|
return coalesce(cacheKey, async () => {
|
|
85
86
|
const { data: { error, results }, } = await request_1.default.get('/gateway.php', { params: { method, ...params } });
|
|
86
|
-
if (Object.keys(results).length > 0) {
|
|
87
|
+
if (results && Object.keys(results).length > 0) {
|
|
87
88
|
cache_1.default.set(cacheKey, results);
|
|
88
89
|
return results;
|
|
89
90
|
}
|
|
90
|
-
throw new
|
|
91
|
+
throw new errors_1.DeezerError(error);
|
|
91
92
|
});
|
|
92
93
|
};
|
|
93
94
|
exports.requestGet = requestGet;
|
|
@@ -99,8 +100,7 @@ const requestPublicApi = async (slug) => {
|
|
|
99
100
|
return coalesce(slug, async () => {
|
|
100
101
|
const { data } = await request_1.default.get('https://api.deezer.com' + slug);
|
|
101
102
|
if (data.error) {
|
|
102
|
-
|
|
103
|
-
throw new Error(errorMessage);
|
|
103
|
+
throw new errors_1.DeezerError(data.error);
|
|
104
104
|
}
|
|
105
105
|
cache_1.default.set(slug, data);
|
|
106
106
|
return data;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { initDeezerApi } from './lib/request';
|
|
1
|
+
export { initDeezerApi, RETRY_POLICY } from './lib/request';
|
|
2
|
+
export { DeezerError } from './lib/errors';
|
|
3
|
+
export type { DeezerErrorPayload } from './lib/errors';
|
|
2
4
|
export * from './api';
|
|
3
5
|
export * from './converter';
|
|
4
6
|
export * from './lib/decrypt';
|
package/dist/index.js
CHANGED
|
@@ -14,9 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.getText = exports.getJson = exports.getBuffer = exports.httpsAgent = exports.httpAgent = exports.initDeezerApi = void 0;
|
|
17
|
+
exports.getText = exports.getJson = exports.getBuffer = exports.httpsAgent = exports.httpAgent = exports.DeezerError = exports.RETRY_POLICY = exports.initDeezerApi = void 0;
|
|
18
18
|
var request_1 = require("./lib/request");
|
|
19
19
|
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return request_1.initDeezerApi; } });
|
|
20
|
+
Object.defineProperty(exports, "RETRY_POLICY", { enumerable: true, get: function () { return request_1.RETRY_POLICY; } });
|
|
21
|
+
var errors_1 = require("./lib/errors");
|
|
22
|
+
Object.defineProperty(exports, "DeezerError", { enumerable: true, get: function () { return errors_1.DeezerError; } });
|
|
20
23
|
__exportStar(require("./api"), exports);
|
|
21
24
|
__exportStar(require("./converter"), exports);
|
|
22
25
|
__exportStar(require("./lib/decrypt"), exports);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The raw `error` object a Deezer gateway response carries. */
|
|
2
|
+
export type DeezerErrorPayload = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* A structured error from the Deezer gateway or media API — replaces the old
|
|
5
|
+
* `new Error(Object.entries(error).join(', '))`. Carries the numeric `code`, the
|
|
6
|
+
* gateway error `keys`, a `retryable` hint, and the raw `payload`.
|
|
7
|
+
*
|
|
8
|
+
* The `message` is still human-readable (`"VALID_TOKEN_REQUIRED: …"`), so code
|
|
9
|
+
* that only reads `err.message` keeps working — but prefer `err.code` /
|
|
10
|
+
* `err.keys` / `err.retryable`.
|
|
11
|
+
*/
|
|
12
|
+
export declare class DeezerError extends Error {
|
|
13
|
+
/** `error.code` when Deezer sent a numeric one */
|
|
14
|
+
readonly code?: number;
|
|
15
|
+
/** the gateway error keys, e.g. `['VALID_TOKEN_REQUIRED']`, `['DATA_ERROR']` */
|
|
16
|
+
readonly keys: string[];
|
|
17
|
+
/** whether a retry could plausibly succeed */
|
|
18
|
+
readonly retryable: boolean;
|
|
19
|
+
/** the raw error payload */
|
|
20
|
+
readonly payload: DeezerErrorPayload;
|
|
21
|
+
constructor(payload: DeezerErrorPayload | null | undefined, opts?: {
|
|
22
|
+
retryable?: boolean;
|
|
23
|
+
message?: string;
|
|
24
|
+
});
|
|
25
|
+
/** Whether a `code` / key set is worth retrying. */
|
|
26
|
+
static retryable(code: number | undefined, keys: string[]): boolean;
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeezerError = void 0;
|
|
4
|
+
/** Gateway error keys where retrying (after a token refresh / re-auth) can help. */
|
|
5
|
+
const RETRYABLE_KEYS = new Set(['NEED_API_AUTH_REQUIRED', 'GATEWAY_ERROR', 'VALID_TOKEN_REQUIRED']);
|
|
6
|
+
/**
|
|
7
|
+
* A structured error from the Deezer gateway or media API — replaces the old
|
|
8
|
+
* `new Error(Object.entries(error).join(', '))`. Carries the numeric `code`, the
|
|
9
|
+
* gateway error `keys`, a `retryable` hint, and the raw `payload`.
|
|
10
|
+
*
|
|
11
|
+
* The `message` is still human-readable (`"VALID_TOKEN_REQUIRED: …"`), so code
|
|
12
|
+
* that only reads `err.message` keeps working — but prefer `err.code` /
|
|
13
|
+
* `err.keys` / `err.retryable`.
|
|
14
|
+
*/
|
|
15
|
+
class DeezerError extends Error {
|
|
16
|
+
constructor(payload, opts = {}) {
|
|
17
|
+
var _a, _b;
|
|
18
|
+
const p = payload && typeof payload === 'object' ? payload : {};
|
|
19
|
+
const keys = Object.keys(p);
|
|
20
|
+
const rawCode = p.code;
|
|
21
|
+
const code = typeof rawCode === 'number' ? rawCode : undefined;
|
|
22
|
+
super((_a = opts.message) !== null && _a !== void 0 ? _a : describe(p, keys));
|
|
23
|
+
this.name = 'DeezerError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.keys = keys;
|
|
26
|
+
this.payload = p;
|
|
27
|
+
this.retryable = (_b = opts.retryable) !== null && _b !== void 0 ? _b : isRetryable(code, keys);
|
|
28
|
+
}
|
|
29
|
+
/** Whether a `code` / key set is worth retrying. */
|
|
30
|
+
static retryable(code, keys) {
|
|
31
|
+
return isRetryable(code, keys);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.DeezerError = DeezerError;
|
|
35
|
+
const describe = (payload, keys) => {
|
|
36
|
+
if (!keys.length)
|
|
37
|
+
return 'Deezer request failed';
|
|
38
|
+
return keys.map((k) => `${k}: ${String(payload[k])}`).join(', ');
|
|
39
|
+
};
|
|
40
|
+
const isRetryable = (code, keys) => {
|
|
41
|
+
if (code === 4)
|
|
42
|
+
return true; // Deezer's transient "quota exceeded"
|
|
43
|
+
return keys.some((k) => RETRYABLE_KEYS.has(k));
|
|
44
|
+
};
|
package/dist/lib/get-url.d.ts
CHANGED
|
@@ -13,8 +13,24 @@ export declare class ExpiredTrackToken extends Error {
|
|
|
13
13
|
readonly sngId: string;
|
|
14
14
|
constructor(sngId: string);
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
62
|
+
* @param qualities preference order — default `[9, 3, 1]` (FLAC → MP3 320 → MP3 128)
|
|
44
63
|
*/
|
|
45
|
-
export declare const resolveDownloadUrls: (tracks: trackType[], qualities?:
|
|
64
|
+
export declare const resolveDownloadUrls: (tracks: trackType[], qualities?: Quality[]) => Promise<(ResolvedUrl | null)[]>;
|
package/dist/lib/get-url.js
CHANGED
|
@@ -3,9 +3,10 @@ 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
|
+
const errors_1 = require("../lib/errors");
|
|
9
10
|
const http_1 = require("../lib/http");
|
|
10
11
|
const request_1 = __importDefault(require("../lib/request"));
|
|
11
12
|
class WrongLicense extends Error {
|
|
@@ -37,17 +38,50 @@ class ExpiredTrackToken extends Error {
|
|
|
37
38
|
}
|
|
38
39
|
exports.ExpiredTrackToken = ExpiredTrackToken;
|
|
39
40
|
let user_data = null;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Every audio format Deezer's `get_url` understands, best → worst. `FLAC`,
|
|
43
|
+
* `MP3_320` and `MP3_128` are the classic `9 / 3 / 1` qualities; the rest are
|
|
44
|
+
* additional tiers (`MP3_256`, `MP3_64`, the `AAC_64` / `MP4_RA*` HE-AAC ladder)
|
|
45
|
+
* that some accounts and regions expose.
|
|
46
|
+
*/
|
|
47
|
+
exports.DEEZER_FORMATS = [
|
|
48
|
+
'FLAC',
|
|
49
|
+
'MP3_320',
|
|
50
|
+
'MP3_256',
|
|
51
|
+
'MP3_128',
|
|
52
|
+
'MP3_64',
|
|
53
|
+
'AAC_64',
|
|
54
|
+
'MP4_RA3',
|
|
55
|
+
'MP4_RA2',
|
|
56
|
+
'MP4_RA1',
|
|
57
|
+
];
|
|
58
|
+
const FORMAT_FILESIZE_KEY = {
|
|
59
|
+
FLAC: 'FILESIZE_FLAC',
|
|
60
|
+
MP3_320: 'FILESIZE_MP3_320',
|
|
61
|
+
MP3_256: 'FILESIZE_MP3_256',
|
|
62
|
+
MP3_128: 'FILESIZE_MP3_128',
|
|
63
|
+
MP3_64: 'FILESIZE_MP3_64',
|
|
64
|
+
MP3_MISC: 'FILESIZE_MP3_MISC',
|
|
65
|
+
AAC_64: 'FILESIZE_AAC_64',
|
|
66
|
+
MP4_RA1: 'FILESIZE_MP4_RA1',
|
|
67
|
+
MP4_RA2: 'FILESIZE_MP4_RA2',
|
|
68
|
+
MP4_RA3: 'FILESIZE_MP4_RA3',
|
|
69
|
+
};
|
|
70
|
+
const NUMERIC_FORMAT = { 9: 'FLAC', 3: 'MP3_320', 1: 'MP3_128' };
|
|
71
|
+
/** Normalise a {@link Quality} (number shorthand or format string) to a format string. */
|
|
72
|
+
const toFormat = (quality) => {
|
|
73
|
+
if (typeof quality === 'number') {
|
|
74
|
+
const f = NUMERIC_FORMAT[quality];
|
|
75
|
+
if (!f)
|
|
76
|
+
throw new Error(`Unknown quality ${quality}`);
|
|
77
|
+
return f;
|
|
50
78
|
}
|
|
79
|
+
return quality;
|
|
80
|
+
};
|
|
81
|
+
exports.toFormat = toFormat;
|
|
82
|
+
const getTrackFileSize = (track, quality) => {
|
|
83
|
+
const key = FORMAT_FILESIZE_KEY[(0, exports.toFormat)(quality)];
|
|
84
|
+
return key ? Number(track[key]) || 0 : 0;
|
|
51
85
|
};
|
|
52
86
|
const dzAuthenticate = async () => {
|
|
53
87
|
const { data } = await request_1.default.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
@@ -66,19 +100,12 @@ const dzAuthenticate = async () => {
|
|
|
66
100
|
return user_data;
|
|
67
101
|
};
|
|
68
102
|
const MEDIA_MAX_RETRIES = 3;
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return 'MP3_320';
|
|
76
|
-
case 1:
|
|
77
|
-
return 'MP3_128';
|
|
78
|
-
default:
|
|
79
|
-
throw new Error(`Unknown quality ${quality}`);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
103
|
+
/**
|
|
104
|
+
* Quality code (`1 | 3 | 9`) or format string → the `format` string the media
|
|
105
|
+
* API expects. Format strings pass straight through, so this doubles as a
|
|
106
|
+
* validator-free identity for `'AAC_64'`, `'MP3_256'`, …
|
|
107
|
+
*/
|
|
108
|
+
const formatName = (quality) => (0, exports.toFormat)(quality);
|
|
82
109
|
exports.formatName = formatName;
|
|
83
110
|
/** POST media.deezer.com/v1/get_url with re-auth + exponential-backoff retry on 403/429/5xx. */
|
|
84
111
|
const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
|
|
@@ -104,22 +131,23 @@ const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
|
|
|
104
131
|
};
|
|
105
132
|
/** Parse one `data[i]` entry from a get_url response into a source URL (or null / throw). */
|
|
106
133
|
const parseMediaEntry = (entry, token, country) => {
|
|
107
|
-
var _a, _b;
|
|
134
|
+
var _a, _b, _c, _d;
|
|
108
135
|
if (entry === null || entry === void 0 ? void 0 : entry.errors) {
|
|
109
136
|
const { code } = entry.errors[0];
|
|
110
137
|
if (code === 2002)
|
|
111
138
|
throw new GeoBlocked(country);
|
|
112
139
|
if (code === 2000 || code === 2001)
|
|
113
140
|
throw new ExpiredTrackToken(token);
|
|
114
|
-
throw new
|
|
141
|
+
throw new errors_1.DeezerError(entry.errors[0]);
|
|
115
142
|
}
|
|
116
143
|
const media = (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a[0];
|
|
117
144
|
if (!((_b = media === null || media === void 0 ? void 0 : media.sources) === null || _b === void 0 ? void 0 : _b.length))
|
|
118
145
|
return null;
|
|
119
|
-
return { url: media.sources[0].url, format: media.format };
|
|
146
|
+
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
147
|
};
|
|
148
|
+
/** Whether a track fetched with the given cipher needs Blowfish stripe decryption. */
|
|
149
|
+
const cipherIsEncrypted = (cipher, url) => cipher ? cipher !== 'NONE' : url.includes('/mobile/') || url.includes('/media/');
|
|
121
150
|
const getTrackUrlFromServer = async (track_token, format) => {
|
|
122
|
-
var _a, _b;
|
|
123
151
|
const user = user_data ? user_data : await dzAuthenticate();
|
|
124
152
|
if ((format === 'FLAC' && !user.can_stream_lossless) || (format === 'MP3_320' && !user.can_stream_hq)) {
|
|
125
153
|
throw new WrongLicense(format);
|
|
@@ -127,7 +155,7 @@ const getTrackUrlFromServer = async (track_token, format) => {
|
|
|
127
155
|
const { data, country } = await mediaGetUrl([track_token], [{ format, cipher: 'BF_CBC_STRIPE' }]);
|
|
128
156
|
if (!data.length)
|
|
129
157
|
return null;
|
|
130
|
-
return
|
|
158
|
+
return parseMediaEntry(data[0], track_token, country);
|
|
131
159
|
};
|
|
132
160
|
/**
|
|
133
161
|
* @param track Track info json returned from `getTrackInfo`
|
|
@@ -148,11 +176,11 @@ const getTrackDownloadUrl = async (track, quality) => {
|
|
|
148
176
|
else {
|
|
149
177
|
// Get URL with the official API
|
|
150
178
|
try {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
179
|
+
const resolved = await getTrackUrlFromServer(track.TRACK_TOKEN, format);
|
|
180
|
+
if (resolved) {
|
|
153
181
|
return {
|
|
154
|
-
trackUrl: url,
|
|
155
|
-
isEncrypted:
|
|
182
|
+
trackUrl: resolved.url,
|
|
183
|
+
isEncrypted: cipherIsEncrypted(resolved.cipher, resolved.url),
|
|
156
184
|
fileSize: getTrackFileSize(track, quality),
|
|
157
185
|
};
|
|
158
186
|
}
|
|
@@ -213,17 +241,17 @@ const testUrl = async (url) => {
|
|
|
213
241
|
return 0;
|
|
214
242
|
}
|
|
215
243
|
};
|
|
216
|
-
const QUALITY_OF_FORMAT = { FLAC: 9, MP3_320: 3, MP3_128: 1, MP3_256: 3, MP3_64: 1 };
|
|
217
244
|
/**
|
|
218
245
|
* Resolve download URLs for many tracks in a **single** `get_url` request.
|
|
219
246
|
*
|
|
220
|
-
* `qualities` is an ordered preference list
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
247
|
+
* `qualities` is an ordered preference list: Deezer returns the best each track
|
|
248
|
+
* is licensed for, so there is no per-quality retry. Entries may be the numeric
|
|
249
|
+
* `1 | 3 | 9` shorthand **or** any format string from {@link DEEZER_FORMATS}
|
|
250
|
+
* (e.g. `['FLAC', 'MP3_320', 'AAC_64']`). The result has one entry per input
|
|
251
|
+
* track, in order; `null` for a track that is geo-blocked, unavailable, or errored.
|
|
224
252
|
*
|
|
225
253
|
* @param tracks from `getTrackInfo` / `parseInfo` (needs `TRACK_TOKEN`, `SNG_ID`, `FILESIZE_*`)
|
|
226
|
-
* @param qualities preference order — 9
|
|
254
|
+
* @param qualities preference order — default `[9, 3, 1]` (FLAC → MP3 320 → MP3 128)
|
|
227
255
|
*/
|
|
228
256
|
const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
|
|
229
257
|
if (!tracks.length)
|
|
@@ -231,7 +259,6 @@ const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
|
|
|
231
259
|
const formats = qualities.map((q) => ({ format: (0, exports.formatName)(q), cipher: 'BF_CBC_STRIPE' }));
|
|
232
260
|
const { data, country } = await mediaGetUrl(tracks.map((t) => t.TRACK_TOKEN), formats);
|
|
233
261
|
return tracks.map((track, i) => {
|
|
234
|
-
var _a;
|
|
235
262
|
let parsed;
|
|
236
263
|
try {
|
|
237
264
|
parsed = parseMediaEntry(data[i], track.TRACK_TOKEN, country);
|
|
@@ -242,12 +269,12 @@ const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
|
|
|
242
269
|
}
|
|
243
270
|
if (!parsed)
|
|
244
271
|
return null;
|
|
245
|
-
const q = (_a = QUALITY_OF_FORMAT[parsed.format]) !== null && _a !== void 0 ? _a : qualities[0];
|
|
246
272
|
return {
|
|
247
273
|
trackUrl: parsed.url,
|
|
248
|
-
isEncrypted: parsed.
|
|
249
|
-
fileSize: getTrackFileSize(track,
|
|
274
|
+
isEncrypted: cipherIsEncrypted(parsed.cipher, parsed.url),
|
|
275
|
+
fileSize: getTrackFileSize(track, parsed.format),
|
|
250
276
|
format: parsed.format,
|
|
277
|
+
cipher: parsed.cipher,
|
|
251
278
|
};
|
|
252
279
|
});
|
|
253
280
|
};
|
package/dist/lib/request.d.ts
CHANGED
|
@@ -3,6 +3,26 @@ type DeezerRequestConfig = {
|
|
|
3
3
|
headers?: Record<string, string>;
|
|
4
4
|
params?: HttpQuery;
|
|
5
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* Bounded-retry policy for the gateway. Every retry class has its own attempt
|
|
8
|
+
* cap **and** there is a wall-clock deadline, so a persistently failing endpoint
|
|
9
|
+
* can no longer spin forever (the old code had no cap on `code === 4`,
|
|
10
|
+
* `NEED_API_AUTH_REQUIRED` or `GATEWAY_ERROR`).
|
|
11
|
+
*/
|
|
12
|
+
export declare const RETRY_POLICY: {
|
|
13
|
+
/** total attempts for the transient `code === 4` class */
|
|
14
|
+
code4Attempts: number;
|
|
15
|
+
/** re-inits allowed for `NEED_API_AUTH_REQUIRED` */
|
|
16
|
+
authReinits: number;
|
|
17
|
+
/** token refreshes allowed for `GATEWAY_ERROR` / `VALID_TOKEN_REQUIRED` */
|
|
18
|
+
tokenRefreshes: number;
|
|
19
|
+
/** base backoff (ms) — grows exponentially, full-jittered */
|
|
20
|
+
baseMs: number;
|
|
21
|
+
/** cap on a single backoff wait */
|
|
22
|
+
maxDelayMs: number;
|
|
23
|
+
/** overall wall-clock budget from the first attempt */
|
|
24
|
+
deadlineMs: number;
|
|
25
|
+
};
|
|
6
26
|
export declare const initDeezerApi: (arl: string) => Promise<string>;
|
|
7
27
|
declare const _default: {
|
|
8
28
|
defaults: {
|
package/dist/lib/request.js
CHANGED
|
@@ -3,10 +3,36 @@ 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.initDeezerApi = void 0;
|
|
6
|
+
exports.initDeezerApi = exports.RETRY_POLICY = void 0;
|
|
7
7
|
const delay_1 = __importDefault(require("delay"));
|
|
8
8
|
const http_1 = require("./http");
|
|
9
|
+
const errors_1 = require("./errors");
|
|
9
10
|
let user_arl = 'c973964816688562722418b5200c1515dffaad15a42643ebf87cc72824a54612ec51c2ad42d566743f9e424c774e98ccae7737770acff59251328e6cd598c7bcac38ca269adf78bfb88ec5bbad6cd800db3c0b88b2af645bb22b99e71de26416';
|
|
11
|
+
/**
|
|
12
|
+
* Bounded-retry policy for the gateway. Every retry class has its own attempt
|
|
13
|
+
* cap **and** there is a wall-clock deadline, so a persistently failing endpoint
|
|
14
|
+
* can no longer spin forever (the old code had no cap on `code === 4`,
|
|
15
|
+
* `NEED_API_AUTH_REQUIRED` or `GATEWAY_ERROR`).
|
|
16
|
+
*/
|
|
17
|
+
exports.RETRY_POLICY = {
|
|
18
|
+
/** total attempts for the transient `code === 4` class */
|
|
19
|
+
code4Attempts: 6,
|
|
20
|
+
/** re-inits allowed for `NEED_API_AUTH_REQUIRED` */
|
|
21
|
+
authReinits: 3,
|
|
22
|
+
/** token refreshes allowed for `GATEWAY_ERROR` / `VALID_TOKEN_REQUIRED` */
|
|
23
|
+
tokenRefreshes: 15,
|
|
24
|
+
/** base backoff (ms) — grows exponentially, full-jittered */
|
|
25
|
+
baseMs: 800,
|
|
26
|
+
/** cap on a single backoff wait */
|
|
27
|
+
maxDelayMs: 8000,
|
|
28
|
+
/** overall wall-clock budget from the first attempt */
|
|
29
|
+
deadlineMs: 30000,
|
|
30
|
+
};
|
|
31
|
+
/** Exponential backoff (ms) with full jitter on the top half of the window. */
|
|
32
|
+
const backoffDelay = (attempt) => {
|
|
33
|
+
const windowMs = Math.min(exports.RETRY_POLICY.baseMs * 2 ** attempt, exports.RETRY_POLICY.maxDelayMs);
|
|
34
|
+
return windowMs / 2 + Math.random() * (windowMs / 2);
|
|
35
|
+
};
|
|
10
36
|
const instance = new http_1.HttpClient({
|
|
11
37
|
baseURL: 'https://api.deezer.com/1.0',
|
|
12
38
|
timeout: 15000,
|
|
@@ -54,30 +80,41 @@ const initDeezerApi = async (arl) => {
|
|
|
54
80
|
return data.results.SESSION;
|
|
55
81
|
};
|
|
56
82
|
exports.initDeezerApi = initDeezerApi;
|
|
57
|
-
let token_retry = 0;
|
|
58
83
|
const requestWithRetry = async (method, url, body, config = {}) => {
|
|
59
84
|
var _a;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
const startedAt = Date.now();
|
|
86
|
+
let authReinits = 0;
|
|
87
|
+
let tokenRefreshes = 0;
|
|
88
|
+
let code4Attempts = 0;
|
|
89
|
+
// eslint-disable-next-line no-constant-condition
|
|
90
|
+
while (true) {
|
|
91
|
+
const response = method === 'POST' ? await instance.post(url, body, config) : await instance.get(url, config);
|
|
92
|
+
const error = (_a = response.data) === null || _a === void 0 ? void 0 : _a.error;
|
|
93
|
+
if (!error || Object.keys(error).length === 0) {
|
|
94
|
+
return response;
|
|
95
|
+
}
|
|
96
|
+
const overDeadline = Date.now() - startedAt > exports.RETRY_POLICY.deadlineMs;
|
|
97
|
+
if (error.NEED_API_AUTH_REQUIRED && authReinits < exports.RETRY_POLICY.authReinits && !overDeadline) {
|
|
98
|
+
authReinits += 1;
|
|
99
|
+
await (0, exports.initDeezerApi)(user_arl);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if ((error.GATEWAY_ERROR || error.VALID_TOKEN_REQUIRED) &&
|
|
103
|
+
tokenRefreshes < exports.RETRY_POLICY.tokenRefreshes &&
|
|
104
|
+
!overDeadline) {
|
|
105
|
+
tokenRefreshes += 1;
|
|
106
|
+
await getApiToken();
|
|
107
|
+
await (0, delay_1.default)(backoffDelay(tokenRefreshes - 1));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (error.code === 4 && code4Attempts < exports.RETRY_POLICY.code4Attempts && !overDeadline) {
|
|
111
|
+
await (0, delay_1.default)(backoffDelay(code4Attempts));
|
|
112
|
+
code4Attempts += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Unhandled error, or a retry class ran out of attempts / hit the deadline.
|
|
116
|
+
throw new errors_1.DeezerError(error);
|
|
78
117
|
}
|
|
79
|
-
token_retry = 0;
|
|
80
|
-
return response;
|
|
81
118
|
};
|
|
82
119
|
exports.default = {
|
|
83
120
|
defaults: instance.defaults,
|
package/dist/types/tracks.d.ts
CHANGED
|
@@ -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:
|
|
62
|
+
FILESIZE_AAC_64: string;
|
|
63
63
|
FILESIZE_MP3_64: string;
|
|
64
64
|
FILESIZE_MP3_128: string;
|
|
65
|
-
FILESIZE_MP3_256:
|
|
66
|
-
FILESIZE_MP3_320:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|