gerdur-core 2.6.0 → 2.7.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 +27 -0
- package/README.md +26 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -4
- package/dist/lib/get-url.js +7 -22
- package/dist/lib/request.d.ts +6 -20
- package/dist/lib/request.js +17 -117
- package/dist/lib/session.d.ts +112 -0
- package/dist/lib/session.js +238 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.7.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
Phase 3.2 — session lifecycle. The scattered module-level state
|
|
6
|
+
(`request.ts`: `arl` / `sid` / `api_token`; `get-url.ts`: `license_token` /
|
|
7
|
+
`country` / streaming rights) is consolidated into one **`Session`** object.
|
|
8
|
+
Backwards compatible — `initDeezerApi` and every free function are unchanged.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`Session`** — owns one account's `arl`, HTTP client (`sid` / `api_token`),
|
|
13
|
+
`license_token` / `country` / `canStreamLossless` / `canStreamHq`, the
|
|
14
|
+
bounded-retry request loop, and token refresh (`init`, `refreshApiToken`,
|
|
15
|
+
`loadUserData`).
|
|
16
|
+
- **`createSession(arl?)`** — an isolated session you hold and inspect, so
|
|
17
|
+
multiple accounts can be used concurrently.
|
|
18
|
+
- **`defaultSession()`** — the process-wide session `initDeezerApi` and the free
|
|
19
|
+
functions run against. `setDefaultSession(session)` swaps it.
|
|
20
|
+
- `SessionUserData` type; `DEFAULT_ARL` constant.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Proactive `license_token` refresh** — `loadUserData()` caches the account's
|
|
25
|
+
media-API credentials for 25 min and force-refreshes on a media 403, instead of
|
|
26
|
+
only re-fetching after a failed download. Fewer opaque CDN 403s on long
|
|
27
|
+
playlists.
|
|
28
|
+
- `RETRY_POLICY` now lives in `lib/session.ts` (still exported, same shape).
|
|
29
|
+
|
|
3
30
|
## 2.6.0 - 2026-08-31
|
|
4
31
|
|
|
5
32
|
Phase 2.4 round 2 — Flow, radios, and a user's library. All public REST, all
|
package/README.md
CHANGED
|
@@ -97,6 +97,32 @@ Retries are bounded — `RETRY_POLICY` (exported) sets per-class attempt caps an
|
|
|
97
97
|
`DeezerError` instead of spinning. `GeoBlocked`, `WrongLicense` and
|
|
98
98
|
`ExpiredTrackToken` are still thrown as their own types from the download path.
|
|
99
99
|
|
|
100
|
+
### Sessions
|
|
101
|
+
|
|
102
|
+
A **`Session`** owns one account's state — the `arl`, the HTTP client (`sid` /
|
|
103
|
+
`api_token`), and the account's `license_token` / `country` / streaming rights,
|
|
104
|
+
plus the bounded-retry loop and token refresh. This state used to be
|
|
105
|
+
module-level globals; bundling it means multiple accounts can coexist.
|
|
106
|
+
|
|
107
|
+
- **`initDeezerApi(arl)`** — (re)authenticate the **default** session. Every free
|
|
108
|
+
function (`getTrackInfo`, `searchMusic`, `getTrackDownloadUrl`, …) runs against
|
|
109
|
+
it. Unchanged: same signature, returns the gateway `SESSION` id.
|
|
110
|
+
- **`createSession(arl?)`** — an **isolated** session you hold and inspect:
|
|
111
|
+
`session.arl`, `session.sid`, and — after `await session.loadUserData()` —
|
|
112
|
+
`session.country`, `session.licenseToken`, `session.canStreamLossless`,
|
|
113
|
+
`session.canStreamHq`. `loadUserData()` caches for 25 min and refreshes on a
|
|
114
|
+
media-API 403.
|
|
115
|
+
- **`defaultSession()`** — the `Session` the free functions use.
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
await initDeezerApi(arl); // default session, as before
|
|
119
|
+
const track = await getTrackInfo('3135556');
|
|
120
|
+
|
|
121
|
+
const s = await createSession(otherArl); // a second account, isolated
|
|
122
|
+
await s.loadUserData();
|
|
123
|
+
console.log(s.country, s.canStreamLossless);
|
|
124
|
+
```
|
|
125
|
+
|
|
100
126
|
### `.initDeezerApi(arl_cookie);`
|
|
101
127
|
|
|
102
128
|
> It is recommended that you first init the app with this method using your arl cookie.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { initDeezerApi, RETRY_POLICY } from './lib/
|
|
1
|
+
export { initDeezerApi, createSession, defaultSession, Session, RETRY_POLICY, DEFAULT_ARL } from './lib/session';
|
|
2
|
+
export type { SessionUserData } from './lib/session';
|
|
2
3
|
export { DeezerError } from './lib/errors';
|
|
3
4
|
export type { DeezerErrorPayload } from './lib/errors';
|
|
4
5
|
export * from './api';
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,14 @@ 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.getStream = exports.getText = exports.getJson = exports.getBuffer = exports.httpsAgent = exports.httpAgent = exports.DeezerError = exports.RETRY_POLICY = exports.initDeezerApi = void 0;
|
|
18
|
-
var
|
|
19
|
-
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return
|
|
20
|
-
Object.defineProperty(exports, "
|
|
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
|
+
var session_1 = require("./lib/session");
|
|
19
|
+
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return session_1.initDeezerApi; } });
|
|
20
|
+
Object.defineProperty(exports, "createSession", { enumerable: true, get: function () { return session_1.createSession; } });
|
|
21
|
+
Object.defineProperty(exports, "defaultSession", { enumerable: true, get: function () { return session_1.defaultSession; } });
|
|
22
|
+
Object.defineProperty(exports, "Session", { enumerable: true, get: function () { return session_1.Session; } });
|
|
23
|
+
Object.defineProperty(exports, "RETRY_POLICY", { enumerable: true, get: function () { return session_1.RETRY_POLICY; } });
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_ARL", { enumerable: true, get: function () { return session_1.DEFAULT_ARL; } });
|
|
21
25
|
var errors_1 = require("./lib/errors");
|
|
22
26
|
Object.defineProperty(exports, "DeezerError", { enumerable: true, get: function () { return errors_1.DeezerError; } });
|
|
23
27
|
__exportStar(require("./api"), exports);
|
package/dist/lib/get-url.js
CHANGED
|
@@ -9,6 +9,7 @@ const decrypt_1 = require("../lib/decrypt");
|
|
|
9
9
|
const errors_1 = require("../lib/errors");
|
|
10
10
|
const http_1 = require("../lib/http");
|
|
11
11
|
const request_1 = __importDefault(require("../lib/request"));
|
|
12
|
+
const session_1 = require("../lib/session");
|
|
12
13
|
class WrongLicense extends Error {
|
|
13
14
|
constructor(format) {
|
|
14
15
|
super();
|
|
@@ -37,7 +38,6 @@ class ExpiredTrackToken extends Error {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
exports.ExpiredTrackToken = ExpiredTrackToken;
|
|
40
|
-
let user_data = null;
|
|
41
41
|
/**
|
|
42
42
|
* Every audio format Deezer's `get_url` understands, best → worst. `FLAC`,
|
|
43
43
|
* `MP3_320` and `MP3_128` are the classic `9 / 3 / 1` qualities; the rest are
|
|
@@ -83,22 +83,6 @@ const getTrackFileSize = (track, quality) => {
|
|
|
83
83
|
const key = FORMAT_FILESIZE_KEY[(0, exports.toFormat)(quality)];
|
|
84
84
|
return key ? Number(track[key]) || 0 : 0;
|
|
85
85
|
};
|
|
86
|
-
const dzAuthenticate = async () => {
|
|
87
|
-
const { data } = await request_1.default.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
88
|
-
params: {
|
|
89
|
-
method: 'deezer.getUserData',
|
|
90
|
-
api_version: '1.0',
|
|
91
|
-
api_token: 'null',
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
user_data = {
|
|
95
|
-
license_token: data.results.USER.OPTIONS.license_token,
|
|
96
|
-
can_stream_lossless: data.results.USER.OPTIONS.web_lossless || data.results.USER.OPTIONS.mobile_loseless,
|
|
97
|
-
can_stream_hq: data.results.USER.OPTIONS.web_hq || data.results.USER.OPTIONS.mobile_hq,
|
|
98
|
-
country: data.results.COUNTRY,
|
|
99
|
-
};
|
|
100
|
-
return user_data;
|
|
101
|
-
};
|
|
102
86
|
const MEDIA_MAX_RETRIES = 3;
|
|
103
87
|
/**
|
|
104
88
|
* Quality code (`1 | 3 | 9`) or format string → the `format` string the media
|
|
@@ -110,10 +94,10 @@ exports.formatName = formatName;
|
|
|
110
94
|
/** POST media.deezer.com/v1/get_url with re-auth + exponential-backoff retry on 403/429/5xx. */
|
|
111
95
|
const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
|
|
112
96
|
var _a;
|
|
113
|
-
const user =
|
|
97
|
+
const user = await (0, session_1.defaultSession)().loadUserData();
|
|
114
98
|
try {
|
|
115
99
|
const { data } = await request_1.default.post('https://media.deezer.com/v1/get_url', {
|
|
116
|
-
license_token: user.
|
|
100
|
+
license_token: user.licenseToken,
|
|
117
101
|
media: [{ type: 'FULL', formats }],
|
|
118
102
|
track_tokens,
|
|
119
103
|
});
|
|
@@ -122,7 +106,8 @@ const mediaGetUrl = async (track_tokens, formats, attempt = 0) => {
|
|
|
122
106
|
catch (err) {
|
|
123
107
|
const status = err instanceof http_1.HttpStatusError ? err.statusCode : 0;
|
|
124
108
|
if ((status === 403 || status === 429 || status >= 500) && attempt < MEDIA_MAX_RETRIES) {
|
|
125
|
-
|
|
109
|
+
// a stale license token is a common cause — force a refresh before the retry
|
|
110
|
+
await (0, session_1.defaultSession)().loadUserData(true);
|
|
126
111
|
await (0, delay_1.default)(500 * 2 ** attempt + Math.floor(Math.random() * 250));
|
|
127
112
|
return mediaGetUrl(track_tokens, formats, attempt + 1);
|
|
128
113
|
}
|
|
@@ -148,8 +133,8 @@ const parseMediaEntry = (entry, token, country) => {
|
|
|
148
133
|
/** Whether a track fetched with the given cipher needs Blowfish stripe decryption. */
|
|
149
134
|
const cipherIsEncrypted = (cipher, url) => cipher ? cipher !== 'NONE' : url.includes('/mobile/') || url.includes('/media/');
|
|
150
135
|
const getTrackUrlFromServer = async (track_token, format) => {
|
|
151
|
-
const user =
|
|
152
|
-
if ((format === 'FLAC' && !user.
|
|
136
|
+
const user = await (0, session_1.defaultSession)().loadUserData();
|
|
137
|
+
if ((format === 'FLAC' && !user.canStreamLossless) || (format === 'MP3_320' && !user.canStreamHq)) {
|
|
153
138
|
throw new WrongLicense(format);
|
|
154
139
|
}
|
|
155
140
|
const { data, country } = await mediaGetUrl([track_token], [{ format, cipher: 'BF_CBC_STRIPE' }]);
|
package/dist/lib/request.d.ts
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
import type { HttpQuery, HttpResponse } from './http';
|
|
2
|
+
export { initDeezerApi, createSession, defaultSession, setDefaultSession, Session, RETRY_POLICY } from './session';
|
|
3
|
+
export type { SessionUserData } from './session';
|
|
2
4
|
type DeezerRequestConfig = {
|
|
3
5
|
headers?: Record<string, string>;
|
|
4
6
|
params?: HttpQuery;
|
|
5
7
|
};
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `NEED_API_AUTH_REQUIRED` or `GATEWAY_ERROR`).
|
|
9
|
+
* A thin proxy over the **default** {@link Session} — kept so the api / get-url
|
|
10
|
+
* layers can keep calling `client.get` / `client.post` unchanged. Each call runs
|
|
11
|
+
* through the session's bounded-retry loop.
|
|
11
12
|
*/
|
|
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
|
-
};
|
|
26
|
-
export declare const initDeezerApi: (arl: string) => Promise<string>;
|
|
27
13
|
declare const _default: {
|
|
28
|
-
defaults: {
|
|
14
|
+
readonly defaults: {
|
|
29
15
|
baseURL?: string | undefined;
|
|
30
16
|
headers: import("./http").HttpHeaders;
|
|
31
17
|
maxRedirects: number;
|
package/dist/lib/request.js
CHANGED
|
@@ -1,123 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
exports.RETRY_POLICY = exports.Session = exports.setDefaultSession = exports.defaultSession = exports.createSession = exports.initDeezerApi = void 0;
|
|
4
|
+
const session_1 = require("./session");
|
|
5
|
+
var session_2 = require("./session");
|
|
6
|
+
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return session_2.initDeezerApi; } });
|
|
7
|
+
Object.defineProperty(exports, "createSession", { enumerable: true, get: function () { return session_2.createSession; } });
|
|
8
|
+
Object.defineProperty(exports, "defaultSession", { enumerable: true, get: function () { return session_2.defaultSession; } });
|
|
9
|
+
Object.defineProperty(exports, "setDefaultSession", { enumerable: true, get: function () { return session_2.setDefaultSession; } });
|
|
10
|
+
Object.defineProperty(exports, "Session", { enumerable: true, get: function () { return session_2.Session; } });
|
|
11
|
+
Object.defineProperty(exports, "RETRY_POLICY", { enumerable: true, get: function () { return session_2.RETRY_POLICY; } });
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `NEED_API_AUTH_REQUIRED` or `GATEWAY_ERROR`).
|
|
13
|
+
* A thin proxy over the **default** {@link Session} — kept so the api / get-url
|
|
14
|
+
* layers can keep calling `client.get` / `client.post` unchanged. Each call runs
|
|
15
|
+
* through the session's bounded-retry loop.
|
|
16
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
|
-
};
|
|
36
|
-
const instance = new http_1.HttpClient({
|
|
37
|
-
baseURL: 'https://api.deezer.com/1.0',
|
|
38
|
-
timeout: 15000,
|
|
39
|
-
headers: {
|
|
40
|
-
Accept: '*/*',
|
|
41
|
-
'Accept-Encoding': 'gzip, deflate',
|
|
42
|
-
'Accept-Language': 'en-US',
|
|
43
|
-
'Cache-Control': 'no-cache',
|
|
44
|
-
'Content-Type': 'application/json; charset=UTF-8',
|
|
45
|
-
'User-Agent': 'Deezer/8.32.0.2 (iOS; 14.4; Mobile; en; iPhone10_5)',
|
|
46
|
-
},
|
|
47
|
-
params: {
|
|
48
|
-
version: '8.32.0',
|
|
49
|
-
api_key: 'ZAIVAHCEISOHWAICUQUEXAEPICENGUAFAEZAIPHAELEEVAHPHUCUFONGUAPASUAY',
|
|
50
|
-
output: 3,
|
|
51
|
-
input: 3,
|
|
52
|
-
buildId: 'ios12_universal',
|
|
53
|
-
screenHeight: '480',
|
|
54
|
-
screenWidth: '320',
|
|
55
|
-
lang: 'en',
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
const getApiToken = async () => {
|
|
59
|
-
const { data } = await instance.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
60
|
-
params: {
|
|
61
|
-
method: 'deezer.getUserData',
|
|
62
|
-
api_version: '1.0',
|
|
63
|
-
api_token: 'null',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
instance.defaults.params.sid = data.results.SESSION_ID;
|
|
67
|
-
instance.defaults.params.api_token = data.results.checkForm;
|
|
68
|
-
return data.results.checkForm;
|
|
69
|
-
};
|
|
70
|
-
const initDeezerApi = async (arl) => {
|
|
71
|
-
if (arl.length !== 192) {
|
|
72
|
-
throw new Error(`Invalid arl. Length should be 192 characters. You have provided ${arl.length} characters.`);
|
|
73
|
-
}
|
|
74
|
-
user_arl = arl;
|
|
75
|
-
const { data } = await instance.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
76
|
-
params: { method: 'deezer.ping', api_version: '1.0', api_token: '' },
|
|
77
|
-
headers: { cookie: 'arl=' + arl },
|
|
78
|
-
});
|
|
79
|
-
instance.defaults.params.sid = data.results.SESSION;
|
|
80
|
-
return data.results.SESSION;
|
|
81
|
-
};
|
|
82
|
-
exports.initDeezerApi = initDeezerApi;
|
|
83
|
-
const requestWithRetry = async (method, url, body, config = {}) => {
|
|
84
|
-
var _a;
|
|
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);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
17
|
exports.default = {
|
|
120
|
-
defaults
|
|
121
|
-
|
|
122
|
-
|
|
18
|
+
get defaults() {
|
|
19
|
+
return (0, session_1.defaultSession)().http.defaults;
|
|
20
|
+
},
|
|
21
|
+
get: (url, config = {}) => (0, session_1.defaultSession)().request('GET', url, undefined, config),
|
|
22
|
+
post: (url, body, config = {}) => (0, session_1.defaultSession)().request('POST', url, body, config),
|
|
123
23
|
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { HttpClient } from './http';
|
|
2
|
+
import type { HttpQuery, HttpResponse } from './http';
|
|
3
|
+
/**
|
|
4
|
+
* The bundled default `arl`. It is shared, rate-limited and expiring — good for
|
|
5
|
+
* unauthenticated endpoints, useless for downloads. `initDeezerApi` /
|
|
6
|
+
* `createSession` replace it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_ARL = "c973964816688562722418b5200c1515dffaad15a42643ebf87cc72824a54612ec51c2ad42d566743f9e424c774e98ccae7737770acff59251328e6cd598c7bcac38ca269adf78bfb88ec5bbad6cd800db3c0b88b2af645bb22b99e71de26416";
|
|
9
|
+
/**
|
|
10
|
+
* Bounded-retry policy for the gateway. Every retry class has its own attempt
|
|
11
|
+
* cap **and** there is a wall-clock deadline, so a persistently failing endpoint
|
|
12
|
+
* can no longer spin forever.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RETRY_POLICY: {
|
|
15
|
+
/** total attempts for the transient `code === 4` class */
|
|
16
|
+
code4Attempts: number;
|
|
17
|
+
/** re-inits allowed for `NEED_API_AUTH_REQUIRED` */
|
|
18
|
+
authReinits: number;
|
|
19
|
+
/** token refreshes allowed for `GATEWAY_ERROR` / `VALID_TOKEN_REQUIRED` */
|
|
20
|
+
tokenRefreshes: number;
|
|
21
|
+
/** base backoff (ms) — grows exponentially, full-jittered */
|
|
22
|
+
baseMs: number;
|
|
23
|
+
/** cap on a single backoff wait */
|
|
24
|
+
maxDelayMs: number;
|
|
25
|
+
/** overall wall-clock budget from the first attempt */
|
|
26
|
+
deadlineMs: number;
|
|
27
|
+
};
|
|
28
|
+
type SessionRequestConfig = {
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
params?: HttpQuery;
|
|
31
|
+
};
|
|
32
|
+
/** The account-scoped bits of `deezer.getUserData` a download needs. */
|
|
33
|
+
export interface SessionUserData {
|
|
34
|
+
/** the `license_token` the media API's `get_url` requires */
|
|
35
|
+
licenseToken: string;
|
|
36
|
+
/** ISO country the account resolves to (drives geo-blocking) */
|
|
37
|
+
country: string;
|
|
38
|
+
/** account may stream FLAC */
|
|
39
|
+
canStreamLossless: boolean;
|
|
40
|
+
/** account may stream 320 kbps */
|
|
41
|
+
canStreamHq: boolean;
|
|
42
|
+
offerId?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One Deezer session — owns the `arl`, the HTTP client (and its `sid` /
|
|
46
|
+
* `api_token`), and the account's `license_token` / `country` / streaming
|
|
47
|
+
* rights, plus the bounded-retry request loop and token refresh.
|
|
48
|
+
*
|
|
49
|
+
* The state used to be spread across module-level variables in `request.ts` and
|
|
50
|
+
* `get-url.ts`; a `Session` bundles it so multiple accounts can coexist. The
|
|
51
|
+
* free functions (`getTrackInfo`, `request`, …) run against a process-wide
|
|
52
|
+
* default session; `createSession(arl)` gives you an isolated one.
|
|
53
|
+
*/
|
|
54
|
+
export declare class Session {
|
|
55
|
+
/** the arl cookie in use */
|
|
56
|
+
arl: string;
|
|
57
|
+
/** the underlying HTTP client — its `defaults.params` carry `sid` / `api_token` */
|
|
58
|
+
readonly http: HttpClient;
|
|
59
|
+
private userData;
|
|
60
|
+
private userDataAt;
|
|
61
|
+
constructor(arl?: string);
|
|
62
|
+
/** current gateway session id */
|
|
63
|
+
get sid(): string | undefined;
|
|
64
|
+
/** current CSRF-ish api token */
|
|
65
|
+
get apiToken(): string | undefined;
|
|
66
|
+
/** ISO country the account resolves to, once `loadUserData` has run */
|
|
67
|
+
get country(): string | undefined;
|
|
68
|
+
/** the media-API `license_token`, once `loadUserData` has run */
|
|
69
|
+
get licenseToken(): string | undefined;
|
|
70
|
+
get canStreamLossless(): boolean;
|
|
71
|
+
get canStreamHq(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Ping `gw-light.php` for a fresh `SESSION` id. Pass an `arl` to (re)authenticate
|
|
74
|
+
* this session as that account — it must be exactly 192 characters.
|
|
75
|
+
* Returns the new `SESSION`.
|
|
76
|
+
*/
|
|
77
|
+
init(arl?: string): Promise<string>;
|
|
78
|
+
/** Refresh `sid` + `api_token` from `deezer.getUserData`. Returns the api token. */
|
|
79
|
+
refreshApiToken(): Promise<string>;
|
|
80
|
+
/**
|
|
81
|
+
* The account's `license_token` / `country` / streaming rights, from
|
|
82
|
+
* `deezer.getUserData`. Cached for {@link USER_DATA_TTL_MS}; pass `force` (or
|
|
83
|
+
* see a media-API 403) to refresh. Also refreshes `sid` / `api_token`.
|
|
84
|
+
*/
|
|
85
|
+
loadUserData(force?: boolean): Promise<SessionUserData>;
|
|
86
|
+
/** Drop the cached user data so the next `loadUserData` re-fetches. */
|
|
87
|
+
invalidateUserData(): void;
|
|
88
|
+
/**
|
|
89
|
+
* The bounded-retry gateway request loop. Handles `NEED_API_AUTH_REQUIRED`
|
|
90
|
+
* (re-init), `GATEWAY_ERROR` / `VALID_TOKEN_REQUIRED` (token refresh) and
|
|
91
|
+
* `code === 4` (transient), each with its own attempt cap, full-jittered
|
|
92
|
+
* backoff, and a wall-clock deadline. Throws `DeezerError` on exhaustion.
|
|
93
|
+
*/
|
|
94
|
+
request<T>(method: 'GET' | 'POST', url: string, body?: unknown, config?: SessionRequestConfig): Promise<HttpResponse<T>>;
|
|
95
|
+
}
|
|
96
|
+
/** The process-wide default session every free function runs against. */
|
|
97
|
+
export declare const defaultSession: () => Session;
|
|
98
|
+
/**
|
|
99
|
+
* (Re)authenticate the **default** session. Kept for backwards compatibility —
|
|
100
|
+
* `createSession` is the way to hold an isolated session. `arl` must be exactly
|
|
101
|
+
* 192 characters. Returns the gateway `SESSION` id.
|
|
102
|
+
*/
|
|
103
|
+
export declare const initDeezerApi: (arl: string) => Promise<string>;
|
|
104
|
+
/**
|
|
105
|
+
* Create an **isolated** Deezer session — its own `arl`, `sid`, tokens and
|
|
106
|
+
* `license_token`, so multiple accounts can be used concurrently. Without an
|
|
107
|
+
* `arl` it runs on the bundled (shared, rate-limited) default.
|
|
108
|
+
*/
|
|
109
|
+
export declare const createSession: (arl?: string) => Promise<Session>;
|
|
110
|
+
/** Replace the default session (used by tests). */
|
|
111
|
+
export declare const setDefaultSession: (session: Session) => void;
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setDefaultSession = exports.createSession = exports.initDeezerApi = exports.defaultSession = exports.Session = exports.RETRY_POLICY = exports.DEFAULT_ARL = void 0;
|
|
7
|
+
const delay_1 = __importDefault(require("delay"));
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const http_1 = require("./http");
|
|
10
|
+
/**
|
|
11
|
+
* The bundled default `arl`. It is shared, rate-limited and expiring — good for
|
|
12
|
+
* unauthenticated endpoints, useless for downloads. `initDeezerApi` /
|
|
13
|
+
* `createSession` replace it.
|
|
14
|
+
*/
|
|
15
|
+
exports.DEFAULT_ARL = 'c973964816688562722418b5200c1515dffaad15a42643ebf87cc72824a54612ec51c2ad42d566743f9e424c774e98ccae7737770acff59251328e6cd598c7bcac38ca269adf78bfb88ec5bbad6cd800db3c0b88b2af645bb22b99e71de26416';
|
|
16
|
+
/**
|
|
17
|
+
* Bounded-retry policy for the gateway. Every retry class has its own attempt
|
|
18
|
+
* cap **and** there is a wall-clock deadline, so a persistently failing endpoint
|
|
19
|
+
* can no longer spin forever.
|
|
20
|
+
*/
|
|
21
|
+
exports.RETRY_POLICY = {
|
|
22
|
+
/** total attempts for the transient `code === 4` class */
|
|
23
|
+
code4Attempts: 6,
|
|
24
|
+
/** re-inits allowed for `NEED_API_AUTH_REQUIRED` */
|
|
25
|
+
authReinits: 3,
|
|
26
|
+
/** token refreshes allowed for `GATEWAY_ERROR` / `VALID_TOKEN_REQUIRED` */
|
|
27
|
+
tokenRefreshes: 15,
|
|
28
|
+
/** base backoff (ms) — grows exponentially, full-jittered */
|
|
29
|
+
baseMs: 800,
|
|
30
|
+
/** cap on a single backoff wait */
|
|
31
|
+
maxDelayMs: 8000,
|
|
32
|
+
/** overall wall-clock budget from the first attempt */
|
|
33
|
+
deadlineMs: 30000,
|
|
34
|
+
};
|
|
35
|
+
/** Exponential backoff (ms) with full jitter on the top half of the window. */
|
|
36
|
+
const backoffDelay = (attempt) => {
|
|
37
|
+
const windowMs = Math.min(exports.RETRY_POLICY.baseMs * 2 ** attempt, exports.RETRY_POLICY.maxDelayMs);
|
|
38
|
+
return windowMs / 2 + Math.random() * (windowMs / 2);
|
|
39
|
+
};
|
|
40
|
+
/** How long a loaded `deezer.getUserData` payload is trusted before a refresh. */
|
|
41
|
+
const USER_DATA_TTL_MS = 25 * 60 * 1000;
|
|
42
|
+
/**
|
|
43
|
+
* One Deezer session — owns the `arl`, the HTTP client (and its `sid` /
|
|
44
|
+
* `api_token`), and the account's `license_token` / `country` / streaming
|
|
45
|
+
* rights, plus the bounded-retry request loop and token refresh.
|
|
46
|
+
*
|
|
47
|
+
* The state used to be spread across module-level variables in `request.ts` and
|
|
48
|
+
* `get-url.ts`; a `Session` bundles it so multiple accounts can coexist. The
|
|
49
|
+
* free functions (`getTrackInfo`, `request`, …) run against a process-wide
|
|
50
|
+
* default session; `createSession(arl)` gives you an isolated one.
|
|
51
|
+
*/
|
|
52
|
+
class Session {
|
|
53
|
+
constructor(arl) {
|
|
54
|
+
this.userData = null;
|
|
55
|
+
this.userDataAt = 0;
|
|
56
|
+
this.arl = arl !== null && arl !== void 0 ? arl : exports.DEFAULT_ARL;
|
|
57
|
+
this.http = new http_1.HttpClient({
|
|
58
|
+
baseURL: 'https://api.deezer.com/1.0',
|
|
59
|
+
timeout: 15000,
|
|
60
|
+
headers: {
|
|
61
|
+
Accept: '*/*',
|
|
62
|
+
'Accept-Encoding': 'gzip, deflate',
|
|
63
|
+
'Accept-Language': 'en-US',
|
|
64
|
+
'Cache-Control': 'no-cache',
|
|
65
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
66
|
+
'User-Agent': 'Deezer/8.32.0.2 (iOS; 14.4; Mobile; en; iPhone10_5)',
|
|
67
|
+
},
|
|
68
|
+
params: {
|
|
69
|
+
version: '8.32.0',
|
|
70
|
+
api_key: 'ZAIVAHCEISOHWAICUQUEXAEPICENGUAFAEZAIPHAELEEVAHPHUCUFONGUAPASUAY',
|
|
71
|
+
output: 3,
|
|
72
|
+
input: 3,
|
|
73
|
+
buildId: 'ios12_universal',
|
|
74
|
+
screenHeight: '480',
|
|
75
|
+
screenWidth: '320',
|
|
76
|
+
lang: 'en',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** current gateway session id */
|
|
81
|
+
get sid() {
|
|
82
|
+
return this.http.defaults.params.sid;
|
|
83
|
+
}
|
|
84
|
+
/** current CSRF-ish api token */
|
|
85
|
+
get apiToken() {
|
|
86
|
+
return this.http.defaults.params.api_token;
|
|
87
|
+
}
|
|
88
|
+
/** ISO country the account resolves to, once `loadUserData` has run */
|
|
89
|
+
get country() {
|
|
90
|
+
var _a;
|
|
91
|
+
return (_a = this.userData) === null || _a === void 0 ? void 0 : _a.country;
|
|
92
|
+
}
|
|
93
|
+
/** the media-API `license_token`, once `loadUserData` has run */
|
|
94
|
+
get licenseToken() {
|
|
95
|
+
var _a;
|
|
96
|
+
return (_a = this.userData) === null || _a === void 0 ? void 0 : _a.licenseToken;
|
|
97
|
+
}
|
|
98
|
+
get canStreamLossless() {
|
|
99
|
+
var _a, _b;
|
|
100
|
+
return (_b = (_a = this.userData) === null || _a === void 0 ? void 0 : _a.canStreamLossless) !== null && _b !== void 0 ? _b : false;
|
|
101
|
+
}
|
|
102
|
+
get canStreamHq() {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
return (_b = (_a = this.userData) === null || _a === void 0 ? void 0 : _a.canStreamHq) !== null && _b !== void 0 ? _b : false;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ping `gw-light.php` for a fresh `SESSION` id. Pass an `arl` to (re)authenticate
|
|
108
|
+
* this session as that account — it must be exactly 192 characters.
|
|
109
|
+
* Returns the new `SESSION`.
|
|
110
|
+
*/
|
|
111
|
+
async init(arl) {
|
|
112
|
+
if (arl !== undefined) {
|
|
113
|
+
if (arl.length !== 192) {
|
|
114
|
+
throw new Error(`Invalid arl. Length should be 192 characters. You have provided ${arl.length} characters.`);
|
|
115
|
+
}
|
|
116
|
+
this.arl = arl;
|
|
117
|
+
this.userData = null;
|
|
118
|
+
}
|
|
119
|
+
const { data } = await this.http.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
120
|
+
params: { method: 'deezer.ping', api_version: '1.0', api_token: '' },
|
|
121
|
+
headers: { cookie: 'arl=' + this.arl },
|
|
122
|
+
});
|
|
123
|
+
this.http.defaults.params.sid = data.results.SESSION;
|
|
124
|
+
return data.results.SESSION;
|
|
125
|
+
}
|
|
126
|
+
/** Refresh `sid` + `api_token` from `deezer.getUserData`. Returns the api token. */
|
|
127
|
+
async refreshApiToken() {
|
|
128
|
+
const { data } = await this.http.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
129
|
+
params: { method: 'deezer.getUserData', api_version: '1.0', api_token: 'null' },
|
|
130
|
+
});
|
|
131
|
+
this.http.defaults.params.sid = data.results.SESSION_ID;
|
|
132
|
+
this.http.defaults.params.api_token = data.results.checkForm;
|
|
133
|
+
return data.results.checkForm;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The account's `license_token` / `country` / streaming rights, from
|
|
137
|
+
* `deezer.getUserData`. Cached for {@link USER_DATA_TTL_MS}; pass `force` (or
|
|
138
|
+
* see a media-API 403) to refresh. Also refreshes `sid` / `api_token`.
|
|
139
|
+
*/
|
|
140
|
+
async loadUserData(force = false) {
|
|
141
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
142
|
+
if (this.userData && !force && Date.now() - this.userDataAt < USER_DATA_TTL_MS) {
|
|
143
|
+
return this.userData;
|
|
144
|
+
}
|
|
145
|
+
const { data } = await this.http.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
146
|
+
params: { method: 'deezer.getUserData', api_version: '1.0', api_token: 'null' },
|
|
147
|
+
});
|
|
148
|
+
const options = (_c = (_b = (_a = data.results) === null || _a === void 0 ? void 0 : _a.USER) === null || _b === void 0 ? void 0 : _b.OPTIONS) !== null && _c !== void 0 ? _c : {};
|
|
149
|
+
this.userData = {
|
|
150
|
+
licenseToken: options.license_token,
|
|
151
|
+
country: (_d = data.results) === null || _d === void 0 ? void 0 : _d.COUNTRY,
|
|
152
|
+
canStreamLossless: Boolean(options.web_lossless || options.mobile_loseless),
|
|
153
|
+
canStreamHq: Boolean(options.web_hq || options.mobile_hq),
|
|
154
|
+
offerId: (_e = data.results) === null || _e === void 0 ? void 0 : _e.OFFER_ID,
|
|
155
|
+
};
|
|
156
|
+
this.userDataAt = Date.now();
|
|
157
|
+
if ((_f = data.results) === null || _f === void 0 ? void 0 : _f.checkForm) {
|
|
158
|
+
this.http.defaults.params.api_token = data.results.checkForm;
|
|
159
|
+
}
|
|
160
|
+
if ((_g = data.results) === null || _g === void 0 ? void 0 : _g.SESSION_ID) {
|
|
161
|
+
this.http.defaults.params.sid = data.results.SESSION_ID;
|
|
162
|
+
}
|
|
163
|
+
return this.userData;
|
|
164
|
+
}
|
|
165
|
+
/** Drop the cached user data so the next `loadUserData` re-fetches. */
|
|
166
|
+
invalidateUserData() {
|
|
167
|
+
this.userData = null;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The bounded-retry gateway request loop. Handles `NEED_API_AUTH_REQUIRED`
|
|
171
|
+
* (re-init), `GATEWAY_ERROR` / `VALID_TOKEN_REQUIRED` (token refresh) and
|
|
172
|
+
* `code === 4` (transient), each with its own attempt cap, full-jittered
|
|
173
|
+
* backoff, and a wall-clock deadline. Throws `DeezerError` on exhaustion.
|
|
174
|
+
*/
|
|
175
|
+
async request(method, url, body, config = {}) {
|
|
176
|
+
var _a;
|
|
177
|
+
const startedAt = Date.now();
|
|
178
|
+
let authReinits = 0;
|
|
179
|
+
let tokenRefreshes = 0;
|
|
180
|
+
let code4Attempts = 0;
|
|
181
|
+
// eslint-disable-next-line no-constant-condition
|
|
182
|
+
while (true) {
|
|
183
|
+
const response = method === 'POST' ? await this.http.post(url, body, config) : await this.http.get(url, config);
|
|
184
|
+
const error = (_a = response.data) === null || _a === void 0 ? void 0 : _a.error;
|
|
185
|
+
if (!error || Object.keys(error).length === 0) {
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
const overDeadline = Date.now() - startedAt > exports.RETRY_POLICY.deadlineMs;
|
|
189
|
+
if (error.NEED_API_AUTH_REQUIRED && authReinits < exports.RETRY_POLICY.authReinits && !overDeadline) {
|
|
190
|
+
authReinits += 1;
|
|
191
|
+
await this.init();
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if ((error.GATEWAY_ERROR || error.VALID_TOKEN_REQUIRED) &&
|
|
195
|
+
tokenRefreshes < exports.RETRY_POLICY.tokenRefreshes &&
|
|
196
|
+
!overDeadline) {
|
|
197
|
+
tokenRefreshes += 1;
|
|
198
|
+
await this.refreshApiToken();
|
|
199
|
+
await (0, delay_1.default)(backoffDelay(tokenRefreshes - 1));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (error.code === 4 && code4Attempts < exports.RETRY_POLICY.code4Attempts && !overDeadline) {
|
|
203
|
+
await (0, delay_1.default)(backoffDelay(code4Attempts));
|
|
204
|
+
code4Attempts += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
throw new errors_1.DeezerError(error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
exports.Session = Session;
|
|
212
|
+
let _default = new Session();
|
|
213
|
+
/** The process-wide default session every free function runs against. */
|
|
214
|
+
const defaultSession = () => _default;
|
|
215
|
+
exports.defaultSession = defaultSession;
|
|
216
|
+
/**
|
|
217
|
+
* (Re)authenticate the **default** session. Kept for backwards compatibility —
|
|
218
|
+
* `createSession` is the way to hold an isolated session. `arl` must be exactly
|
|
219
|
+
* 192 characters. Returns the gateway `SESSION` id.
|
|
220
|
+
*/
|
|
221
|
+
const initDeezerApi = (arl) => _default.init(arl);
|
|
222
|
+
exports.initDeezerApi = initDeezerApi;
|
|
223
|
+
/**
|
|
224
|
+
* Create an **isolated** Deezer session — its own `arl`, `sid`, tokens and
|
|
225
|
+
* `license_token`, so multiple accounts can be used concurrently. Without an
|
|
226
|
+
* `arl` it runs on the bundled (shared, rate-limited) default.
|
|
227
|
+
*/
|
|
228
|
+
const createSession = async (arl) => {
|
|
229
|
+
const session = new Session(arl);
|
|
230
|
+
await session.init();
|
|
231
|
+
return session;
|
|
232
|
+
};
|
|
233
|
+
exports.createSession = createSession;
|
|
234
|
+
/** Replace the default session (used by tests). */
|
|
235
|
+
const setDefaultSession = (session) => {
|
|
236
|
+
_default = session;
|
|
237
|
+
};
|
|
238
|
+
exports.setDefaultSession = setDefaultSession;
|