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 +13 -5
- package/lib/cli-install.js +75 -0
- package/lib/cli-interactive.js +5 -4
- package/lib/engine.js +42 -0
- package/lib/urls.js +67 -1
- package/main.js +647 -0
- package/package.json +9 -8
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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 };
|
package/lib/cli-interactive.js
CHANGED
|
@@ -192,11 +192,12 @@ async function runInteractive(handlers, version, color) {
|
|
|
192
192
|
case 'info':
|
|
193
193
|
case 'i': {
|
|
194
194
|
const url = arg || '';
|
|
195
|
-
|
|
196
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "CLI: pobieranie YouTube (yt-dlp) i konwersja mediów (ffmpeg) — WavesConverter",
|
|
5
|
-
"main": "
|
|
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/
|
|
19
|
+
"url": "git+https://github.com/idunnowhytf/WavesConverter.git"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://github.com/idunnowhytf/
|
|
21
|
+
"homepage": "https://github.com/idunnowhytf/WavesConverter#readme",
|
|
22
22
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/idunnowhytf/
|
|
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
|
}
|