gerdur-core 2.3.0 → 2.5.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 +48 -0
- package/README.md +42 -1
- package/dist/api/request.js +8 -8
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -1
- package/dist/lib/decrypt.d.ts +14 -1
- package/dist/lib/decrypt.js +32 -3
- package/dist/lib/errors.d.ts +27 -0
- package/dist/lib/errors.js +44 -0
- package/dist/lib/get-url.js +2 -1
- package/dist/lib/http.d.ts +17 -0
- package/dist/lib/http.js +75 -1
- package/dist/lib/request.d.ts +20 -0
- package/dist/lib/request.js +59 -22
- package/dist/lib/stream-download.d.ts +37 -0
- package/dist/lib/stream-download.js +54 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
Phase 3.1 — streaming download primitives. Additive.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`streamTrackDownload(track, quality, options?)`** — download a track as a
|
|
10
|
+
stream of decrypted audio (`get_url` → CDN fetch → stripe-decrypt `Transform`
|
|
11
|
+
→ your sink). Peak memory is ~one 2048-byte stripe regardless of file size or
|
|
12
|
+
concurrency. `options.onProgress(received, total)`; `options.resumeFrom`
|
|
13
|
+
(bytes, 2048-aligned) sends a `Range` header and resumes stripe decryption
|
|
14
|
+
in phase. Verified byte-identical to the buffered `decryptDownload` path,
|
|
15
|
+
resume included.
|
|
16
|
+
- **`createDecryptStream(sngId, startChunk?)`** — a Node `Transform` wrapping the
|
|
17
|
+
stripe cipher, for composing your own pipelines. `TrackDecryptStream` gained a
|
|
18
|
+
`startChunk` constructor arg.
|
|
19
|
+
- **`getStream(url, {rangeStart?})`** — a content-decoded response stream from
|
|
20
|
+
the internal HTTP client (`gunzip` / `brotli` / `inflate` handled), following
|
|
21
|
+
redirects, rejecting `HttpStatusError` on non-2xx.
|
|
22
|
+
|
|
23
|
+
Streaming the tag write (in-place FLAC metadata-block rewrite) is still open.
|
|
24
|
+
|
|
25
|
+
## 2.4.0 - 2026-08-31
|
|
26
|
+
|
|
27
|
+
Phase 3.3 — deterministic retries and typed errors.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **The gateway retry loop had no cap** on `error.code === 4`,
|
|
32
|
+
`NEED_API_AUTH_REQUIRED`, or `GATEWAY_ERROR` — a failing endpoint could spin
|
|
33
|
+
forever. `requestWithRetry` is now a bounded loop: per-class attempt caps
|
|
34
|
+
(`code 4` ×6, re-auth ×3, token refresh ×15), full-jittered exponential
|
|
35
|
+
backoff, and a 30 s wall-clock deadline. On exhaustion it throws.
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`DeezerError`** (`extends Error`) — thrown for gateway / media-API failures
|
|
40
|
+
instead of `new Error(Object.entries(error).join(', '))`. Carries `code`,
|
|
41
|
+
`keys`, `retryable`, and the raw `payload`. `message` stays human-readable.
|
|
42
|
+
- **`RETRY_POLICY`** — the exported, inspectable retry configuration.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- Gateway helpers (`request`, `requestLight`, `requestGet`, `requestPublicApi`)
|
|
47
|
+
and `resolveDownloadUrls` now throw `DeezerError`. `message` text changed shape
|
|
48
|
+
(`"KEY: value"` rather than `"KEY,value"`); read `err.code` / `err.keys`
|
|
49
|
+
instead of matching the string.
|
|
50
|
+
|
|
3
51
|
## 2.3.0 - 2026-08-31
|
|
4
52
|
|
|
5
53
|
Phase 2.1 — all the formats, and previews. Additive; the `1 / 3 / 9` path is
|
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
|
|
|
@@ -276,6 +289,34 @@ Useful for "audition before download" and for CI that shouldn't pull full tracks
|
|
|
276
289
|
| `data` | Yes | `buffer` | downloaded song buffer |
|
|
277
290
|
| `song_id` | Yes | `string` | track id |
|
|
278
291
|
|
|
292
|
+
### Streaming download
|
|
293
|
+
|
|
294
|
+
For large files / high concurrency — peak memory is ~one 2048-byte stripe
|
|
295
|
+
instead of 2× the file.
|
|
296
|
+
|
|
297
|
+
- **`streamTrackDownload(track, quality, options?)`** → `{stream, size, startedAt, isEncrypted}`.
|
|
298
|
+
`stream` is decrypted audio bytes (`get_url` → CDN fetch → stripe-decrypt
|
|
299
|
+
`Transform` → your sink). `options.onProgress(received, total)`;
|
|
300
|
+
`options.resumeFrom` (bytes, rounded down to a 2048 boundary) sends a `Range`
|
|
301
|
+
header and resumes stripe-decryption in phase.
|
|
302
|
+
- **`createDecryptStream(sngId, startChunk?)`** → a `Transform` for your own `pipeline`.
|
|
303
|
+
- **`getStream(url, {rangeStart?})`** → `{stream, headers, status}` — a raw,
|
|
304
|
+
content-decoded response stream.
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
import {pipeline} from 'stream/promises';
|
|
308
|
+
import {createWriteStream} from 'fs';
|
|
309
|
+
|
|
310
|
+
const {stream} = await streamTrackDownload(track, 9, {
|
|
311
|
+
onProgress: (got, total) => process.stdout.write(`\r${((got / total) * 100) | 0}%`),
|
|
312
|
+
});
|
|
313
|
+
await pipeline(stream, createWriteStream('track.flac'));
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Streaming the **tag write** (rewriting the FLAC metadata block in place, no
|
|
317
|
+
full-file `Buffer.concat`) is not done yet — buffer the result and call
|
|
318
|
+
`addTrackTags`, or tag the file afterward.
|
|
319
|
+
|
|
279
320
|
### `.addTrackTags(data, track, options?)`
|
|
280
321
|
|
|
281
322
|
Resolves album info, credits, lyrics and artwork from Deezer and writes them into
|
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,7 +1,11 @@
|
|
|
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';
|
|
5
7
|
export * from './lib/get-url';
|
|
6
|
-
export
|
|
8
|
+
export * from './lib/stream-download';
|
|
9
|
+
export { httpAgent, httpsAgent, getBuffer, getJson, getText, getStream } from './lib/http';
|
|
10
|
+
export type { StreamResponse } from './lib/http';
|
|
7
11
|
export * from './metadata-writer';
|
package/dist/index.js
CHANGED
|
@@ -14,17 +14,22 @@ 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.getStream = 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);
|
|
23
26
|
__exportStar(require("./lib/get-url"), exports);
|
|
27
|
+
__exportStar(require("./lib/stream-download"), exports);
|
|
24
28
|
var http_1 = require("./lib/http");
|
|
25
29
|
Object.defineProperty(exports, "httpAgent", { enumerable: true, get: function () { return http_1.httpAgent; } });
|
|
26
30
|
Object.defineProperty(exports, "httpsAgent", { enumerable: true, get: function () { return http_1.httpsAgent; } });
|
|
27
31
|
Object.defineProperty(exports, "getBuffer", { enumerable: true, get: function () { return http_1.getBuffer; } });
|
|
28
32
|
Object.defineProperty(exports, "getJson", { enumerable: true, get: function () { return http_1.getJson; } });
|
|
29
33
|
Object.defineProperty(exports, "getText", { enumerable: true, get: function () { return http_1.getText; } });
|
|
34
|
+
Object.defineProperty(exports, "getStream", { enumerable: true, get: function () { return http_1.getStream; } });
|
|
30
35
|
__exportStar(require("./metadata-writer"), exports);
|
package/dist/lib/decrypt.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
import { Transform } from 'stream';
|
|
2
3
|
import type { trackType } from '../types';
|
|
3
4
|
export declare const getSongFileName: ({ MD5_ORIGIN, SNG_ID, MEDIA_VERSION }: trackType, quality: number) => string;
|
|
4
5
|
/**
|
|
@@ -19,9 +20,21 @@ export declare class TrackDecryptStream {
|
|
|
19
20
|
private readonly bf;
|
|
20
21
|
private carry;
|
|
21
22
|
private chunkIndex;
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* @param trackId `SNG_ID`
|
|
25
|
+
* @param startChunk the 2048-byte chunk index the first byte you'll `write()`
|
|
26
|
+
* corresponds to — non-zero when resuming a `Range` download
|
|
27
|
+
* (`resumeFromByte / 2048`). Per-chunk IVs make this exact.
|
|
28
|
+
*/
|
|
29
|
+
constructor(trackId: string, startChunk?: number);
|
|
23
30
|
/** Returns the decrypted bytes for every complete 2048-byte stripe now available. */
|
|
24
31
|
write(part: Buffer): Buffer;
|
|
25
32
|
/** The trailing partial chunk (always plaintext). */
|
|
26
33
|
final(): Buffer;
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* A Node `Transform` that decrypts a Deezer download stripe-by-stripe as bytes
|
|
37
|
+
* flow through it — `fetch(url) → createDecryptStream(sngId) → sink`, constant
|
|
38
|
+
* memory. `startChunk` (`resumeFromByte / 2048`) supports resumed `Range` fetches.
|
|
39
|
+
*/
|
|
40
|
+
export declare const createDecryptStream: (trackId: string, startChunk?: number) => Transform;
|
package/dist/lib/decrypt.js
CHANGED
|
@@ -3,8 +3,9 @@ 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.TrackDecryptStream = exports.decryptDownload = exports.getSongFileName = void 0;
|
|
6
|
+
exports.createDecryptStream = exports.TrackDecryptStream = exports.decryptDownload = exports.getSongFileName = void 0;
|
|
7
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const stream_1 = require("stream");
|
|
8
9
|
const blowfish_1 = require("./blowfish");
|
|
9
10
|
const md5 = (data, type = 'ascii') => {
|
|
10
11
|
const md5sum = crypto_1.default.createHash('md5');
|
|
@@ -66,10 +67,16 @@ exports.decryptDownload = decryptDownload;
|
|
|
66
67
|
* and the CPU work hides behind the (slower) network read.
|
|
67
68
|
*/
|
|
68
69
|
class TrackDecryptStream {
|
|
69
|
-
|
|
70
|
+
/**
|
|
71
|
+
* @param trackId `SNG_ID`
|
|
72
|
+
* @param startChunk the 2048-byte chunk index the first byte you'll `write()`
|
|
73
|
+
* corresponds to — non-zero when resuming a `Range` download
|
|
74
|
+
* (`resumeFromByte / 2048`). Per-chunk IVs make this exact.
|
|
75
|
+
*/
|
|
76
|
+
constructor(trackId, startChunk = 0) {
|
|
70
77
|
this.carry = Buffer.alloc(0);
|
|
71
|
-
this.chunkIndex = 0;
|
|
72
78
|
this.bf = new blowfish_1.Blowfish(getBlowfishKey(trackId));
|
|
79
|
+
this.chunkIndex = startChunk;
|
|
73
80
|
}
|
|
74
81
|
/** Returns the decrypted bytes for every complete 2048-byte stripe now available. */
|
|
75
82
|
write(part) {
|
|
@@ -100,3 +107,25 @@ class TrackDecryptStream {
|
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
exports.TrackDecryptStream = TrackDecryptStream;
|
|
110
|
+
/**
|
|
111
|
+
* A Node `Transform` that decrypts a Deezer download stripe-by-stripe as bytes
|
|
112
|
+
* flow through it — `fetch(url) → createDecryptStream(sngId) → sink`, constant
|
|
113
|
+
* memory. `startChunk` (`resumeFromByte / 2048`) supports resumed `Range` fetches.
|
|
114
|
+
*/
|
|
115
|
+
const createDecryptStream = (trackId, startChunk = 0) => {
|
|
116
|
+
const engine = new TrackDecryptStream(trackId, startChunk);
|
|
117
|
+
return new stream_1.Transform({
|
|
118
|
+
transform(chunk, _enc, cb) {
|
|
119
|
+
try {
|
|
120
|
+
cb(null, engine.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
cb(err);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
flush(cb) {
|
|
127
|
+
cb(null, engine.final());
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
exports.createDecryptStream = createDecryptStream;
|
|
@@ -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.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
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 {
|
|
@@ -137,7 +138,7 @@ const parseMediaEntry = (entry, token, country) => {
|
|
|
137
138
|
throw new GeoBlocked(country);
|
|
138
139
|
if (code === 2000 || code === 2001)
|
|
139
140
|
throw new ExpiredTrackToken(token);
|
|
140
|
-
throw new
|
|
141
|
+
throw new errors_1.DeezerError(entry.errors[0]);
|
|
141
142
|
}
|
|
142
143
|
const media = (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a[0];
|
|
143
144
|
if (!((_b = media === null || media === void 0 ? void 0 : media.sources) === null || _b === void 0 ? void 0 : _b.length))
|
package/dist/lib/http.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
/// <reference types="node" />
|
|
4
|
+
/// <reference types="node" />
|
|
4
5
|
import { Agent as HttpAgent, IncomingHttpHeaders } from 'http';
|
|
5
6
|
import { Agent as HttpsAgent } from 'https';
|
|
7
|
+
import { Readable } from 'stream';
|
|
6
8
|
type HttpMethod = 'GET' | 'POST' | 'HEAD';
|
|
7
9
|
type ResponseType = 'buffer' | 'json' | 'text';
|
|
8
10
|
type QueryValue = string | number | boolean | null | undefined;
|
|
@@ -63,6 +65,21 @@ export declare class HttpClient {
|
|
|
63
65
|
head(url: string, config?: Omit<HttpRequestConfig, 'responseType'>): Promise<HttpResponse<Buffer>>;
|
|
64
66
|
private request;
|
|
65
67
|
}
|
|
68
|
+
export interface StreamResponse {
|
|
69
|
+
/** the response body as a Readable — content-decoded (gzip/br/deflate unwrapped) */
|
|
70
|
+
stream: Readable;
|
|
71
|
+
headers: IncomingHttpHeaders;
|
|
72
|
+
status: number;
|
|
73
|
+
/** the final URL after redirects */
|
|
74
|
+
url: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Stream a URL's body (for large downloads — constant memory). Optionally send a
|
|
78
|
+
* `Range` header to resume from `rangeStart` bytes.
|
|
79
|
+
*/
|
|
80
|
+
export declare const getStream: (url: string, config?: HttpRequestConfig & {
|
|
81
|
+
rangeStart?: number;
|
|
82
|
+
}) => Promise<StreamResponse>;
|
|
66
83
|
export declare const getBuffer: (url: string, config?: HttpRequestConfig) => Promise<Buffer>;
|
|
67
84
|
export declare const getJson: <T = unknown>(url: string, config?: HttpRequestConfig) => Promise<T>;
|
|
68
85
|
export declare const getText: (url: string, config?: HttpRequestConfig) => Promise<string>;
|
package/dist/lib/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.headRequest = exports.getText = exports.getJson = exports.getBuffer = exports.HttpClient = exports.HttpStatusError = exports.httpsAgent = exports.httpAgent = void 0;
|
|
3
|
+
exports.headRequest = exports.getText = exports.getJson = exports.getBuffer = exports.getStream = exports.HttpClient = exports.HttpStatusError = exports.httpsAgent = exports.httpAgent = void 0;
|
|
4
4
|
const http_1 = require("http");
|
|
5
5
|
const https_1 = require("https");
|
|
6
6
|
const zlib_1 = require("zlib");
|
|
@@ -172,6 +172,80 @@ const requestRaw = async (config, redirectCount = 0) => {
|
|
|
172
172
|
req.end();
|
|
173
173
|
});
|
|
174
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* Like a GET, but resolves with the response **stream** instead of a buffered
|
|
177
|
+
* body — for large downloads that should not sit in memory. Follows redirects;
|
|
178
|
+
* rejects with `HttpStatusError` on a non-2xx (buffering only the small error
|
|
179
|
+
* body). The caller owns the returned stream and must consume or destroy it.
|
|
180
|
+
*/
|
|
181
|
+
const streamRaw = async (config, redirectCount = 0) => {
|
|
182
|
+
const requestUrl = buildUrl(config.url, config.baseURL, config.params);
|
|
183
|
+
const headers = normalizeHeaders(config.headers);
|
|
184
|
+
return await new Promise((resolve, reject) => {
|
|
185
|
+
var _a;
|
|
186
|
+
const isHttps = requestUrl.protocol === 'https:';
|
|
187
|
+
const requestFn = isHttps ? https_1.request : http_1.request;
|
|
188
|
+
const req = requestFn({
|
|
189
|
+
agent: isHttps ? exports.httpsAgent : exports.httpAgent,
|
|
190
|
+
headers,
|
|
191
|
+
hostname: requestUrl.hostname,
|
|
192
|
+
method: 'GET',
|
|
193
|
+
path: requestUrl.pathname + requestUrl.search,
|
|
194
|
+
port: requestUrl.port,
|
|
195
|
+
protocol: requestUrl.protocol,
|
|
196
|
+
}, (response) => {
|
|
197
|
+
var _a, _b, _c;
|
|
198
|
+
const status = (_a = response.statusCode) !== null && _a !== void 0 ? _a : 0;
|
|
199
|
+
const locationHeader = response.headers.location;
|
|
200
|
+
const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
|
|
201
|
+
if (location && isRedirectStatus(status) && redirectCount < ((_b = config.maxRedirects) !== null && _b !== void 0 ? _b : 5)) {
|
|
202
|
+
response.resume();
|
|
203
|
+
streamRaw({ ...config, baseURL: undefined, url: new URL(location, requestUrl).toString() }, redirectCount + 1)
|
|
204
|
+
.then(resolve)
|
|
205
|
+
.catch(reject);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (status < 200 || status >= 300) {
|
|
209
|
+
const chunks = [];
|
|
210
|
+
response.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
|
|
211
|
+
response.on('end', () => reject(new HttpStatusError(status, response.headers, Buffer.concat(chunks))));
|
|
212
|
+
response.on('error', reject);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const encoding = String((_c = response.headers['content-encoding']) !== null && _c !== void 0 ? _c : '').toLowerCase();
|
|
216
|
+
let stream = response;
|
|
217
|
+
if (encoding === 'gzip') {
|
|
218
|
+
stream = response.pipe((0, zlib_1.createGunzip)());
|
|
219
|
+
}
|
|
220
|
+
else if (encoding === 'br') {
|
|
221
|
+
stream = response.pipe((0, zlib_1.createBrotliDecompress)());
|
|
222
|
+
}
|
|
223
|
+
else if (encoding === 'deflate') {
|
|
224
|
+
stream = response.pipe((0, zlib_1.createInflate)());
|
|
225
|
+
}
|
|
226
|
+
response.on('error', (err) => stream.destroy(err));
|
|
227
|
+
resolve({ stream, headers: response.headers, status, url: requestUrl.toString() });
|
|
228
|
+
});
|
|
229
|
+
req.on('error', reject);
|
|
230
|
+
req.setTimeout((_a = config.timeout) !== null && _a !== void 0 ? _a : 30000, () => {
|
|
231
|
+
var _a;
|
|
232
|
+
req.destroy(new Error(`Request timed out after ${(_a = config.timeout) !== null && _a !== void 0 ? _a : 30000}ms`));
|
|
233
|
+
});
|
|
234
|
+
req.end();
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Stream a URL's body (for large downloads — constant memory). Optionally send a
|
|
239
|
+
* `Range` header to resume from `rangeStart` bytes.
|
|
240
|
+
*/
|
|
241
|
+
const getStream = async (url, config = {}) => {
|
|
242
|
+
const headers = { ...config.headers };
|
|
243
|
+
if (config.rangeStart && config.rangeStart > 0) {
|
|
244
|
+
headers.Range = `bytes=${config.rangeStart}-`;
|
|
245
|
+
}
|
|
246
|
+
return streamRaw({ url, headers, params: config.params, timeout: config.timeout });
|
|
247
|
+
};
|
|
248
|
+
exports.getStream = getStream;
|
|
175
249
|
const buildUrl = (url, baseURL, params) => {
|
|
176
250
|
const parsedUrl = new URL(resolveUrl(url, baseURL));
|
|
177
251
|
if (params) {
|
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,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
import type { trackType } from '../types';
|
|
4
|
+
export interface StreamTrackOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Resume from this many bytes already on disk. Rounded **down** to a 2048-byte
|
|
7
|
+
* boundary so stripe decryption stays aligned; the returned `startedAt` tells
|
|
8
|
+
* you where the stream actually begins.
|
|
9
|
+
*/
|
|
10
|
+
resumeFrom?: number;
|
|
11
|
+
/** progress callback — `(bytesReceived, totalBytes)`; `total` is 0 when unknown */
|
|
12
|
+
onProgress?: (received: number, total: number) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface TrackStream {
|
|
15
|
+
/** decrypted audio bytes, ready to pipe to a file or a tag muxer */
|
|
16
|
+
stream: Readable;
|
|
17
|
+
/** total size of the (decrypted) file in bytes, or 0 if Deezer didn't say */
|
|
18
|
+
size: number;
|
|
19
|
+
/** byte offset the stream starts at (0, or the aligned `resumeFrom`) */
|
|
20
|
+
startedAt: number;
|
|
21
|
+
/** `false` for `cipher: NONE` content — the stream is the raw file */
|
|
22
|
+
isEncrypted: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Download a track as a **stream** of decrypted audio — `get_url` → CDN fetch →
|
|
26
|
+
* stripe-decrypt transform → your sink. Peak memory is ~one 2048-byte stripe
|
|
27
|
+
* regardless of file size or how many run concurrently.
|
|
28
|
+
*
|
|
29
|
+
* Buffer-and-tag still works: `pipeline(ts.stream, fs.createWriteStream(tmp))`,
|
|
30
|
+
* then read the temp file back for `addTrackTags`. Streaming the tag write
|
|
31
|
+
* itself (especially FLAC) is not done yet.
|
|
32
|
+
*
|
|
33
|
+
* @throws the same `WrongLicense` / `GeoBlocked` / `ExpiredTrackToken` /
|
|
34
|
+
* `DeezerError` as `getTrackDownloadUrl`, plus `Error('unavailable')` when the
|
|
35
|
+
* track+quality can't be resolved at all.
|
|
36
|
+
*/
|
|
37
|
+
export declare const streamTrackDownload: (track: trackType, quality: number, options?: StreamTrackOptions) => Promise<TrackStream>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.streamTrackDownload = void 0;
|
|
4
|
+
const stream_1 = require("stream");
|
|
5
|
+
const decrypt_1 = require("./decrypt");
|
|
6
|
+
const http_1 = require("./http");
|
|
7
|
+
const get_url_1 = require("./get-url");
|
|
8
|
+
const CHUNK = 2048;
|
|
9
|
+
/**
|
|
10
|
+
* Download a track as a **stream** of decrypted audio — `get_url` → CDN fetch →
|
|
11
|
+
* stripe-decrypt transform → your sink. Peak memory is ~one 2048-byte stripe
|
|
12
|
+
* regardless of file size or how many run concurrently.
|
|
13
|
+
*
|
|
14
|
+
* Buffer-and-tag still works: `pipeline(ts.stream, fs.createWriteStream(tmp))`,
|
|
15
|
+
* then read the temp file back for `addTrackTags`. Streaming the tag write
|
|
16
|
+
* itself (especially FLAC) is not done yet.
|
|
17
|
+
*
|
|
18
|
+
* @throws the same `WrongLicense` / `GeoBlocked` / `ExpiredTrackToken` /
|
|
19
|
+
* `DeezerError` as `getTrackDownloadUrl`, plus `Error('unavailable')` when the
|
|
20
|
+
* track+quality can't be resolved at all.
|
|
21
|
+
*/
|
|
22
|
+
const streamTrackDownload = async (track, quality, options = {}) => {
|
|
23
|
+
const resolved = await (0, get_url_1.getTrackDownloadUrl)(track, quality);
|
|
24
|
+
if (!resolved) {
|
|
25
|
+
throw new Error(`Track ${track.SNG_ID} is unavailable at quality ${quality}`);
|
|
26
|
+
}
|
|
27
|
+
const startedAt = options.resumeFrom ? Math.floor(options.resumeFrom / CHUNK) * CHUNK : 0;
|
|
28
|
+
const { stream: raw, headers } = await (0, http_1.getStream)(resolved.trackUrl, { rangeStart: startedAt });
|
|
29
|
+
const contentLength = Number(headers['content-length']) || 0;
|
|
30
|
+
const size = resolved.fileSize || (contentLength ? contentLength + startedAt : 0);
|
|
31
|
+
let received = startedAt;
|
|
32
|
+
if (options.onProgress) {
|
|
33
|
+
const meter = new stream_1.PassThrough();
|
|
34
|
+
raw.on('data', (c) => {
|
|
35
|
+
var _a;
|
|
36
|
+
received += c.length;
|
|
37
|
+
(_a = options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, received, size);
|
|
38
|
+
});
|
|
39
|
+
raw.pipe(meter);
|
|
40
|
+
return {
|
|
41
|
+
stream: resolved.isEncrypted ? meter.pipe((0, decrypt_1.createDecryptStream)(track.SNG_ID, startedAt / CHUNK)) : meter,
|
|
42
|
+
size,
|
|
43
|
+
startedAt,
|
|
44
|
+
isEncrypted: resolved.isEncrypted,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
stream: resolved.isEncrypted ? raw.pipe((0, decrypt_1.createDecryptStream)(track.SNG_ID, startedAt / CHUNK)) : raw,
|
|
49
|
+
size,
|
|
50
|
+
startedAt,
|
|
51
|
+
isEncrypted: resolved.isEncrypted,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
exports.streamTrackDownload = streamTrackDownload;
|