gerdur 1.0.0 → 1.0.2

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1 - 2026-08-30
4
+
5
+ ### Changed
6
+
7
+ - Decryption now relies entirely on `gerdur-core@^1.0.3` (correct on every Node, ~290 MiB/s). Removed the `egoroof-blowfish` dependency and the OpenSSL-3 fallback dance.
8
+ - Removed the `worker_threads` decrypt pool — with a fast native-speed decrypt it only added thread-spawn latency. `decryptDownloadFile` is now a thin sync wrapper.
9
+
3
10
  ## 1.0.0 - 2026-08-30
4
11
 
5
12
  Initial public release.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "The crucial streaming module for dabox systems.",
5
5
  "author": "Christian",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -48,9 +48,8 @@
48
48
  "adm-zip": "^0.5.16",
49
49
  "chalk": "^4.1.2",
50
50
  "commander": "^9.5.0",
51
- "gerdur-core": "^1.0.0",
51
+ "gerdur-core": "^1.0.4",
52
52
  "dot-prop": "^6.0.1",
53
- "egoroof-blowfish": "^4.0.2",
54
53
  "got": "^11.8.6",
55
54
  "gradient-string": "^2.0.2",
56
55
  "log-update": "^4.0.0",
@@ -219,6 +219,32 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
219
219
  const resolveFullPath = options.resolveFullPath ?? conf.get('playlist.resolveFullPath');
220
220
  const savedFiles = [];
221
221
  let m3u8 = [];
