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 +11 -20
- package/cli.js +17 -7
- package/lib/cli-install.js +75 -0
- package/lib/cli-interactive.js +6 -15
- package/lib/engine.js +81 -4
- package/lib/urls.js +133 -0
- package/main.js +647 -0
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="https://
|
|
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
|
-
|
|
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
|
-
[](https://idunnowhytf.github.io/
|
|
19
|
-
[](https://idunnowhytf.github.io/
|
|
20
|
-
[](https://idunnowhytf.github.io/WavesConverter/#download)
|
|
17
|
+
[](https://idunnowhytf.github.io/WavesConverter/)
|
|
18
|
+
[](https://github.com/idunnowhytf/WavesConverter/releases)
|
|
21
19
|
[](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/
|
|
52
|
-
| 🍎 **macOS Intel** (x64) | [WavesConverter.dmg](https://github.com/idunnowhytf/
|
|
53
|
-
| 🪟 **Windows 10+** (x64) | [WavesConverter-Setup.exe](https://github.com/idunnowhytf/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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();
|
|
@@ -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
|
-
|
|
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 };
|
package/lib/cli-interactive.js
CHANGED
|
@@ -29,17 +29,7 @@ function sessionToFlags(session) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function extractYouTubeUrls(text) {
|
|
32
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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(
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
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.
|
|
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
|
}
|