wavesconv 1.7.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/cli.js CHANGED
@@ -68,6 +68,7 @@ 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;
@@ -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.isSupportedMediaUrl(url)) {
105
- throw new Error('Nieobsługiwany URL. Użyj linku YouTube lub Instagram (post / Reel / Stories).');
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();
@@ -257,7 +264,7 @@ async function run(argv) {
257
264
  switch (cmd) {
258
265
  case 'download': {
259
266
  const url = positional[1];
260
- 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)');
261
268
  await cmdDownload(url, flags, color);
262
269
  return 0;
263
270
  }
@@ -296,7 +303,8 @@ function shouldRunAsCli(argv) {
296
303
  return ['download', 'info', 'convert', 'tools', 'help'].includes(first);
297
304
  }
298
305
 
299
- 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) {
300
308
  run(process.argv.slice(2)).then(code => process.exit(code));
301
309
  }
302
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 };
@@ -192,11 +192,12 @@ async function runInteractive(handlers, version, color) {
192
192
  case 'info':
193
193
  case 'i': {
194
194
  const url = arg || '';
195
- if (!url || !engine.isSupportedMediaUrl(url)) {
196
- ui.warn(color, 'Użyj: /info <url> (YouTube lub Instagram)');
195
+ const media = url ? engine.resolveInputUrl(url).mediaUrl : null;
196
+ if (!media) {
197
+ ui.warn(color, 'Użyj: /info <url> (YouTube, Instagram lub wavesconverter://…)');
197
198
  return;
198
199
  }
199
- await handlers.info(url, {}, color);
200
+ await handlers.info(media, {}, color);
200
201
  return;
201
202
  }
202
203
  case 'clear':
@@ -242,7 +243,7 @@ async function runInteractive(handlers, version, color) {
242
243
 
243
244
  const urls = extractYouTubeUrls(trimmed);
244
245
  if (!urls.length) {
245
- ui.warn(color, 'Nie wykryto linku YouTube/Instagram. Wklej URL lub wpisz /help');
246
+ ui.warn(color, 'Nie wykryto linku. Wklej URL (opcjonalnie z wavesconverter://) lub wpisz /help');
246
247
  rl.resume();
247
248
  loop();
248
249
  return;
package/lib/engine.js CHANGED
@@ -321,6 +321,21 @@ function parseDeepLink(raw) {
321
321
  const trimmed = raw.trim();
322
322
  if (!trimmed.toLowerCase().startsWith('wavesconverter://')) return null;
323
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
+
324
339
  try {
325
340
  const u = new URL(trimmed);
326
341
  const action = (u.hostname || 'open').toLowerCase();
@@ -332,8 +347,12 @@ function parseDeepLink(raw) {
332
347
  if (pathPart) {
333
348
  try { pathPart = decodeURIComponent(pathPart); } catch (_) {}
334
349
  if (/^https?:\/\//i.test(pathPart)) targetUrl = pathPart;
350
+ else if (pathPart.startsWith('//')) targetUrl = `https:${pathPart}`;
335
351
  }
336
352
  }
353
+ if (!targetUrl && (u.hostname === 'https' || u.hostname === 'http') && u.pathname.startsWith('//')) {
354
+ targetUrl = `${u.hostname}:${u.pathname}`;
355
+ }
337
356
  if (targetUrl) {
338
357
  try { targetUrl = decodeURIComponent(targetUrl); } catch (_) {}
339
358
  }
@@ -358,6 +377,28 @@ function findDeepLinkInArgv(argv) {
358
377
  return (argv || process.argv).find(a => typeof a === 'string' && a.toLowerCase().startsWith('wavesconverter://')) || null;
359
378
  }
360
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
+
361
402
  function getDefaultDownloadDir() {
362
403
  return path.join(os.homedir(), 'Downloads');
363
404
  }
@@ -380,4 +421,5 @@ module.exports = {
380
421
  getInstagramContentKind: urls.getInstagramContentKind,
381
422
  parseDeepLink,
382
423
  findDeepLinkInArgv,
424
+ resolveInputUrl,
383
425
  };
package/lib/urls.js CHANGED
@@ -40,16 +40,80 @@ function getInstagramContentKind(url) {
40
40
  return 'post';
41
41
  }
42
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
+
43
89
  function extractMediaUrls(text) {
44
90
  const found = new Set();
45
91
  const raw = text || '';
46
- const re = /https?:\/\/[^\s<>"']+/gi;
92
+
93
+ const protoWrapped = /wavesconverter:\/\/\/?\/?(https?:\/\/[^\s<>"']+)/gi;
47
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;
48
107
  while ((m = re.exec(raw)) !== null) {
49
108
  const cleaned = normalizeUrl(m[0]);
50
109
  if (isSupportedMediaUrl(cleaned)) found.add(cleaned);
51
110
  }
52
111
  raw.split(/\s+/).filter(Boolean).forEach(part => {
112
+ const unwrapped = unwrapWavesProtocolLink(part);
113
+ if (unwrapped) {
114
+ found.add(unwrapped);
115
+ return;
116
+ }
53
117
  const cleaned = normalizeUrl(part);
54
118
  if (isSupportedMediaUrl(cleaned)) found.add(cleaned);
55
119
  });
@@ -64,4 +128,6 @@ module.exports = {
64
128
  isInstagramUrl,
65
129
  getInstagramContentKind,
66
130
  extractMediaUrls,
131
+ unwrapWavesProtocolLink,
132
+ stripWavesProtocol,
67
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.7.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
  }