222
+ // One batched get_url for the whole selection instead of a request per track.
223
+ // Deezer returns the best licensed format, so pass an ordered preference list.
224
+ const QUALITY_PREF = {
225
+ '1': [1],
226
+ '128': [1],
227
+ '3': [3, 1],
228
+ '320': [3, 1],
229
+ '9': [9, 3, 1],
230
+ flac: [9, 3, 1],
231
+ };
232
+ const prefetchedUrls = new Map();
233
+ if (data.tracks.length > 1 && !process.env.SIMULATE) {
234
+ try {
235
+ const base = QUALITY_PREF[String(options.quality).toLowerCase()] ?? [3, 1];
236
+ const pref = fallbackQuality ? base : [base[0]];
237
+ const resolved = await (0, gerdur_core_1.resolveDownloadUrls)(data.tracks, pref);
238
+ resolved.forEach((r, i) => {
239
+ if (r) {
240
+ prefetchedUrls.set(data.tracks[i].SNG_ID, r);
241
+ }
242
+ });
243
+ }
244
+ catch {
245
+ // batch resolve failed wholesale — every track falls back to per-track resolution
246
+ }
247
+ }
222
248
  await queue.addAll(data.tracks.map((track, index) => {
223
249
  return async () => {
224
250
  const savedPath = await (0, download_track_1.default)({
@@ -233,6 +259,7 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
233
259
  fallbackQuality,
234
260
  overwrite,
235
261
  message: `(${index}/${data.tracks.length})`,
262
+ prefetched: prefetchedUrls.get(track.SNG_ID) ?? null,
236
263
  });
237
264
  // Add to saved list
238
265
  if (savedPath) {
@@ -1,3 +1,4 @@
1
1
  /// <reference types="node" />
2
- export declare const decryptDownload: (source: Buffer, trackId: string) => Buffer;
2
+ export { decryptDownload, TrackDecryptStream } from 'gerdur-core';
3
+ /** Decrypt a track that was streamed to a temp file. */
3
4
  export declare const decryptDownloadFile: (tmpfile: string, trackId: string) => Buffer;
@@ -1,114 +1,17 @@
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.decryptDownloadFile = exports.decryptDownload = void 0;
7
- const crypto_1 = __importDefault(require("crypto"));
3
+ exports.decryptDownloadFile = exports.TrackDecryptStream = exports.decryptDownload = void 0;
4
+ /**
5
+ * Track decryption. As of gerdur-core@1.0.2 the core `decryptDownload` is a
6
+ * correct, dependency-free Blowfish that works on every Node (it no longer
7
+ * touches OpenSSL's removed `bf-cbc`), at ~290 MiB/s — so there is nothing left
8
+ * to wrap or fall back to here.
9
+ */
8
10
  const fs_1 = require("fs");
9
11
  const gerdur_core_1 = require("gerdur-core");
10
- const egoroof_blowfish_1 = require("egoroof-blowfish");
11
- const md5 = (data, type = 'ascii') => {
12
- const md5sum = crypto_1.default.createHash('md5');
13
- md5sum.update(data.toString(), type);
14
- return md5sum.digest('hex');
15
- };
16
- const getBlowfishKey = (trackId) => {
17
- const SECRET = 'g4el58wc0zvf9na1';
18
- const idMd5 = md5(trackId);
19
- let bfKey = '';
20
- for (let i = 0; i < 16; i++) {
21
- bfKey += String.fromCharCode(idMd5.charCodeAt(i) ^ idMd5.charCodeAt(i + 16) ^ SECRET.charCodeAt(i));
22
- }
23
- return bfKey;
24
- };
25
- const decryptChunkFallback = (chunk, blowFishKey) => {
26
- const bf = new egoroof_blowfish_1.Blowfish(blowFishKey, egoroof_blowfish_1.Blowfish.MODE.CBC, egoroof_blowfish_1.Blowfish.PADDING.NULL);
27
- bf.setIv(new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]));
28
- return Buffer.from(bf.decode(new Uint8Array(chunk)));
29
- };
30
- const decryptChunkNative = (chunk, blowFishKey) => {
31
- const cipher = crypto_1.default.createDecipheriv('bf-cbc', blowFishKey, Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]));
32
- cipher.setAutoPadding(false);
33
- return Buffer.concat([cipher.update(chunk), cipher.final()]);
34
- };
35
- const decryptChunk = (chunk, blowFishKey) => {
36
- try {
37
- return decryptChunkNative(chunk, blowFishKey);
38
- }
39
- catch (err) {
40
- if (!canFallback(err)) {
41
- throw err;
42
- }
43
- return decryptChunkFallback(chunk, blowFishKey);
44
- }
45
- };
46
- const decryptDownloadFallback = (source, trackId) => {
47
- const chunkSize = 2048;
48
- const blowFishKey = getBlowfishKey(trackId);
49
- const destBuffer = Buffer.alloc(source.length);
50
- let chunkIndex = 0;
51
- let position = 0;
52
- while (position < source.length) {
53
- const currentChunkSize = Math.min(chunkSize, source.length - position);
54
- const sourceChunk = source.subarray(position, position + currentChunkSize);
55
- if (chunkIndex % 3 > 0 || currentChunkSize < chunkSize) {
56
- sourceChunk.copy(destBuffer, position);
57
- }
58
- else {
59
- decryptChunk(sourceChunk, blowFishKey).copy(destBuffer, position);
60
- }
61
- position += currentChunkSize;
62
- chunkIndex++;
63
- }
64
- return destBuffer;
65
- };
66
- const canFallback = (err) => {
67
- if (!(err instanceof Error)) {
68
- return false;
69
- }
70
- const code = err.code;
71
- return code === 'ERR_OSSL_EVP_UNSUPPORTED' || err.message.includes('digital envelope routines::unsupported');
72
- };
73
- const decryptDownload = (source, trackId) => {
74
- try {
75
- return (0, gerdur_core_1.decryptDownload)(source, trackId);
76
- }
77
- catch (err) {
78
- if (!canFallback(err)) {
79
- throw err;
80
- }
81
- return decryptDownloadFallback(source, trackId);
82
- }
83
- };
84
- exports.decryptDownload = decryptDownload;
85
- const decryptDownloadFile = (tmpfile, trackId) => {
86
- const chunkSize = 2048;
87
- const blowFishKey = getBlowfishKey(trackId);
88
- const sourceLength = (0, fs_1.statSync)(tmpfile).size;
89
- const destBuffer = Buffer.alloc(sourceLength);
90
- const chunk = Buffer.alloc(chunkSize);
91
- const fd = (0, fs_1.openSync)(tmpfile, 'r');
92
- let chunkIndex = 0;
93
- let position = 0;
94
- try {
95
- while (position < sourceLength) {
96
- const currentChunkSize = Math.min(chunkSize, sourceLength - position);
97
- const bytesRead = (0, fs_1.readSync)(fd, chunk, 0, currentChunkSize, position);
98
- const sourceChunk = chunk.subarray(0, bytesRead);
99
- if (chunkIndex % 3 > 0 || bytesRead < chunkSize) {
100
- sourceChunk.copy(destBuffer, position);
101
- }
102
- else {
103
- decryptChunk(sourceChunk, blowFishKey).copy(destBuffer, position);
104
- }
105
- position += bytesRead;
106
- chunkIndex++;
107
- }
108
- }
109
- finally {
110
- (0, fs_1.closeSync)(fd);
111
- }
112
- return destBuffer;
113
- };
12
+ var gerdur_core_2 = require("gerdur-core");
13
+ Object.defineProperty(exports, "decryptDownload", { enumerable: true, get: function () { return gerdur_core_2.decryptDownload; } });
14
+ Object.defineProperty(exports, "TrackDecryptStream", { enumerable: true, get: function () { return gerdur_core_2.TrackDecryptStream; } });
15
+ /** Decrypt a track that was streamed to a temp file. */
16
+ const decryptDownloadFile = (tmpfile, trackId) => (0, gerdur_core_1.decryptDownload)((0, fs_1.readFileSync)(tmpfile), trackId);
114
17
  exports.decryptDownloadFile = decryptDownloadFile;
@@ -19,6 +19,18 @@ interface downloadTrackProps {
19
19
  isQualityFallback?: boolean;
20
20
  overwrite?: boolean;
21
21
  message?: string;
22
+ /**
23
+ * A download URL already resolved for this track by a batch `resolveDownloadUrls`
24
+ * pass. When set, the per-track `get_url` request and quality-fallback round-trips
25
+ * are skipped and `quality` is taken from the format Deezer actually granted.
26
+ * Falls through to the normal path when absent or `null` (track not in the batch).
27
+ */
28
+ prefetched?: {
29
+ trackUrl: string;
30
+ isEncrypted: boolean;
31
+ fileSize: number;
32
+ format: string;
33
+ } | null;
22
34
  }
23
- declare const downloadTrack: ({ track, quality, info, coverSizes, path, totalTracks, trackNumber, fallbackTrack, fallbackQuality, isFallback, isQualityFallback, overwrite, message, }: downloadTrackProps) => Promise<string | undefined>;
35
+ declare const downloadTrack: ({ track, quality, info, coverSizes, path, totalTracks, trackNumber, fallbackTrack, fallbackQuality, isFallback, isQualityFallback, overwrite, message, prefetched, }: downloadTrackProps) => Promise<string | undefined>;
24
36
  export default downloadTrack;
@@ -13,13 +13,20 @@ const log_update_1 = __importDefault(require("log-update"));
13
13
  const chalk_1 = __importDefault(require("chalk"));
14
14
  const signale_1 = __importDefault(require("../lib/signale"));
15
15
  const util_2 = require("./util");
16
- const decrypt_pool_1 = require("./decrypt-pool");
16
+ const decrypt_1 = require("./decrypt");
17
17
  const pipeline = (0, util_1.promisify)(stream_1.default.pipeline);
18
18
  const simulate = process.env.SIMULATE;
19
- const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTracks, trackNumber = true, fallbackTrack = true, fallbackQuality = true, isFallback = false, isQualityFallback = false, overwrite = false, message = '', }) => {
19
+ /** media-API format string -> gerdur's numeric quality */
20
+ const FORMAT_TO_QUALITY = { FLAC: 9, MP3_320: 3, MP3_256: 3, MP3_128: 1, MP3_64: 1 };
21
+ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTracks, trackNumber = true, fallbackTrack = true, fallbackQuality = true, isFallback = false, isQualityFallback = false, overwrite = false, message = '', prefetched = null, }) => {
20
22
  (0, log_update_1.default)(signale_1.default.pending(track.SNG_TITLE + ' by ' + track.ART_NAME + ' from ' + track.ALB_TITLE));
21
23
  try {
22
24
  let ext = '.mp3', fileSize = 0, downloaded = 0, coverSize = 500;
25
+ // A batch resolve already picked the best licensed format for this track —
26
+ // align `quality` with it so the extension, cover size and file size match.
27
+ if (prefetched && FORMAT_TO_QUALITY[prefetched.format]) {
28
+ quality = FORMAT_TO_QUALITY[prefetched.format];
29
+ }
23
30
  switch (quality) {
24
31
  case 1:
25
32
  case '1':
@@ -54,12 +61,17 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
54
61
  return savePath;
55
62
  }
56
63
  let trackData;
57
- try {
58
- trackData = await (0, gerdur_core_1.getTrackDownloadUrl)(track, quality);
64
+ if (prefetched) {
65
+ trackData = prefetched;
59
66
  }
60
- catch (err) {
61
- if (!(err instanceof gerdur_core_1.GeoBlocked) || !track.FALLBACK) {
62
- throw err;
67
+ else {
68
+ try {
69
+ trackData = await (0, gerdur_core_1.getTrackDownloadUrl)(track, quality);
70
+ }
71
+ catch (err) {
72
+ if (!(err instanceof gerdur_core_1.GeoBlocked) || !track.FALLBACK) {
73
+ throw err;
74
+ }
63
75
  }
64
76
  }
65
77
  if (!trackData) {
@@ -114,7 +126,13 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
114
126
  const bar = (0, util_2.progressBar)(fileSize, 40);
115
127
  const humanSizeTotal = (fileSize / 1024 / 1024).toFixed(2);
116
128
  let transferredLast = downloaded;
117
- await pipeline(got_1.default.stream(trackData.trackUrl, { responseType: 'buffer', headers }).on('downloadProgress', ({ transferred }) => {
129
+ await pipeline(got_1.default
130
+ .stream(trackData.trackUrl, {
131
+ responseType: 'buffer',
132
+ headers,
133
+ agent: { http: gerdur_core_1.httpAgent, https: gerdur_core_1.httpsAgent },
134
+ })
135
+ .on('downloadProgress', ({ transferred }) => {
118
136
  // Report download progress
119
137
  transferred += downloaded;
120
138
  if (transferred - transferredLast > 50000) {
@@ -125,7 +143,7 @@ const downloadTrack = async ({ track, quality, info, coverSizes, path, totalTrac
125
143
  let outFile;
126
144
  if (trackData.isEncrypted) {
127
145
  (0, log_update_1.default)(signale_1.default.pending('Decrypting ' + track.SNG_TITLE + ' by ' + track.ART_NAME));
128
- outFile = await (0, decrypt_pool_1.decryptDownloadFile)(tmpfile, track.SNG_ID);
146
+ outFile = await (0, decrypt_1.decryptDownloadFile)(tmpfile, track.SNG_ID);
129
147
  }
130
148
  else {
131
149
  outFile = (0, fs_1.readFileSync)(tmpfile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "The crucial streaming module for dabox systems.",
5
5
  "author": "Christian",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -48,9 +48,8 @@
48
48
  "adm-zip": "^0.5.16",
49
49
  "chalk": "^4.1.2",
50
50
  "commander": "^9.5.0",
51
- "gerdur-core": "^1.0.0",
51
+ "gerdur-core": "^1.0.4",
52
52
  "dot-prop": "^6.0.1",
53
- "egoroof-blowfish": "^4.0.2",
54
53
  "got": "^11.8.6",
55
54
  "gradient-string": "^2.0.2",
56
55
  "log-update": "^4.0.0",
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
@@ -1,345 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- const os_1 = require("os");
8
- const fs_1 = require("fs");
9
- const path_1 = require("path");
10
- const commander_1 = require("commander");
11
- const gradient_string_1 = __importDefault(require("gradient-string"));
12
- const hoenir_core_1 = require("hoenir-core");
13
- const prompts_1 = __importDefault(require("prompts"));
14
- const log_update_1 = __importDefault(require("log-update"));
15
- const p_queue_1 = __importDefault(require("p-queue"));
16
- const chalk_1 = __importDefault(require("chalk"));
17
- const true_case_path_1 = require("true-case-path");
18
- const signale_1 = __importDefault(require("./lib/signale"));
19
- const download_track_1 = __importDefault(require("./lib/download-track"));
20
- const config_1 = __importDefault(require("./lib/config"));
21
- const update_check_1 = __importDefault(require("./lib/update-check"));
22
- const auto_updater_1 = __importDefault(require("./lib/auto-updater"));
23
- const arl_setup_1 = require("./lib/arl-setup");
24
- const util_1 = require("./lib/util");
25
- const package_json_1 = __importDefault(require("../package.json"));
26
- // App info
27
- console.log((0, gradient_string_1.default)('red', 'yellow', 'orange')(` ♥ hoenir - ${package_json_1.default.version} ♥ `) +
28
- '\n' +
29
- (0, gradient_string_1.default)('orange', 'yellow', 'red')(' ──────────────────────────────────────────────') +
30
- '\n' +
31
- (0, gradient_string_1.default)('red', 'yellow', 'orange')(' │ repo https://github.com/soulwax/hoenir │ ') +
32
- '\n' +
33
- (0, gradient_string_1.default)('red', 'yellow', 'orange')(' │ github https://github.com/soulwax │ ') +
34
- '\n' +
35
- (0, gradient_string_1.default)('red', 'yellow', 'orange')(' ──────────────────────────────────────────────'));
36
- const cmd = new commander_1.Command()
37
- .option('-q, --quality <quality>', 'The quality of the files to download: 128/320/flac ')
38
- .option('-o, --output <template>', 'Output filename template')
39
- .option('-u, --url <url>', 'Deezer album/artist/playlist/track url')
40
- .option('-i, --input-file <file>', 'Downloads all urls listed in text file')
41
- .option('-c, --concurrency <number>', 'Download concurrency for album, artists and playlist')
42
- .option('-a, --set-arl <string>', 'Set arl cookie')
43
- .option('-s, --setup', 'Run guided first-time setup (enter your arl cookie)')
44
- .option('--experimental-login', 'Deprecated: email/password login is now offered by default')
45
- .option('-w, --overwrite', 'Re-download and overwrite files that already exist')
46
- .option('-d, --headless', 'Run in headless mode for scripting automation', false)
47
- .option('-conf, --config-file <file>', 'Custom location to your config file', 'hoenir.config.json')
48
- .option('-rfp, --resolve-full-path', 'Use absolute path for playlists')
49
- .option('-cp, --create-playlist', 'Force create a playlist file for non playlists');
50
- if (process.pkg) {
51
- cmd.option('-U, --update', 'Update this program to latest version');
52
- }
53
- const options = cmd.parse(process.argv).opts();
54
- // Support a bare `hoenir setup` subcommand as an alias for `--setup`.
55
- if (cmd.args[0] && cmd.args[0].toLowerCase() === 'setup') {
56
- options.setup = true;
57
- cmd.args.shift();
58
- }
59
- if (!options.url && cmd.args[0]) {
60
- options.url = cmd.args[0];
61
- }
62
- if (options.headless && !options.quality) {
63
- console.error(signale_1.default.error('Missing parameters --quality'));
64
- console.error(signale_1.default.note('Quality must be provided with headless mode'));
65
- process.exit(1);
66
- }
67
- if (options.headless && !options.url && !options.inputFile) {
68
- console.error(signale_1.default.error('Missing parameters --url'));
69
- console.error(signale_1.default.note('URL must be provided with headless mode'));
70
- process.exit(1);
71
- }
72
- const conf = new config_1.default(options.configFile);
73
- if (conf.userConfigLocation) {
74
- console.log(signale_1.default.info('Config loaded --> ' + conf.userConfigLocation));
75
- }
76
- const queue = new p_queue_1.default({ concurrency: Number(options.concurrency || conf.get('concurrency')) });
77
- const urlRegex = /https?:\/\/.*\w+\.\w+\/\w+/;
78
- const onCancel = () => {
79
- console.info(signale_1.default.note('Aborted!'));
80
- process.exit();
81
- };
82
- const startDownload = async (saveLayout, url, skipPrompt) => {
83
- try {
84
- if (!options.quality) {
85
- const { musicQuality } = await (0, prompts_1.default)([
86
- {
87
- type: 'select',
88
- name: 'musicQuality',
89
- message: 'Select music quality:',
90
- choices: [
91
- { title: 'MP3 - 128 kbps', value: '128' },
92
- { title: 'MP3 - 320 kbps', value: '320' },
93
- { title: 'FLAC - 1411 kbps', value: 'flac' },
94
- ],
95
- initial: 1,
96
- },
97
- ], { onCancel });
98
- options.quality = musicQuality;
99
- }
100
- if (!url) {
101
- const { query } = await (0, prompts_1.default)([
102
- {
103
- type: 'text',
104
- name: 'query',
105
- message: 'Enter URL or search:',
106
- validate: (value) => (value ? true : false),
107
- },
108
- ], { onCancel });
109
- url = query;
110
- }
111
- let searchData = null;
112
- if (!url.match(urlRegex)) {
113
- if (options.headless) {
114
- throw new Error('Please provide a valid URL. Unknown URL: ' + url);
115
- }
116
- if (url.startsWith('artist:')) {
117
- const { ARTIST } = await (0, hoenir_core_1.searchMusic)(url.replace('artist:', ''), ['ARTIST'], 50);
118
- const choice = await (0, prompts_1.default)([
119
- {
120
- type: 'select',
121
- name: 'items',
122
- message: `Select one artist. (found ${ARTIST.data.length} artists)`,
123
- choices: ARTIST.data.map((a) => ({
124
- title: a.ART_NAME,
125
- value: a,
126
- description: `${a.NB_FAN} fans`,
127
- })),
128
- },
129
- ], { onCancel });
130
- console.log(signale_1.default.info('Fetching data. Please hold on.'));
131
- url = `https://deezer.com/us/artist/${choice.items.ART_ID}`;
132
- }
133
- else if (url.startsWith('album:')) {
134
- const { ALBUM } = await (0, hoenir_core_1.searchMusic)(url.replace('album:', ''), ['ALBUM'], 50);
135
- const choice = await (0, prompts_1.default)([
136
- {
137
- type: 'select',
138
- name: 'items',
139
- message: `Select one album. (found ${ALBUM.data.length} albums)`,
140
- choices: ALBUM.data.map((a) => ({
141
- title: a.ALB_TITLE,
142
- value: a,
143
- description: `by ${a.ART_NAME}, ${a.NUMBER_TRACK} tracks`,
144
- })),
145
- },
146
- ], { onCancel });
147
- url = `https://deezer.com/us/album/${choice.items.ALB_ID}`;
148
- }
149
- else if (url.startsWith('playlist:')) {
150
- const { PLAYLIST } = await (0, hoenir_core_1.searchMusic)(url.replace('playlist:', ''), ['PLAYLIST'], 50);
151
- const choice = await (0, prompts_1.default)([
152
- {
153
- type: 'select',
154
- name: 'items',
155
- message: `Select one playlist. (found ${PLAYLIST.data.length} playlists)`,
156
- choices: PLAYLIST.data.map((p) => ({
157
- title: p.TITLE,
158
- value: p,
159
- description: `by ${p.PARENT_USERNAME}, ${p.NB_SONG} tracks`,
160
- })),
161
- },
162
- ], { onCancel });
163
- url = `https://deezer.com/us/playlist/${choice.items.PLAYLIST_ID}`;
164
- }
165
- else {
166
- const { TRACK } = await (0, hoenir_core_1.searchMusic)(url, ['TRACK']);
167
- searchData = {
168
- info: { type: 'track', id: url },
169
- linktype: 'track',
170
- linkinfo: {},
171
- tracks: TRACK.data.map((t) => {
172
- if (t.VERSION && !t.SNG_TITLE.includes(t.VERSION)) {
173
- t.SNG_TITLE += ' ' + t.VERSION;
174
- }
175
- return t;
176
- }),
177
- };
178
- }
179
- }
180
- else if (url.match(/playlist|artist/)) {
181
- console.log(signale_1.default.info('Fetching data. Please hold on.'));
182
- }
183
- const data = searchData ? searchData : await (0, hoenir_core_1.parseInfo)(url);
184
- if (!options.headless && data.tracks.length > 1) {
185
- const choices = await (0, prompts_1.default)([
186
- {
187
- type: 'multiselect',
188
- name: 'items',
189
- message: `Select songs to download. Total of ${data.tracks.length} tracks.`,
190
- choices: data.tracks.map((t) => ({
191
- title: t.SNG_TITLE,
192
- value: t,
193
- description: `Artist: ${t.ART_NAME}\nAlbum: ${t.ALB_TITLE}\nDuration: ${(0, util_1.formatSecondsReadable)(Number(t.DURATION))}`,
194
- })),
195
- },
196
- ], { onCancel });
197
- data.tracks = choices.items;
198
- }
199
- if (data && data.tracks.length > 0) {
200
- console.log(signale_1.default.info(`Proceeding to download ${data.tracks.length} tracks. Be patient.`));
201
- if (data.linktype === 'playlist') {
202
- const filteredTracks = data.tracks.filter((item, index, self) => index === self.findIndex((t) => t.SNG_ID === item.SNG_ID));
203
- const duplicateTracks = data.tracks.length - filteredTracks.length;
204
- if (duplicateTracks > 0) {
205
- data.tracks = filteredTracks
206
- .sort((a, b) => a.TRACK_POSITION - b.TRACK_POSITION)
207
- .map((t, i) => {
208
- t.TRACK_POSITION = i + 1;
209
- return t;
210
- });
211
- console.log(signale_1.default.warn(`Removed ${duplicateTracks} duplicate ${duplicateTracks > 1 ? 'tracks' : 'track'}.`));
212
- }
213
- }
214
- const coverSizes = conf.get('coverSize');
215
- const trackNumber = conf.get('trackNumber', true);
216
- const fallbackTrack = conf.get('fallbackTrack', true);
217
- const fallbackQuality = conf.get('fallbackQuality', true);
218
- const overwrite = options.overwrite ?? conf.get('overwrite', false);
219
- const resolveFullPath = options.resolveFullPath ?? conf.get('playlist.resolveFullPath');
220
- const savedFiles = [];
221
- let m3u8 = [];
222
- await queue.addAll(data.tracks.map((track, index) => {
223
- return async () => {
224
- const savedPath = await (0, download_track_1.default)({
225
- track,
226
- quality: options.quality,
227
- info: data.linkinfo,
228
- coverSizes,
229
- path: options.output ? options.output : saveLayout[data.linktype],
230
- totalTracks: data ? data.tracks.length : 10,
231
- trackNumber,
232
- fallbackTrack,
233
- fallbackQuality,
234
- overwrite,
235
- message: `(${index}/${data.tracks.length})`,
236
- });
237
- // Add to saved list
238
- if (savedPath) {
239
- m3u8.push((0, path_1.resolve)(process.env.SIMULATE ? savedPath : (0, true_case_path_1.trueCasePathSync)(savedPath)));
240
- savedFiles.push(savedPath);
241
- }
242
- };
243
- }));
244
- // Display downloaded location
245
- if (savedFiles.length > 0) {
246
- const savedIn = new Set(savedFiles.map((l) => (0, path_1.dirname)(l)));
247
- console.log(signale_1.default.info('Saved in ' + [...savedIn].map((d) => chalk_1.default.bgGreen(d)).join(', ')));
248
- }
249
- if ((options.createPlaylist || data.linktype === 'playlist') && !process.env.SIMULATE && m3u8.length > 1) {
250
- const playlistDir = (0, util_1.commonPath)([...new Set(savedFiles.map(path_1.dirname))]);
251
- const playlistFile = (0, path_1.join)(playlistDir, (0, util_1.sanitizeFilename)(data.linkinfo.TITLE || data.linkinfo.ALB_TITLE));
252
- if (!resolveFullPath) {
253
- const resolvedPlaylistDir = (0, path_1.resolve)(playlistDir) + path_1.sep;
254
- m3u8 = m3u8.map((file) => file.replace(resolvedPlaylistDir, ''));
255
- }
256
- const m3u8Content = '#EXTM3U' + os_1.EOL + m3u8.sort().join(os_1.EOL);
257
- (0, fs_1.writeFileSync)(playlistFile + '.m3u8', m3u8Content, { encoding: 'utf-8' });
258
- }
259
- }
260
- else {
261
- console.log(signale_1.default.info('No items to download!'));
262
- }
263
- }
264
- catch (err) {
265
- console.error(signale_1.default.error(err.message));
266
- }
267
- // Ask for new download
268
- if (!options.headless && !skipPrompt) {
269
- startDownload(saveLayout, '', skipPrompt);
270
- }
271
- };
272
- /**
273
- * Application init.
274
- */
275
- const initApp = async () => {
276
- if (options.setup) {
277
- await (0, arl_setup_1.runSetup)(conf, options.headless, options.experimentalLogin);
278
- return;
279
- }
280
- if (options.setArl) {
281
- const configPath = conf.set('cookies.arl', options.setArl);
282
- console.log(signale_1.default.info('cookies.arl set to --> ' + options.setArl));
283
- console.log(signale_1.default.note(configPath));
284
- process.exit();
285
- }
286
- (0, log_update_1.default)(signale_1.default.pending('Initializing session...'));
287
- let arl = await (0, arl_setup_1.ensureArl)(conf, options.headless, options.experimentalLogin);
288
- const login = async (cookie) => {
289
- (0, log_update_1.default)(signale_1.default.pending('Verifying session...'));
290
- await (0, hoenir_core_1.initDeezerApi)(cookie);
291
- return (0, hoenir_core_1.getUser)();
292
- };
293
- let user;
294
- try {
295
- user = await login(arl);
296
- }
297
- catch (err) {
298
- log_update_1.default.clear();
299
- const newArl = await (0, arl_setup_1.recoverFromLoginFailure)(conf, options.headless, err, options.experimentalLogin);
300
- if (!newArl) {
301
- throw err;
302
- }
303
- arl = newArl;
304
- user = await login(arl);
305
- }
306
- (0, log_update_1.default)(signale_1.default.success('Logged in as ' + user.BLOG_NAME));
307
- log_update_1.default.done();
308
- const saveLayout = conf.get('saveLayout');
309
- if (options.inputFile) {
310
- const urls = (0, fs_1.readFileSync)(options.inputFile, 'utf-8')
311
- .split(/\r?\n/)
312
- .map((line) => line.trim())
313
- .filter((line) => line.match(urlRegex));
314
- if (!options.quality && !options.headless) {
315
- for (const url of urls) {
316
- console.log(signale_1.default.info('Starting download: ' + url));
317
- await startDownload(saveLayout, url, true);
318
- }
319
- }
320
- else {
321
- await Promise.all(urls.map((url) => {
322
- console.log(signale_1.default.info('Starting download: ' + url));
323
- return startDownload(saveLayout, url, true);
324
- }));
325
- }
326
- }
327
- else {
328
- startDownload(saveLayout, options.url, false);
329
- }
330
- };
331
- if (options.update) {
332
- (0, auto_updater_1.default)(package_json_1.default).catch((err) => {
333
- console.error(signale_1.default.error(err.message));
334
- process.exit(1);
335
- });
336
- }
337
- else {
338
- // Check for update
339
- (0, update_check_1.default)(package_json_1.default);
340
- // Init interface
341
- initApp().catch((err) => {
342
- console.error(signale_1.default.error(err.message));
343
- process.exit(1);
344
- });
345
- }