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.
Files changed (3) hide show
  1. package/cli.js +110 -94
  2. package/lib/cli-ui.js +288 -0
  3. 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
- flags.audio = true;
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 log(msg) {
87
- process.stderr.write(msg + '\n');
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 formatBytes(n) {
91
- if (n < 1024) return n + ' B';
92
- if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
93
- return (n / (1024 * 1024)).toFixed(2) + ' MB';
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 cmdToolsInstall() {
97
- await engine.ensureYtDlp(msg => log(msg));
98
- log('yt-dlp: ' + (engine.findYtDlp() || 'błąd'));
99
- log('ffmpeg: ' + (engine.findFfmpeg() || 'brak — zainstaluj systemowo lub z poziomu aplikacji'));
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
- function cmdToolsStatus() {
103
- log('yt-dlp: ' + (engine.findYtDlp() || 'BRAK'));
104
- log('ffmpeg: ' + (engine.findFfmpeg() || 'BRAK'));
105
- log('userData: ' + engine.getUserDataDir());
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
- async function cmdInfo(url) {
109
- const items = await engine.fetchInfo(url);
110
- console.log(JSON.stringify(items, null, 2));
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
- await engine.ensureYtDlp(msg => log(msg));
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
- } catch (_) {}
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
- log(`Pobieranie: ${title}`);
144
- log(`Folder: ${outputDir}`);
145
- log(`Format: ${format}${audioOnly ? ' (audio)' : ''} | Jakość: ${job.quality}`);
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 rounded = Math.floor(pct);
151
- if (rounded !== lastPct && rounded % 5 === 0) {
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 (line.includes('ERROR') || line.includes('error')) log(line);
159
+ if (/\bERROR\b/i.test(line)) ui.warn(color, line.slice(0, 100));
158
160
  },
159
161
  });
160
162
 
161
- process.stdout.write('\n');
163
+ bar.succeed(`Zapisano · ${ui.formatBytes(result.size || 0)}`);
162
164
  if (result.path) {
163
- log(`Gotowe: ${result.path} (${formatBytes(result.size)})`);
165
+ ui.success(color, result.path);
164
166
  } else {
165
- log('Pobrano (ścieżka nieznana sprawdź folder docelowy)');
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
- log(`Konwersja ${outputPath}`);
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 => process.stdout.write(`\rCzas: ${t} `),
208
+ onProgress: t => spin.draw(t),
201
209
  });
202
- process.stdout.write('\n');
203
- log('Gotowe: ' + outputPath);
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
- console.log(HELP);
221
+ ui.printHelp(VERSION, color);
211
222
  return 0;
212
223
  }
213
224
  if (flags.version) {
214
- console.log(VERSION);
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
- console.log(HELP);
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
- log('Błąd: ' + (e.message || e));
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, HELP };
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wavesconv",
3
- "version": "1.4.9",
3
+ "version": "1.5.0",
4
4
  "description": "CLI: pobieranie YouTube (yt-dlp) i konwersja mediów (ffmpeg) — WavesConverter",
5
5
  "main": "cli.js",
6
6
  "bin": {