gerdur 2.15.0 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.17.0 - 2026-08-31
4
+
5
+ ### Fixed
6
+
7
+ - **`DEEZER_EMAIL` / `DEEZER_PASSWORD` are now read** as aliases for
8
+ `GERDUR_EMAIL` / `GERDUR_PASSWORD`. Those spellings are what people tend to put
9
+ in a `.env` alongside their other Deezer settings, and until now they were
10
+ silently ignored — the login just looked unconfigured.
11
+
12
+ ### Changed
13
+
14
+ - **A refused login no longer blames your password.** It used to report
15
+ `wrong-credentials` with "check your email and password". That is not something
16
+ this code can know: Deezer's `user_auth.php` returns an identical error
17
+ (code 160) for a real account, a wrong password, **and** an address that does
18
+ not exist. The reason is now `rejected`, and the message says so.
19
+
20
+ Probed against the live endpoints while making this change, and every
21
+ documented password-to-`arl` path is currently refused:
22
+
23
+ | path | result |
24
+ | :--- | :--- |
25
+ | `connect.deezer.com/oauth/user_auth.php` | `authenticate user failed`, code 160 |
26
+ | `www.deezer.com/ajax/action.php` | `error` |
27
+ | `auth.deezer.com/login/arl` | 400 — needs a session the first two can't provide |
28
+
29
+ A deliberately malformed request returns a *different* error (code 150, "wrong
30
+ hash !"), which is how we know the request itself is still well formed and the
31
+ refusal is about authentication. **Paste an `arl` instead.** The code is kept
32
+ in case the flow returns, and `reason: 'rejected'` now carries an explanation
33
+ rather than a misdiagnosis.
34
+
35
+ ### Breaking (programmatic API, minor)
36
+
37
+ - `LoginResult`'s `wrong-credentials` reason is renamed **`rejected`**. Anything
38
+ switching exhaustively on `reason` needs updating; `LoginError.reason` carries
39
+ the new value.
40
+
41
+ ## 2.16.0 - 2026-08-31
42
+
43
+ ### Changed
44
+
45
+ - **The CLI starts about twice as fast: ~249 ms → 120 ms** (min of 10 runs;
46
+ mean 137 ms). Two things were being loaded before they could possibly be
47
+ needed:
48
+ - `gerdur-core@^2.20.0` now defers the Spotify SDK and the HTML parser, so
49
+ `require('gerdur-core')` costs 43 ms instead of 160 ms.
50
+ - `got` (~53 ms to require) is now loaded on first use rather than at import.
51
+ Every call site was already inside a function, so this is a pure deferral —
52
+ but it means `--help`, `gerdur setup`, `--set-arl` and the interactive prompt
53
+ no longer pay for an HTTP client before you have typed anything.
54
+
55
+ Verified against live downloads: a full 320 kbps track (9.19 MB, 226 s,
56
+ decodes clean under ffmpeg) and a `--preview` clip (480 KB, 30 s).
57
+
3
58
  ## 2.15.0 - 2026-08-31
4
59
 
5
60
  ### Changed
package/README.md CHANGED
@@ -89,11 +89,20 @@ gerdur --set-arl <your_arl>
89
89
  To copy it manually: open <https://www.deezer.com> logged in → DevTools (`F12`) →
90
90
  Application → Cookies → `deezer.com` → copy the `arl` value (192 hex chars).
91
91
 
92
- **Log in with email &amp; password** — the guided setup can fetch the `arl` for you
93
- via Deezer's mobile OAuth flow, so you never open DevTools. Only the resulting
94
- `arl` is stored; your password is never written to disk unless you explicitly opt
95
- in when prompted. Deezer bot-protects scripted logins, so this is best-effort and
96
- falls back to pasting an `arl`.
92
+ **Log in with email &amp; password** — the guided setup can try to fetch the `arl`
93
+ from your credentials, so you never open DevTools. Only the resulting `arl` is
94
+ stored; your password is never written to disk unless you explicitly opt in when
95
+ prompted. Credentials are also read from `GERDUR_EMAIL` / `GERDUR_PASSWORD`, or
96
+ `DEEZER_EMAIL` / `DEEZER_PASSWORD`.
97
+
98
+ > **This currently does not work.** Every documented password-to-`arl` path is
99
+ > refused: `connect.deezer.com/oauth/user_auth.php` answers `authenticate user
100
+ > failed` (code 160), the web `action.php` login answers `error`, and
101
+ > `auth.deezer.com/login/arl` needs a session you cannot get without the first
102
+ > two. Deezer returns *the same* code 160 for a real account, a wrong password
103
+ > and an address that does not exist, so a failure here says nothing about your
104
+ > credentials. **Paste an `arl` instead** — `gerdur --set-arl <arl>` or
105
+ > `GERDUR_ARL=<arl>`. The code is kept because the flow may come back.
97
106
 
98
107
  **Zero-config / CI** — set `GERDUR_ARL` (or `GERDUR_EMAIL` + `GERDUR_PASSWORD`)
99
108
  and skip the config file entirely. `GERDUR_ARL` takes precedence over the config
@@ -434,7 +443,7 @@ import {loginWithEmail, LoginError, Config, globalConfigPath} from 'gerdur';
434
443
 
435
444
  const result = await loginWithEmail('you@example.com', 'password');
436
445
  if (result.ok) console.log(result.arl);
437
- else console.error(result.reason, result.message); // 'wrong-credentials' | 'no-arl' | 'network' | 'unknown'
446
+ else console.error(result.reason, result.message); // 'rejected' | 'no-arl' | 'network' | 'unknown'
438
447
 
439
448
  const conf = new Config(); // the same config the CLI uses
440
449
  conf.set('cookies.arl', 'xxx…');
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.15.0",
3
+ "version": "2.17.0",
4
4
  "description": "Command-line music downloader for Deezer (Spotify/Tidal links resolved via ISRC matching) with automatic MP3/FLAC tagging, synced lyrics and a side-effect-free programmatic API.",
5
5
  "keywords": [
6
6
  "deezer",
@@ -63,7 +63,7 @@
63
63
  "adm-zip": "^0.5.16",
64
64
  "chalk": "^4.1.2",
65
65
  "commander": "^9.5.0",
66
- "gerdur-core": "^2.19.0",
66
+ "gerdur-core": "^2.20.0",
67
67
  "dot-prop": "^6.0.1",
68
68
  "got": "^11.8.6",
69
69
  "gradient-string": "^2.0.2",
@@ -9,7 +9,10 @@ const fs_1 = require("fs");
9
9
  const path_1 = require("path");
10
10
  const commander_1 = require("commander");
11
11
  const gradient_string_1 = __importDefault(require("gradient-string"));
12
- const got_1 = __importDefault(require("got"));
12
+ /* eslint-disable @typescript-eslint/no-var-requires */
13
+ // `got` costs ~53 ms to require and is only needed once a transfer actually
14
+ // starts — deferring it keeps `--help`, setup and the interactive prompt snappy.
15
+ const got = () => require('got');
13
16
  const gerdur_core_1 = require("gerdur-core");
14
17
  const prompts_1 = __importDefault(require("prompts"));
15
18
  const log_update_1 = __importDefault(require("log-update"));
@@ -369,7 +372,7 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
369
372
  console.log(signale_1.default.info(`Downloading episode: ${episode.EPISODE_TITLE}`));
370
373
  if (options.overwrite || !(0, fs_1.existsSync)(dest)) {
371
374
  (0, fs_1.mkdirSync)(dir, { recursive: true });
372
- const { body } = await (0, got_1.default)(episode.EPISODE_DIRECT_STREAM_URL, { responseType: 'buffer' });
375
+ const { body } = await got()(episode.EPISODE_DIRECT_STREAM_URL, { responseType: 'buffer' });
373
376
  (0, fs_1.writeFileSync)(dest, body);
374
377
  }
375
378
  console.log(signale_1.default.success(`Saved ${dest}`));
@@ -1,12 +1,12 @@
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
3
  exports.downloadTrackToFile = exports.getTaggedTrack = exports.getTrackBuffer = void 0;
7
4
  const fs_1 = require("fs");
8
5
  const path_1 = require("path");
9
- const got_1 = __importDefault(require("got"));
6
+ /* eslint-disable @typescript-eslint/no-var-requires */
7
+ // `got` costs ~53 ms to require and is only needed once a transfer actually
8
+ // starts — deferring it keeps `--help`, setup and the interactive prompt snappy.
9
+ const got = () => require('got');
10
10
  const gerdur_core_1 = require("gerdur-core");
11
11
  const decrypt_1 = require("./decrypt");
12
12
  const util_1 = require("./util");
@@ -48,7 +48,10 @@ const fetchDecryptTag = async (track, quality, options) => {
48
48
  if (!trackData) {
49
49
  continue;
50
50
  }
51
- const { body } = await (0, got_1.default)(trackData.trackUrl, { responseType: 'buffer', agent: { http: gerdur_core_1.httpAgent, https: gerdur_core_1.httpsAgent } });
51
+ const { body } = await got()(trackData.trackUrl, {
52
+ responseType: 'buffer',
53
+ agent: { http: gerdur_core_1.httpAgent, https: gerdur_core_1.httpsAgent },
54
+ });
52
55
  const decrypted = trackData.isEncrypted ? (0, decrypt_1.decryptDownload)(body, track.SNG_ID) : body;
53
56
  return (0, gerdur_core_1.addTrackTags)(decrypted, track, tagOptions(options));
54
57
  }
@@ -17,8 +17,13 @@ export declare const printArlInstructions: () => void;
17
17
  export declare const promptForArl: (conf: Config) => Promise<string | null>;
18
18
  /**
19
19
  * Resolve email/password credentials from (in order): explicit config, then the
20
- * `GERDUR_EMAIL` / `GERDUR_PASSWORD` environment variables. Used for non-interactive
21
- * login attempts. Returns `null` if either half is missing.
20
+ * `GERDUR_EMAIL` / `GERDUR_PASSWORD` environment variables, then `DEEZER_EMAIL` /
21
+ * `DEEZER_PASSWORD`. Used for non-interactive login attempts. Returns `null` if
22
+ * either half is missing.
23
+ *
24
+ * The `DEEZER_*` spellings are accepted because that is what people tend to put
25
+ * in a `.env` next to their other Deezer settings; before, those were silently
26
+ * ignored and the login simply looked unconfigured.
22
27
  */
23
28
  export declare const resolveCredentials: (conf: Config) => {
24
29
  email: string;
@@ -68,12 +68,23 @@ const promptForArl = async (conf) => {
68
68
  exports.promptForArl = promptForArl;
69
69
  /**
70
70
  * Resolve email/password credentials from (in order): explicit config, then the
71
- * `GERDUR_EMAIL` / `GERDUR_PASSWORD` environment variables. Used for non-interactive
72
- * login attempts. Returns `null` if either half is missing.
71
+ * `GERDUR_EMAIL` / `GERDUR_PASSWORD` environment variables, then `DEEZER_EMAIL` /
72
+ * `DEEZER_PASSWORD`. Used for non-interactive login attempts. Returns `null` if
73
+ * either half is missing.
74
+ *
75
+ * The `DEEZER_*` spellings are accepted because that is what people tend to put
76
+ * in a `.env` next to their other Deezer settings; before, those were silently
77
+ * ignored and the login simply looked unconfigured.
73
78
  */
74
79
  const resolveCredentials = (conf) => {
75
- const email = (conf.get('cookies.email') || process.env.GERDUR_EMAIL || '').trim();
76
- const password = (conf.get('cookies.password') || process.env.GERDUR_PASSWORD || '').trim();
80
+ const email = (conf.get('cookies.email') ||
81
+ process.env.GERDUR_EMAIL ||
82
+ process.env.DEEZER_EMAIL ||
83
+ '').trim();
84
+ const password = (conf.get('cookies.password') ||
85
+ process.env.GERDUR_PASSWORD ||
86
+ process.env.DEEZER_PASSWORD ||
87
+ '').trim();
77
88
  if (email && password) {
78
89
  return { email, password };
79
90
  }
@@ -4,7 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = require("fs");
7
- const got_1 = __importDefault(require("got"));
7
+ /* eslint-disable @typescript-eslint/no-var-requires */
8
+ // `got` costs ~53 ms to require and is only needed once a transfer actually
9
+ // starts — deferring it keeps `--help`, setup and the interactive prompt snappy.
10
+ const got = () => require('got');
8
11
  const adm_zip_1 = __importDefault(require("adm-zip"));
9
12
  const chalk_1 = __importDefault(require("chalk"));
10
13
  const log_update_1 = __importDefault(require("log-update"));
@@ -12,7 +15,7 @@ const signale_1 = __importDefault(require("./signale"));
12
15
  const util_1 = require("./util");
13
16
  const updateCheck = async (pkg) => {
14
17
  const beta = pkg.version.includes('beta');
15
- const releases = await (0, got_1.default)('https://api.github.com/repos/soulwax/gerdur/releases').json();
18
+ const releases = await got()('https://api.github.com/repos/soulwax/gerdur/releases').json();
16
19
  const data = releases.filter((r) => r.prerelease === beta)[0];
17
20
  if (data.tag_name > pkg.version) {
18
21
  return data;
@@ -51,7 +54,7 @@ const updateBinary = async (pkg) => {
51
54
  console.log(` Downloading ${asset.browser_download_url}`);
52
55
  const bar = (0, util_1.progressBar)(asset.size, 40);
53
56
  const humanSizeTotal = (asset.size / 1024 / 1024).toFixed(2);
54
- const { body } = await (0, got_1.default)(asset.browser_download_url, { responseType: 'buffer' }).on('downloadProgress', ({ transferred }) => {
57
+ const { body } = await got()(asset.browser_download_url, { responseType: 'buffer' }).on('downloadProgress', ({ transferred }) => {
55
58
  (0, log_update_1.default)(` ${bar(transferred)} | ${humanSizeTotal}MiB`);
56
59
  });
57
60
  log_update_1.default.done();
@@ -3,7 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const got_1 = __importDefault(require("got"));
6
+ /* eslint-disable @typescript-eslint/no-var-requires */
7
+ // `got` costs ~53 ms to require and is only needed once a transfer actually
8
+ // starts — deferring it keeps `--help`, setup and the interactive prompt snappy.
9
+ const got = () => require('got');
7
10
  const stream_1 = __importDefault(require("stream"));
8
11
  const fs_1 = require("fs");
9
12
  const util_1 = require("util");
@@ -127,7 +130,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
127
130
  const bar = (0, util_2.progressBar)(fileSize, 40);
128
131
  const humanSizeTotal = (fileSize / 1024 / 1024).toFixed(2);
129
132
  let transferredLast = downloaded;
130
- await pipeline(got_1.default
133
+ await pipeline(got()
131
134
  .stream(trackData.trackUrl, {
132
135
  responseType: 'buffer',
133
136
  headers,
@@ -2,9 +2,16 @@
2
2
  export type LoginResult = {
3
3
  ok: true;
4
4
  arl: string;
5
- } | {
5
+ }
6
+ /**
7
+ * `rejected` means Deezer refused the login. It deliberately does **not** say
8
+ * "wrong password": the endpoint returns an identical error (code 160) for a
9
+ * real account, a wrong password and an address that does not exist, so the
10
+ * cause genuinely cannot be told apart from here.
11
+ */
12
+ | {
6
13
  ok: false;
7
- reason: 'wrong-credentials' | 'no-arl' | 'network' | 'unknown';
14
+ reason: 'rejected' | 'no-arl' | 'network' | 'unknown';
8
15
  message: string;
9
16
  };
10
17
  /**
@@ -1,11 +1,11 @@
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
3
  exports.loginWithEmail = void 0;
7
4
  const crypto_1 = require("crypto");
8
- const got_1 = __importDefault(require("got"));
5
+ /* eslint-disable @typescript-eslint/no-var-requires */
6
+ // `got` costs ~53 ms to require and is only needed once a transfer actually
7
+ // starts — deferring it keeps `--help`, setup and the interactive prompt snappy.
8
+ const got = () => require('got');
9
9
  /**
10
10
  * Email/password -> arl login for Deezer.
11
11
  *
@@ -23,6 +23,13 @@ const got_1 = __importDefault(require("got"));
23
23
  const CLIENT_ID = '447462';
24
24
  const CLIENT_SECRET = 'a83bf7f38ad2f137e444727cfc3775cf';
25
25
  const UA = 'Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0';
26
+ /**
27
+ * Deezer's `user_auth.php` error codes, as far as they can be told apart.
28
+ * `150` proves the request itself was well formed, which is how we know a `160`
29
+ * is about the account rather than the app hash.
30
+ */
31
+ const AUTH_WRONG_HASH = 150;
32
+ const AUTH_FAILED = 160;
26
33
  const md5 = (data) => (0, crypto_1.createHash)('md5').update(Buffer.from(data, 'utf8')).digest('hex');
27
34
  /**
28
35
  * Merge `set-cookie` response headers into a cookie map (last value wins),
@@ -55,7 +62,7 @@ const loginWithEmail = async (email, password) => {
55
62
  const jar = new Map();
56
63
  try {
57
64
  // 1. Bootstrap a session so Deezer sets an initial sid cookie.
58
- const boot = await (0, got_1.default)('https://www.deezer.com', {
65
+ const boot = await got()('https://www.deezer.com', {
59
66
  method: 'POST',
60
67
  headers: { 'User-Agent': UA },
61
68
  timeout: { request: 15000 },
@@ -67,7 +74,7 @@ const loginWithEmail = async (email, password) => {
67
74
  const passwordHash = md5(password);
68
75
  const hash = md5([CLIENT_ID, email, passwordHash, CLIENT_SECRET].join(''));
69
76
  let accessToken = null;
70
- const auth = await (0, got_1.default)('https://connect.deezer.com/oauth/user_auth.php', {
77
+ const auth = await got()('https://connect.deezer.com/oauth/user_auth.php', {
71
78
  searchParams: { app_id: CLIENT_ID, login: email, password: passwordHash, hash },
72
79
  headers: { 'User-Agent': UA, cookie: cookieHeader(jar) },
73
80
  responseType: 'json',
@@ -80,10 +87,26 @@ const loginWithEmail = async (email, password) => {
80
87
  accessToken = authBody.access_token;
81
88
  }
82
89
  if (!accessToken && !jar.has('arl') && (authBody.error || auth.statusCode >= 400)) {
83
- return { ok: false, reason: 'wrong-credentials', message: 'Login failed — check your email and password.' };
90
+ const code = authBody?.error?.code;
91
+ if (code === AUTH_WRONG_HASH) {
92
+ return {
93
+ ok: false,
94
+ reason: 'unknown',
95
+ message: 'Deezer rejected the request signature — the login flow has changed. Please paste an arl instead.',
96
+ };
97
+ }
98
+ return {
99
+ ok: false,
100
+ reason: 'rejected',
101
+ message: code === AUTH_FAILED
102
+ ? 'Deezer refused the login. It returns this same response for a wrong password and for an address ' +
103
+ 'that does not exist, and it currently refuses valid credentials too, so this is most likely the ' +
104
+ 'flow being closed rather than anything wrong with your details. Paste an arl instead.'
105
+ : 'Deezer refused the login. Paste an arl instead.',
106
+ };
84
107
  }
85
108
  // 3. Read the arl for the now-authenticated session.
86
- const arlResp = await (0, got_1.default)('https://www.deezer.com/ajax/gw-light.php', {
109
+ const arlResp = await got()('https://www.deezer.com/ajax/gw-light.php', {
87
110
  searchParams: { method: 'user.getArl', input: 3, api_version: '1.0', api_token: 'null' },
88
111
  headers: { 'User-Agent': UA, cookie: cookieHeader(jar) },
89
112
  responseType: 'json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.15.0",
3
+ "version": "2.17.0",
4
4
  "description": "Command-line music downloader for Deezer (Spotify/Tidal links resolved via ISRC matching) with automatic MP3/FLAC tagging, synced lyrics and a side-effect-free programmatic API.",
5
5
  "keywords": [
6
6
  "deezer",
@@ -63,7 +63,7 @@
63
63
  "adm-zip": "^0.5.16",
64
64
  "chalk": "^4.1.2",
65
65
  "commander": "^9.5.0",
66
- "gerdur-core": "^2.19.0",
66
+ "gerdur-core": "^2.20.0",
67
67
  "dot-prop": "^6.0.1",
68
68
  "got": "^11.8.6",
69
69
  "gradient-string": "^2.0.2",