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.
- package/README.md +179 -0
- package/cli.js +271 -0
- package/lib/engine.js +348 -0
- 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
|
+
[](https://idunnowhytf.github.io/wavesconvsite/#download)
|
|
19
|
+
[](https://idunnowhytf.github.io/wavesconvsite/)
|
|
20
|
+
[](https://github.com/idunnowhytf/wavesconvsite/releases)
|
|
21
|
+
[](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
|
+
}
|