gerdur 0.0.0-probe → 1.0.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 +10 -0
- package/LICENSE +0 -0
- package/README.md +231 -1
- package/dist/package.json +90 -0
- package/dist/src/gerdur.d.ts +2 -0
- package/dist/src/gerdur.js +345 -0
- package/dist/src/hoenir.d.ts +2 -0
- package/dist/src/hoenir.js +345 -0
- package/dist/src/index.d.ts +62 -0
- package/dist/src/index.js +101 -0
- package/dist/src/kyrie.d.ts +2 -0
- package/dist/src/kyrie.js +345 -0
- package/dist/src/lib/api-download.d.ts +54 -0
- package/dist/src/lib/api-download.js +89 -0
- package/dist/src/lib/arl-setup.d.ts +82 -0
- package/dist/src/lib/arl-setup.js +274 -0
- package/dist/src/lib/auto-updater.d.ts +2 -0
- package/dist/src/lib/auto-updater.js +70 -0
- package/dist/src/lib/config.d.ts +53 -0
- package/dist/src/lib/config.js +164 -0
- package/dist/src/lib/decrypt-pool.d.ts +2 -0
- package/dist/src/lib/decrypt-pool.js +122 -0
- package/dist/src/lib/decrypt-worker.d.ts +1 -0
- package/dist/src/lib/decrypt-worker.js +17 -0
- package/dist/src/lib/decrypt.d.ts +3 -0
- package/dist/src/lib/decrypt.js +114 -0
- package/dist/src/lib/download-track.d.ts +24 -0
- package/dist/src/lib/download-track.js +163 -0
- package/dist/src/lib/email-login.d.ts +16 -0
- package/dist/src/lib/email-login.js +111 -0
- package/dist/src/lib/session.d.ts +84 -0
- package/dist/src/lib/session.js +118 -0
- package/dist/src/lib/signale.d.ts +15 -0
- package/dist/src/lib/signale.js +20 -0
- package/dist/src/lib/update-check.d.ts +2 -0
- package/dist/src/lib/update-check.js +30 -0
- package/dist/src/lib/util.d.ts +17 -0
- package/dist/src/lib/util.js +94 -0
- package/dist/src/q-fi.d.ts +2 -0
- package/dist/src/q-fi.js +345 -0
- package/package.json +88 -5
- package/setup-termux.js +41 -0
|
@@ -0,0 +1,345 @@
|
|
|
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 kyrie_core_1 = require("kyrie-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')(` ♥ kyrie - ${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/kyrie │ ') +
|
|
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', 'kyrie.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 `kyrie 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, kyrie_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, kyrie_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, kyrie_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, kyrie_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, kyrie_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, kyrie_core_1.initDeezerApi)(cookie);
|
|
291
|
+
return (0, kyrie_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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { trackType } from 'gerdur-core/types';
|
|
3
|
+
/** Music quality accepted by the API. */
|
|
4
|
+
export type Quality = '128' | '320' | 'flac' | 1 | 3 | 9;
|
|
5
|
+
export interface GetTrackBufferOptions {
|
|
6
|
+
/** Album cover size (px) embedded in tags. Default 500. */
|
|
7
|
+
coverSize?: number;
|
|
8
|
+
/**
|
|
9
|
+
* If the requested quality is unavailable, try lower qualities (320 -> 128)
|
|
10
|
+
* before giving up. Default true.
|
|
11
|
+
*/
|
|
12
|
+
fallbackQuality?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Download, decrypt, and tag a single track entirely in memory.
|
|
16
|
+
*
|
|
17
|
+
* Returns the ready-to-write audio Buffer (MP3 or FLAC), or `null` if the track
|
|
18
|
+
* is not available for download. Nothing is written to disk and nothing is
|
|
19
|
+
* logged.
|
|
20
|
+
*
|
|
21
|
+
* @param track Track info (e.g. from `getTrackInfo` or `parseUrl`).
|
|
22
|
+
* @param quality '128' | '320' | 'flac' (or numeric 1 | 3 | 9).
|
|
23
|
+
*/
|
|
24
|
+
export declare const getTrackBuffer: (track: trackType, quality?: Quality, { coverSize, fallbackQuality }?: GetTrackBufferOptions) => Promise<Buffer | null>;
|
|
25
|
+
export interface DownloadTrackOptions extends GetTrackBufferOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Output file path or a `saveLayout` template (e.g. `{ART_NAME} - {SNG_TITLE}`).
|
|
28
|
+
* The correct extension (`.mp3` / `.flac`) is appended automatically.
|
|
29
|
+
* Default: `{ART_NAME} - {SNG_TITLE}`.
|
|
30
|
+
*/
|
|
31
|
+
output?: string;
|
|
32
|
+
/** Album info used to resolve album-level template tokens. */
|
|
33
|
+
albumInfo?: {
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
};
|
|
36
|
+
/** Prefix the filename with the track number. Default false. */
|
|
37
|
+
trackNumber?: boolean;
|
|
38
|
+
/** Re-download even if the destination file already exists. Default false. */
|
|
39
|
+
overwrite?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface DownloadResult {
|
|
42
|
+
/** The path the file was written to (or would have been, if skipped). */
|
|
43
|
+
path: string;
|
|
44
|
+
/** True if the file was written; false if skipped because it already existed. */
|
|
45
|
+
written: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Download a single track to disk (silent). Resolves the destination path from
|
|
49
|
+
* the `output` template, skips existing files unless `overwrite` is set, and
|
|
50
|
+
* creates parent directories as needed.
|
|
51
|
+
*
|
|
52
|
+
* Returns `{path, written}`, or `null` if the track was not available.
|
|
53
|
+
*/
|
|
54
|
+
export declare const downloadTrackToFile: (track: trackType, quality?: Quality, options?: DownloadTrackOptions) => Promise<DownloadResult | null>;
|
|
@@ -0,0 +1,89 @@
|
|
|
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.downloadTrackToFile = exports.getTrackBuffer = void 0;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const got_1 = __importDefault(require("got"));
|
|
10
|
+
const gerdur_core_1 = require("gerdur-core");
|
|
11
|
+
const decrypt_1 = require("./decrypt");
|
|
12
|
+
const util_1 = require("./util");
|
|
13
|
+
const QUALITY_MAP = {
|
|
14
|
+
'1': { q: 1, ext: '.mp3', size: 'FILESIZE_MP3_128' },
|
|
15
|
+
'128': { q: 1, ext: '.mp3', size: 'FILESIZE_MP3_128' },
|
|
16
|
+
'3': { q: 3, ext: '.mp3', size: 'FILESIZE_MP3_320' },
|
|
17
|
+
'320': { q: 3, ext: '.mp3', size: 'FILESIZE_MP3_320' },
|
|
18
|
+
'9': { q: 9, ext: '.flac', size: 'FILESIZE_FLAC' },
|
|
19
|
+
flac: { q: 9, ext: '.flac', size: 'FILESIZE_FLAC' },
|
|
20
|
+
};
|
|
21
|
+
const resolveQuality = (quality) => {
|
|
22
|
+
const key = String(quality).toLowerCase();
|
|
23
|
+
return QUALITY_MAP[key] ?? QUALITY_MAP['320'];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Download, decrypt, and tag a single track entirely in memory.
|
|
27
|
+
*
|
|
28
|
+
* Returns the ready-to-write audio Buffer (MP3 or FLAC), or `null` if the track
|
|
29
|
+
* is not available for download. Nothing is written to disk and nothing is
|
|
30
|
+
* logged.
|
|
31
|
+
*
|
|
32
|
+
* @param track Track info (e.g. from `getTrackInfo` or `parseUrl`).
|
|
33
|
+
* @param quality '128' | '320' | 'flac' (or numeric 1 | 3 | 9).
|
|
34
|
+
*/
|
|
35
|
+
const getTrackBuffer = async (track, quality = '320', { coverSize = 500, fallbackQuality = true } = {}) => {
|
|
36
|
+
const order = fallbackQuality ? [quality, '320', '128'] : [quality];
|
|
37
|
+
const tried = new Set();
|
|
38
|
+
for (const q of order) {
|
|
39
|
+
const { q: qNum } = resolveQuality(q);
|
|
40
|
+
if (tried.has(qNum)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
tried.add(qNum);
|
|
44
|
+
let trackData;
|
|
45
|
+
try {
|
|
46
|
+
trackData = await (0, gerdur_core_1.getTrackDownloadUrl)(track, qNum);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof gerdur_core_1.GeoBlocked) {
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
trackData = null;
|
|
53
|
+
}
|
|
54
|
+
if (!trackData) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const { body } = await (0, got_1.default)(trackData.trackUrl, { responseType: 'buffer' });
|
|
58
|
+
const decrypted = trackData.isEncrypted ? (0, decrypt_1.decryptDownload)(body, track.SNG_ID) : body;
|
|
59
|
+
return (0, gerdur_core_1.addTrackTags)(decrypted, track, coverSize);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
exports.getTrackBuffer = getTrackBuffer;
|
|
64
|
+
/**
|
|
65
|
+
* Download a single track to disk (silent). Resolves the destination path from
|
|
66
|
+
* the `output` template, skips existing files unless `overwrite` is set, and
|
|
67
|
+
* creates parent directories as needed.
|
|
68
|
+
*
|
|
69
|
+
* Returns `{path, written}`, or `null` if the track was not available.
|
|
70
|
+
*/
|
|
71
|
+
const downloadTrackToFile = async (track, quality = '320', options = {}) => {
|
|
72
|
+
const { output = '{ART_NAME} - {SNG_TITLE}', albumInfo = {}, trackNumber = false, overwrite = false } = options;
|
|
73
|
+
const { ext } = resolveQuality(quality);
|
|
74
|
+
const savePath = (0, util_1.saveLayout)({ track, album: albumInfo, path: output, trackNumber, minimumIntegerDigits: 2 }) + ext;
|
|
75
|
+
if ((0, fs_1.existsSync)(savePath) && !overwrite) {
|
|
76
|
+
return { path: savePath, written: false };
|
|
77
|
+
}
|
|
78
|
+
const buffer = await (0, exports.getTrackBuffer)(track, quality, options);
|
|
79
|
+
if (!buffer) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const dir = (0, path_1.dirname)(savePath);
|
|
83
|
+
if (dir && !(0, fs_1.existsSync)(dir)) {
|
|
84
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
(0, fs_1.writeFileSync)(savePath, buffer);
|
|
87
|
+
return { path: savePath, written: true };
|
|
88
|
+
};
|
|
89
|
+
exports.downloadTrackToFile = downloadTrackToFile;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type Config from './config';
|
|
2
|
+
/**
|
|
3
|
+
* True when we are attached to an interactive terminal and not running in a
|
|
4
|
+
* scripted / headless context. Only then is it safe to block on a prompt.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isInteractive: (headless: boolean) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Print short, copy-pasteable instructions on where to obtain a Deezer arl cookie.
|
|
9
|
+
*/
|
|
10
|
+
export declare const printArlInstructions: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Interactively ask the user to paste an arl cookie and persist it to config.
|
|
13
|
+
*
|
|
14
|
+
* Returns the entered arl, or `null` if the user aborted / gave nothing.
|
|
15
|
+
* The caller is responsible for only invoking this in an interactive context.
|
|
16
|
+
*/
|
|
17
|
+
export declare const promptForArl: (conf: Config) => Promise<string | null>;
|
|
18
|
+
/**
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
export declare const resolveCredentials: (conf: Config) => {
|
|
24
|
+
email: string;
|
|
25
|
+
password: string;
|
|
26
|
+
} | null;
|
|
27
|
+
/**
|
|
28
|
+
* Attempt an email/password login and, on success, persist the fetched arl.
|
|
29
|
+
* The password itself is never written to disk by this function.
|
|
30
|
+
*
|
|
31
|
+
* Returns the arl on success, or `null` on any failure (the caller should fall
|
|
32
|
+
* back to pasting an arl).
|
|
33
|
+
*/
|
|
34
|
+
export declare const tryEmailLogin: (conf: Config, email: string, password: string, { persistCredentials }?: {
|
|
35
|
+
persistCredentials?: boolean | undefined;
|
|
36
|
+
}) => Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Interactively prompt for email + password (password masked), attempt login,
|
|
39
|
+
* and persist the arl on success. Offers to save the credentials to config
|
|
40
|
+
* (with a plaintext warning) if the login worked.
|
|
41
|
+
*
|
|
42
|
+
* Returns the arl on success, or `null` on failure/cancel so the caller can
|
|
43
|
+
* fall back to pasting an arl.
|
|
44
|
+
*/
|
|
45
|
+
export declare const promptForEmailLogin: (conf: Config) => Promise<string | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Interactive picker for how to configure an arl.
|
|
48
|
+
*
|
|
49
|
+
* Offers email/password login (which fetches the arl automatically via Deezer's
|
|
50
|
+
* mobile OAuth flow) and pasting an arl. Email login falls back to arl paste if
|
|
51
|
+
* it fails. The `_experimentalLogin` parameter is retained for backward
|
|
52
|
+
* compatibility and no longer gates the email option.
|
|
53
|
+
*
|
|
54
|
+
* Returns the arl on success or `null` if nothing was configured.
|
|
55
|
+
*/
|
|
56
|
+
export declare const chooseAndConfigureArl: (conf: Config, _experimentalLogin?: boolean) => Promise<string | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Ensure a usable arl is configured before attempting to log in.
|
|
59
|
+
*
|
|
60
|
+
* When no user-supplied arl exists (empty or still the bundled default) and we
|
|
61
|
+
* are in an interactive terminal, print instructions and prompt for one.
|
|
62
|
+
* In non-interactive / headless contexts this is a no-op so existing scripted
|
|
63
|
+
* behavior is preserved.
|
|
64
|
+
*
|
|
65
|
+
* Returns the arl to use for the session.
|
|
66
|
+
*/
|
|
67
|
+
export declare const ensureArl: (conf: Config, headless: boolean, experimentalLogin?: boolean) => Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* On-demand guided setup, triggered by `gerdur setup` / `--setup`.
|
|
70
|
+
*
|
|
71
|
+
* Always prints instructions and prompts for an arl (even when one is already
|
|
72
|
+
* configured), so users can (re)configure without hunting for the flag.
|
|
73
|
+
* Requires an interactive terminal.
|
|
74
|
+
*/
|
|
75
|
+
export declare const runSetup: (conf: Config, headless: boolean, experimentalLogin?: boolean) => Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Handle a failed login by offering to enter a fresh arl and retrying.
|
|
78
|
+
*
|
|
79
|
+
* Returns the new arl if the user provided one, otherwise `null` (and the
|
|
80
|
+
* original error should be surfaced by the caller).
|
|
81
|
+
*/
|
|
82
|
+
export declare const recoverFromLoginFailure: (conf: Config, headless: boolean, err: Error, experimentalLogin?: boolean) => Promise<string | null>;
|