gerdur-core 2.8.0 → 2.10.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 +30 -0
- package/README.md +29 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/podcast.d.ts +16 -0
- package/dist/api/podcast.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/get-url.d.ts +23 -2
- package/dist/lib/get-url.js +47 -14
- package/dist/lib/session-augment.d.ts +22 -0
- package/dist/lib/session-augment.js +26 -0
- package/dist/lib/stream-download.d.ts +11 -0
- package/dist/lib/stream-download.js +26 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.10.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
Leftovers — batch token refresh + podcast episodes. Additive.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`refreshTrackTokens(tracks, {graceSeconds?, session?})`** — one
|
|
10
|
+
`song.getListData` request refreshes every `TRACK_TOKEN` that has expired (or
|
|
11
|
+
is about to). Run a long playlist through this before `resolveDownloadUrls` so
|
|
12
|
+
it doesn't die on stale tokens at track 40. Also `session.refreshTrackTokens`.
|
|
13
|
+
- **`getEpisode(episodeId)`** (`episode.getData`) and **`getShowEpisodes(showId,
|
|
14
|
+
nb?, start?)`** — podcast episodes; `EPISODE_DIRECT_STREAM_URL` is a plain MP3
|
|
15
|
+
(no licence, no decryption).
|
|
16
|
+
|
|
17
|
+
## 2.9.0 - 2026-08-31
|
|
18
|
+
|
|
19
|
+
Phase 3.2 (cont.) — downloads are session-aware. Additive.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`Session.getTrackDownloadUrl` / `resolveDownloadUrls` / `streamTrack` /
|
|
24
|
+
`getTrackBuffer`** — resolve and download **as a specific account**, so a
|
|
25
|
+
`createSession(arl)` client is now end-to-end usable.
|
|
26
|
+
- **`downloadTrackBuffer(track, quality, options?)`** — download + decrypt a
|
|
27
|
+
track fully into a `Buffer` (no tagging), free-function form of
|
|
28
|
+
`session.getTrackBuffer`.
|
|
29
|
+
- `getTrackDownloadUrl(track, quality, session?)`, `resolveDownloadUrls(tracks,
|
|
30
|
+
qualities?, session?)` and `streamTrackDownload(track, quality, {session})`
|
|
31
|
+
gained an optional session argument (defaults to the process default).
|
|
32
|
+
|
|
3
33
|
## 2.8.0 - 2026-08-31
|
|
4
34
|
|
|
5
35
|
Phase 3.2 (cont.) — a `Session` is now usable per-account. Additive; free
|
package/README.md
CHANGED
|
@@ -127,9 +127,18 @@ responses carry account-specific `TRACK_TOKEN`s) and its own query methods:
|
|
|
127
127
|
| `getArtistInfo(id)` / `getDiscography(id, nb?)` | artist metadata / discography |
|
|
128
128
|
| `getProfile(userId)` | a public profile |
|
|
129
129
|
| `searchMusic(query, types?, nb?)` | search (`deezer.pageSearch`) |
|
|
130
|
+
| `getTrackDownloadUrl(track, quality)` | resolve a CDN URL **as this account** |
|
|
131
|
+
| `resolveDownloadUrls(tracks, qualities?)` | batch-resolve, one request, as this account |
|
|
132
|
+
| `streamTrack(track, quality, opts?)` | constant-memory stream of decrypted audio |
|
|
133
|
+
| `getTrackBuffer(track, quality, opts?)` | download + decrypt fully into a `Buffer` |
|
|
130
134
|
| `gw(body, method)` / `gwLight(body, method)` / `gwGet(method, params?)` | the raw coalesced request channels |
|
|
131
135
|
| `init(arl?)` / `refreshApiToken()` / `loadUserData(force?)` / `invalidateUserData()` | lifecycle |
|
|
132
136
|
|
|
137
|
+
The free `getTrackDownloadUrl(track, quality, session?)` / `resolveDownloadUrls(…, session?)`
|
|
138
|
+
/ `streamTrackDownload(…, {session})` all take an optional session too;
|
|
139
|
+
`downloadTrackBuffer(track, quality, opts?)` is the free-function form of
|
|
140
|
+
`session.getTrackBuffer`.
|
|
141
|
+
|
|
133
142
|
```js
|
|
134
143
|
await initDeezerApi(arl); // default session, as before
|
|
135
144
|
const track = await getTrackInfo('3135556');
|
|
@@ -312,6 +321,13 @@ const {data: loved} = await getUserFavoriteTracks(me.USER_ID);
|
|
|
312
321
|
const {data: eighties} = await getRadioTracks(38305); // "The '80s"
|
|
313
322
|
```
|
|
314
323
|
|
|
324
|
+
### Podcasts
|
|
325
|
+
|
|
326
|
+
| Method | |
|
|
327
|
+
| :--- | :--- |
|
|
328
|
+
| `getShowEpisodes(showId, nb = 25, start = 0)` | a page of a show's episodes, newest first |
|
|
329
|
+
| `getEpisode(episodeId)` | one episode — `EPISODE_DIRECT_STREAM_URL` is a plain MP3, no licence / decryption |
|
|
330
|
+
|
|
315
331
|
### `.getTrackDownloadUrl(track, quality);`
|
|
316
332
|
|
|
317
333
|
| Parameters | Required | Type | Description |
|
|
@@ -322,6 +338,19 @@ const {data: eighties} = await getRadioTracks(38305); // "The '80s"
|
|
|
322
338
|
Resolves `{trackUrl, isEncrypted, fileSize}`. `isEncrypted` now comes from the
|
|
323
339
|
media API's `cipher` field (authoritative) rather than a URL guess.
|
|
324
340
|
|
|
341
|
+
### `.refreshTrackTokens(tracks, options?);`
|
|
342
|
+
|
|
343
|
+
`TRACK_TOKEN`s live ~1 hour, so a token fetched at the start of a long playlist
|
|
344
|
+
download is dead by track 40 (surfacing as an opaque CDN 403). Run the selection
|
|
345
|
+
through this first — **one** `song.getListData` request refreshes every token
|
|
346
|
+
that has expired (or expires within `options.graceSeconds`, default 300).
|
|
347
|
+
Tracks with a still-valid token come back untouched. Also `session.refreshTrackTokens(tracks, graceSeconds?)`.
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
const fresh = await refreshTrackTokens(playlist.tracks);
|
|
351
|
+
const urls = await resolveDownloadUrls(fresh, [9, 3, 1]);
|
|
352
|
+
```
|
|
353
|
+
|
|
325
354
|
### Formats
|
|
326
355
|
|
|
327
356
|
Deezer's `get_url` understands more than `1 / 3 / 9`. `DEEZER_FORMATS` lists them
|
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { publicApiList, showEpisodeType } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* One podcast episode (`episode.getData`). Carries `EPISODE_DIRECT_STREAM_URL`
|
|
4
|
+
* (a plain MP3 — no licence, no decryption) plus `MD5_ORIGIN` / `FILESIZE_MP3_*`
|
|
5
|
+
* / `TRACK_TOKEN` for the licensed stream.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getEpisode: (episodeId: string) => Promise<showEpisodeType>;
|
|
8
|
+
/**
|
|
9
|
+
* A page of a show's episodes, newest first — a thin view over `getShowInfo`'s
|
|
10
|
+
* `EPISODES` block.
|
|
11
|
+
*
|
|
12
|
+
* @param showId `SHOW_ID`
|
|
13
|
+
* @param nb page size (default 25)
|
|
14
|
+
* @param start offset
|
|
15
|
+
*/
|
|
16
|
+
export declare const getShowEpisodes: (showId: string, nb?: number, start?: number) => Promise<publicApiList<showEpisodeType>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getShowEpisodes = exports.getEpisode = void 0;
|
|
4
|
+
const request_1 = require("./request");
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
/**
|
|
7
|
+
* One podcast episode (`episode.getData`). Carries `EPISODE_DIRECT_STREAM_URL`
|
|
8
|
+
* (a plain MP3 — no licence, no decryption) plus `MD5_ORIGIN` / `FILESIZE_MP3_*`
|
|
9
|
+
* / `TRACK_TOKEN` for the licensed stream.
|
|
10
|
+
*/
|
|
11
|
+
const getEpisode = (episodeId) => (0, request_1.request)({ episode_id: episodeId }, 'episode.getData');
|
|
12
|
+
exports.getEpisode = getEpisode;
|
|
13
|
+
/**
|
|
14
|
+
* A page of a show's episodes, newest first — a thin view over `getShowInfo`'s
|
|
15
|
+
* `EPISODES` block.
|
|
16
|
+
*
|
|
17
|
+
* @param showId `SHOW_ID`
|
|
18
|
+
* @param nb page size (default 25)
|
|
19
|
+
* @param start offset
|
|
20
|
+
*/
|
|
21
|
+
const getShowEpisodes = async (showId, nb = 25, start = 0) => {
|
|
22
|
+
const { EPISODES } = await (0, api_1.getShowInfo)(showId, nb, start);
|
|
23
|
+
return { data: EPISODES.data, total: EPISODES.total };
|
|
24
|
+
};
|
|
25
|
+
exports.getShowEpisodes = getShowEpisodes;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.getStream = exports.getText = exports.getJson = exports.getBuffer = exports.httpsAgent = exports.httpAgent = exports.DeezerError = exports.DEFAULT_ARL = exports.RETRY_POLICY = exports.Session = exports.defaultSession = exports.createSession = exports.initDeezerApi = void 0;
|
|
18
|
+
require("./lib/session-augment"); // wires the download methods onto Session.prototype
|
|
18
19
|
var session_1 = require("./lib/session");
|
|
19
20
|
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return session_1.initDeezerApi; } });
|
|
20
21
|
Object.defineProperty(exports, "createSession", { enumerable: true, get: function () { return session_1.createSession; } });
|
package/dist/lib/get-url.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Session } from '../lib/session';
|
|
1
2
|
import type { trackType } from '../types';
|
|
2
3
|
export declare class WrongLicense extends Error {
|
|
3
4
|
constructor(format: string);
|
|
@@ -34,8 +35,9 @@ export declare const formatName: (quality: Quality) => string;
|
|
|
34
35
|
/**
|
|
35
36
|
* @param track Track info json returned from `getTrackInfo`
|
|
36
37
|
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
|
38
|
+
* @param session which account to resolve for — defaults to the process default
|
|
37
39
|
*/
|
|
38
|
-
export declare const getTrackDownloadUrl: (track: trackType, quality: number) => Promise<{
|
|
40
|
+
export declare const getTrackDownloadUrl: (track: trackType, quality: number, session?: Session) => Promise<{
|
|
39
41
|
trackUrl: string;
|
|
40
42
|
isEncrypted: boolean;
|
|
41
43
|
fileSize: number;
|
|
@@ -60,5 +62,24 @@ export interface ResolvedUrl {
|
|
|
60
62
|
*
|
|
61
63
|
* @param tracks from `getTrackInfo` / `parseInfo` (needs `TRACK_TOKEN`, `SNG_ID`, `FILESIZE_*`)
|
|
62
64
|
* @param qualities preference order — default `[9, 3, 1]` (FLAC → MP3 320 → MP3 128)
|
|
65
|
+
* @param session which account to resolve for — defaults to the process default
|
|
63
66
|
*/
|
|
64
|
-
export declare const resolveDownloadUrls: (tracks: trackType[], qualities?: Quality[]) => Promise<(ResolvedUrl | null)[]>;
|
|
67
|
+
export declare const resolveDownloadUrls: (tracks: trackType[], qualities?: Quality[], session?: Session) => Promise<(ResolvedUrl | null)[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Batch-refresh `TRACK_TOKEN`s that have (or are about to) expire — one
|
|
70
|
+
* `song.getListData` request for the lot, instead of a `getTrackInfo` per track.
|
|
71
|
+
* `TRACK_TOKEN`s live ~1 hour, so a token fetched at the start of a long
|
|
72
|
+
* playlist download is dead by track 40 and surfaces as an opaque CDN 403; run
|
|
73
|
+
* the selection through this first.
|
|
74
|
+
*
|
|
75
|
+
* Returns the input tracks with fresh `TRACK_TOKEN` / `TRACK_TOKEN_EXPIRE`
|
|
76
|
+
* merged in; tracks whose token is still comfortably valid are returned as-is.
|
|
77
|
+
*
|
|
78
|
+
* @param tracks from `getTrackInfo` / `getAlbumTracks` / `parseInfo`
|
|
79
|
+
* @param options `graceSeconds` — treat a token expiring within this window as
|
|
80
|
+
* stale (default 300); `session` — which account (default: process default)
|
|
81
|
+
*/
|
|
82
|
+
export declare const refreshTrackTokens: (tracks: trackType[], options?: {
|
|
83
|
+
graceSeconds?: number;
|
|
84
|
+
session?: Session;
|
|
85
|
+
}) => Promise<trackType[]>;
|
package/dist/lib/get-url.js
CHANGED
|
@@ -3,12 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.formatName = exports.toFormat = exports.DEEZER_FORMATS = exports.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
6
|
+
exports.refreshTrackTokens = exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.formatName = exports.toFormat = exports.DEEZER_FORMATS = exports.ExpiredTrackToken = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
7
7
|
const delay_1 = __importDefault(require("delay"));
|
|
8
8
|
const decrypt_1 = require("../lib/decrypt");
|
|
9
9
|
const errors_1 = require("../lib/errors");
|
|
10
10
|
const http_1 = require("../lib/http");
|
|
11
|
-
const request_1 = __importDefault(require("../lib/request"));
|
|
12
11
|
const session_1 = require("../lib/session");
|
|
13
12
|
class WrongLicense extends Error {
|
|
14
13
|
constructor(format) {
|
|
@@ -92,11 +91,11 @@ const MEDIA_MAX_RETRIES = 3;
|
|
|
92
91
|
const formatName = (quality) => (0, exports.toFormat)(quality);
|
|
93
92
|
exports.formatName = formatName;
|
|
94
93
|
/** POST media.deezer.com/v1/get_url with re-auth + exponential-backoff retry on 403/429/5xx. */
|
|
95
|
-
const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
|
|
94
|
+
const mediaGetUrl = async (session, track_tokens, formats, attempt = 0) => {
|
|
96
95
|
var _a;
|
|
97
|
-
const user = await
|
|
96
|
+
const user = await session.loadUserData();
|
|
98
97
|
try {
|
|
99
|
-
const { data } = await
|
|
98
|
+
const { data } = await session.request('POST', 'https://media.deezer.com/v1/get_url', {
|
|
100
99
|
license_token: user.licenseToken,
|
|
101
100
|
media: [{ type: 'FULL', formats }],
|
|
102
101
|
track_tokens,
|
|
@@ -107,9 +106,9 @@ const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
|
|
|
107
106
|
const status = err instanceof http_1.HttpStatusError ? err.statusCode : 0;
|
|
108
107
|
if ((status === 403 || status === 429 || status >= 500) && attempt < MEDIA_MAX_RETRIES) {
|
|
109
108
|
// a stale license token is a common cause — force a refresh before the retry
|
|
110
|
-
await
|
|
109
|
+
await session.loadUserData(true);
|
|
111
110
|
await (0, delay_1.default)(500 * 2 ** attempt + Math.floor(Math.random() * 250));
|
|
112
|
-
return mediaGetUrl(track_tokens, formats, attempt + 1);
|
|
111
|
+
return mediaGetUrl(session, track_tokens, formats, attempt + 1);
|
|
113
112
|
}
|
|
114
113
|
throw err;
|
|
115
114
|
}
|
|
@@ -132,12 +131,12 @@ const parseMediaEntry = (entry, token, country) => {
|
|
|
132
131
|
};
|
|
133
132
|
/** Whether a track fetched with the given cipher needs Blowfish stripe decryption. */
|
|
134
133
|
const cipherIsEncrypted = (cipher, url) => cipher ? cipher !== 'NONE' : url.includes('/mobile/') || url.includes('/media/');
|
|
135
|
-
const getTrackUrlFromServer = async (track_token, format) => {
|
|
136
|
-
const user = await
|
|
134
|
+
const getTrackUrlFromServer = async (session, track_token, format) => {
|
|
135
|
+
const user = await session.loadUserData();
|
|
137
136
|
if ((format === 'FLAC' && !user.canStreamLossless) || (format === 'MP3_320' && !user.canStreamHq)) {
|
|
138
137
|
throw new WrongLicense(format);
|
|
139
138
|
}
|
|
140
|
-
const { data, country } = await mediaGetUrl([track_token], [{ format, cipher: 'BF_CBC_STRIPE' }]);
|
|
139
|
+
const { data, country } = await mediaGetUrl(session, [track_token], [{ format, cipher: 'BF_CBC_STRIPE' }]);
|
|
141
140
|
if (!data.length)
|
|
142
141
|
return null;
|
|
143
142
|
return parseMediaEntry(data[0], track_token, country);
|
|
@@ -145,8 +144,9 @@ const getTrackUrlFromServer = async (track_token, format) => {
|
|
|
145
144
|
/**
|
|
146
145
|
* @param track Track info json returned from `getTrackInfo`
|
|
147
146
|
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
|
147
|
+
* @param session which account to resolve for — defaults to the process default
|
|
148
148
|
*/
|
|
149
|
-
const getTrackDownloadUrl = async (track, quality) => {
|
|
149
|
+
const getTrackDownloadUrl = async (track, quality, session = (0, session_1.defaultSession)()) => {
|
|
150
150
|
let wrongLicense = null;
|
|
151
151
|
let geoBlocked = null;
|
|
152
152
|
let expiredToken = null;
|
|
@@ -161,7 +161,7 @@ const getTrackDownloadUrl = async (track, quality) => {
|
|
|
161
161
|
else {
|
|
162
162
|
// Get URL with the official API
|
|
163
163
|
try {
|
|
164
|
-
const resolved = await getTrackUrlFromServer(track.TRACK_TOKEN, format);
|
|
164
|
+
const resolved = await getTrackUrlFromServer(session, track.TRACK_TOKEN, format);
|
|
165
165
|
if (resolved) {
|
|
166
166
|
return {
|
|
167
167
|
trackUrl: resolved.url,
|
|
@@ -237,12 +237,13 @@ const testUrl = async (url) => {
|
|
|
237
237
|
*
|
|
238
238
|
* @param tracks from `getTrackInfo` / `parseInfo` (needs `TRACK_TOKEN`, `SNG_ID`, `FILESIZE_*`)
|
|
239
239
|
* @param qualities preference order — default `[9, 3, 1]` (FLAC → MP3 320 → MP3 128)
|
|
240
|
+
* @param session which account to resolve for — defaults to the process default
|
|
240
241
|
*/
|
|
241
|
-
const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
|
|
242
|
+
const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1], session = (0, session_1.defaultSession)()) => {
|
|
242
243
|
if (!tracks.length)
|
|
243
244
|
return [];
|
|
244
245
|
const formats = qualities.map((q) => ({ format: (0, exports.formatName)(q), cipher: 'BF_CBC_STRIPE' }));
|
|
245
|
-
const { data, country } = await mediaGetUrl(tracks.map((t) => t.TRACK_TOKEN), formats);
|
|
246
|
+
const { data, country } = await mediaGetUrl(session, tracks.map((t) => t.TRACK_TOKEN), formats);
|
|
246
247
|
return tracks.map((track, i) => {
|
|
247
248
|
let parsed;
|
|
248
249
|
try {
|
|
@@ -264,3 +265,35 @@ const resolveDownloadUrls = async (tracks, qualities = [9, 3, 1]) => {
|
|
|
264
265
|
});
|
|
265
266
|
};
|
|
266
267
|
exports.resolveDownloadUrls = resolveDownloadUrls;
|
|
268
|
+
/**
|
|
269
|
+
* Batch-refresh `TRACK_TOKEN`s that have (or are about to) expire — one
|
|
270
|
+
* `song.getListData` request for the lot, instead of a `getTrackInfo` per track.
|
|
271
|
+
* `TRACK_TOKEN`s live ~1 hour, so a token fetched at the start of a long
|
|
272
|
+
* playlist download is dead by track 40 and surfaces as an opaque CDN 403; run
|
|
273
|
+
* the selection through this first.
|
|
274
|
+
*
|
|
275
|
+
* Returns the input tracks with fresh `TRACK_TOKEN` / `TRACK_TOKEN_EXPIRE`
|
|
276
|
+
* merged in; tracks whose token is still comfortably valid are returned as-is.
|
|
277
|
+
*
|
|
278
|
+
* @param tracks from `getTrackInfo` / `getAlbumTracks` / `parseInfo`
|
|
279
|
+
* @param options `graceSeconds` — treat a token expiring within this window as
|
|
280
|
+
* stale (default 300); `session` — which account (default: process default)
|
|
281
|
+
*/
|
|
282
|
+
const refreshTrackTokens = async (tracks, options = {}) => {
|
|
283
|
+
var _a, _b;
|
|
284
|
+
const session = (_a = options.session) !== null && _a !== void 0 ? _a : (0, session_1.defaultSession)();
|
|
285
|
+
const graceMs = ((_b = options.graceSeconds) !== null && _b !== void 0 ? _b : 300) * 1000;
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const staleIds = new Set(tracks
|
|
288
|
+
.filter((t) => !t.TRACK_TOKEN || !t.TRACK_TOKEN_EXPIRE || t.TRACK_TOKEN_EXPIRE * 1000 - now < graceMs)
|
|
289
|
+
.map((t) => t.SNG_ID));
|
|
290
|
+
if (!staleIds.size)
|
|
291
|
+
return tracks;
|
|
292
|
+
const { data } = await session.gw({ sng_ids: [...staleIds] }, 'song.getListData');
|
|
293
|
+
const fresh = new Map(data.map((d) => [d.SNG_ID, d]));
|
|
294
|
+
return tracks.map((track) => {
|
|
295
|
+
const f = fresh.get(track.SNG_ID);
|
|
296
|
+
return f ? { ...track, TRACK_TOKEN: f.TRACK_TOKEN, TRACK_TOKEN_EXPIRE: Number(f.TRACK_TOKEN_EXPIRE) } : track;
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
exports.refreshTrackTokens = refreshTrackTokens;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { Quality, ResolvedUrl } from './get-url';
|
|
3
|
+
import type { StreamTrackOptions, TrackStream } from './stream-download';
|
|
4
|
+
import type { trackType } from '../types';
|
|
5
|
+
declare module './session' {
|
|
6
|
+
interface Session {
|
|
7
|
+
/** Resolve a downloadable URL for a track **as this account** (`1 | 3 | 9`). */
|
|
8
|
+
getTrackDownloadUrl(track: trackType, quality: number): Promise<{
|
|
9
|
+
trackUrl: string;
|
|
10
|
+
isEncrypted: boolean;
|
|
11
|
+
fileSize: number;
|
|
12
|
+
} | null>;
|
|
13
|
+
/** Batch-resolve download URLs for many tracks in one request, as this account. */
|
|
14
|
+
resolveDownloadUrls(tracks: trackType[], qualities?: Quality[]): Promise<(ResolvedUrl | null)[]>;
|
|
15
|
+
/** Download a track as a constant-memory stream of decrypted audio, as this account. */
|
|
16
|
+
streamTrack(track: trackType, quality: number, options?: Omit<StreamTrackOptions, 'session'>): Promise<TrackStream>;
|
|
17
|
+
/** Download + decrypt a track fully into a `Buffer`, as this account. */
|
|
18
|
+
getTrackBuffer(track: trackType, quality: number, options?: Omit<StreamTrackOptions, 'session' | 'resumeFrom'>): Promise<Buffer | null>;
|
|
19
|
+
/** Batch-refresh the `TRACK_TOKEN`s on these tracks (as this account) before a long download. */
|
|
20
|
+
refreshTrackTokens(tracks: trackType[], graceSeconds?: number): Promise<trackType[]>;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Download methods on {@link Session}, kept in a separate module so `session.ts`
|
|
5
|
+
* doesn't have to import the download stack (which imports `session.ts` back).
|
|
6
|
+
* `import`ing this file for its side effect wires the methods onto the prototype;
|
|
7
|
+
* `src/index.ts` does that.
|
|
8
|
+
*/
|
|
9
|
+
const get_url_1 = require("./get-url");
|
|
10
|
+
const session_1 = require("./session");
|
|
11
|
+
const stream_download_1 = require("./stream-download");
|
|
12
|
+
session_1.Session.prototype.getTrackDownloadUrl = function (track, quality) {
|
|
13
|
+
return (0, get_url_1.getTrackDownloadUrl)(track, quality, this);
|
|
14
|
+
};
|
|
15
|
+
session_1.Session.prototype.resolveDownloadUrls = function (tracks, qualities) {
|
|
16
|
+
return (0, get_url_1.resolveDownloadUrls)(tracks, qualities, this);
|
|
17
|
+
};
|
|
18
|
+
session_1.Session.prototype.streamTrack = function (track, quality, options = {}) {
|
|
19
|
+
return (0, stream_download_1.streamTrackDownload)(track, quality, { ...options, session: this });
|
|
20
|
+
};
|
|
21
|
+
session_1.Session.prototype.getTrackBuffer = function (track, quality, options = {}) {
|
|
22
|
+
return (0, stream_download_1.downloadTrackBuffer)(track, quality, { ...options, session: this });
|
|
23
|
+
};
|
|
24
|
+
session_1.Session.prototype.refreshTrackTokens = function (tracks, graceSeconds) {
|
|
25
|
+
return (0, get_url_1.refreshTrackTokens)(tracks, { graceSeconds, session: this });
|
|
26
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
2
3
|
import { Readable } from 'stream';
|
|
4
|
+
import type { Session } from './session';
|
|
3
5
|
import type { trackType } from '../types';
|
|
4
6
|
export interface StreamTrackOptions {
|
|
5
7
|
/**
|
|
@@ -10,6 +12,8 @@ export interface StreamTrackOptions {
|
|
|
10
12
|
resumeFrom?: number;
|
|
11
13
|
/** progress callback — `(bytesReceived, totalBytes)`; `total` is 0 when unknown */
|
|
12
14
|
onProgress?: (received: number, total: number) => void;
|
|
15
|
+
/** which account to download as — defaults to the process default session */
|
|
16
|
+
session?: Session;
|
|
13
17
|
}
|
|
14
18
|
export interface TrackStream {
|
|
15
19
|
/** decrypted audio bytes, ready to pipe to a file or a tag muxer */
|
|
@@ -35,3 +39,10 @@ export interface TrackStream {
|
|
|
35
39
|
* track+quality can't be resolved at all.
|
|
36
40
|
*/
|
|
37
41
|
export declare const streamTrackDownload: (track: trackType, quality: number, options?: StreamTrackOptions) => Promise<TrackStream>;
|
|
42
|
+
/**
|
|
43
|
+
* Download + decrypt a track fully into memory. Convenience over
|
|
44
|
+
* {@link streamTrackDownload} for callers that just want the bytes (no tagging —
|
|
45
|
+
* pipe through `addTrackTags` yourself). `null` when the track+quality can't be
|
|
46
|
+
* resolved. Does **not** support resume.
|
|
47
|
+
*/
|
|
48
|
+
export declare const downloadTrackBuffer: (track: trackType, quality: number, options?: Omit<StreamTrackOptions, 'resumeFrom'>) => Promise<Buffer | null>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.streamTrackDownload = void 0;
|
|
3
|
+
exports.downloadTrackBuffer = exports.streamTrackDownload = void 0;
|
|
4
4
|
const stream_1 = require("stream");
|
|
5
5
|
const decrypt_1 = require("./decrypt");
|
|
6
6
|
const http_1 = require("./http");
|
|
@@ -20,7 +20,7 @@ const CHUNK = 2048;
|
|
|
20
20
|
* track+quality can't be resolved at all.
|
|
21
21
|
*/
|
|
22
22
|
const streamTrackDownload = async (track, quality, options = {}) => {
|
|
23
|
-
const resolved = await (0, get_url_1.getTrackDownloadUrl)(track, quality);
|
|
23
|
+
const resolved = await (0, get_url_1.getTrackDownloadUrl)(track, quality, options.session);
|
|
24
24
|
if (!resolved) {
|
|
25
25
|
throw new Error(`Track ${track.SNG_ID} is unavailable at quality ${quality}`);
|
|
26
26
|
}
|
|
@@ -52,3 +52,27 @@ const streamTrackDownload = async (track, quality, options = {}) => {
|
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
54
|
exports.streamTrackDownload = streamTrackDownload;
|
|
55
|
+
/**
|
|
56
|
+
* Download + decrypt a track fully into memory. Convenience over
|
|
57
|
+
* {@link streamTrackDownload} for callers that just want the bytes (no tagging —
|
|
58
|
+
* pipe through `addTrackTags` yourself). `null` when the track+quality can't be
|
|
59
|
+
* resolved. Does **not** support resume.
|
|
60
|
+
*/
|
|
61
|
+
const downloadTrackBuffer = async (track, quality, options = {}) => {
|
|
62
|
+
let ts;
|
|
63
|
+
try {
|
|
64
|
+
ts = await (0, exports.streamTrackDownload)(track, quality, { onProgress: options.onProgress, session: options.session });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err instanceof Error && /unavailable at quality/.test(err.message)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
const chunks = [];
|
|
73
|
+
for await (const chunk of ts.stream) {
|
|
74
|
+
chunks.push(chunk);
|
|
75
|
+
}
|
|
76
|
+
return Buffer.concat(chunks);
|
|
77
|
+
};
|
|
78
|
+
exports.downloadTrackBuffer = downloadTrackBuffer;
|