wavesconv 1.6.0 → 1.7.1

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/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  <div align="center">
2
2
 
3
- <img src="https://raw.githubusercontent.com/idunnowhytf/wavesconvsite/main/docs/og.png" alt="WavesConverter" width="100%"/>
3
+ <img src="https://res.cloudinary.com/dyozzp82h/image/upload/v1780341159/Gemini_Generated_Image_ve6asxve6asxve6a-Photoroom_clfoi2.png" alt="WavesConverter" width="100%"/>
4
4
 
5
5
  <br/>
6
6
 
7
- <h1>
8
- <img src="https://img.shields.io/badge/WavesConverter-v1.2.0-7c3aed?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMiAxMyBRNCA3IDYgMTMgUTggMTkgMTAgMTMgUTEyIDcgMTQgMTMgUTE2IDE5IDE4IDEzIFEyMCA3IDIyIDEzIiBzdHJva2U9IiNjMDg0ZmMiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=" alt="WavesConverter v1.2.0"/>
9
- </h1>
7
+
10
8
 
11
9
  **Download anything. Convert everything.**
12
10
 
@@ -15,9 +13,9 @@ converts any media file — all offline, no account needed, completely free.
15
13
 
16
14
  <br/>
17
15
 
18
- [![Download](https://img.shields.io/badge/⬇_Download-macOS_&_Windows-7c3aed?style=for-the-badge)](https://idunnowhytf.github.io/wavesconvsite/#download)
19
- [![Website](https://img.shields.io/badge/🌐_Website-wavesconverter-a855f7?style=for-the-badge)](https://idunnowhytf.github.io/wavesconvsite/)
20
- [![Releases](https://img.shields.io/github/v/release/idunnowhytf/wavesconvsite?style=for-the-badge&color=d946ef&label=Latest)](https://github.com/idunnowhytf/wavesconvsite/releases)
16
+ [![Download](https://img.shields.io/badge/⬇_Download-macOS_&_Windows-7c3aed?style=for-the-badge)](https://idunnowhytf.github.io/WavesConverter/#download)
17
+ [![Website](https://img.shields.io/badge/🌐_Website-wavesconverter-a855f7?style=for-the-badge)](https://idunnowhytf.github.io/WavesConverter/)
18
+ [![Releases](https://img.shields.io/github/v/release/idunnowhytf/WavesConverter?style=for-the-badge&color=d946ef&label=Latest)](https://github.com/idunnowhytf/WavesConverter/releases)
21
19
  [![License](https://img.shields.io/badge/License-ISC-6d28d9?style=for-the-badge)](LICENSE)
22
20
 
23
21
  <br/>
@@ -48,9 +46,9 @@ converts any media file — all offline, no account needed, completely free.
48
46
 
49
47
  | Platform | Link |
50
48
  |---|---|
51
- | 🍎 **macOS Apple Silicon** (M1/M2/M3/M4) | [WavesConverter-arm64.dmg](https://github.com/idunnowhytf/wavesconvsite/releases/latest/download/WavesConverter-1.0.0-arm64.dmg) |
52
- | 🍎 **macOS Intel** (x64) | [WavesConverter.dmg](https://github.com/idunnowhytf/wavesconvsite/releases/latest/download/WavesConverter-1.0.0.dmg) |
53
- | 🪟 **Windows 10+** (x64) | [WavesConverter-Setup.exe](https://github.com/idunnowhytf/wavesconvsite/releases/latest/download/WavesConverter.Setup.1.0.0.exe) |
49
+ | 🍎 **macOS Apple Silicon** (M1/M2/M3/M4) | [WavesConverter-arm64.dmg](https://github.com/idunnowhytf/WavesConverter/releases/latest/download/WavesConverter-1.0.0-arm64.dmg) |
50
+ | 🍎 **macOS Intel** (x64) | [WavesConverter.dmg](https://github.com/idunnowhytf/WavesConverter/releases/latest/download/WavesConverter-1.0.0.dmg) |
51
+ | 🪟 **Windows 10+** (x64) | [WavesConverter-Setup.exe](https://github.com/idunnowhytf/WavesConverter/releases/latest/download/WavesConverter.Setup.1.0.0.exe) |
54
52
 
55
53
  > **Windows users:** SmartScreen may show a warning since the app isn't signed with a paid certificate.
56
54
  > Click **"More info" → "Run anyway"** to proceed. The source code is fully open and auditable here.
@@ -144,19 +142,12 @@ wavesconvsite/
144
142
 
145
143
  ## 📋 Changelog
146
144
 
147
- See [**Releases →**](https://github.com/idunnowhytf/wavesconvsite/releases) for full version history.
148
-
149
- | Version | Highlights |
150
- |---|---|
151
- | **v1.2.0** | Batch paste for multiple URLs, ETA & speed display on downloads |
152
- | **v1.1.0** | Download history tab, keyboard shortcuts, native notifications, drag & drop, Windows support |
153
- | **v1.0.0** | Initial release — macOS only |
145
+ See [**Releases →**](https://github.com/idunnowhytf/WavesConverter/releases) for full version history.
154
146
 
155
- ---
156
147
 
157
148
  ## 🤝 Contributing
158
149
 
159
- Found a bug or have a feature idea? [Open an issue](https://github.com/idunnowhytf/wavesconvsite/issues) — all feedback welcome.
150
+ Found a bug or have a feature idea? [Open an issue](https://github.com/idunnowhytf/WavesConverter/issues) — all feedback welcome.
160
151
 
161
152
  ---
162
153
 
@@ -170,7 +161,7 @@ This tool is intended for downloading content you own or have permission to down
170
161
 
171
162
  <div align="center">
172
163
 
173
- **[Website](https://idunnowhytf.github.io/wavesconvsite/) · [Releases](https://github.com/idunnowhytf/wavesconvsite/releases) · [Docs](https://idunnowhytf.github.io/wavesconvsite/docs.html) · [Changelog](https://idunnowhytf.github.io/wavesconvsite/changelog.html)**
164
+ **[Website](https://idunnowhytf.github.io/WavesConverter/) · [Releases](https://github.com/idunnowhytf/WavesConverter/releases) · [Docs](https://idunnowhytf.github.io/WavesConverter/docs.html) · [Changelog](https://idunnowhytf.github.io/WavesConverter/changelog.html)**
174
165
 
175
166
  <br/>
176
167
 
package/cli.js CHANGED
@@ -68,11 +68,12 @@ function cmdToolsStatus(color) {
68
68
  }
69
69
 
70
70
  async function cmdInfo(url, flags, color) {
71
+ url = resolveCliMediaUrl(url);
71
72
  const spin = new ui.Spinner('Pobieranie metadanych', color);
72
73
  spin.start();
73
74
  let items;
74
75
  try {
75
- items = await engine.fetchInfo(url);
76
+ items = await engine.fetchInfo(url, { cookiesPath: process.env.WAVESCONVERTER_COOKIES || '' });
76
77
  spin.succeed(`${items.length} element(ów)`);
77
78
  } catch (e) {
78
79
  spin.fail(e.message);
@@ -100,10 +101,16 @@ async function cmdInfo(url, flags, color) {
100
101
  console.log('');
101
102
  }
102
103
 
103
- async function cmdDownload(url, flags, color) {
104
- if (!engine.isYouTubeUrl(url)) {
105
- throw new Error('Nieprawidłowy URL YouTube: ' + url);
104
+ function resolveCliMediaUrl(raw) {
105
+ const { mediaUrl } = engine.resolveInputUrl(raw);
106
+ if (!mediaUrl) {
107
+ throw new Error('Nieobsługiwany URL. Wklej YouTube/Instagram lub wavesconverter:// przed linkiem.');
106
108
  }
109
+ return mediaUrl;
110
+ }
111
+
112
+ async function cmdDownload(url, flags, color) {
113
+ url = resolveCliMediaUrl(url);
107
114
 
108
115
  const setupSpin = new ui.Spinner('Przygotowanie yt-dlp', color);
109
116
  setupSpin.start();
@@ -125,7 +132,7 @@ async function cmdDownload(url, flags, color) {
125
132
  const metaSpin = new ui.Spinner('Wczytywanie metadanych', color);
126
133
  metaSpin.start();
127
134
  try {
128
- const items = await engine.fetchInfo(url);
135
+ const items = await engine.fetchInfo(url, { cookiesPath: process.env.WAVESCONVERTER_COOKIES || '' });
129
136
  if (items[0]) title = items[0].title || items[0].id || title;
130
137
  metaSpin.succeed(title.slice(0, 52) + (title.length > 52 ? '…' : ''));
131
138
  } catch (_) {
@@ -136,6 +143,8 @@ async function cmdDownload(url, flags, color) {
136
143
  id: 'cli-' + Date.now(),
137
144
  title,
138
145
  url,
146
+ platform: engine.getMediaPlatform(url),
147
+ cookiesPath: process.env.WAVESCONVERTER_COOKIES || '',
139
148
  audioOnly,
140
149
  outputFormat: format,
141
150
  quality: flags.quality || 'best',
@@ -255,7 +264,7 @@ async function run(argv) {
255
264
  switch (cmd) {
256
265
  case 'download': {
257
266
  const url = positional[1];
258
- if (!url) throw new Error('Podaj URL: wavesconv download <url>');
267
+ if (!url) throw new Error('Podaj URL: wavesconv download <url> (możesz dodać wavesconverter:// przed linkiem)');
259
268
  await cmdDownload(url, flags, color);
260
269
  return 0;
261
270
  }
@@ -294,7 +303,8 @@ function shouldRunAsCli(argv) {
294
303
  return ['download', 'info', 'convert', 'tools', 'help'].includes(first);
295
304
  }
296
305
 
297
- if (require.main === module) {
306
+ // Gdy Electron ładuje ten plik przez pomyłkę (zły "main" w package.json), nie uruchamiaj CLI.
307
+ if (require.main === module && !process.versions.electron) {
298
308
  run(process.argv.slice(2)).then(code => process.exit(code));
299
309
  }
300
310
 
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+
5
+ function run(cmd, args = []) {
6
+ return new Promise((resolve, reject) => {
7
+ const proc = spawn(cmd, args, { shell: true, windowsHide: true });
8
+ let out = '';
9
+ let err = '';
10
+ proc.stdout?.on('data', d => { out += d.toString(); });
11
+ proc.stderr?.on('data', d => { err += d.toString(); });
12
+ proc.on('error', reject);
13
+ proc.on('close', code => {
14
+ if (code === 0) resolve((out || err).trim());
15
+ else reject(new Error((err || out || `Kod wyjścia ${code}`).trim().slice(0, 400)));
16
+ });
17
+ });
18
+ }
19
+
20
+ async function checkCliEnvironment() {
21
+ const result = { node: null, npm: null, cli: null, cliInstalled: false };
22
+ try {
23
+ result.node = await run('node', ['--version']);
24
+ } catch (e) {
25
+ result.nodeError = e.message;
26
+ return result;
27
+ }
28
+ try {
29
+ result.npm = await run('npm', ['--version']);
30
+ } catch (e) {
31
+ result.npmError = e.message;
32
+ }
33
+ try {
34
+ result.cli = await run('wavesconv', ['--version']);
35
+ result.cliInstalled = true;
36
+ } catch (_) {
37
+ result.cliInstalled = false;
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function installCliGlobal(onStatus) {
43
+ const notify = (data) => { if (onStatus) onStatus(data); };
44
+ return new Promise((resolve, reject) => {
45
+ notify({ status: 'installing', message: 'Instalacja wavesconv z npm…', progress: 10 });
46
+ const proc = spawn('npm', ['install', '-g', 'wavesconv@latest'], {
47
+ shell: true,
48
+ windowsHide: true,
49
+ });
50
+ let err = '';
51
+ proc.stderr?.on('data', d => {
52
+ err += d.toString();
53
+ notify({ status: 'installing', message: d.toString().trim().slice(-80) || 'Instalowanie…', progress: 50 });
54
+ });
55
+ proc.stdout?.on('data', d => {
56
+ notify({ status: 'installing', message: d.toString().trim().slice(-80) || 'Instalowanie…', progress: 70 });
57
+ });
58
+ proc.on('error', reject);
59
+ proc.on('close', async code => {
60
+ if (code !== 0) {
61
+ reject(new Error(err.trim() || `npm zakończył się kodem ${code}`));
62
+ return;
63
+ }
64
+ notify({ status: 'verifying', message: 'Sprawdzanie instalacji…', progress: 90 });
65
+ try {
66
+ const ver = await run('wavesconv', ['--version']);
67
+ resolve({ success: true, version: ver });
68
+ } catch (e) {
69
+ reject(new Error('Instalacja zakończona, ale polecenie wavesconv nie działa. Uruchom terminal ponownie lub dodaj npm global bin do PATH.'));
70
+ }
71
+ });
72
+ });
73
+ }
74
+
75
+ module.exports = { checkCliEnvironment, installCliGlobal, run };
@@ -29,17 +29,7 @@ function sessionToFlags(session) {
29
29
  }
30
30
 
31
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];
32
+ return engine.extractMediaUrls(text);
43
33
  }
44
34
 
45
35
  function printSession(color, session) {
@@ -202,11 +192,12 @@ async function runInteractive(handlers, version, color) {
202
192
  case 'info':
203
193
  case 'i': {
204
194
  const url = arg || '';
205
- if (!url || !engine.isYouTubeUrl(url)) {
206
- ui.warn(color, 'Użyj: /info <url>');
195
+ const media = url ? engine.resolveInputUrl(url).mediaUrl : null;
196
+ if (!media) {
197
+ ui.warn(color, 'Użyj: /info <url> (YouTube, Instagram lub wavesconverter://…)');
207
198
  return;
208
199
  }
209
- await handlers.info(url, {}, color);
200
+ await handlers.info(media, {}, color);
210
201
  return;
211
202
  }
212
203
  case 'clear':
@@ -252,7 +243,7 @@ async function runInteractive(handlers, version, color) {
252
243
 
253
244
  const urls = extractYouTubeUrls(trimmed);
254
245
  if (!urls.length) {
255
- ui.warn(color, 'Nie wykryto linku YouTube. Wklej URL lub wpisz /help');
246
+ ui.warn(color, 'Nie wykryto linku. Wklej URL (opcjonalnie z wavesconverter://) lub wpisz /help');
256
247
  rl.resume();
257
248
  loop();
258
249
  return;
package/lib/engine.js CHANGED
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const os = require('os');
4
4
  const https = require('https');
5
5
  const { spawn, exec } = require('child_process');
6
+ const urls = require('./urls');
6
7
 
7
8
  const APP_FOLDER = 'waves-converter';
8
9
 
@@ -78,13 +79,25 @@ async function ensureYtDlp(onStatus) {
78
79
  return binFile;
79
80
  }
80
81
 
81
- function fetchInfo(url) {
82
+ function appendCookiesArgs(args, options = {}) {
83
+ const cookiesPath = options.cookiesPath;
84
+ if (cookiesPath && fs.existsSync(cookiesPath)) {
85
+ args.push('--cookies', cookiesPath);
86
+ }
87
+ }
88
+
89
+ function fetchInfo(url, options = {}) {
82
90
  const activeYtDlp = findYtDlp();
83
91
  if (!activeYtDlp) {
84
92
  return Promise.reject(new Error('yt-dlp nie znaleziony. Uruchom: wavesconv tools install'));
85
93
  }
94
+ if (!urls.isSupportedMediaUrl(url)) {
95
+ return Promise.reject(new Error('Nieobsługiwany link. Wklej URL YouTube lub Instagram (post, Reel, Stories).'));
96
+ }
86
97
  return new Promise((resolve, reject) => {
87
- const args = ['--dump-json', '--flat-playlist', '--no-warnings', url];
98
+ const args = ['--dump-json', '--flat-playlist', '--no-warnings'];
99
+ appendCookiesArgs(args, options);
100
+ args.push(url);
88
101
  let out = '';
89
102
  let err = '';
90
103
  const proc = spawn(activeYtDlp, args);
@@ -104,14 +117,20 @@ function fetchInfo(url) {
104
117
 
105
118
  function buildDownloadArgs(job) {
106
119
  const ffmpeg = findFfmpeg();
107
- const { url, outputFormat, quality, bitrate, outputDir, filename, audioOnly } = job;
120
+ const { url, outputFormat, quality, bitrate, outputDir, filename, audioOnly, cookiesPath } = job;
121
+ const platform = job.platform || urls.getMediaPlatform(url);
108
122
  const safeName = (filename || '%(title)s').replace(/[<>:"/\\|?*]/g, '_');
109
123
  const outTpl = path.join(outputDir, safeName + '.%(ext)s');
110
124
  const args = ['--no-warnings', '--newline'];
125
+ appendCookiesArgs(args, { cookiesPath });
111
126
  if (ffmpeg) args.push('--ffmpeg-location', path.dirname(ffmpeg));
112
127
  if (audioOnly) {
113
128
  args.push('-x', '--audio-format', outputFormat || 'mp3');
114
129
  if (bitrate) args.push('--audio-quality', bitrate.replace('k', '') + 'K');
130
+ } else if (platform === 'instagram') {
131
+ args.push('-f', 'best');
132
+ args.push('--merge-output-format', outputFormat || 'mp4');
133
+ if (bitrate) args.push('--postprocessor-args', `ffmpeg:-b:v ${bitrate}`);
115
134
  } else {
116
135
  const h = quality && quality !== 'best' ? quality.replace('p', '') : null;
117
136
  args.push('-f', h ? `bestvideo[height<=${h}]+bestaudio/best[height<=${h}]/best` : 'bestvideo+bestaudio/best');
@@ -282,7 +301,19 @@ function runConvert(job, callbacks = {}) {
282
301
  }
283
302
 
284
303
  function isYouTubeUrl(str) {
285
- return /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+/i.test((str || '').trim());
304
+ return urls.isYouTubeUrl(str);
305
+ }
306
+
307
+ function isSupportedMediaUrl(str) {
308
+ return urls.isSupportedMediaUrl(str);
309
+ }
310
+
311
+ function getMediaPlatform(str) {
312
+ return urls.getMediaPlatform(str);
313
+ }
314
+
315
+ function extractMediaUrls(text) {
316
+ return urls.extractMediaUrls(text);
286
317
  }
287
318
 
288
319
  function parseDeepLink(raw) {
@@ -290,6 +321,21 @@ function parseDeepLink(raw) {
290
321
  const trimmed = raw.trim();
291
322
  if (!trimmed.toLowerCase().startsWith('wavesconverter://')) return null;
292
323
 
324
+ const quickMedia = urls.unwrapWavesProtocolLink(trimmed);
325
+ if (quickMedia) {
326
+ return {
327
+ action: 'download',
328
+ url: quickMedia,
329
+ file: null,
330
+ autoQueue: false,
331
+ autoStart: false,
332
+ format: null,
333
+ quality: null,
334
+ audioOnly: false,
335
+ bitrate: null,
336
+ };
337
+ }
338
+
293
339
  try {
294
340
  const u = new URL(trimmed);
295
341
  const action = (u.hostname || 'open').toLowerCase();
@@ -301,8 +347,12 @@ function parseDeepLink(raw) {
301
347
  if (pathPart) {
302
348
  try { pathPart = decodeURIComponent(pathPart); } catch (_) {}
303
349
  if (/^https?:\/\//i.test(pathPart)) targetUrl = pathPart;
350
+ else if (pathPart.startsWith('//')) targetUrl = `https:${pathPart}`;
304
351
  }
305
352
  }
353
+ if (!targetUrl && (u.hostname === 'https' || u.hostname === 'http') && u.pathname.startsWith('//')) {
354
+ targetUrl = `${u.hostname}:${u.pathname}`;
355
+ }
306
356
  if (targetUrl) {
307
357
  try { targetUrl = decodeURIComponent(targetUrl); } catch (_) {}
308
358
  }
@@ -327,6 +377,28 @@ function findDeepLinkInArgv(argv) {
327
377
  return (argv || process.argv).find(a => typeof a === 'string' && a.toLowerCase().startsWith('wavesconverter://')) || null;
328
378
  }
329
379
 
380
+ /** Rozpoznaje zwykły URL, wavesconverter://https://… lub pełny deep link. */
381
+ function resolveInputUrl(text) {
382
+ const raw = (text || '').trim();
383
+ if (!raw) return { mediaUrl: null, deeplink: null };
384
+
385
+ const deeplink = parseDeepLink(raw);
386
+ if (deeplink?.url) {
387
+ return { mediaUrl: deeplink.url, deeplink };
388
+ }
389
+ if (deeplink && !deeplink.url) {
390
+ return { mediaUrl: null, deeplink };
391
+ }
392
+
393
+ const unwrapped = urls.unwrapWavesProtocolLink(raw);
394
+ if (unwrapped) return { mediaUrl: unwrapped, deeplink: null };
395
+
396
+ const plain = urls.normalizeUrl(raw);
397
+ if (urls.isSupportedMediaUrl(plain)) return { mediaUrl: plain, deeplink: null };
398
+
399
+ return { mediaUrl: null, deeplink: null };
400
+ }
401
+
330
402
  function getDefaultDownloadDir() {
331
403
  return path.join(os.homedir(), 'Downloads');
332
404
  }
@@ -343,6 +415,11 @@ module.exports = {
343
415
  runDownload,
344
416
  runConvert,
345
417
  isYouTubeUrl,
418
+ isSupportedMediaUrl,
419
+ getMediaPlatform,
420
+ extractMediaUrls,
421
+ getInstagramContentKind: urls.getInstagramContentKind,
346
422
  parseDeepLink,
347
423
  findDeepLinkInArgv,
424
+ resolveInputUrl,
348
425
  };
package/lib/urls.js ADDED
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const YOUTUBE_RE = /^(https?:\/\/)?(www\.)?(youtube\.com\/(watch(\?.*)?|shorts\/|playlist\?|embed\/|live\/)|youtu\.be\/|music\.youtube\.com\/)/i;
4
+ const INSTAGRAM_RE = /^(https?:\/\/)?(www\.)?instagram\.com\/(p\/|reel\/|reels\/|tv\/|stories\/)/i;
5
+ const INSTAGRAM_ANY_RE = /^(https?:\/\/)?(www\.)?instagram\.com\//i;
6
+
7
+ function normalizeUrl(str) {
8
+ return (str || '').trim().replace(/[)\]},.;]+$/g, '');
9
+ }
10
+
11
+ function getMediaPlatform(url) {
12
+ const u = normalizeUrl(url);
13
+ if (!u) return null;
14
+ if (YOUTUBE_RE.test(u) || /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+/i.test(u)) {
15
+ return 'youtube';
16
+ }
17
+ if (INSTAGRAM_ANY_RE.test(u) || /^(https?:\/\/)?(www\.)?instagr\.am\//i.test(u)) {
18
+ return 'instagram';
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function isSupportedMediaUrl(url) {
24
+ return !!getMediaPlatform(url);
25
+ }
26
+
27
+ function isYouTubeUrl(url) {
28
+ return getMediaPlatform(url) === 'youtube';
29
+ }
30
+
31
+ function isInstagramUrl(url) {
32
+ return getMediaPlatform(url) === 'instagram';
33
+ }
34
+
35
+ function getInstagramContentKind(url) {
36
+ const u = normalizeUrl(url).toLowerCase();
37
+ if (u.includes('/stories/')) return 'story';
38
+ if (u.includes('/reel/') || u.includes('/reels/')) return 'reel';
39
+ if (u.includes('/p/') || u.includes('/tv/')) return 'post';
40
+ return 'post';
41
+ }
42
+
43
+ const PROTO_PREFIX = /^wavesconverter:\/\//i;
44
+
45
+ function stripWavesProtocol(text) {
46
+ return (text || '').trim().replace(PROTO_PREFIX, '');
47
+ }
48
+
49
+ /** wavesconverter://https://youtu.be/… lub wavesconverter://youtu.be/… → zwykły https URL */
50
+ function unwrapWavesProtocolLink(text) {
51
+ const raw = (text || '').trim();
52
+ if (!PROTO_PREFIX.test(raw)) return null;
53
+
54
+ const after = stripWavesProtocol(raw);
55
+
56
+ const embedded = after.match(/https?:\/\/[^\s<>"']+/i);
57
+ if (embedded) {
58
+ const u = normalizeUrl(embedded[0]);
59
+ if (isSupportedMediaUrl(u)) return u;
60
+ }
61
+
62
+ try {
63
+ const u = new URL(raw);
64
+ const action = (u.hostname || '').toLowerCase();
65
+ const knownActions = new Set(['download', 'queue', 'open', 'convert']);
66
+ if (!knownActions.has(action) && u.hostname) {
67
+ const candidate = normalizeUrl(`https://${u.hostname}${u.pathname}${u.search}${u.hash}`);
68
+ if (isSupportedMediaUrl(candidate)) return candidate;
69
+ }
70
+ let pathPart = u.pathname.replace(/^\//, '');
71
+ if (pathPart.startsWith('//')) {
72
+ const candidate = normalizeUrl(`https:${pathPart}${u.search}${u.hash}`);
73
+ if (isSupportedMediaUrl(candidate)) return candidate;
74
+ }
75
+ if (/^https?:\/\//i.test(pathPart)) {
76
+ const candidate = normalizeUrl(pathPart);
77
+ if (isSupportedMediaUrl(candidate)) return candidate;
78
+ }
79
+ } catch (_) {}
80
+
81
+ if (after && !/^https?:\/\//i.test(after)) {
82
+ const candidate = normalizeUrl(`https://${after.replace(/^\/+/, '')}`);
83
+ if (isSupportedMediaUrl(candidate)) return candidate;
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ function extractMediaUrls(text) {
90
+ const found = new Set();
91
+ const raw = text || '';
92
+
93
+ const protoWrapped = /wavesconverter:\/\/\/?\/?(https?:\/\/[^\s<>"']+)/gi;
94
+ let m;
95
+ while ((m = protoWrapped.exec(raw)) !== null) {
96
+ const cleaned = normalizeUrl(m[1]);
97
+ if (isSupportedMediaUrl(cleaned)) found.add(cleaned);
98
+ }
99
+
100
+ const protoBare = /wavesconverter:\/\/([^\s<>"']+)/gi;
101
+ while ((m = protoBare.exec(raw)) !== null) {
102
+ const unwrapped = unwrapWavesProtocolLink(`wavesconverter://${m[1]}`);
103
+ if (unwrapped) found.add(unwrapped);
104
+ }
105
+
106
+ const re = /https?:\/\/[^\s<>"']+/gi;
107
+ while ((m = re.exec(raw)) !== null) {
108
+ const cleaned = normalizeUrl(m[0]);
109
+ if (isSupportedMediaUrl(cleaned)) found.add(cleaned);
110
+ }
111
+ raw.split(/\s+/).filter(Boolean).forEach(part => {
112
+ const unwrapped = unwrapWavesProtocolLink(part);
113
+ if (unwrapped) {
114
+ found.add(unwrapped);
115
+ return;
116
+ }
117
+ const cleaned = normalizeUrl(part);
118
+ if (isSupportedMediaUrl(cleaned)) found.add(cleaned);
119
+ });
120
+ return [...found];
121
+ }
122
+
123
+ module.exports = {
124
+ normalizeUrl,
125
+ getMediaPlatform,
126
+ isSupportedMediaUrl,
127
+ isYouTubeUrl,
128
+ isInstagramUrl,
129
+ getInstagramContentKind,
130
+ extractMediaUrls,
131
+ unwrapWavesProtocolLink,
132
+ stripWavesProtocol,
133
+ };
package/main.js ADDED
@@ -0,0 +1,647 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { exec } = require('child_process');
4
+ const os = require('os');
5
+ const https = require('https');
6
+ const http = require('http');
7
+ const engine = require('./lib/engine');
8
+ const cli = require('./cli');
9
+
10
+ // Tryb CLI: node cli.js … lub npx electron . --cli download …
11
+ const rawArgv = process.argv.slice(1);
12
+ const cliFlagIdx = rawArgv.indexOf('--cli');
13
+ if (cliFlagIdx !== -1) {
14
+ cli.run(rawArgv.slice(cliFlagIdx + 1)).then(code => process.exit(code ?? 0)).catch(e => {
15
+ console.error(e.message || e);
16
+ process.exit(1);
17
+ });
18
+ } else {
19
+ startElectronApp();
20
+ }
21
+
22
+ function startElectronApp() {
23
+ const { app, BrowserWindow, ipcMain, dialog, shell, Menu, Tray, globalShortcut, clipboard } = require('electron');
24
+ const isDev = !app.isPackaged;
25
+
26
+ function loadQRCode() {
27
+ try {
28
+ return require('qrcode');
29
+ } catch (e) {
30
+ console.error('qrcode module missing:', e.message);
31
+ return null;
32
+ }
33
+ }
34
+
35
+ let mainWindow;
36
+ let ytDlpPath;
37
+ let tray = null;
38
+ let shareServer = null;
39
+ let isQuitting = false;
40
+ let pendingDeepLink = null;
41
+ let pendingClipboardText = null;
42
+
43
+ const PROTOCOL = 'wavesconverter';
44
+
45
+ function isMainWindowAlive() {
46
+ try {
47
+ return !!(mainWindow && !mainWindow.isDestroyed());
48
+ } catch (_) {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function safeSend(channel, data) {
54
+ try {
55
+ if (!isMainWindowAlive()) return false;
56
+ const wc = mainWindow.webContents;
57
+ if (!wc || wc.isDestroyed()) return false;
58
+ mainWindow.webContents.send(channel, data);
59
+ return true;
60
+ } catch (e) {
61
+ console.error('safeSend:', e.message);
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function focusMainWindow() {
67
+ try {
68
+ if (!isMainWindowAlive()) return;
69
+ if (mainWindow.isMinimized()) mainWindow.restore();
70
+ mainWindow.show();
71
+ mainWindow.focus();
72
+ } catch (_) {}
73
+ }
74
+
75
+ function ensureMainWindow() {
76
+ if (isMainWindowAlive()) return true;
77
+ createWindow();
78
+ return isMainWindowAlive();
79
+ }
80
+
81
+ function flushPendingToRenderer() {
82
+ flushPendingDeepLink();
83
+ if (pendingClipboardText) {
84
+ safeSend('clipboard-search-trigger', pendingClipboardText);
85
+ pendingClipboardText = null;
86
+ }
87
+ }
88
+
89
+ function syncEngineUserData() {
90
+ if (app.isReady()) process.env.WAVESCONVERTER_USER_DATA = app.getPath('userData');
91
+ }
92
+
93
+ function findYtDlp() {
94
+ syncEngineUserData();
95
+ return engine.findYtDlp();
96
+ }
97
+
98
+ function findFfmpeg() {
99
+ syncEngineUserData();
100
+ let p = engine.findFfmpeg();
101
+ if (p) return p;
102
+ try {
103
+ const i = require('@ffmpeg-installer/ffmpeg');
104
+ let fp = i.path;
105
+ if (app.isPackaged) fp = fp.replace('app.asar', 'app.asar.unpacked');
106
+ if (fs.existsSync(fp)) return fp;
107
+ } catch (_) {}
108
+ return null;
109
+ }
110
+
111
+ function registerProtocol() {
112
+ if (process.defaultApp) {
113
+ if (process.argv.length >= 2) {
114
+ app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, [path.resolve(process.argv[1])]);
115
+ }
116
+ } else {
117
+ app.setAsDefaultProtocolClient(PROTOCOL);
118
+ }
119
+ }
120
+
121
+ function deliverDeepLink(payload) {
122
+ if (!payload) return;
123
+ if (isMainWindowAlive()) {
124
+ const wc = mainWindow.webContents;
125
+ if (wc && !wc.isDestroyed() && !wc.isLoading()) {
126
+ focusMainWindow();
127
+ safeSend('deep-link', payload);
128
+ pendingDeepLink = null;
129
+ return;
130
+ }
131
+ }
132
+ pendingDeepLink = payload;
133
+ }
134
+
135
+ function handleDeepLinkUrl(url) {
136
+ const payload = engine.parseDeepLink(url);
137
+ if (payload) deliverDeepLink(payload);
138
+ }
139
+
140
+ function flushPendingDeepLink() {
141
+ if (pendingDeepLink) deliverDeepLink(pendingDeepLink);
142
+ }
143
+
144
+ // Helper: Get ffbinaries platform tag
145
+ function getFfmpegPlatformTag() {
146
+ const p = process.platform;
147
+ const a = process.arch;
148
+ if (p === 'win32') return a === 'ia32' ? 'windows-32' : 'windows-64';
149
+ if (p === 'darwin') return 'osx-64';
150
+ if (p === 'linux') {
151
+ if (a === 'arm64') return 'linux-arm-64';
152
+ if (a === 'arm') return 'linux-armhf';
153
+ return 'linux-64';
154
+ }
155
+ return 'windows-64';
156
+ }
157
+
158
+ // Helper: Get ffmpeg URL
159
+ function getFfmpegDownloadUrl() {
160
+ return new Promise((resolve) => {
161
+ const tag = getFfmpegPlatformTag();
162
+ const fallbackUrl = `https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v6.1/ffmpeg-6.1-${tag === 'osx-64' ? 'osx-64' : tag === 'windows-64' ? 'win-64' : tag === 'windows-32' ? 'win-32' : tag}.zip`;
163
+
164
+ https.get('https://ffbinaries.com/api/v1/version/latest', { headers: { 'User-Agent': 'WavesConverter' } }, (res) => {
165
+ let data = '';
166
+ res.on('data', chunk => data += chunk);
167
+ res.on('end', () => {
168
+ try {
169
+ const json = JSON.parse(data);
170
+ const url = json?.bin?.[tag]?.ffmpeg;
171
+ if (url) resolve(url);
172
+ else resolve(fallbackUrl);
173
+ } catch (_) {
174
+ resolve(fallbackUrl);
175
+ }
176
+ });
177
+ }).on('error', () => {
178
+ resolve(fallbackUrl);
179
+ });
180
+ });
181
+ }
182
+
183
+ // Helper: Download file following redirects
184
+ function downloadFile(url, dest) {
185
+ return new Promise((resolve, reject) => {
186
+ const file = fs.createWriteStream(dest);
187
+
188
+ function get(fileUrl) {
189
+ https.get(fileUrl, { headers: { 'User-Agent': 'WavesConverter' } }, (response) => {
190
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
191
+ get(response.headers.location);
192
+ return;
193
+ }
194
+ if (response.statusCode !== 200) {
195
+ reject(new Error(`Failed to download: Status ${response.statusCode}`));
196
+ return;
197
+ }
198
+ response.pipe(file);
199
+ file.on('finish', () => {
200
+ file.close();
201
+ resolve();
202
+ });
203
+ }).on('error', (err) => {
204
+ try { fs.unlinkSync(dest); } catch (_) {}
205
+ reject(err);
206
+ });
207
+ }
208
+
209
+ get(url);
210
+ });
211
+ }
212
+
213
+ // Helper: Extract Zip
214
+ function extractZip(zipPath, destDir) {
215
+ return new Promise((resolve, reject) => {
216
+ const isWin = process.platform === 'win32';
217
+ if (isWin) {
218
+ const cmd = `powershell.exe -NoProfile -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`;
219
+ exec(cmd, (err) => {
220
+ if (err) reject(err);
221
+ else resolve();
222
+ });
223
+ } else {
224
+ const cmd = `unzip -o "${zipPath}" -d "${destDir}"`;
225
+ exec(cmd, (err) => {
226
+ if (err) reject(err);
227
+ else resolve();
228
+ });
229
+ }
230
+ });
231
+ }
232
+
233
+ // Helper: Find File Recursively
234
+ function findFileRecursively(dir, fileName) {
235
+ const files = fs.readdirSync(dir);
236
+ for (const file of files) {
237
+ const fullPath = path.join(dir, file);
238
+ const stat = fs.statSync(fullPath);
239
+ if (stat.isDirectory()) {
240
+ const found = findFileRecursively(fullPath, fileName);
241
+ if (found) return found;
242
+ } else if (file.toLowerCase() === fileName.toLowerCase()) {
243
+ return fullPath;
244
+ }
245
+ }
246
+ return null;
247
+ }
248
+
249
+ // Helper: Clean nested folders in bin
250
+ function cleanupExtractedFolders(dir) {
251
+ const files = fs.readdirSync(dir);
252
+ for (const file of files) {
253
+ const fullPath = path.join(dir, file);
254
+ if (fs.statSync(fullPath).isDirectory()) {
255
+ try {
256
+ fs.rmSync(fullPath, { recursive: true, force: true });
257
+ } catch (_) {}
258
+ }
259
+ }
260
+ }
261
+
262
+ function createWindow() {
263
+ if (isMainWindowAlive()) return;
264
+
265
+ const isWin = process.platform === 'win32';
266
+ // On Windows, transparent + vibrancy-like effects are a common cause of "app runs but window is invisible".
267
+ // Keep the premium glass look for macOS, but force a safe, opaque window on Windows.
268
+ const windowOpts = {
269
+ width: 1300, height: 860, minWidth: 920, minHeight: 660,
270
+ frame: false,
271
+ transparent: !isWin,
272
+ backgroundColor: isWin ? '#0a0118' : '#00000000',
273
+ webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false },
274
+ title: 'WavesConverter',
275
+ show: false,
276
+ };
277
+ if (!isWin) {
278
+ windowOpts.vibrancy = 'under-window';
279
+ windowOpts.visualEffectState = 'active';
280
+ }
281
+
282
+ mainWindow = new BrowserWindow(windowOpts);
283
+ mainWindow.loadFile('index.html');
284
+ mainWindow.once('ready-to-show', () => {
285
+ if (isMainWindowAlive()) mainWindow.show();
286
+ });
287
+ // Fallback: if ready-to-show never fires, still show the window.
288
+ setTimeout(() => {
289
+ try {
290
+ if (isMainWindowAlive() && !mainWindow.isVisible()) mainWindow.show();
291
+ } catch (_) {}
292
+ }, 2500);
293
+ mainWindow.webContents.on('did-finish-load', flushPendingToRenderer);
294
+ mainWindow.on('maximize', () => safeSend('window-state', 'maximized'));
295
+ mainWindow.on('unmaximize', () => safeSend('window-state', 'normal'));
296
+ mainWindow.on('closed', () => { mainWindow = null; });
297
+
298
+ // Close to tray logic
299
+ mainWindow.on('close', (event) => {
300
+ if (!isQuitting && !isDev) {
301
+ event.preventDefault();
302
+ mainWindow.hide();
303
+ return false;
304
+ }
305
+ });
306
+ }
307
+
308
+ function createTray() {
309
+ const iconPath = path.join(__dirname, 'assets', process.platform === 'win32' ? 'icon.ico' : 'icon.png');
310
+ if (!fs.existsSync(iconPath)) return;
311
+
312
+ tray = new Tray(iconPath);
313
+ const contextMenu = Menu.buildFromTemplate([
314
+ { label: 'Pokaż WavesConverter', click: () => { showAndCheckClipboard(); } },
315
+ { label: 'Minimalizuj do zasobnika', click: () => { mainWindow?.hide(); } },
316
+ { type: 'separator' },
317
+ { label: 'Zakończ', click: () => { isQuitting = true; app.quit(); } }
318
+ ]);
319
+
320
+ tray.setToolTip('WavesConverter');
321
+ tray.setContextMenu(contextMenu);
322
+
323
+ tray.on('click', () => {
324
+ if (isMainWindowAlive() && mainWindow.isVisible()) {
325
+ mainWindow.hide();
326
+ } else {
327
+ showAndCheckClipboard();
328
+ }
329
+ });
330
+ }
331
+
332
+ function registerGlobalShortcut() {
333
+ // Alt+Shift+W is a safe global shortcut combination
334
+ globalShortcut.register('Alt+Shift+W', () => {
335
+ if (isMainWindowAlive() && mainWindow.isVisible() && mainWindow.isFocused()) {
336
+ mainWindow.hide();
337
+ } else {
338
+ showAndCheckClipboard();
339
+ }
340
+ });
341
+ }
342
+
343
+ function showAndCheckClipboard() {
344
+ const text = clipboard.readText();
345
+ if (!ensureMainWindow()) {
346
+ if (text) pendingClipboardText = text;
347
+ return;
348
+ }
349
+ focusMainWindow();
350
+ if (text) {
351
+ if (!safeSend('clipboard-search-trigger', text)) {
352
+ pendingClipboardText = text;
353
+ }
354
+ }
355
+ }
356
+
357
+ const gotSingleInstanceLock = app.requestSingleInstanceLock();
358
+ if (!gotSingleInstanceLock) {
359
+ app.quit();
360
+ } else {
361
+ app.on('second-instance', (_, argv) => {
362
+ const link = engine.findDeepLinkInArgv(argv);
363
+ if (link) handleDeepLinkUrl(link);
364
+ if (!isMainWindowAlive()) ensureMainWindow();
365
+ else focusMainWindow();
366
+ });
367
+
368
+ app.on('open-url', (event, url) => {
369
+ event.preventDefault();
370
+ handleDeepLinkUrl(url);
371
+ });
372
+
373
+ app.whenReady().then(async () => {
374
+ syncEngineUserData();
375
+ registerProtocol();
376
+
377
+ const startupLink = engine.findDeepLinkInArgv(process.argv);
378
+ if (startupLink) handleDeepLinkUrl(startupLink);
379
+
380
+ ytDlpPath = findYtDlp();
381
+ if (!ytDlpPath) {
382
+ try {
383
+ ytDlpPath = await engine.ensureYtDlp();
384
+ } catch (e) { console.error('yt-dlp download failed on startup:', e.message); }
385
+ }
386
+ createWindow();
387
+ if (!isDev) {
388
+ createTray();
389
+ registerGlobalShortcut();
390
+ }
391
+ // In dev mode, always force focus/visibility to avoid "running but no window" confusion.
392
+ if (isDev) {
393
+ setTimeout(() => focusMainWindow(), 800);
394
+ }
395
+ setupAutoUpdater();
396
+ });
397
+
398
+ app.on('will-quit', () => {
399
+ globalShortcut.unregisterAll();
400
+ });
401
+
402
+ app.on('window-all-closed', () => {
403
+ // Keep running in system tray
404
+ });
405
+
406
+ app.on('activate', () => {
407
+ if (!isMainWindowAlive()) createWindow();
408
+ else focusMainWindow();
409
+ });
410
+ }
411
+
412
+ function setupAutoUpdater() {
413
+ if (!app.isPackaged) return;
414
+ try {
415
+ const { autoUpdater } = require('electron-updater');
416
+ autoUpdater.autoDownload = true;
417
+ autoUpdater.autoInstallOnAppQuit = true;
418
+ autoUpdater.on('checking-for-update', () => safeSend('update-status', { type: 'checking' }));
419
+ autoUpdater.on('update-available', info => safeSend('update-status', { type: 'available', version: info.version }));
420
+ autoUpdater.on('update-not-available', () => safeSend('update-status', { type: 'latest' }));
421
+ autoUpdater.on('download-progress', p => safeSend('update-status', { type: 'downloading', percent: Math.round(p.percent) }));
422
+ autoUpdater.on('update-downloaded', info => safeSend('update-status', { type: 'ready', version: info.version }));
423
+ autoUpdater.on('error', err => safeSend('update-status', { type: 'error', message: err.message }));
424
+ autoUpdater.checkForUpdatesAndNotify();
425
+ } catch (e) { console.error('Updater error:', e.message); }
426
+ }
427
+
428
+ ipcMain.on('install-update', () => { if (app.isPackaged) try { require('electron-updater').autoUpdater.quitAndInstall(); } catch (_) {} });
429
+ ipcMain.handle('check-update', async () => { if (!app.isPackaged) return { type: 'dev' }; try { await require('electron-updater').autoUpdater.checkForUpdates(); } catch (e) { return { type: 'error', message: e.message }; } });
430
+ ipcMain.handle('get-version', () => app.getVersion());
431
+
432
+ ipcMain.on('window-minimize', () => { if (isMainWindowAlive()) mainWindow.minimize(); });
433
+ ipcMain.on('window-maximize', () => {
434
+ if (!isMainWindowAlive()) return;
435
+ mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
436
+ });
437
+ ipcMain.on('window-close', () => { if (isMainWindowAlive()) mainWindow.close(); });
438
+
439
+ ipcMain.handle('choose-folder', async () => { const r = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] }); return r.canceled ? null : r.filePaths[0]; });
440
+ ipcMain.handle('choose-file', async () => { const r = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: [{ name: 'Media', extensions: ['mp4','mkv','avi','mov','webm','mp3','wav','flac','aac','m4a','ogg'] }] }); return r.canceled ? null : r.filePaths[0]; });
441
+ ipcMain.on('open-folder', (_, p) => shell.openPath(p));
442
+ ipcMain.handle('get-default-dir', () => path.join(os.homedir(), 'Downloads'));
443
+ ipcMain.handle('check-ytdlp', () => !!findYtDlp());
444
+ ipcMain.handle('check-ffmpeg', () => !!findFfmpeg());
445
+
446
+ // Tool Installer IPC handler
447
+ ipcMain.handle('install-tools', async (event) => {
448
+ const binDir = path.join(app.getPath('userData'), 'bin');
449
+ if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true });
450
+ const isWin = process.platform === 'win32';
451
+
452
+ // 1. Download yt-dlp
453
+ try {
454
+ event.sender.send('install-status', { status: 'downloading-ytdlp', progress: 10, message: 'Downloading yt-dlp...' });
455
+ const YTDlpWrap = require('yt-dlp-wrap').default || require('yt-dlp-wrap');
456
+ const binFile = path.join(binDir, isWin ? 'yt-dlp.exe' : 'yt-dlp');
457
+ await YTDlpWrap.downloadFromGithub(binFile);
458
+ if (!isWin) fs.chmodSync(binFile, 0o755);
459
+ ytDlpPath = binFile;
460
+ } catch (e) {
461
+ console.error('yt-dlp install failed:', e);
462
+ throw new Error('Failed to install yt-dlp: ' + e.message);
463
+ }
464
+
465
+ // 2. Download ffmpeg
466
+ try {
467
+ event.sender.send('install-status', { status: 'downloading-ffmpeg', progress: 40, message: 'Downloading ffmpeg...' });
468
+ const ffmpegUrl = await getFfmpegDownloadUrl();
469
+ const zipFile = path.join(binDir, 'ffmpeg.zip');
470
+ await downloadFile(ffmpegUrl, zipFile);
471
+
472
+ event.sender.send('install-status', { status: 'extracting-ffmpeg', progress: 80, message: 'Extracting ffmpeg...' });
473
+ await extractZip(zipFile, binDir);
474
+ try { fs.unlinkSync(zipFile); } catch (_) {}
475
+
476
+ const ffmpegExeName = isWin ? 'ffmpeg.exe' : 'ffmpeg';
477
+ const foundFfmpeg = findFileRecursively(binDir, ffmpegExeName);
478
+ if (foundFfmpeg && foundFfmpeg !== path.join(binDir, ffmpegExeName)) {
479
+ fs.renameSync(foundFfmpeg, path.join(binDir, ffmpegExeName));
480
+ }
481
+
482
+ const finalFfmpegPath = path.join(binDir, ffmpegExeName);
483
+ if (!fs.existsSync(finalFfmpegPath)) {
484
+ throw new Error('ffmpeg binary not found in extracted files');
485
+ }
486
+
487
+ if (!isWin) fs.chmodSync(finalFfmpegPath, 0o755);
488
+ cleanupExtractedFolders(binDir);
489
+ } catch (e) {
490
+ console.error('ffmpeg install failed:', e);
491
+ throw new Error('Failed to install ffmpeg: ' + e.message);
492
+ }
493
+
494
+ event.sender.send('install-status', { status: 'success', progress: 100, message: 'Tools installed successfully!' });
495
+ return { success: true };
496
+ });
497
+
498
+ ipcMain.handle('fetch-info', async (_, url, options) => engine.fetchInfo(url, options || {}));
499
+ ipcMain.handle('resolve-input-url', (_, text) => engine.resolveInputUrl(text));
500
+ ipcMain.handle('is-supported-url', (_, url) => !!engine.resolveInputUrl(url).mediaUrl);
501
+ ipcMain.handle('get-url-platform', (_, url) => {
502
+ const media = engine.resolveInputUrl(url).mediaUrl;
503
+ return media ? engine.getMediaPlatform(media) : null;
504
+ });
505
+ ipcMain.handle('choose-cookies-file', async () => {
506
+ const r = await dialog.showOpenDialog(mainWindow, {
507
+ properties: ['openFile'],
508
+ filters: [{ name: 'Cookies', extensions: ['txt'] }],
509
+ });
510
+ return r.canceled ? null : r.filePaths[0];
511
+ });
512
+
513
+ const cliInstall = require('./lib/cli-install');
514
+
515
+ ipcMain.handle('check-cli', async () => cliInstall.checkCliEnvironment());
516
+
517
+ ipcMain.handle('install-cli', async (event) => {
518
+ const env = await cliInstall.checkCliEnvironment();
519
+ if (!env.node) {
520
+ throw new Error('Node.js nie jest zainstalowany. Pobierz Node 18+ z nodejs.org i spróbuj ponownie.');
521
+ }
522
+ if (!env.npm) {
523
+ throw new Error('npm nie jest dostępny. Zainstaluj Node.js (zawiera npm) i spróbuj ponownie.');
524
+ }
525
+ return cliInstall.installCliGlobal(data => {
526
+ event.sender.send('cli-install-status', data);
527
+ });
528
+ });
529
+
530
+ ipcMain.handle('copy-text', (_, text) => {
531
+ clipboard.writeText(text || '');
532
+ return true;
533
+ });
534
+
535
+ ipcMain.handle('open-external', (_, url) => shell.openExternal(url));
536
+
537
+ const active = new Map();
538
+
539
+ ipcMain.handle('start-download', (_, job) => {
540
+ if (!findYtDlp()) return Promise.reject(new Error('yt-dlp not found. Please click the "Install / Repair Tools" button in Settings.'));
541
+ const { id } = job;
542
+ return engine.runDownload(job, {
543
+ onSpawn: proc => active.set(id, proc),
544
+ onProgress: (progress, line) => {
545
+ safeSend('download-progress', { id, progress, line });
546
+ safeSend('download-log', { id, line });
547
+ },
548
+ onLog: line => safeSend('download-log', { id, line }),
549
+ }).finally(() => active.delete(id));
550
+ });
551
+
552
+ ipcMain.on('cancel-download', (_, id) => { const p = active.get(id); if (p) { p.kill('SIGTERM'); active.delete(id); } });
553
+
554
+ ipcMain.handle('convert-file', (_, job) => {
555
+ if (!findFfmpeg()) return Promise.reject(new Error('ffmpeg not found. Please click the "Install / Repair Tools" button in Settings.'));
556
+ const { id } = job;
557
+ return engine.runConvert(job, {
558
+ onSpawn: proc => active.set(id, proc),
559
+ onProgress: time => safeSend('convert-progress', { id, time }),
560
+ }).finally(() => active.delete(id));
561
+ });
562
+
563
+ // Expose download-thumbnail handler
564
+ ipcMain.handle('download-thumbnail', async (_, { url, dest }) => {
565
+ return downloadFile(url, dest);
566
+ });
567
+
568
+ // Wi-Fi Local File Sharing Server Handlers
569
+ ipcMain.handle('start-share-server', async (event, { filePath, fileName }) => {
570
+ if (!fs.existsSync(filePath)) {
571
+ throw new Error('Plik nie istnieje lub został usunięty z dysku.');
572
+ }
573
+
574
+ if (shareServer) {
575
+ try {
576
+ shareServer.close();
577
+ } catch (_) {}
578
+ shareServer = null;
579
+ }
580
+
581
+ // Get local IP address
582
+ const networkInterfaces = os.networkInterfaces();
583
+ let localIp = '127.0.0.1';
584
+ for (const name of Object.keys(networkInterfaces)) {
585
+ for (const iface of networkInterfaces[name]) {
586
+ if (iface.family === 'IPv4' && !iface.internal) {
587
+ localIp = iface.address;
588
+ break;
589
+ }
590
+ }
591
+ if (localIp !== '127.0.0.1') break;
592
+ }
593
+
594
+ return new Promise((resolve, reject) => {
595
+ shareServer = http.createServer((req, res) => {
596
+ if (fs.existsSync(filePath)) {
597
+ const stat = fs.statSync(filePath);
598
+ const safeFileName = fileName.replace(/["\\]/g, ''); // strip quotes and backslashes
599
+ res.writeHead(200, {
600
+ 'Content-Type': 'application/octet-stream',
601
+ 'Content-Length': stat.size,
602
+ 'Content-Disposition': `attachment; filename="${encodeURIComponent(safeFileName)}"; filename*=UTF-8''${encodeURIComponent(safeFileName)}`
603
+ });
604
+ const readStream = fs.createReadStream(filePath);
605
+ readStream.pipe(res);
606
+ } else {
607
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
608
+ res.end('Plik nie istnieje lub został usunięty.');
609
+ }
610
+ });
611
+
612
+ shareServer.listen(0, localIp, async () => {
613
+ const port = shareServer.address().port;
614
+ const shareUrl = `http://${localIp}:${port}/download`;
615
+ try {
616
+ const QRCode = loadQRCode();
617
+ let qrDataUrl = null;
618
+ if (QRCode) {
619
+ qrDataUrl = await QRCode.toDataURL(shareUrl, {
620
+ color: {
621
+ dark: '#1e0b36',
622
+ light: '#f0e6ff'
623
+ },
624
+ margin: 2
625
+ });
626
+ }
627
+ resolve({ shareUrl, qrDataUrl });
628
+ } catch (err) {
629
+ reject(err);
630
+ }
631
+ });
632
+
633
+ shareServer.on('error', (err) => {
634
+ reject(err);
635
+ });
636
+ });
637
+ });
638
+
639
+ ipcMain.handle('stop-share-server', async () => {
640
+ if (shareServer) {
641
+ shareServer.close();
642
+ shareServer = null;
643
+ }
644
+ return { success: true };
645
+ });
646
+
647
+ } // startElectronApp
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "wavesconv",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "CLI: pobieranie YouTube (yt-dlp) i konwersja mediów (ffmpeg) — WavesConverter",
5
- "main": "cli.js",
5
+ "main": "main.js",
6
6
  "bin": {
7
7
  "wavesconv": "./cli.js"
8
8
  },
@@ -16,11 +16,11 @@
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/idunnowhytf/wavesconvsite.git"
19
+ "url": "git+https://github.com/idunnowhytf/WavesConverter.git"
20
20
  },
21
- "homepage": "https://github.com/idunnowhytf/wavesconvsite#readme",
21
+ "homepage": "https://github.com/idunnowhytf/WavesConverter#readme",
22
22
  "bugs": {
23
- "url": "https://github.com/idunnowhytf/wavesconvsite/issues"
23
+ "url": "https://github.com/idunnowhytf/WavesConverter/issues"
24
24
  },
25
25
  "keywords": [
26
26
  "youtube",
@@ -34,6 +34,7 @@
34
34
  "author": "WavesConverter",
35
35
  "scripts": {
36
36
  "start": "electron .",
37
+ "start:cli": "node cli.js",
37
38
  "cli": "node cli.js",
38
39
  "prepublishOnly": "node cli.js --version",
39
40
  "build": "electron-builder",
@@ -98,12 +99,12 @@
98
99
  "license": "ISC",
99
100
  "dependencies": {
100
101
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
102
+ "electron-updater": "^6.8.3",
103
+ "qrcode": "^1.5.4",
101
104
  "yt-dlp-wrap": "^2.3.12"
102
105
  },
103
106
  "devDependencies": {
104
107
  "electron": "^42.3.0",
105
- "electron-builder": "^26.8.1",
106
- "electron-updater": "^6.8.3",
107
- "qrcode": "^1.5.4"
108
+ "electron-builder": "^26.8.1"
108
109
  }
109
110
  }