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 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
- flags.audio = true;
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 log(msg) {
87
- process.stderr.write(msg + '\n');
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 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';
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 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
- }
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
- function cmdToolsStatus() {
103
- log('yt-dlp: ' + (engine.findYtDlp() || 'BRAK'));
104
- log('ffmpeg: ' + (engine.findFfmpeg() || 'BRAK'));
105
- log('userData: ' + engine.getUserDataDir());
106
- }
82
+ if (flags.json || !color) {
83
+ console.log(JSON.stringify(items, null, 2));
84
+ return;
85
+ }
107
86
 
108
- async function cmdInfo(url) {
109
- const items = await engine.fetchInfo(url);
110
- console.log(JSON.stringify(items, null, 2));
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
- await engine.ensureYtDlp(msg => log(msg));
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
- } catch (_) {}
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
- log(`Pobieranie: ${title}`);
144
- log(`Folder: ${outputDir}`);
145
- log(`Format: ${format}${audioOnly ? ' (audio)' : ''} | Jakość: ${job.quality}`);
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 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
- }
156
+ const detail = line.replace(/\[download\]\s*/i, '').trim();
157
+ bar.update(pct, detail);
155
158
  },
156
159
  onLog: line => {
157
- if (line.includes('ERROR') || line.includes('error')) log(line);
160
+ if (/\bERROR\b/i.test(line)) ui.warn(color, line.slice(0, 100));
158
161
  },
159
162
  });
160
163
 
161
- process.stdout.write('\n');
164
+ bar.succeed(`Zapisano · ${ui.formatBytes(result.size || 0)}`);
162
165
  if (result.path) {
163
- log(`Gotowe: ${result.path} (${formatBytes(result.size)})`);
166
+ ui.success(color, result.path);
164
167
  } else {
165
- log('Pobrano (ścieżka nieznana sprawdź folder docelowy)');
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
- log(`Konwersja ${outputPath}`);
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 => process.stdout.write(`\rCzas: ${t} `),
209
+ onProgress: t => spin.draw(t),
201
210
  });
202
- process.stdout.write('\n');
203
- log('Gotowe: ' + outputPath);
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
- console.log(HELP);
222
+ ui.printHelp(VERSION, color);
211
223
  return 0;
212
224
  }
213
225
  if (flags.version) {
214
- console.log(VERSION);
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
- if (!cmd) {
220
- console.log(HELP);
221
- return 1;
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
- log('Błąd: ' + (e.message || e));
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, HELP };
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wavesconv",
3
- "version": "1.4.9",
3
+ "version": "1.6.0",
4
4
  "description": "CLI: pobieranie YouTube (yt-dlp) i konwersja mediów (ffmpeg) — WavesConverter",
5
5
  "main": "cli.js",
6
6
  "bin": {