wavesconv 1.4.9 → 1.6.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/cli.js +126 -96
- package/lib/cli-interactive.js +288 -0
- package/lib/cli-ui.js +289 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -4,46 +4,11 @@
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const engine = require('./lib/engine');
|
|
7
|
+
const ui = require('./lib/cli-ui');
|
|
8
|
+
const { runInteractive } = require('./lib/cli-interactive');
|
|
7
9
|
|
|
8
10
|
const VERSION = require('./package.json').version;
|
|
9
11
|
|
|
10
|
-
const HELP = `
|
|
11
|
-
WavesConverter CLI v${VERSION}
|
|
12
|
-
|
|
13
|
-
Użycie:
|
|
14
|
-
wavesconv download <url> [opcje] Pobierz wideo/audio z YouTube
|
|
15
|
-
wavesconv info <url> Pokaż metadane (JSON)
|
|
16
|
-
wavesconv convert <plik> [opcje] Konwertuj plik lokalny
|
|
17
|
-
wavesconv tools install Zainstaluj yt-dlp (do folderu użytkownika)
|
|
18
|
-
wavesconv tools status Sprawdź yt-dlp i ffmpeg
|
|
19
|
-
wavesconv --help Ta pomoc
|
|
20
|
-
wavesconv --version Wersja
|
|
21
|
-
|
|
22
|
-
Opcje download:
|
|
23
|
-
-o, --output <dir> Folder docelowy (domyślnie: Downloads)
|
|
24
|
-
-f, --format <fmt> mp4, mkv, webm, mp3, wav, flac… (domyślnie: mp4)
|
|
25
|
-
-q, --quality <q> best, 1080p, 720p, 480p, 360p
|
|
26
|
-
-a, --audio Tylko audio
|
|
27
|
-
--bitrate <rate> np. 320k
|
|
28
|
-
--filename <szablon> np. "%(title)s" lub "%(uploader)s - %(title)s"
|
|
29
|
-
|
|
30
|
-
Opcje convert:
|
|
31
|
-
-o, --output <ścieżka> Plik lub folder wyjściowy
|
|
32
|
-
-f, --format <fmt> mp4, mp3, wav, gif…
|
|
33
|
-
-q, --quality <q> Rozdzielczość wideo (1080p, 720p…)
|
|
34
|
-
--bitrate <rate> Bitrate audio
|
|
35
|
-
|
|
36
|
-
Przykłady:
|
|
37
|
-
wavesconv download "https://youtu.be/xxx" -a -f mp3
|
|
38
|
-
wavesconv download "https://youtube.com/watch?v=xxx" -q 1080p -o ~/Videos
|
|
39
|
-
wavesconv convert film.mp4 -f mp3 -o ~/Music/out.mp3
|
|
40
|
-
wavesconv info "https://youtu.be/xxx"
|
|
41
|
-
|
|
42
|
-
Deep link (aplikacja graficzna):
|
|
43
|
-
wavesconverter://download?url=<encoded_url>
|
|
44
|
-
wavesconverter://queue?url=<encoded_url>&start=1
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
12
|
function parseArgs(argv) {
|
|
48
13
|
const args = [...argv];
|
|
49
14
|
const positional = [];
|
|
@@ -59,7 +24,7 @@ function parseArgs(argv) {
|
|
|
59
24
|
if (a.startsWith('--')) {
|
|
60
25
|
const key = a.slice(2);
|
|
61
26
|
args.shift();
|
|
62
|
-
if (['help', 'version', 'audio'].includes(key)) {
|
|
27
|
+
if (['help', 'version', 'audio', 'plain', 'json'].includes(key)) {
|
|
63
28
|
flags[key] = true;
|
|
64
29
|
} else if (args.length) {
|
|
65
30
|
flags[key] = args.shift();
|
|
@@ -71,11 +36,8 @@ function parseArgs(argv) {
|
|
|
71
36
|
args.shift();
|
|
72
37
|
const map = { o: 'output', f: 'format', q: 'quality', a: 'audio' };
|
|
73
38
|
const flagKey = map[key] || key;
|
|
74
|
-
if (key === 'a')
|
|
75
|
-
|
|
76
|
-
} else if (args.length) {
|
|
77
|
-
flags[flagKey] = args.shift();
|
|
78
|
-
}
|
|
39
|
+
if (key === 'a') flags.audio = true;
|
|
40
|
+
else if (args.length) flags[flagKey] = args.shift();
|
|
79
41
|
} else {
|
|
80
42
|
positional.push(args.shift());
|
|
81
43
|
}
|
|
@@ -83,38 +45,75 @@ function parseArgs(argv) {
|
|
|
83
45
|
return { positional, flags };
|
|
84
46
|
}
|
|
85
47
|
|
|
86
|
-
function
|
|
87
|
-
|
|
48
|
+
async function cmdToolsInstall(color) {
|
|
49
|
+
const spin = new ui.Spinner('Instalacja yt-dlp', color);
|
|
50
|
+
spin.start();
|
|
51
|
+
try {
|
|
52
|
+
await engine.ensureYtDlp(msg => spin.draw(msg));
|
|
53
|
+
spin.succeed('yt-dlp zainstalowany');
|
|
54
|
+
} catch (e) {
|
|
55
|
+
spin.fail(e.message);
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
ui.kv(color, 'yt-dlp', engine.findYtDlp() || 'błąd', !!engine.findYtDlp());
|
|
59
|
+
ui.kv(color, 'ffmpeg', engine.findFfmpeg() || 'brak (zainstaluj systemowo)', !!engine.findFfmpeg());
|
|
88
60
|
}
|
|
89
61
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
function cmdToolsStatus(color) {
|
|
63
|
+
ui.printBanner(VERSION, color);
|
|
64
|
+
ui.kv(color, 'yt-dlp', engine.findYtDlp() || 'BRAK', !!engine.findYtDlp());
|
|
65
|
+
ui.kv(color, 'ffmpeg', engine.findFfmpeg() || 'BRAK', !!engine.findFfmpeg());
|
|
66
|
+
ui.kv(color, 'userData', engine.getUserDataDir());
|
|
67
|
+
console.log('');
|
|
94
68
|
}
|
|
95
69
|
|
|
96
|
-
async function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
70
|
+
async function cmdInfo(url, flags, color) {
|
|
71
|
+
const spin = new ui.Spinner('Pobieranie metadanych', color);
|
|
72
|
+
spin.start();
|
|
73
|
+
let items;
|
|
74
|
+
try {
|
|
75
|
+
items = await engine.fetchInfo(url);
|
|
76
|
+
spin.succeed(`${items.length} element(ów)`);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
spin.fail(e.message);
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
101
81
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
82
|
+
if (flags.json || !color) {
|
|
83
|
+
console.log(JSON.stringify(items, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
107
86
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
87
|
+
if (items.length === 1) {
|
|
88
|
+
ui.printVideoCard(color, items[0]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ui.info(color, `Playlista: ${items.length} pozycji`);
|
|
93
|
+
items.slice(0, 15).forEach((item, i) => {
|
|
94
|
+
const title = (item.title || item.id || '?').slice(0, 50);
|
|
95
|
+
console.log(ui.c(color, ui.palette.cyan, ` ${String(i + 1).padStart(2)}. `) + title);
|
|
96
|
+
});
|
|
97
|
+
if (items.length > 15) {
|
|
98
|
+
console.log(ui.c(color, ui.palette.dim, ` … i ${items.length - 15} więcej`));
|
|
99
|
+
}
|
|
100
|
+
console.log('');
|
|
111
101
|
}
|
|
112
102
|
|
|
113
|
-
async function cmdDownload(url, flags) {
|
|
103
|
+
async function cmdDownload(url, flags, color) {
|
|
114
104
|
if (!engine.isYouTubeUrl(url)) {
|
|
115
105
|
throw new Error('Nieprawidłowy URL YouTube: ' + url);
|
|
116
106
|
}
|
|
117
|
-
|
|
107
|
+
|
|
108
|
+
const setupSpin = new ui.Spinner('Przygotowanie yt-dlp', color);
|
|
109
|
+
setupSpin.start();
|
|
110
|
+
try {
|
|
111
|
+
await engine.ensureYtDlp(msg => setupSpin.draw(msg));
|
|
112
|
+
setupSpin.succeed('Silnik gotowy');
|
|
113
|
+
} catch (e) {
|
|
114
|
+
setupSpin.fail(e.message);
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
118
117
|
|
|
119
118
|
const outputDir = path.resolve(flags.output || engine.getDefaultDownloadDir());
|
|
120
119
|
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -123,10 +122,15 @@ async function cmdDownload(url, flags) {
|
|
|
123
122
|
const format = flags.format || (audioOnly ? 'mp3' : 'mp4');
|
|
124
123
|
|
|
125
124
|
let title = 'download';
|
|
125
|
+
const metaSpin = new ui.Spinner('Wczytywanie metadanych', color);
|
|
126
|
+
metaSpin.start();
|
|
126
127
|
try {
|
|
127
128
|
const items = await engine.fetchInfo(url);
|
|
128
129
|
if (items[0]) title = items[0].title || items[0].id || title;
|
|
129
|
-
|
|
130
|
+
metaSpin.succeed(title.slice(0, 52) + (title.length > 52 ? '…' : ''));
|
|
131
|
+
} catch (_) {
|
|
132
|
+
metaSpin.succeed('Metadane pominięte');
|
|
133
|
+
}
|
|
130
134
|
|
|
131
135
|
const job = {
|
|
132
136
|
id: 'cli-' + Date.now(),
|
|
@@ -140,35 +144,35 @@ async function cmdDownload(url, flags) {
|
|
|
140
144
|
filename: flags.filename || '%(title)s',
|
|
141
145
|
};
|
|
142
146
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
log(
|
|
147
|
+
ui.info(color, `Folder: ${outputDir}`);
|
|
148
|
+
ui.info(color, `Format: ${format}${audioOnly ? ' (audio)' : ''} · Jakość: ${job.quality}`);
|
|
149
|
+
console.log('');
|
|
150
|
+
|
|
151
|
+
const bar = new ui.ProgressBar('Pobieranie', color);
|
|
152
|
+
bar.start();
|
|
146
153
|
|
|
147
|
-
let lastPct = -1;
|
|
148
154
|
const result = await engine.runDownload(job, {
|
|
149
155
|
onProgress: (pct, line) => {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
lastPct = rounded;
|
|
153
|
-
process.stdout.write(`\r[${rounded}%] ${line.slice(0, 60).padEnd(60)}`);
|
|
154
|
-
}
|
|
156
|
+
const detail = line.replace(/\[download\]\s*/i, '').trim();
|
|
157
|
+
bar.update(pct, detail);
|
|
155
158
|
},
|
|
156
159
|
onLog: line => {
|
|
157
|
-
if (
|
|
160
|
+
if (/\bERROR\b/i.test(line)) ui.warn(color, line.slice(0, 100));
|
|
158
161
|
},
|
|
159
162
|
});
|
|
160
163
|
|
|
161
|
-
|
|
164
|
+
bar.succeed(`Zapisano · ${ui.formatBytes(result.size || 0)}`);
|
|
162
165
|
if (result.path) {
|
|
163
|
-
|
|
166
|
+
ui.success(color, result.path);
|
|
164
167
|
} else {
|
|
165
|
-
|
|
168
|
+
ui.warn(color, 'Sprawdź folder docelowy (ścieżka nie wykryta automatycznie)');
|
|
166
169
|
}
|
|
170
|
+
console.log('');
|
|
167
171
|
}
|
|
168
172
|
|
|
169
|
-
async function cmdConvert(inputPath, flags) {
|
|
173
|
+
async function cmdConvert(inputPath, flags, color) {
|
|
170
174
|
const ffmpeg = engine.findFfmpeg();
|
|
171
|
-
if (!ffmpeg) throw new Error('ffmpeg nie znaleziony');
|
|
175
|
+
if (!ffmpeg) throw new Error('ffmpeg nie znaleziony — zainstaluj ffmpeg lub użyj aplikacji desktop');
|
|
172
176
|
|
|
173
177
|
const resolvedIn = path.resolve(inputPath);
|
|
174
178
|
if (!fs.existsSync(resolvedIn)) throw new Error('Plik nie istnieje: ' + resolvedIn);
|
|
@@ -195,30 +199,56 @@ async function cmdConvert(inputPath, flags) {
|
|
|
195
199
|
gifDuration: flags['gif-duration'] || 5,
|
|
196
200
|
};
|
|
197
201
|
|
|
198
|
-
|
|
202
|
+
ui.info(color, `Wejście: ${path.basename(resolvedIn)}`);
|
|
203
|
+
ui.info(color, `Wyjście: ${outputPath}`);
|
|
204
|
+
|
|
205
|
+
const spin = new ui.Spinner('Konwersja ffmpeg', color);
|
|
206
|
+
spin.start();
|
|
207
|
+
|
|
199
208
|
await engine.runConvert(job, {
|
|
200
|
-
onProgress: t =>
|
|
209
|
+
onProgress: t => spin.draw(t),
|
|
201
210
|
});
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
|
|
212
|
+
spin.succeed('Konwersja zakończona');
|
|
213
|
+
ui.success(color, outputPath);
|
|
214
|
+
console.log('');
|
|
204
215
|
}
|
|
205
216
|
|
|
206
217
|
async function run(argv) {
|
|
207
218
|
const { positional, flags } = parseArgs(argv);
|
|
219
|
+
const color = ui.useColor() && !flags.plain;
|
|
208
220
|
|
|
209
221
|
if (flags.help || positional[0] === 'help') {
|
|
210
|
-
|
|
222
|
+
ui.printHelp(VERSION, color);
|
|
211
223
|
return 0;
|
|
212
224
|
}
|
|
213
225
|
if (flags.version) {
|
|
214
|
-
|
|
226
|
+
if (color) ui.printBanner(VERSION, true);
|
|
227
|
+
else console.log(VERSION);
|
|
215
228
|
return 0;
|
|
216
229
|
}
|
|
217
230
|
|
|
218
231
|
const cmd = positional[0];
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
232
|
+
|
|
233
|
+
// wavesconv — tryb interaktywny (wklej linki w konsoli)
|
|
234
|
+
if (!cmd || cmd === 'interactive' || cmd === 'i') {
|
|
235
|
+
if (!process.stdin.isTTY) {
|
|
236
|
+
ui.error(color, 'Tryb interaktywny wymaga terminala. Użyj: wavesconv download <url>');
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
return runInteractive(
|
|
240
|
+
{
|
|
241
|
+
download: cmdDownload,
|
|
242
|
+
info: cmdInfo,
|
|
243
|
+
toolsInstall: cmdToolsInstall,
|
|
244
|
+
},
|
|
245
|
+
VERSION,
|
|
246
|
+
color
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (color && cmd !== 'tools') {
|
|
251
|
+
ui.printBanner(VERSION, true);
|
|
222
252
|
}
|
|
223
253
|
|
|
224
254
|
try {
|
|
@@ -226,25 +256,25 @@ async function run(argv) {
|
|
|
226
256
|
case 'download': {
|
|
227
257
|
const url = positional[1];
|
|
228
258
|
if (!url) throw new Error('Podaj URL: wavesconv download <url>');
|
|
229
|
-
await cmdDownload(url, flags);
|
|
259
|
+
await cmdDownload(url, flags, color);
|
|
230
260
|
return 0;
|
|
231
261
|
}
|
|
232
262
|
case 'info': {
|
|
233
263
|
const url = positional[1];
|
|
234
264
|
if (!url) throw new Error('Podaj URL: wavesconv info <url>');
|
|
235
|
-
await cmdInfo(url);
|
|
265
|
+
await cmdInfo(url, flags, color);
|
|
236
266
|
return 0;
|
|
237
267
|
}
|
|
238
268
|
case 'convert': {
|
|
239
269
|
const file = positional[1];
|
|
240
270
|
if (!file) throw new Error('Podaj plik: wavesconv convert <plik>');
|
|
241
|
-
await cmdConvert(file, flags);
|
|
271
|
+
await cmdConvert(file, flags, color);
|
|
242
272
|
return 0;
|
|
243
273
|
}
|
|
244
274
|
case 'tools': {
|
|
245
275
|
const sub = positional[1];
|
|
246
|
-
if (sub === 'install') await cmdToolsInstall();
|
|
247
|
-
else if (sub === 'status') cmdToolsStatus();
|
|
276
|
+
if (sub === 'install') await cmdToolsInstall(color);
|
|
277
|
+
else if (sub === 'status') cmdToolsStatus(color);
|
|
248
278
|
else throw new Error('Użyj: wavesconv tools install|status');
|
|
249
279
|
return 0;
|
|
250
280
|
}
|
|
@@ -252,7 +282,7 @@ async function run(argv) {
|
|
|
252
282
|
throw new Error('Nieznane polecenie: ' + cmd);
|
|
253
283
|
}
|
|
254
284
|
} catch (e) {
|
|
255
|
-
|
|
285
|
+
ui.error(color, e.message || String(e));
|
|
256
286
|
return 1;
|
|
257
287
|
}
|
|
258
288
|
}
|
|
@@ -268,4 +298,4 @@ if (require.main === module) {
|
|
|
268
298
|
run(process.argv.slice(2)).then(code => process.exit(code));
|
|
269
299
|
}
|
|
270
300
|
|
|
271
|
-
module.exports = { run, shouldRunAsCli
|
|
301
|
+
module.exports = { run, shouldRunAsCli };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const engine = require('./engine');
|
|
7
|
+
const ui = require('./cli-ui');
|
|
8
|
+
|
|
9
|
+
function defaultSession() {
|
|
10
|
+
return {
|
|
11
|
+
output: engine.getDefaultDownloadDir(),
|
|
12
|
+
format: 'mp4',
|
|
13
|
+
audioOnly: false,
|
|
14
|
+
quality: 'best',
|
|
15
|
+
bitrate: '',
|
|
16
|
+
filename: '%(title)s',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sessionToFlags(session) {
|
|
21
|
+
return {
|
|
22
|
+
output: session.output,
|
|
23
|
+
format: session.format,
|
|
24
|
+
audio: session.audioOnly,
|
|
25
|
+
quality: session.quality,
|
|
26
|
+
bitrate: session.bitrate,
|
|
27
|
+
filename: session.filename,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractYouTubeUrls(text) {
|
|
32
|
+
const found = new Set();
|
|
33
|
+
const re = /https?:\/\/(?:www\.)?(?:youtube\.com\/[^\s]+|youtu\.be\/[^\s]+)/gi;
|
|
34
|
+
let m;
|
|
35
|
+
while ((m = re.exec(text)) !== null) {
|
|
36
|
+
found.add(m[0].replace(/[)\]},.;]+$/, ''));
|
|
37
|
+
}
|
|
38
|
+
const parts = text.trim().split(/\s+/).filter(Boolean);
|
|
39
|
+
for (const p of parts) {
|
|
40
|
+
if (engine.isYouTubeUrl(p)) found.add(p.replace(/[)\]},.;]+$/, ''));
|
|
41
|
+
}
|
|
42
|
+
return [...found];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printSession(color, session) {
|
|
46
|
+
const mode = session.audioOnly ? 'audio' : 'wideo';
|
|
47
|
+
const fmt = session.format;
|
|
48
|
+
ui.info(
|
|
49
|
+
color,
|
|
50
|
+
`Tryb: ${mode} · ${fmt} · ${session.quality} · folder: ${session.output}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printInteractiveHelp(color) {
|
|
55
|
+
console.log('');
|
|
56
|
+
ui.info(color, 'Wklej link YouTube i naciśnij Enter — pobieranie startuje automatycznie.');
|
|
57
|
+
console.log(ui.c(color, ui.palette.dim, ' Komendy:'));
|
|
58
|
+
const cmds = [
|
|
59
|
+
['/help', 'Ta pomoc'],
|
|
60
|
+
['/info <url>', 'Podgląd bez pobierania'],
|
|
61
|
+
['/audio', 'Tryb audio (mp3 domyślnie)'],
|
|
62
|
+
['/video', 'Tryb wideo (mp4 domyślnie)'],
|
|
63
|
+
['/fmt mp3', 'Format: mp4, mp3, webm, mkv…'],
|
|
64
|
+
['/q 1080p', 'Jakość: best, 1080p, 720p…'],
|
|
65
|
+
['/folder', 'Zmień folder pobierania'],
|
|
66
|
+
['/folder C:\\Videos', 'Ustaw folder od razu'],
|
|
67
|
+
['/status', 'Narzędzia yt-dlp / ffmpeg'],
|
|
68
|
+
['/install', 'Zainstaluj yt-dlp'],
|
|
69
|
+
['/quit', 'Wyjście (lub Ctrl+C)'],
|
|
70
|
+
];
|
|
71
|
+
for (const [cmd, desc] of cmds) {
|
|
72
|
+
console.log(' ' + ui.c(color, ui.palette.cyan, cmd.padEnd(22)) + ui.c(color, ui.palette.dim, desc));
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runInteractive(handlers, version, color) {
|
|
78
|
+
const session = defaultSession();
|
|
79
|
+
let ytDlpReady = false;
|
|
80
|
+
|
|
81
|
+
ui.printBanner(version, color);
|
|
82
|
+
printSession(color, session);
|
|
83
|
+
printInteractiveHelp(color);
|
|
84
|
+
|
|
85
|
+
const promptText = color
|
|
86
|
+
? ui.c(true, ui.palette.pink, '🔗 ') + ui.c(true, ui.palette.bold, 'Wklej link') + ui.c(true, ui.palette.dim, ' › ')
|
|
87
|
+
: 'link> ';
|
|
88
|
+
|
|
89
|
+
const rl = readline.createInterface({
|
|
90
|
+
input: process.stdin,
|
|
91
|
+
output: process.stdout,
|
|
92
|
+
terminal: true,
|
|
93
|
+
historySize: 100,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const ensureTools = async () => {
|
|
97
|
+
if (ytDlpReady && engine.findYtDlp()) return;
|
|
98
|
+
const spin = new ui.Spinner('Przygotowanie yt-dlp', color);
|
|
99
|
+
spin.start();
|
|
100
|
+
try {
|
|
101
|
+
await engine.ensureYtDlp(msg => spin.draw(msg));
|
|
102
|
+
spin.succeed('Gotowe');
|
|
103
|
+
ytDlpReady = true;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
spin.fail(e.message);
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const askLine = (question) => new Promise(resolve => {
|
|
111
|
+
rl.question(question, answer => resolve(answer.trim()));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const processSlashCommand = async (line) => {
|
|
115
|
+
const parts = line.slice(1).trim().split(/\s+/);
|
|
116
|
+
const cmd = (parts[0] || '').toLowerCase();
|
|
117
|
+
const arg = parts.slice(1).join(' ');
|
|
118
|
+
|
|
119
|
+
switch (cmd) {
|
|
120
|
+
case 'help':
|
|
121
|
+
case 'h':
|
|
122
|
+
case '?':
|
|
123
|
+
printInteractiveHelp(color);
|
|
124
|
+
return;
|
|
125
|
+
case 'quit':
|
|
126
|
+
case 'exit':
|
|
127
|
+
case 'q':
|
|
128
|
+
ui.info(color, 'Do zobaczenia! 🌊');
|
|
129
|
+
rl.close();
|
|
130
|
+
return 'exit';
|
|
131
|
+
case 'audio':
|
|
132
|
+
case 'a':
|
|
133
|
+
session.audioOnly = true;
|
|
134
|
+
if (!['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'].includes(session.format)) {
|
|
135
|
+
session.format = 'mp3';
|
|
136
|
+
}
|
|
137
|
+
ui.success(color, `Tryb audio · format: ${session.format}`);
|
|
138
|
+
printSession(color, session);
|
|
139
|
+
return;
|
|
140
|
+
case 'video':
|
|
141
|
+
case 'v':
|
|
142
|
+
session.audioOnly = false;
|
|
143
|
+
if (['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'].includes(session.format)) {
|
|
144
|
+
session.format = 'mp4';
|
|
145
|
+
}
|
|
146
|
+
ui.success(color, `Tryb wideo · format: ${session.format}`);
|
|
147
|
+
printSession(color, session);
|
|
148
|
+
return;
|
|
149
|
+
case 'fmt':
|
|
150
|
+
case 'format':
|
|
151
|
+
case 'f':
|
|
152
|
+
if (!arg) {
|
|
153
|
+
ui.warn(color, 'Użyj: /fmt mp3 lub /fmt mp4');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
session.format = arg.toLowerCase();
|
|
157
|
+
if (['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'].includes(session.format)) {
|
|
158
|
+
session.audioOnly = true;
|
|
159
|
+
}
|
|
160
|
+
ui.success(color, `Format: ${session.format}`);
|
|
161
|
+
printSession(color, session);
|
|
162
|
+
return;
|
|
163
|
+
case 'q':
|
|
164
|
+
case 'quality':
|
|
165
|
+
if (!arg) {
|
|
166
|
+
ui.warn(color, 'Użyj: /q 1080p lub /q best');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
session.quality = arg;
|
|
170
|
+
ui.success(color, `Jakość: ${session.quality}`);
|
|
171
|
+
printSession(color, session);
|
|
172
|
+
return;
|
|
173
|
+
case 'folder':
|
|
174
|
+
case 'o':
|
|
175
|
+
case 'output':
|
|
176
|
+
if (arg) {
|
|
177
|
+
const dir = path.resolve(arg);
|
|
178
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
179
|
+
session.output = dir;
|
|
180
|
+
ui.success(color, 'Folder: ' + dir);
|
|
181
|
+
} else {
|
|
182
|
+
const entered = await askLine(
|
|
183
|
+
color ? ui.c(true, ui.palette.cyan, 'Ścieżka folderu: ') : 'Folder: '
|
|
184
|
+
);
|
|
185
|
+
if (entered) {
|
|
186
|
+
const dir = path.resolve(entered);
|
|
187
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
188
|
+
session.output = dir;
|
|
189
|
+
ui.success(color, 'Folder: ' + dir);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
printSession(color, session);
|
|
193
|
+
return;
|
|
194
|
+
case 'status':
|
|
195
|
+
ui.kv(color, 'yt-dlp', engine.findYtDlp() || 'BRAK', !!engine.findYtDlp());
|
|
196
|
+
ui.kv(color, 'ffmpeg', engine.findFfmpeg() || 'BRAK', !!engine.findFfmpeg());
|
|
197
|
+
return;
|
|
198
|
+
case 'install':
|
|
199
|
+
await handlers.toolsInstall(color);
|
|
200
|
+
ytDlpReady = true;
|
|
201
|
+
return;
|
|
202
|
+
case 'info':
|
|
203
|
+
case 'i': {
|
|
204
|
+
const url = arg || '';
|
|
205
|
+
if (!url || !engine.isYouTubeUrl(url)) {
|
|
206
|
+
ui.warn(color, 'Użyj: /info <url>');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await handlers.info(url, {}, color);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
case 'clear':
|
|
213
|
+
console.clear();
|
|
214
|
+
ui.printBanner(version, color);
|
|
215
|
+
printSession(color, session);
|
|
216
|
+
return;
|
|
217
|
+
default:
|
|
218
|
+
ui.warn(color, `Nieznana komenda: /${cmd} — wpisz /help`);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
rl.setPrompt(promptText);
|
|
223
|
+
|
|
224
|
+
return new Promise(resolve => {
|
|
225
|
+
rl.on('close', () => resolve(0));
|
|
226
|
+
|
|
227
|
+
const loop = () => {
|
|
228
|
+
rl.prompt();
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
rl.on('line', async line => {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
rl.pause();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
if (!trimmed) {
|
|
237
|
+
rl.resume();
|
|
238
|
+
loop();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (trimmed.startsWith('/')) {
|
|
243
|
+
const result = await processSlashCommand(trimmed);
|
|
244
|
+
if (result === 'exit') {
|
|
245
|
+
resolve(0);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
rl.resume();
|
|
249
|
+
loop();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const urls = extractYouTubeUrls(trimmed);
|
|
254
|
+
if (!urls.length) {
|
|
255
|
+
ui.warn(color, 'Nie wykryto linku YouTube. Wklej URL lub wpisz /help');
|
|
256
|
+
rl.resume();
|
|
257
|
+
loop();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await ensureTools();
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < urls.length; i++) {
|
|
264
|
+
if (urls.length > 1) {
|
|
265
|
+
ui.info(color, `Pobieranie ${i + 1}/${urls.length}`);
|
|
266
|
+
}
|
|
267
|
+
await handlers.download(urls[i], sessionToFlags(session), color);
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
ui.error(color, e.message || String(e));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
rl.resume();
|
|
274
|
+
loop();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
rl.on('SIGINT', () => {
|
|
278
|
+
console.log('');
|
|
279
|
+
ui.info(color, 'Wyjście (Ctrl+C)');
|
|
280
|
+
rl.close();
|
|
281
|
+
resolve(0);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
loop();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { runInteractive, defaultSession };
|
package/lib/cli-ui.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const tty = process.stdout.isTTY;
|
|
4
|
+
|
|
5
|
+
const ESC = '\x1b[';
|
|
6
|
+
const reset = `${ESC}0m`;
|
|
7
|
+
|
|
8
|
+
const palette = {
|
|
9
|
+
purple: `${ESC}38;2;192;132;252m`,
|
|
10
|
+
pink: `${ESC}38;2;232;121;249m`,
|
|
11
|
+
violet: `${ESC}38;2;124;58;237m`,
|
|
12
|
+
cyan: `${ESC}38;2;34;211;238m`,
|
|
13
|
+
green: `${ESC}38;2;74;222;128m`,
|
|
14
|
+
yellow: `${ESC}38;2;250;204;21m`,
|
|
15
|
+
red: `${ESC}38;2;248;113;113m`,
|
|
16
|
+
dim: `${ESC}2m`,
|
|
17
|
+
bold: `${ESC}1m`,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function useColor() {
|
|
21
|
+
if (process.env.NO_COLOR !== undefined) return false;
|
|
22
|
+
if (process.env.WAVESCONV_PLAIN === '1') return false;
|
|
23
|
+
if (process.argv.includes('--plain')) return false;
|
|
24
|
+
return tty;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function c(enabled, code, text) {
|
|
28
|
+
if (!enabled) return String(text);
|
|
29
|
+
return `${code}${text}${reset}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function gradientLine(enabled, text) {
|
|
33
|
+
if (!enabled) return text;
|
|
34
|
+
const chars = [...text];
|
|
35
|
+
const stops = [
|
|
36
|
+
[192, 132, 252],
|
|
37
|
+
[216, 126, 251],
|
|
38
|
+
[232, 121, 249],
|
|
39
|
+
[196, 116, 248],
|
|
40
|
+
[168, 85, 247],
|
|
41
|
+
];
|
|
42
|
+
return chars.map((ch, i) => {
|
|
43
|
+
const t = chars.length <= 1 ? 0 : i / (chars.length - 1);
|
|
44
|
+
const idx = Math.min(stops.length - 1, Math.floor(t * (stops.length - 1)));
|
|
45
|
+
const [r, g, b] = stops[idx];
|
|
46
|
+
return `${ESC}38;2;${r};${g};${b}m${ch}`;
|
|
47
|
+
}).join('') + reset;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printBanner(version, enabled) {
|
|
51
|
+
const wave = '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~';
|
|
52
|
+
const title = ` WavesConverter CLI v${version} `;
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(c(enabled, palette.dim, ' ' + gradientLine(enabled, wave)));
|
|
55
|
+
console.log(c(enabled, palette.bold, gradientLine(enabled, title)));
|
|
56
|
+
console.log(c(enabled, palette.dim, ' ' + gradientLine(enabled, wave)));
|
|
57
|
+
console.log(c(enabled, palette.dim, ' Download anything · Convert everything\n'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printHelp(version, enabled) {
|
|
61
|
+
printBanner(version, enabled);
|
|
62
|
+
const rows = [
|
|
63
|
+
['Polecenia', ''],
|
|
64
|
+
[' download <url>', 'Pobierz wideo/audio z YouTube'],
|
|
65
|
+
[' info <url>', 'Metadane (kolorowy podgląd lub JSON)'],
|
|
66
|
+
[' convert <plik>', 'Konwertuj plik lokalny'],
|
|
67
|
+
[' tools install', 'Zainstaluj yt-dlp'],
|
|
68
|
+
[' tools status', 'Status yt-dlp i ffmpeg'],
|
|
69
|
+
[' (bez argumentów)', 'Tryb interaktywny — wklej linki w konsoli'],
|
|
70
|
+
['', ''],
|
|
71
|
+
['Opcje download', ''],
|
|
72
|
+
[' -o, --output <dir>', 'Folder docelowy'],
|
|
73
|
+
[' -f, --format <fmt>', 'mp4, mp3, webm…'],
|
|
74
|
+
[' -q, --quality <q>', 'best, 1080p, 720p…'],
|
|
75
|
+
[' -a, --audio', 'Tylko audio'],
|
|
76
|
+
[' --plain', 'Bez kolorów i animacji'],
|
|
77
|
+
['', ''],
|
|
78
|
+
['Przykłady', ''],
|
|
79
|
+
[' wavesconv download URL -a -f mp3', ''],
|
|
80
|
+
[' wavesconv convert film.mp4 -f mp3', ''],
|
|
81
|
+
];
|
|
82
|
+
for (const [cmd, desc] of rows) {
|
|
83
|
+
if (!cmd && !desc) { console.log(''); continue; }
|
|
84
|
+
if (desc === '' && cmd && !cmd.startsWith(' ')) {
|
|
85
|
+
console.log(c(enabled, palette.pink, `\n ${cmd}`));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (cmd.startsWith(' ') && !cmd.includes('<') && desc === '') {
|
|
89
|
+
console.log(c(enabled, palette.cyan, ` ${cmd.trim()}`));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
console.log(
|
|
93
|
+
c(enabled, palette.cyan, cmd.padEnd(28)) +
|
|
94
|
+
c(enabled, palette.dim, desc)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class Spinner {
|
|
101
|
+
constructor(text, enabled) {
|
|
102
|
+
this.text = text;
|
|
103
|
+
this.enabled = enabled && tty;
|
|
104
|
+
this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
105
|
+
this.colors = [palette.purple, palette.pink, palette.violet, palette.cyan];
|
|
106
|
+
this.i = 0;
|
|
107
|
+
this.timer = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
start() {
|
|
111
|
+
if (!this.enabled) {
|
|
112
|
+
process.stderr.write(this.text + '…\n');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.timer = setInterval(() => this.draw(), 80);
|
|
116
|
+
this.draw();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
draw(extra = '') {
|
|
120
|
+
if (!this.enabled) return;
|
|
121
|
+
const frame = this.frames[this.i % this.frames.length];
|
|
122
|
+
const color = this.colors[Math.floor(this.i / 2) % this.colors.length];
|
|
123
|
+
const line = `${color}${frame}${reset} ${palette.bold}${this.text}${reset}${extra ? palette.dim + ' · ' + extra + reset : ''}`;
|
|
124
|
+
process.stderr.write(`\r\x1b[2K${line}`);
|
|
125
|
+
this.i++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
succeed(msg) {
|
|
129
|
+
this.stop();
|
|
130
|
+
process.stderr.write(c(this.enabled, palette.green, `✔ ${msg || this.text}\n`));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fail(msg) {
|
|
134
|
+
this.stop();
|
|
135
|
+
process.stderr.write(c(this.enabled, palette.red, `✖ ${msg || this.text}\n`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
stop() {
|
|
139
|
+
if (this.timer) clearInterval(this.timer);
|
|
140
|
+
this.timer = null;
|
|
141
|
+
if (this.enabled) process.stderr.write('\r\x1b[2K');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class ProgressBar {
|
|
146
|
+
constructor(label, enabled, width = 32) {
|
|
147
|
+
this.label = label;
|
|
148
|
+
this.enabled = enabled && tty;
|
|
149
|
+
this.width = width;
|
|
150
|
+
this.pct = 0;
|
|
151
|
+
this.detail = '';
|
|
152
|
+
this.pulse = 0;
|
|
153
|
+
this.timer = null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
start() {
|
|
157
|
+
if (!this.enabled) return;
|
|
158
|
+
this.timer = setInterval(() => {
|
|
159
|
+
this.pulse = (this.pulse + 1) % 4;
|
|
160
|
+
this.render();
|
|
161
|
+
}, 120);
|
|
162
|
+
this.render();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
update(pct, detail = '') {
|
|
166
|
+
this.pct = Math.max(0, Math.min(100, pct));
|
|
167
|
+
if (detail) this.detail = detail.slice(0, 42);
|
|
168
|
+
if (!this.enabled) {
|
|
169
|
+
const r = Math.floor(this.pct);
|
|
170
|
+
if (r % 10 === 0 && r !== this._lastPlain) {
|
|
171
|
+
this._lastPlain = r;
|
|
172
|
+
process.stderr.write(` ${r}%\n`);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.render();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
render() {
|
|
180
|
+
if (!this.enabled) return;
|
|
181
|
+
const filled = Math.round((this.pct / 100) * this.width);
|
|
182
|
+
const blocks = ['░', '▒', '▓', '█'];
|
|
183
|
+
let bar = '';
|
|
184
|
+
for (let i = 0; i < this.width; i++) {
|
|
185
|
+
if (i < filled) bar += c(true, palette.pink, '█');
|
|
186
|
+
else if (i === filled) bar += c(true, palette.purple, blocks[this.pulse]);
|
|
187
|
+
else bar += c(true, palette.dim, '░');
|
|
188
|
+
}
|
|
189
|
+
const pctStr = `${String(Math.floor(this.pct)).padStart(3)}%`;
|
|
190
|
+
const line = `${c(true, palette.bold, this.label)} ${bar} ${c(true, palette.cyan, pctStr)}${this.detail ? c(true, palette.dim, ' ' + this.detail) : ''}`;
|
|
191
|
+
process.stdout.write(`\r\x1b[2K${line}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
succeed(msg) {
|
|
195
|
+
if (this.timer) clearInterval(this.timer);
|
|
196
|
+
this.pct = 100;
|
|
197
|
+
if (this.enabled) {
|
|
198
|
+
this.render();
|
|
199
|
+
process.stdout.write('\n');
|
|
200
|
+
process.stderr.write(c(true, palette.green, `✔ ${msg}\n`));
|
|
201
|
+
} else {
|
|
202
|
+
process.stderr.write(msg + '\n');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fail(msg) {
|
|
207
|
+
if (this.timer) clearInterval(this.timer);
|
|
208
|
+
if (this.enabled) process.stdout.write('\n');
|
|
209
|
+
process.stderr.write(c(this.enabled, palette.red, `✖ ${msg}\n`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function info(enabled, msg) {
|
|
214
|
+
process.stderr.write(c(enabled, palette.cyan, '◆ ') + msg + '\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function success(enabled, msg) {
|
|
218
|
+
process.stderr.write(c(enabled, palette.green, '✔ ') + c(enabled, palette.bold, msg) + '\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function warn(enabled, msg) {
|
|
222
|
+
process.stderr.write(c(enabled, palette.yellow, '⚠ ') + msg + '\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function error(enabled, msg) {
|
|
226
|
+
process.stderr.write(c(enabled, palette.red, '✖ ') + c(enabled, palette.bold, msg) + '\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function kv(enabled, key, value, ok) {
|
|
230
|
+
const icon = ok === true ? c(enabled, palette.green, '●') : ok === false ? c(enabled, palette.red, '○') : c(enabled, palette.purple, '●');
|
|
231
|
+
process.stderr.write(` ${icon} ${c(enabled, palette.dim, key + ':')} ${c(enabled, ok === false ? palette.red : palette.bold, value)}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function printVideoCard(enabled, item) {
|
|
235
|
+
const title = item.title || item.id || 'Bez tytułu';
|
|
236
|
+
const uploader = item.uploader || '—';
|
|
237
|
+
const dur = item.duration ? formatDuration(item.duration) : '—';
|
|
238
|
+
const views = item.view_count ? formatViews(item.view_count) : '—';
|
|
239
|
+
console.log('');
|
|
240
|
+
console.log(c(enabled, palette.bold, gradientLine(enabled, ' ' + title.slice(0, 56))));
|
|
241
|
+
kv(enabled, 'Kanał', uploader);
|
|
242
|
+
kv(enabled, 'Czas', dur);
|
|
243
|
+
kv(enabled, 'Wyświetlenia', views);
|
|
244
|
+
if (item.webpage_url || item.url) kv(enabled, 'URL', item.webpage_url || item.url);
|
|
245
|
+
console.log('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatDuration(sec) {
|
|
249
|
+
const h = Math.floor(sec / 3600);
|
|
250
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
251
|
+
const s = Math.floor(sec % 60);
|
|
252
|
+
if (h) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
253
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function formatViews(n) {
|
|
257
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
258
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
259
|
+
return String(n);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatBytes(n) {
|
|
263
|
+
if (n < 1024) return n + ' B';
|
|
264
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
|
265
|
+
return (n / (1024 * 1024)).toFixed(2) + ' MB';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function sleep(ms) {
|
|
269
|
+
return new Promise(r => setTimeout(r, ms));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
useColor,
|
|
274
|
+
palette,
|
|
275
|
+
c,
|
|
276
|
+
printBanner,
|
|
277
|
+
printHelp,
|
|
278
|
+
Spinner,
|
|
279
|
+
ProgressBar,
|
|
280
|
+
info,
|
|
281
|
+
success,
|
|
282
|
+
warn,
|
|
283
|
+
error,
|
|
284
|
+
kv,
|
|
285
|
+
printVideoCard,
|
|
286
|
+
formatBytes,
|
|
287
|
+
sleep,
|
|
288
|
+
gradientLine,
|
|
289
|
+
};
|