wavesconv 1.4.9 → 1.5.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 +110 -94
- package/lib/cli-ui.js +288 -0
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -4,46 +4,10 @@
|
|
|
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');
|
|
7
8
|
|
|
8
9
|
const VERSION = require('./package.json').version;
|
|
9
10
|
|
|
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
11
|
function parseArgs(argv) {
|
|
48
12
|
const args = [...argv];
|
|
49
13
|
const positional = [];
|
|
@@ -59,7 +23,7 @@ function parseArgs(argv) {
|
|
|
59
23
|
if (a.startsWith('--')) {
|
|
60
24
|
const key = a.slice(2);
|
|
61
25
|
args.shift();
|
|
62
|
-
if (['help', 'version', 'audio'].includes(key)) {
|
|
26
|
+
if (['help', 'version', 'audio', 'plain', 'json'].includes(key)) {
|
|
63
27
|
flags[key] = true;
|
|
64
28
|
} else if (args.length) {
|
|
65
29
|
flags[key] = args.shift();
|
|
@@ -71,11 +35,8 @@ function parseArgs(argv) {
|
|
|
71
35
|
args.shift();
|
|
72
36
|
const map = { o: 'output', f: 'format', q: 'quality', a: 'audio' };
|
|
73
37
|
const flagKey = map[key] || key;
|
|
74
|
-
if (key === 'a')
|
|
75
|
-
|
|
76
|
-
} else if (args.length) {
|
|
77
|
-
flags[flagKey] = args.shift();
|
|
78
|
-
}
|
|
38
|
+
if (key === 'a') flags.audio = true;
|
|
39
|
+
else if (args.length) flags[flagKey] = args.shift();
|
|
79
40
|
} else {
|
|
80
41
|
positional.push(args.shift());
|
|
81
42
|
}
|
|
@@ -83,38 +44,75 @@ function parseArgs(argv) {
|
|
|
83
44
|
return { positional, flags };
|
|
84
45
|
}
|
|
85
46
|
|
|
86
|
-
function
|
|
87
|
-
|
|
47
|
+
async function cmdToolsInstall(color) {
|
|
48
|
+
const spin = new ui.Spinner('Instalacja yt-dlp', color);
|
|
49
|
+
spin.start();
|
|
50
|
+
try {
|
|
51
|
+
await engine.ensureYtDlp(msg => spin.draw(msg));
|
|
52
|
+
spin.succeed('yt-dlp zainstalowany');
|
|
53
|
+
} catch (e) {
|
|
54
|
+
spin.fail(e.message);
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
ui.kv(color, 'yt-dlp', engine.findYtDlp() || 'błąd', !!engine.findYtDlp());
|
|
58
|
+
ui.kv(color, 'ffmpeg', engine.findFfmpeg() || 'brak (zainstaluj systemowo)', !!engine.findFfmpeg());
|
|
88
59
|
}
|
|
89
60
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
61
|
+
function cmdToolsStatus(color) {
|
|
62
|
+
ui.printBanner(VERSION, color);
|
|
63
|
+
ui.kv(color, 'yt-dlp', engine.findYtDlp() || 'BRAK', !!engine.findYtDlp());
|
|
64
|
+
ui.kv(color, 'ffmpeg', engine.findFfmpeg() || 'BRAK', !!engine.findFfmpeg());
|
|
65
|
+
ui.kv(color, 'userData', engine.getUserDataDir());
|
|
66
|
+
console.log('');
|
|
94
67
|
}
|
|
95
68
|
|
|
96
|
-
async function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
69
|
+
async function cmdInfo(url, flags, color) {
|
|
70
|
+
const spin = new ui.Spinner('Pobieranie metadanych', color);
|
|
71
|
+
spin.start();
|
|
72
|
+
let items;
|
|
73
|
+
try {
|
|
74
|
+
items = await engine.fetchInfo(url);
|
|
75
|
+
spin.succeed(`${items.length} element(ów)`);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
spin.fail(e.message);
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
101
80
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
81
|
+
if (flags.json || !color) {
|
|
82
|
+
console.log(JSON.stringify(items, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (items.length === 1) {
|
|
87
|
+
ui.printVideoCard(color, items[0]);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
107
90
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
ui.info(color, `Playlista: ${items.length} pozycji`);
|
|
92
|
+
items.slice(0, 15).forEach((item, i) => {
|
|
93
|
+
const title = (item.title || item.id || '?').slice(0, 50);
|
|
94
|
+
console.log(ui.c(color, ui.palette.cyan, ` ${String(i + 1).padStart(2)}. `) + title);
|
|
95
|
+
});
|
|
96
|
+
if (items.length > 15) {
|
|
97
|
+
console.log(ui.c(color, ui.palette.dim, ` … i ${items.length - 15} więcej`));
|
|
98
|
+
}
|
|
99
|
+
console.log('');
|
|
111
100
|
}
|
|
112
101
|
|
|
113
|
-
async function cmdDownload(url, flags) {
|
|
102
|
+
async function cmdDownload(url, flags, color) {
|
|
114
103
|
if (!engine.isYouTubeUrl(url)) {
|
|
115
104
|
throw new Error('Nieprawidłowy URL YouTube: ' + url);
|
|
116
105
|
}
|
|
117
|
-
|
|
106
|
+
|
|
107
|
+
const setupSpin = new ui.Spinner('Przygotowanie yt-dlp', color);
|
|
108
|
+
setupSpin.start();
|
|
109
|
+
try {
|
|
110
|
+
await engine.ensureYtDlp(msg => setupSpin.draw(msg));
|
|
111
|
+
setupSpin.succeed('Silnik gotowy');
|
|
112
|
+
} catch (e) {
|
|
113
|
+
setupSpin.fail(e.message);
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
118
116
|
|
|
119
117
|
const outputDir = path.resolve(flags.output || engine.getDefaultDownloadDir());
|
|
120
118
|
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -123,10 +121,15 @@ async function cmdDownload(url, flags) {
|
|
|
123
121
|
const format = flags.format || (audioOnly ? 'mp3' : 'mp4');
|
|
124
122
|
|
|
125
123
|
let title = 'download';
|
|
124
|
+
const metaSpin = new ui.Spinner('Wczytywanie metadanych', color);
|
|
125
|
+
metaSpin.start();
|
|
126
126
|
try {
|
|
127
127
|
const items = await engine.fetchInfo(url);
|
|
128
128
|
if (items[0]) title = items[0].title || items[0].id || title;
|
|
129
|
-
|
|
129
|
+
metaSpin.succeed(title.slice(0, 52) + (title.length > 52 ? '…' : ''));
|
|
130
|
+
} catch (_) {
|
|
131
|
+
metaSpin.succeed('Metadane pominięte');
|
|
132
|
+
}
|
|
130
133
|
|
|
131
134
|
const job = {
|
|
132
135
|
id: 'cli-' + Date.now(),
|
|
@@ -140,35 +143,35 @@ async function cmdDownload(url, flags) {
|
|
|
140
143
|
filename: flags.filename || '%(title)s',
|
|
141
144
|
};
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
log(
|
|
146
|
+
ui.info(color, `Folder: ${outputDir}`);
|
|
147
|
+
ui.info(color, `Format: ${format}${audioOnly ? ' (audio)' : ''} · Jakość: ${job.quality}`);
|
|
148
|
+
console.log('');
|
|
149
|
+
|
|
150
|
+
const bar = new ui.ProgressBar('Pobieranie', color);
|
|
151
|
+
bar.start();
|
|
146
152
|
|
|
147
|
-
let lastPct = -1;
|
|
148
153
|
const result = await engine.runDownload(job, {
|
|
149
154
|
onProgress: (pct, line) => {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
lastPct = rounded;
|
|
153
|
-
process.stdout.write(`\r[${rounded}%] ${line.slice(0, 60).padEnd(60)}`);
|
|
154
|
-
}
|
|
155
|
+
const detail = line.replace(/\[download\]\s*/i, '').trim();
|
|
156
|
+
bar.update(pct, detail);
|
|
155
157
|
},
|
|
156
158
|
onLog: line => {
|
|
157
|
-
if (
|
|
159
|
+
if (/\bERROR\b/i.test(line)) ui.warn(color, line.slice(0, 100));
|
|
158
160
|
},
|
|
159
161
|
});
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
bar.succeed(`Zapisano · ${ui.formatBytes(result.size || 0)}`);
|
|
162
164
|
if (result.path) {
|
|
163
|
-
|
|
165
|
+
ui.success(color, result.path);
|
|
164
166
|
} else {
|
|
165
|
-
|
|
167
|
+
ui.warn(color, 'Sprawdź folder docelowy (ścieżka nie wykryta automatycznie)');
|
|
166
168
|
}
|
|
169
|
+
console.log('');
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
async function cmdConvert(inputPath, flags) {
|
|
172
|
+
async function cmdConvert(inputPath, flags, color) {
|
|
170
173
|
const ffmpeg = engine.findFfmpeg();
|
|
171
|
-
if (!ffmpeg) throw new Error('ffmpeg nie znaleziony');
|
|
174
|
+
if (!ffmpeg) throw new Error('ffmpeg nie znaleziony — zainstaluj ffmpeg lub użyj aplikacji desktop');
|
|
172
175
|
|
|
173
176
|
const resolvedIn = path.resolve(inputPath);
|
|
174
177
|
if (!fs.existsSync(resolvedIn)) throw new Error('Plik nie istnieje: ' + resolvedIn);
|
|
@@ -195,56 +198,69 @@ async function cmdConvert(inputPath, flags) {
|
|
|
195
198
|
gifDuration: flags['gif-duration'] || 5,
|
|
196
199
|
};
|
|
197
200
|
|
|
198
|
-
|
|
201
|
+
ui.info(color, `Wejście: ${path.basename(resolvedIn)}`);
|
|
202
|
+
ui.info(color, `Wyjście: ${outputPath}`);
|
|
203
|
+
|
|
204
|
+
const spin = new ui.Spinner('Konwersja ffmpeg', color);
|
|
205
|
+
spin.start();
|
|
206
|
+
|
|
199
207
|
await engine.runConvert(job, {
|
|
200
|
-
onProgress: t =>
|
|
208
|
+
onProgress: t => spin.draw(t),
|
|
201
209
|
});
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
|
|
211
|
+
spin.succeed('Konwersja zakończona');
|
|
212
|
+
ui.success(color, outputPath);
|
|
213
|
+
console.log('');
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
async function run(argv) {
|
|
207
217
|
const { positional, flags } = parseArgs(argv);
|
|
218
|
+
const color = ui.useColor() && !flags.plain;
|
|
208
219
|
|
|
209
220
|
if (flags.help || positional[0] === 'help') {
|
|
210
|
-
|
|
221
|
+
ui.printHelp(VERSION, color);
|
|
211
222
|
return 0;
|
|
212
223
|
}
|
|
213
224
|
if (flags.version) {
|
|
214
|
-
|
|
225
|
+
if (color) ui.printBanner(VERSION, true);
|
|
226
|
+
else console.log(VERSION);
|
|
215
227
|
return 0;
|
|
216
228
|
}
|
|
217
229
|
|
|
218
230
|
const cmd = positional[0];
|
|
219
231
|
if (!cmd) {
|
|
220
|
-
|
|
232
|
+
ui.printHelp(VERSION, color);
|
|
221
233
|
return 1;
|
|
222
234
|
}
|
|
223
235
|
|
|
236
|
+
if (color && cmd !== 'tools') {
|
|
237
|
+
ui.printBanner(VERSION, true);
|
|
238
|
+
}
|
|
239
|
+
|
|
224
240
|
try {
|
|
225
241
|
switch (cmd) {
|
|
226
242
|
case 'download': {
|
|
227
243
|
const url = positional[1];
|
|
228
244
|
if (!url) throw new Error('Podaj URL: wavesconv download <url>');
|
|
229
|
-
await cmdDownload(url, flags);
|
|
245
|
+
await cmdDownload(url, flags, color);
|
|
230
246
|
return 0;
|
|
231
247
|
}
|
|
232
248
|
case 'info': {
|
|
233
249
|
const url = positional[1];
|
|
234
250
|
if (!url) throw new Error('Podaj URL: wavesconv info <url>');
|
|
235
|
-
await cmdInfo(url);
|
|
251
|
+
await cmdInfo(url, flags, color);
|
|
236
252
|
return 0;
|
|
237
253
|
}
|
|
238
254
|
case 'convert': {
|
|
239
255
|
const file = positional[1];
|
|
240
256
|
if (!file) throw new Error('Podaj plik: wavesconv convert <plik>');
|
|
241
|
-
await cmdConvert(file, flags);
|
|
257
|
+
await cmdConvert(file, flags, color);
|
|
242
258
|
return 0;
|
|
243
259
|
}
|
|
244
260
|
case 'tools': {
|
|
245
261
|
const sub = positional[1];
|
|
246
|
-
if (sub === 'install') await cmdToolsInstall();
|
|
247
|
-
else if (sub === 'status') cmdToolsStatus();
|
|
262
|
+
if (sub === 'install') await cmdToolsInstall(color);
|
|
263
|
+
else if (sub === 'status') cmdToolsStatus(color);
|
|
248
264
|
else throw new Error('Użyj: wavesconv tools install|status');
|
|
249
265
|
return 0;
|
|
250
266
|
}
|
|
@@ -252,7 +268,7 @@ async function run(argv) {
|
|
|
252
268
|
throw new Error('Nieznane polecenie: ' + cmd);
|
|
253
269
|
}
|
|
254
270
|
} catch (e) {
|
|
255
|
-
|
|
271
|
+
ui.error(color, e.message || String(e));
|
|
256
272
|
return 1;
|
|
257
273
|
}
|
|
258
274
|
}
|
|
@@ -268,4 +284,4 @@ if (require.main === module) {
|
|
|
268
284
|
run(process.argv.slice(2)).then(code => process.exit(code));
|
|
269
285
|
}
|
|
270
286
|
|
|
271
|
-
module.exports = { run, shouldRunAsCli
|
|
287
|
+
module.exports = { run, shouldRunAsCli };
|
package/lib/cli-ui.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
['', ''],
|
|
70
|
+
['Opcje download', ''],
|
|
71
|
+
[' -o, --output <dir>', 'Folder docelowy'],
|
|
72
|
+
[' -f, --format <fmt>', 'mp4, mp3, webm…'],
|
|
73
|
+
[' -q, --quality <q>', 'best, 1080p, 720p…'],
|
|
74
|
+
[' -a, --audio', 'Tylko audio'],
|
|
75
|
+
[' --plain', 'Bez kolorów i animacji'],
|
|
76
|
+
['', ''],
|
|
77
|
+
['Przykłady', ''],
|
|
78
|
+
[' wavesconv download URL -a -f mp3', ''],
|
|
79
|
+
[' wavesconv convert film.mp4 -f mp3', ''],
|
|
80
|
+
];
|
|
81
|
+
for (const [cmd, desc] of rows) {
|
|
82
|
+
if (!cmd && !desc) { console.log(''); continue; }
|
|
83
|
+
if (desc === '' && cmd && !cmd.startsWith(' ')) {
|
|
84
|
+
console.log(c(enabled, palette.pink, `\n ${cmd}`));
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (cmd.startsWith(' ') && !cmd.includes('<') && desc === '') {
|
|
88
|
+
console.log(c(enabled, palette.cyan, ` ${cmd.trim()}`));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
console.log(
|
|
92
|
+
c(enabled, palette.cyan, cmd.padEnd(28)) +
|
|
93
|
+
c(enabled, palette.dim, desc)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
console.log('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class Spinner {
|
|
100
|
+
constructor(text, enabled) {
|
|
101
|
+
this.text = text;
|
|
102
|
+
this.enabled = enabled && tty;
|
|
103
|
+
this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
104
|
+
this.colors = [palette.purple, palette.pink, palette.violet, palette.cyan];
|
|
105
|
+
this.i = 0;
|
|
106
|
+
this.timer = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
start() {
|
|
110
|
+
if (!this.enabled) {
|
|
111
|
+
process.stderr.write(this.text + '…\n');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.timer = setInterval(() => this.draw(), 80);
|
|
115
|
+
this.draw();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
draw(extra = '') {
|
|
119
|
+
if (!this.enabled) return;
|
|
120
|
+
const frame = this.frames[this.i % this.frames.length];
|
|
121
|
+
const color = this.colors[Math.floor(this.i / 2) % this.colors.length];
|
|
122
|
+
const line = `${color}${frame}${reset} ${palette.bold}${this.text}${reset}${extra ? palette.dim + ' · ' + extra + reset : ''}`;
|
|
123
|
+
process.stderr.write(`\r\x1b[2K${line}`);
|
|
124
|
+
this.i++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
succeed(msg) {
|
|
128
|
+
this.stop();
|
|
129
|
+
process.stderr.write(c(this.enabled, palette.green, `✔ ${msg || this.text}\n`));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fail(msg) {
|
|
133
|
+
this.stop();
|
|
134
|
+
process.stderr.write(c(this.enabled, palette.red, `✖ ${msg || this.text}\n`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
stop() {
|
|
138
|
+
if (this.timer) clearInterval(this.timer);
|
|
139
|
+
this.timer = null;
|
|
140
|
+
if (this.enabled) process.stderr.write('\r\x1b[2K');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class ProgressBar {
|
|
145
|
+
constructor(label, enabled, width = 32) {
|
|
146
|
+
this.label = label;
|
|
147
|
+
this.enabled = enabled && tty;
|
|
148
|
+
this.width = width;
|
|
149
|
+
this.pct = 0;
|
|
150
|
+
this.detail = '';
|
|
151
|
+
this.pulse = 0;
|
|
152
|
+
this.timer = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
start() {
|
|
156
|
+
if (!this.enabled) return;
|
|
157
|
+
this.timer = setInterval(() => {
|
|
158
|
+
this.pulse = (this.pulse + 1) % 4;
|
|
159
|
+
this.render();
|
|
160
|
+
}, 120);
|
|
161
|
+
this.render();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
update(pct, detail = '') {
|
|
165
|
+
this.pct = Math.max(0, Math.min(100, pct));
|
|
166
|
+
if (detail) this.detail = detail.slice(0, 42);
|
|
167
|
+
if (!this.enabled) {
|
|
168
|
+
const r = Math.floor(this.pct);
|
|
169
|
+
if (r % 10 === 0 && r !== this._lastPlain) {
|
|
170
|
+
this._lastPlain = r;
|
|
171
|
+
process.stderr.write(` ${r}%\n`);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.render();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
render() {
|
|
179
|
+
if (!this.enabled) return;
|
|
180
|
+
const filled = Math.round((this.pct / 100) * this.width);
|
|
181
|
+
const blocks = ['░', '▒', '▓', '█'];
|
|
182
|
+
let bar = '';
|
|
183
|
+
for (let i = 0; i < this.width; i++) {
|
|
184
|
+
if (i < filled) bar += c(true, palette.pink, '█');
|
|
185
|
+
else if (i === filled) bar += c(true, palette.purple, blocks[this.pulse]);
|
|
186
|
+
else bar += c(true, palette.dim, '░');
|
|
187
|
+
}
|
|
188
|
+
const pctStr = `${String(Math.floor(this.pct)).padStart(3)}%`;
|
|
189
|
+
const line = `${c(true, palette.bold, this.label)} ${bar} ${c(true, palette.cyan, pctStr)}${this.detail ? c(true, palette.dim, ' ' + this.detail) : ''}`;
|
|
190
|
+
process.stdout.write(`\r\x1b[2K${line}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
succeed(msg) {
|
|
194
|
+
if (this.timer) clearInterval(this.timer);
|
|
195
|
+
this.pct = 100;
|
|
196
|
+
if (this.enabled) {
|
|
197
|
+
this.render();
|
|
198
|
+
process.stdout.write('\n');
|
|
199
|
+
process.stderr.write(c(true, palette.green, `✔ ${msg}\n`));
|
|
200
|
+
} else {
|
|
201
|
+
process.stderr.write(msg + '\n');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fail(msg) {
|
|
206
|
+
if (this.timer) clearInterval(this.timer);
|
|
207
|
+
if (this.enabled) process.stdout.write('\n');
|
|
208
|
+
process.stderr.write(c(this.enabled, palette.red, `✖ ${msg}\n`));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function info(enabled, msg) {
|
|
213
|
+
process.stderr.write(c(enabled, palette.cyan, '◆ ') + msg + '\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function success(enabled, msg) {
|
|
217
|
+
process.stderr.write(c(enabled, palette.green, '✔ ') + c(enabled, palette.bold, msg) + '\n');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function warn(enabled, msg) {
|
|
221
|
+
process.stderr.write(c(enabled, palette.yellow, '⚠ ') + msg + '\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function error(enabled, msg) {
|
|
225
|
+
process.stderr.write(c(enabled, palette.red, '✖ ') + c(enabled, palette.bold, msg) + '\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function kv(enabled, key, value, ok) {
|
|
229
|
+
const icon = ok === true ? c(enabled, palette.green, '●') : ok === false ? c(enabled, palette.red, '○') : c(enabled, palette.purple, '●');
|
|
230
|
+
process.stderr.write(` ${icon} ${c(enabled, palette.dim, key + ':')} ${c(enabled, ok === false ? palette.red : palette.bold, value)}\n`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function printVideoCard(enabled, item) {
|
|
234
|
+
const title = item.title || item.id || 'Bez tytułu';
|
|
235
|
+
const uploader = item.uploader || '—';
|
|
236
|
+
const dur = item.duration ? formatDuration(item.duration) : '—';
|
|
237
|
+
const views = item.view_count ? formatViews(item.view_count) : '—';
|
|
238
|
+
console.log('');
|
|
239
|
+
console.log(c(enabled, palette.bold, gradientLine(enabled, ' ' + title.slice(0, 56))));
|
|
240
|
+
kv(enabled, 'Kanał', uploader);
|
|
241
|
+
kv(enabled, 'Czas', dur);
|
|
242
|
+
kv(enabled, 'Wyświetlenia', views);
|
|
243
|
+
if (item.webpage_url || item.url) kv(enabled, 'URL', item.webpage_url || item.url);
|
|
244
|
+
console.log('');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function formatDuration(sec) {
|
|
248
|
+
const h = Math.floor(sec / 3600);
|
|
249
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
250
|
+
const s = Math.floor(sec % 60);
|
|
251
|
+
if (h) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
252
|
+
return `${m}:${String(s).padStart(2, '0')}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatViews(n) {
|
|
256
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
257
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
258
|
+
return String(n);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatBytes(n) {
|
|
262
|
+
if (n < 1024) return n + ' B';
|
|
263
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
|
264
|
+
return (n / (1024 * 1024)).toFixed(2) + ' MB';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function sleep(ms) {
|
|
268
|
+
return new Promise(r => setTimeout(r, ms));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
useColor,
|
|
273
|
+
palette,
|
|
274
|
+
c,
|
|
275
|
+
printBanner,
|
|
276
|
+
printHelp,
|
|
277
|
+
Spinner,
|
|
278
|
+
ProgressBar,
|
|
279
|
+
info,
|
|
280
|
+
success,
|
|
281
|
+
warn,
|
|
282
|
+
error,
|
|
283
|
+
kv,
|
|
284
|
+
printVideoCard,
|
|
285
|
+
formatBytes,
|
|
286
|
+
sleep,
|
|
287
|
+
gradientLine,
|
|
288
|
+
};
|