wavesconv 1.4.9

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.
Files changed (4) hide show
  1. package/README.md +179 -0
  2. package/cli.js +271 -0
  3. package/lib/engine.js +348 -0
  4. package/package.json +109 -0
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ <div align="center">
2
+
3
+ <img src="https://raw.githubusercontent.com/idunnowhytf/wavesconvsite/main/docs/og.png" alt="WavesConverter" width="100%"/>
4
+
5
+ <br/>
6
+
7
+ <h1>
8
+ <img src="https://img.shields.io/badge/WavesConverter-v1.2.0-7c3aed?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMiAxMyBRNCA3IDYgMTMgUTggMTkgMTAgMTMgUTEyIDcgMTQgMTMgUTE2IDE5IDE4IDEzIFEyMCA3IDIyIDEzIiBzdHJva2U9IiNjMDg0ZmMiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L3N2Zz4=" alt="WavesConverter v1.2.0"/>
9
+ </h1>
10
+
11
+ **Download anything. Convert everything.**
12
+
13
+ A premium desktop app for macOS & Windows that downloads YouTube videos and playlists,
14
+ converts any media file β€” all offline, no account needed, completely free.
15
+
16
+ <br/>
17
+
18
+ [![Download](https://img.shields.io/badge/⬇_Download-macOS_&_Windows-7c3aed?style=for-the-badge)](https://idunnowhytf.github.io/wavesconvsite/#download)
19
+ [![Website](https://img.shields.io/badge/🌐_Website-wavesconverter-a855f7?style=for-the-badge)](https://idunnowhytf.github.io/wavesconvsite/)
20
+ [![Releases](https://img.shields.io/github/v/release/idunnowhytf/wavesconvsite?style=for-the-badge&color=d946ef&label=Latest)](https://github.com/idunnowhytf/wavesconvsite/releases)
21
+ [![License](https://img.shields.io/badge/License-ISC-6d28d9?style=for-the-badge)](LICENSE)
22
+
23
+ <br/>
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## ✨ Features
30
+
31
+ | | Feature | Description |
32
+ |---|---|---|
33
+ | πŸ“‹ | **Playlist Support** | Download entire playlists or cherry-pick individual videos. Override format & quality per item. |
34
+ | ⚑ | **Concurrent Queue** | Run multiple downloads simultaneously. Pause, resume, retry β€” full control. |
35
+ | πŸ”’ | **Batch Paste** | Paste multiple YouTube URLs at once. Batch panel appears with shared format settings. |
36
+ | πŸ“Š | **ETA & Speed** | Live download speed (e.g. `3.2 MiB/s`) and time remaining (`ETA 00:42`) on every active item. |
37
+ | πŸŽ›οΈ | **Quality Control** | 360p β†’ 4K for video, 96 β†’ 320 kbps for audio. Apply globally or per video. |
38
+ | πŸ”„ | **Local File Converter** | Convert any media file β€” change container, resolution, bitrate. No internet needed. |
39
+ | πŸ“– | **Download History** | Every completed download logged with file path, format, and date. |
40
+ | ⌨️ | **Keyboard Shortcuts** | `⌘V` paste & fetch, `⌘D` queue, `⌘1–5` tabs, `Space` start/pause. |
41
+ | πŸ”” | **Native Notifications** | System notification when a download finishes β€” even if the app is in the background. |
42
+ | πŸ”’ | **100% Offline** | No servers, no accounts, no analytics. Powered by `yt-dlp` + `ffmpeg` bundled inside. |
43
+ | ✨ | **Auto Updates** | Silent background updates via GitHub Releases. One click to install. |
44
+
45
+ ---
46
+
47
+ ## πŸ“₯ Download
48
+
49
+ | Platform | Link |
50
+ |---|---|
51
+ | 🍎 **macOS Apple Silicon** (M1/M2/M3/M4) | [WavesConverter-arm64.dmg](https://github.com/idunnowhytf/wavesconvsite/releases/latest/download/WavesConverter-1.0.0-arm64.dmg) |
52
+ | 🍎 **macOS Intel** (x64) | [WavesConverter.dmg](https://github.com/idunnowhytf/wavesconvsite/releases/latest/download/WavesConverter-1.0.0.dmg) |
53
+ | πŸͺŸ **Windows 10+** (x64) | [WavesConverter-Setup.exe](https://github.com/idunnowhytf/wavesconvsite/releases/latest/download/WavesConverter.Setup.1.0.0.exe) |
54
+
55
+ > **Windows users:** SmartScreen may show a warning since the app isn't signed with a paid certificate.
56
+ > Click **"More info" β†’ "Run anyway"** to proceed. The source code is fully open and auditable here.
57
+
58
+ ---
59
+
60
+ ## πŸš€ Quick Start
61
+
62
+ ### Download a video
63
+ 1. Paste any YouTube URL into the input field (or press `⌘V` to auto-paste)
64
+ 2. Click **Fetch** β€” video metadata loads instantly
65
+ 3. Pick your format (`MP4`, `MP3`, `WAV`…) and quality (`1080p`, `4K`, `320kbps`…)
66
+ 4. Click **Add to Queue**, switch to the Queue tab, hit **Start**
67
+
68
+ ### Batch download
69
+ Paste multiple YouTube URLs (newline or space separated) β€” WavesConverter detects them automatically and switches to batch mode. Set format once, add all to queue.
70
+
71
+ ### Convert a local file
72
+ Drag any media file onto the **Convert** tab, pick the output format and settings, click **Convert**.
73
+
74
+ ---
75
+
76
+ ## ⌨️ Keyboard Shortcuts
77
+
78
+ | Shortcut | Action |
79
+ |---|---|
80
+ | `⌘V` | Paste URL & auto-fetch (from anywhere in the app) |
81
+ | `βŒ˜β†΅` | Fetch the current URL |
82
+ | `⌘D` | Add fetched video to queue |
83
+ | `⌘K` | Focus the URL input |
84
+ | `⌘1` – `⌘5` | Switch between tabs |
85
+ | `Space` | Start / pause queue (in Queue tab) |
86
+ | `βŒ˜β‡§C` | Clear completed items from queue |
87
+
88
+ ---
89
+
90
+ ## πŸ› οΈ Tech Stack
91
+
92
+ - **[Electron](https://www.electronjs.org/)** β€” cross-platform desktop shell
93
+ - **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** β€” YouTube downloading engine
94
+ - **[ffmpeg](https://ffmpeg.org/)** β€” media conversion & processing
95
+ - **[@ffmpeg-installer/ffmpeg](https://github.com/nicehash/easy-ffmpeg-installer)** β€” bundled ffmpeg binary
96
+ - **[electron-updater](https://www.electron.build/auto-update)** β€” automatic updates via GitHub Releases
97
+
98
+ ---
99
+
100
+ ## πŸ—οΈ Build from Source
101
+
102
+ ```bash
103
+ # Clone the repository
104
+ git clone https://github.com/idunnowhytf/wavesconvsite.git
105
+ cd wavesconvsite
106
+
107
+ # Install dependencies
108
+ npm install
109
+
110
+ # Run in development
111
+ npx electron .
112
+
113
+ # Build for macOS
114
+ npx electron-builder --mac --publish never
115
+
116
+ # Build for Windows (works from macOS via Wine)
117
+ npx electron-builder --win --x64 --publish never
118
+ ```
119
+
120
+ > **Requirements:** Node.js 18+, npm
121
+
122
+ ---
123
+
124
+ ## πŸ“ Project Structure
125
+
126
+ ```
127
+ wavesconvsite/
128
+ β”œβ”€β”€ main.js # Electron main process β€” IPC, yt-dlp spawning, ffmpeg
129
+ β”œβ”€β”€ preload.js # Context bridge β€” exposes safe APIs to renderer
130
+ β”œβ”€β”€ renderer.js # UI logic β€” queue, history, batch paste, ETA
131
+ β”œβ”€β”€ index.html # App shell β€” tabs, layout
132
+ β”œβ”€β”€ style.css # App styles β€” glassmorphism dark purple theme
133
+ β”œβ”€β”€ assets/
134
+ β”‚ β”œβ”€β”€ icon.icns # macOS app icon
135
+ β”‚ β”œβ”€β”€ icon.ico # Windows app icon
136
+ β”‚ └── icon.png # Generic icon
137
+ └── docs/ # GitHub Pages website
138
+ β”œβ”€β”€ index.html # Landing page
139
+ β”œβ”€β”€ changelog.html
140
+ └── docs.html
141
+ ```
142
+
143
+ ---
144
+
145
+ ## πŸ“‹ Changelog
146
+
147
+ See [**Releases β†’**](https://github.com/idunnowhytf/wavesconvsite/releases) for full version history.
148
+
149
+ | Version | Highlights |
150
+ |---|---|
151
+ | **v1.2.0** | Batch paste for multiple URLs, ETA & speed display on downloads |
152
+ | **v1.1.0** | Download history tab, keyboard shortcuts, native notifications, drag & drop, Windows support |
153
+ | **v1.0.0** | Initial release β€” macOS only |
154
+
155
+ ---
156
+
157
+ ## 🀝 Contributing
158
+
159
+ Found a bug or have a feature idea? [Open an issue](https://github.com/idunnowhytf/wavesconvsite/issues) β€” all feedback welcome.
160
+
161
+ ---
162
+
163
+ ## βš–οΈ Legal
164
+
165
+ WavesConverter uses `yt-dlp` and `ffmpeg` under their respective open-source licenses.
166
+ Downloading copyrighted content without permission may violate YouTube's Terms of Service and local laws.
167
+ This tool is intended for downloading content you own or have permission to download.
168
+
169
+ ---
170
+
171
+ <div align="center">
172
+
173
+ **[Website](https://idunnowhytf.github.io/wavesconvsite/) Β· [Releases](https://github.com/idunnowhytf/wavesconvsite/releases) Β· [Docs](https://idunnowhytf.github.io/wavesconvsite/docs.html) Β· [Changelog](https://idunnowhytf.github.io/wavesconvsite/changelog.html)**
174
+
175
+ <br/>
176
+
177
+ Made with ❀️ using Electron, yt-dlp & ffmpeg · Free & Open Source
178
+
179
+ </div>
package/cli.js ADDED
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const engine = require('./lib/engine');
7
+
8
+ const VERSION = require('./package.json').version;
9
+
10
+ const HELP = `
11
+ WavesConverter CLI v${VERSION}
12
+
13
+ UΕΌycie:
14
+ wavesconv download <url> [opcje] Pobierz wideo/audio z YouTube
15
+ wavesconv info <url> PokaΕΌ metadane (JSON)
16
+ wavesconv convert <plik> [opcje] Konwertuj plik lokalny
17
+ wavesconv tools install Zainstaluj yt-dlp (do folderu uΕΌytkownika)
18
+ wavesconv tools status SprawdΕΊ yt-dlp i ffmpeg
19
+ wavesconv --help Ta pomoc
20
+ wavesconv --version Wersja
21
+
22
+ Opcje download:
23
+ -o, --output <dir> Folder docelowy (domyΕ›lnie: Downloads)
24
+ -f, --format <fmt> mp4, mkv, webm, mp3, wav, flac… (domyΕ›lnie: mp4)
25
+ -q, --quality <q> best, 1080p, 720p, 480p, 360p
26
+ -a, --audio Tylko audio
27
+ --bitrate <rate> np. 320k
28
+ --filename <szablon> np. "%(title)s" lub "%(uploader)s - %(title)s"
29
+
30
+ Opcje convert:
31
+ -o, --output <Ε›cieΕΌka> Plik lub folder wyjΕ›ciowy
32
+ -f, --format <fmt> mp4, mp3, wav, gif…
33
+ -q, --quality <q> RozdzielczoΕ›Δ‡ wideo (1080p, 720p…)
34
+ --bitrate <rate> Bitrate audio
35
+
36
+ PrzykΕ‚ady:
37
+ wavesconv download "https://youtu.be/xxx" -a -f mp3
38
+ wavesconv download "https://youtube.com/watch?v=xxx" -q 1080p -o ~/Videos
39
+ wavesconv convert film.mp4 -f mp3 -o ~/Music/out.mp3
40
+ wavesconv info "https://youtu.be/xxx"
41
+
42
+ Deep link (aplikacja graficzna):
43
+ wavesconverter://download?url=<encoded_url>
44
+ wavesconverter://queue?url=<encoded_url>&start=1
45
+ `;
46
+
47
+ function parseArgs(argv) {
48
+ const args = [...argv];
49
+ const positional = [];
50
+ const flags = {};
51
+
52
+ while (args.length) {
53
+ const a = args[0];
54
+ if (a === '--') {
55
+ args.shift();
56
+ positional.push(...args);
57
+ break;
58
+ }
59
+ if (a.startsWith('--')) {
60
+ const key = a.slice(2);
61
+ args.shift();
62
+ if (['help', 'version', 'audio'].includes(key)) {
63
+ flags[key] = true;
64
+ } else if (args.length) {
65
+ flags[key] = args.shift();
66
+ } else {
67
+ flags[key] = true;
68
+ }
69
+ } else if (a.startsWith('-') && a.length === 2) {
70
+ const key = a.slice(1);
71
+ args.shift();
72
+ const map = { o: 'output', f: 'format', q: 'quality', a: 'audio' };
73
+ const flagKey = map[key] || key;
74
+ if (key === 'a') {
75
+ flags.audio = true;
76
+ } else if (args.length) {
77
+ flags[flagKey] = args.shift();
78
+ }
79
+ } else {
80
+ positional.push(args.shift());
81
+ }
82
+ }
83
+ return { positional, flags };
84
+ }
85
+
86
+ function log(msg) {
87
+ process.stderr.write(msg + '\n');
88
+ }
89
+
90
+ function formatBytes(n) {
91
+ if (n < 1024) return n + ' B';
92
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
93
+ return (n / (1024 * 1024)).toFixed(2) + ' MB';
94
+ }
95
+
96
+ async function cmdToolsInstall() {
97
+ await engine.ensureYtDlp(msg => log(msg));
98
+ log('yt-dlp: ' + (engine.findYtDlp() || 'bΕ‚Δ…d'));
99
+ log('ffmpeg: ' + (engine.findFfmpeg() || 'brak β€” zainstaluj systemowo lub z poziomu aplikacji'));
100
+ }
101
+
102
+ function cmdToolsStatus() {
103
+ log('yt-dlp: ' + (engine.findYtDlp() || 'BRAK'));
104
+ log('ffmpeg: ' + (engine.findFfmpeg() || 'BRAK'));
105
+ log('userData: ' + engine.getUserDataDir());
106
+ }
107
+
108
+ async function cmdInfo(url) {
109
+ const items = await engine.fetchInfo(url);
110
+ console.log(JSON.stringify(items, null, 2));
111
+ }
112
+
113
+ async function cmdDownload(url, flags) {
114
+ if (!engine.isYouTubeUrl(url)) {
115
+ throw new Error('NieprawidΕ‚owy URL YouTube: ' + url);
116
+ }
117
+ await engine.ensureYtDlp(msg => log(msg));
118
+
119
+ const outputDir = path.resolve(flags.output || engine.getDefaultDownloadDir());
120
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
121
+
122
+ const audioOnly = !!flags.audio || ['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'].includes((flags.format || '').toLowerCase());
123
+ const format = flags.format || (audioOnly ? 'mp3' : 'mp4');
124
+
125
+ let title = 'download';
126
+ try {
127
+ const items = await engine.fetchInfo(url);
128
+ if (items[0]) title = items[0].title || items[0].id || title;
129
+ } catch (_) {}
130
+
131
+ const job = {
132
+ id: 'cli-' + Date.now(),
133
+ title,
134
+ url,
135
+ audioOnly,
136
+ outputFormat: format,
137
+ quality: flags.quality || 'best',
138
+ bitrate: flags.bitrate || '',
139
+ outputDir,
140
+ filename: flags.filename || '%(title)s',
141
+ };
142
+
143
+ log(`Pobieranie: ${title}`);
144
+ log(`Folder: ${outputDir}`);
145
+ log(`Format: ${format}${audioOnly ? ' (audio)' : ''} | JakoΕ›Δ‡: ${job.quality}`);
146
+
147
+ let lastPct = -1;
148
+ const result = await engine.runDownload(job, {
149
+ onProgress: (pct, line) => {
150
+ const rounded = Math.floor(pct);
151
+ if (rounded !== lastPct && rounded % 5 === 0) {
152
+ lastPct = rounded;
153
+ process.stdout.write(`\r[${rounded}%] ${line.slice(0, 60).padEnd(60)}`);
154
+ }
155
+ },
156
+ onLog: line => {
157
+ if (line.includes('ERROR') || line.includes('error')) log(line);
158
+ },
159
+ });
160
+
161
+ process.stdout.write('\n');
162
+ if (result.path) {
163
+ log(`Gotowe: ${result.path} (${formatBytes(result.size)})`);
164
+ } else {
165
+ log('Pobrano (Ε›cieΕΌka nieznana β€” sprawdΕΊ folder docelowy)');
166
+ }
167
+ }
168
+
169
+ async function cmdConvert(inputPath, flags) {
170
+ const ffmpeg = engine.findFfmpeg();
171
+ if (!ffmpeg) throw new Error('ffmpeg nie znaleziony');
172
+
173
+ const resolvedIn = path.resolve(inputPath);
174
+ if (!fs.existsSync(resolvedIn)) throw new Error('Plik nie istnieje: ' + resolvedIn);
175
+
176
+ const format = (flags.format || 'mp4').toLowerCase();
177
+ let outputPath = flags.output ? path.resolve(flags.output) : null;
178
+
179
+ if (!outputPath) {
180
+ const base = path.basename(resolvedIn, path.extname(resolvedIn));
181
+ outputPath = path.join(path.dirname(resolvedIn), base + '.' + format);
182
+ } else if (fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()) {
183
+ const base = path.basename(resolvedIn, path.extname(resolvedIn));
184
+ outputPath = path.join(outputPath, base + '.' + format);
185
+ }
186
+
187
+ const job = {
188
+ id: 'cli-convert-' + Date.now(),
189
+ inputPath: resolvedIn,
190
+ outputPath,
191
+ bitrate: flags.bitrate || '',
192
+ videoBitrate: flags['video-bitrate'] || '',
193
+ resolution: flags.quality || 'original',
194
+ gifStart: flags['gif-start'] || '00:00:00',
195
+ gifDuration: flags['gif-duration'] || 5,
196
+ };
197
+
198
+ log(`Konwersja β†’ ${outputPath}`);
199
+ await engine.runConvert(job, {
200
+ onProgress: t => process.stdout.write(`\rCzas: ${t} `),
201
+ });
202
+ process.stdout.write('\n');
203
+ log('Gotowe: ' + outputPath);
204
+ }
205
+
206
+ async function run(argv) {
207
+ const { positional, flags } = parseArgs(argv);
208
+
209
+ if (flags.help || positional[0] === 'help') {
210
+ console.log(HELP);
211
+ return 0;
212
+ }
213
+ if (flags.version) {
214
+ console.log(VERSION);
215
+ return 0;
216
+ }
217
+
218
+ const cmd = positional[0];
219
+ if (!cmd) {
220
+ console.log(HELP);
221
+ return 1;
222
+ }
223
+
224
+ try {
225
+ switch (cmd) {
226
+ case 'download': {
227
+ const url = positional[1];
228
+ if (!url) throw new Error('Podaj URL: wavesconv download <url>');
229
+ await cmdDownload(url, flags);
230
+ return 0;
231
+ }
232
+ case 'info': {
233
+ const url = positional[1];
234
+ if (!url) throw new Error('Podaj URL: wavesconv info <url>');
235
+ await cmdInfo(url);
236
+ return 0;
237
+ }
238
+ case 'convert': {
239
+ const file = positional[1];
240
+ if (!file) throw new Error('Podaj plik: wavesconv convert <plik>');
241
+ await cmdConvert(file, flags);
242
+ return 0;
243
+ }
244
+ case 'tools': {
245
+ const sub = positional[1];
246
+ if (sub === 'install') await cmdToolsInstall();
247
+ else if (sub === 'status') cmdToolsStatus();
248
+ else throw new Error('UΕΌyj: wavesconv tools install|status');
249
+ return 0;
250
+ }
251
+ default:
252
+ throw new Error('Nieznane polecenie: ' + cmd);
253
+ }
254
+ } catch (e) {
255
+ log('BΕ‚Δ…d: ' + (e.message || e));
256
+ return 1;
257
+ }
258
+ }
259
+
260
+ function shouldRunAsCli(argv) {
261
+ const args = argv || process.argv.slice(1);
262
+ if (args.includes('--cli')) return true;
263
+ const first = args.find(a => !a.startsWith('-') && a !== process.execPath);
264
+ return ['download', 'info', 'convert', 'tools', 'help'].includes(first);
265
+ }
266
+
267
+ if (require.main === module) {
268
+ run(process.argv.slice(2)).then(code => process.exit(code));
269
+ }
270
+
271
+ module.exports = { run, shouldRunAsCli, HELP };
package/lib/engine.js ADDED
@@ -0,0 +1,348 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const https = require('https');
5
+ const { spawn, exec } = require('child_process');
6
+
7
+ const APP_FOLDER = 'waves-converter';
8
+
9
+ function getUserDataDir() {
10
+ if (process.env.WAVESCONVERTER_USER_DATA) return process.env.WAVESCONVERTER_USER_DATA;
11
+ if (process.platform === 'win32') {
12
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), APP_FOLDER);
13
+ }
14
+ if (process.platform === 'darwin') {
15
+ return path.join(os.homedir(), 'Library', 'Application Support', APP_FOLDER);
16
+ }
17
+ return path.join(os.homedir(), '.config', APP_FOLDER);
18
+ }
19
+
20
+ function getBinDir() {
21
+ return path.join(getUserDataDir(), 'bin');
22
+ }
23
+
24
+ function findYtDlp() {
25
+ const isWin = process.platform === 'win32';
26
+ const name = isWin ? 'yt-dlp.exe' : 'yt-dlp';
27
+ const userBin = path.join(getBinDir(), name);
28
+ const root = path.join(__dirname, '..');
29
+ const candidates = [
30
+ userBin,
31
+ path.join(root, 'yt-dlp-bin', name),
32
+ path.join(root, 'node_modules', 'yt-dlp-wrap', 'bin', name),
33
+ isWin ? 'C:\\ProgramData\\chocolatey\\bin\\yt-dlp.exe' : null,
34
+ isWin ? 'C:\\scoop\\shims\\yt-dlp.exe' : null,
35
+ '/opt/homebrew/bin/yt-dlp',
36
+ '/usr/local/bin/yt-dlp',
37
+ '/usr/bin/yt-dlp',
38
+ ].filter(Boolean);
39
+ return candidates.find(c => fs.existsSync(c)) || null;
40
+ }
41
+
42
+ function findFfmpeg() {
43
+ const isWin = process.platform === 'win32';
44
+ const name = isWin ? 'ffmpeg.exe' : 'ffmpeg';
45
+ const userBin = path.join(getBinDir(), name);
46
+ if (fs.existsSync(userBin)) return userBin;
47
+
48
+ try {
49
+ const i = require('@ffmpeg-installer/ffmpeg');
50
+ let p = i.path;
51
+ if (process.env.ELECTRON_RUN_AS_NODE || (process.mainModule && process.mainModule.filename.includes('app.asar'))) {
52
+ p = p.replace('app.asar', 'app.asar.unpacked');
53
+ }
54
+ if (fs.existsSync(p)) return p;
55
+ } catch (_) {}
56
+
57
+ const candidates = [
58
+ isWin ? 'C:\\ProgramData\\chocolatey\\bin\\ffmpeg.exe' : null,
59
+ isWin ? 'C:\\scoop\\shims\\ffmpeg.exe' : null,
60
+ '/opt/homebrew/bin/ffmpeg',
61
+ '/usr/local/bin/ffmpeg',
62
+ '/usr/bin/ffmpeg',
63
+ ].filter(Boolean);
64
+ return candidates.find(c => fs.existsSync(c)) || null;
65
+ }
66
+
67
+ async function ensureYtDlp(onStatus) {
68
+ let bin = findYtDlp();
69
+ if (bin) return bin;
70
+ const binDir = getBinDir();
71
+ if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true });
72
+ const isWin = process.platform === 'win32';
73
+ const binFile = path.join(binDir, isWin ? 'yt-dlp.exe' : 'yt-dlp');
74
+ if (onStatus) onStatus('Pobieranie yt-dlp…');
75
+ const YTDlpWrap = require('yt-dlp-wrap').default || require('yt-dlp-wrap');
76
+ await YTDlpWrap.downloadFromGithub(binFile);
77
+ if (!isWin) fs.chmodSync(binFile, 0o755);
78
+ return binFile;
79
+ }
80
+
81
+ function fetchInfo(url) {
82
+ const activeYtDlp = findYtDlp();
83
+ if (!activeYtDlp) {
84
+ return Promise.reject(new Error('yt-dlp nie znaleziony. Uruchom: wavesconv tools install'));
85
+ }
86
+ return new Promise((resolve, reject) => {
87
+ const args = ['--dump-json', '--flat-playlist', '--no-warnings', url];
88
+ let out = '';
89
+ let err = '';
90
+ const proc = spawn(activeYtDlp, args);
91
+ proc.stdout.on('data', d => { out += d; });
92
+ proc.stderr.on('data', d => { err += d; });
93
+ proc.on('close', () => {
94
+ if (!out.trim()) return reject(new Error(err.slice(0, 300) || 'Brak danych z yt-dlp'));
95
+ try {
96
+ resolve(out.trim().split('\n').filter(Boolean).map(l => JSON.parse(l)));
97
+ } catch {
98
+ reject(new Error('Nie udało się odczytać metadanych'));
99
+ }
100
+ });
101
+ proc.on('error', e => reject(e));
102
+ });
103
+ }
104
+
105
+ function buildDownloadArgs(job) {
106
+ const ffmpeg = findFfmpeg();
107
+ const { url, outputFormat, quality, bitrate, outputDir, filename, audioOnly } = job;
108
+ const safeName = (filename || '%(title)s').replace(/[<>:"/\\|?*]/g, '_');
109
+ const outTpl = path.join(outputDir, safeName + '.%(ext)s');
110
+ const args = ['--no-warnings', '--newline'];
111
+ if (ffmpeg) args.push('--ffmpeg-location', path.dirname(ffmpeg));
112
+ if (audioOnly) {
113
+ args.push('-x', '--audio-format', outputFormat || 'mp3');
114
+ if (bitrate) args.push('--audio-quality', bitrate.replace('k', '') + 'K');
115
+ } else {
116
+ const h = quality && quality !== 'best' ? quality.replace('p', '') : null;
117
+ args.push('-f', h ? `bestvideo[height<=${h}]+bestaudio/best[height<=${h}]/best` : 'bestvideo+bestaudio/best');
118
+ args.push('--merge-output-format', outputFormat || 'mp4');
119
+ if (!outputFormat || outputFormat === 'mp4') {
120
+ args.push('--postprocessor-args', 'Merger:-c:a aac');
121
+ }
122
+ if (bitrate) args.push('--postprocessor-args', `ffmpeg:-b:v ${bitrate}`);
123
+ }
124
+ args.push('-o', outTpl, url);
125
+ return args;
126
+ }
127
+
128
+ function resolveDownloadedPath(job, outputDir, detectedPath) {
129
+ let finalPath = '';
130
+ try {
131
+ if (detectedPath && fs.existsSync(detectedPath)) {
132
+ finalPath = detectedPath;
133
+ } else if (detectedPath && fs.existsSync(path.resolve(outputDir, detectedPath))) {
134
+ finalPath = path.resolve(outputDir, detectedPath);
135
+ } else {
136
+ const cleanTitle = (job.title || '').replace(/[<>:"/\\|?*]/g, '_');
137
+ const files = fs.readdirSync(outputDir);
138
+ const match = files.find(f => f.toLowerCase().includes(cleanTitle.toLowerCase()));
139
+ if (match) {
140
+ finalPath = path.join(outputDir, match);
141
+ } else {
142
+ const now = Date.now();
143
+ let bestFile = null;
144
+ let bestMtime = 0;
145
+ for (const f of files) {
146
+ const fp = path.join(outputDir, f);
147
+ try {
148
+ const stat = fs.statSync(fp);
149
+ if (stat.isFile() && (now - stat.mtimeMs < 15000) && stat.mtimeMs > bestMtime) {
150
+ bestMtime = stat.mtimeMs;
151
+ bestFile = fp;
152
+ }
153
+ } catch (_) {}
154
+ }
155
+ if (bestFile) finalPath = bestFile;
156
+ }
157
+ }
158
+ } catch (_) {}
159
+ return finalPath;
160
+ }
161
+
162
+ function runDownload(job, callbacks = {}) {
163
+ const activeYtDlp = findYtDlp();
164
+ if (!activeYtDlp) {
165
+ return Promise.reject(new Error('yt-dlp nie znaleziony. Uruchom: wavesconv tools install'));
166
+ }
167
+ const { onProgress, onLog } = callbacks;
168
+ const args = buildDownloadArgs(job);
169
+ let detectedPath = '';
170
+
171
+ return new Promise((resolve, reject) => {
172
+ const proc = spawn(activeYtDlp, args);
173
+ if (callbacks.onSpawn) callbacks.onSpawn(proc);
174
+
175
+ proc.stdout.on('data', d => {
176
+ const line = d.toString().trim();
177
+ const mDest = line.match(/Destination:\s+(.+)/i) ||
178
+ line.match(/Merging formats into\s+"([^"]+)"/i) ||
179
+ line.match(/Merging formats into\s+(.+)/i) ||
180
+ line.match(/\[download\]\s+(.+?)\s+has already been downloaded/i);
181
+ if (mDest) detectedPath = mDest[1].trim();
182
+ const m = line.match(/(\d+\.?\d*)%/);
183
+ if (m && onProgress) onProgress(parseFloat(m[1]), line);
184
+ if (onLog) onLog(line);
185
+ });
186
+
187
+ proc.stderr.on('data', d => {
188
+ if (onLog) onLog(d.toString().trim());
189
+ });
190
+
191
+ proc.on('close', code => {
192
+ if (code === 0) {
193
+ const finalPath = resolveDownloadedPath(job, job.outputDir, detectedPath);
194
+ let fileSize = 0;
195
+ if (finalPath && fs.existsSync(finalPath)) {
196
+ fileSize = fs.statSync(finalPath).size;
197
+ }
198
+ resolve({ success: true, size: fileSize, path: finalPath });
199
+ } else {
200
+ reject(new Error(`yt-dlp zakoΕ„czyΕ‚ siΔ™ kodem ${code}`));
201
+ }
202
+ });
203
+ proc.on('error', reject);
204
+ });
205
+ }
206
+
207
+ function runConvert(job, callbacks = {}) {
208
+ const ffmpeg = findFfmpeg();
209
+ if (!ffmpeg) {
210
+ return Promise.reject(new Error('ffmpeg nie znaleziony. Uruchom: wavesconv tools install'));
211
+ }
212
+ const { inputPath, outputPath, bitrate, videoBitrate, resolution, gifStart, gifDuration } = job;
213
+ const ext = outputPath.split('.').pop().toLowerCase();
214
+ const isGif = ext === 'gif';
215
+ const args = ['-y'];
216
+ if (isGif) {
217
+ if (gifStart) args.push('-ss', gifStart);
218
+ if (gifDuration) args.push('-t', String(gifDuration));
219
+ }
220
+ args.push('-i', inputPath);
221
+
222
+ if (isGif) {
223
+ args.push('-vf', 'fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse');
224
+ } else {
225
+ if (resolution && resolution !== 'original') {
226
+ args.push('-vf', `scale=-2:${resolution.replace('p', '')}`);
227
+ }
228
+ const isVideo = ['mp4', 'mkv', 'mov', 'avi', 'webm'].includes(ext);
229
+ if (isVideo) {
230
+ if (ext === 'webm') {
231
+ args.push('-c:v', 'libvpx-vp9');
232
+ if (videoBitrate) args.push('-b:v', videoBitrate);
233
+ else args.push('-crf', '28', '-b:v', '0');
234
+ args.push('-c:a', 'libopus');
235
+ if (bitrate) args.push('-b:a', bitrate);
236
+ else args.push('-b:a', '128k');
237
+ } else {
238
+ args.push('-c:v', 'libx264', '-preset', 'fast');
239
+ if (videoBitrate) args.push('-b:v', videoBitrate);
240
+ else args.push('-crf', '18');
241
+ args.push('-c:a', 'aac');
242
+ if (bitrate) args.push('-b:a', bitrate);
243
+ else args.push('-b:a', '192k');
244
+ }
245
+ } else {
246
+ if (ext === 'mp3') {
247
+ args.push('-c:a', 'libmp3lame');
248
+ if (bitrate) args.push('-b:a', bitrate);
249
+ else args.push('-b:a', '256k');
250
+ } else if (ext === 'wav') {
251
+ args.push('-c:a', 'pcm_s16le');
252
+ } else if (ext === 'flac') {
253
+ args.push('-c:a', 'flac');
254
+ } else if (ext === 'aac' || ext === 'm4a') {
255
+ args.push('-c:a', 'aac');
256
+ if (bitrate) args.push('-b:a', bitrate);
257
+ else args.push('-b:a', '256k');
258
+ } else if (ext === 'ogg') {
259
+ args.push('-c:a', 'libvorbis');
260
+ if (bitrate) args.push('-b:a', bitrate);
261
+ else args.push('-b:a', '192k');
262
+ }
263
+ }
264
+ }
265
+ args.push(outputPath);
266
+
267
+ return new Promise((resolve, reject) => {
268
+ const proc = spawn(ffmpeg, args);
269
+ if (callbacks.onSpawn) callbacks.onSpawn(proc);
270
+ let errOut = '';
271
+ proc.stderr.on('data', d => {
272
+ const line = d.toString();
273
+ errOut += line;
274
+ const t = line.match(/time=(\d+:\d+:\d+)/);
275
+ if (t && callbacks.onProgress) callbacks.onProgress(t[1]);
276
+ });
277
+ proc.on('close', code => {
278
+ code === 0 ? resolve({ success: true }) : reject(new Error(errOut.slice(-300) || `ffmpeg kod ${code}`));
279
+ });
280
+ proc.on('error', reject);
281
+ });
282
+ }
283
+
284
+ function isYouTubeUrl(str) {
285
+ return /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+/i.test((str || '').trim());
286
+ }
287
+
288
+ function parseDeepLink(raw) {
289
+ if (!raw || typeof raw !== 'string') return null;
290
+ const trimmed = raw.trim();
291
+ if (!trimmed.toLowerCase().startsWith('wavesconverter://')) return null;
292
+
293
+ try {
294
+ const u = new URL(trimmed);
295
+ const action = (u.hostname || 'open').toLowerCase();
296
+ const params = u.searchParams;
297
+
298
+ let targetUrl = params.get('url') || params.get('link');
299
+ if (!targetUrl) {
300
+ let pathPart = u.pathname.replace(/^\//, '');
301
+ if (pathPart) {
302
+ try { pathPart = decodeURIComponent(pathPart); } catch (_) {}
303
+ if (/^https?:\/\//i.test(pathPart)) targetUrl = pathPart;
304
+ }
305
+ }
306
+ if (targetUrl) {
307
+ try { targetUrl = decodeURIComponent(targetUrl); } catch (_) {}
308
+ }
309
+
310
+ return {
311
+ action,
312
+ url: targetUrl || null,
313
+ file: params.get('file') || null,
314
+ autoQueue: action === 'queue' || params.get('queue') === '1',
315
+ autoStart: params.get('start') === '1',
316
+ format: params.get('format') || null,
317
+ quality: params.get('quality') || params.get('q') || null,
318
+ audioOnly: params.get('audio') === '1' || params.get('type') === 'audio',
319
+ bitrate: params.get('bitrate') || null,
320
+ };
321
+ } catch {
322
+ return null;
323
+ }
324
+ }
325
+
326
+ function findDeepLinkInArgv(argv) {
327
+ return (argv || process.argv).find(a => typeof a === 'string' && a.toLowerCase().startsWith('wavesconverter://')) || null;
328
+ }
329
+
330
+ function getDefaultDownloadDir() {
331
+ return path.join(os.homedir(), 'Downloads');
332
+ }
333
+
334
+ module.exports = {
335
+ getUserDataDir,
336
+ getBinDir,
337
+ getDefaultDownloadDir,
338
+ findYtDlp,
339
+ findFfmpeg,
340
+ ensureYtDlp,
341
+ fetchInfo,
342
+ buildDownloadArgs,
343
+ runDownload,
344
+ runConvert,
345
+ isYouTubeUrl,
346
+ parseDeepLink,
347
+ findDeepLinkInArgv,
348
+ };
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "wavesconv",
3
+ "version": "1.4.9",
4
+ "description": "CLI: pobieranie YouTube (yt-dlp) i konwersja mediΓ³w (ffmpeg) β€” WavesConverter",
5
+ "main": "cli.js",
6
+ "bin": {
7
+ "wavesconv": "./cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js",
11
+ "lib",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/idunnowhytf/wavesconvsite.git"
20
+ },
21
+ "homepage": "https://github.com/idunnowhytf/wavesconvsite#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/idunnowhytf/wavesconvsite/issues"
24
+ },
25
+ "keywords": [
26
+ "youtube",
27
+ "yt-dlp",
28
+ "ffmpeg",
29
+ "downloader",
30
+ "converter",
31
+ "cli",
32
+ "wavesconverter"
33
+ ],
34
+ "author": "WavesConverter",
35
+ "scripts": {
36
+ "start": "electron .",
37
+ "cli": "node cli.js",
38
+ "prepublishOnly": "node cli.js --version",
39
+ "build": "electron-builder",
40
+ "build:mac": "electron-builder --mac",
41
+ "publish:app": "electron-builder --publish always",
42
+ "publish:npm": "npm publish --access public"
43
+ },
44
+ "build": {
45
+ "appId": "com.wavesconverter.app",
46
+ "productName": "WavesConverter",
47
+ "directories": {
48
+ "output": "dist"
49
+ },
50
+ "files": [
51
+ "main.js",
52
+ "cli.js",
53
+ "lib/**",
54
+ "preload.js",
55
+ "index.html",
56
+ "style.css",
57
+ "renderer.js",
58
+ "assets/**"
59
+ ],
60
+ "protocols": [
61
+ {
62
+ "name": "WavesConverter",
63
+ "schemes": ["wavesconverter"]
64
+ }
65
+ ],
66
+ "mac": {
67
+ "category": "public.app-category.video",
68
+ "icon": "assets/icon.icns",
69
+ "target": [
70
+ {
71
+ "target": "dmg",
72
+ "arch": [
73
+ "arm64",
74
+ "x64"
75
+ ]
76
+ }
77
+ ]
78
+ },
79
+ "win": {
80
+ "icon": "assets/icon.ico",
81
+ "artifactName": "${productName}-Setup-${version}.${ext}",
82
+ "target": [
83
+ {
84
+ "target": "nsis",
85
+ "arch": [
86
+ "x64"
87
+ ]
88
+ }
89
+ ]
90
+ },
91
+ "publish": {
92
+ "provider": "github",
93
+ "owner": "idunnowhytf",
94
+ "repo": "WavesConverter",
95
+ "releaseType": "release"
96
+ }
97
+ },
98
+ "license": "ISC",
99
+ "dependencies": {
100
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
101
+ "yt-dlp-wrap": "^2.3.12"
102
+ },
103
+ "devDependencies": {
104
+ "electron": "^42.3.0",
105
+ "electron-builder": "^26.8.1",
106
+ "electron-updater": "^6.8.3",
107
+ "qrcode": "^1.5.4"
108
+ }
109
+ }