wavesconv 1.5.0 → 1.7.0
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 +23 -7
- package/lib/cli-interactive.js +278 -0
- package/lib/cli-ui.js +1 -0
- package/lib/engine.js +39 -4
- package/lib/urls.js +67 -0
- package/package.json +1 -1
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
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const engine = require('./lib/engine');
|
|
7
7
|
const ui = require('./lib/cli-ui');
|
|
8
|
+
const { runInteractive } = require('./lib/cli-interactive');
|
|
8
9
|
|
|
9
10
|
const VERSION = require('./package.json').version;
|
|
10
11
|
|
|
@@ -71,7 +72,7 @@ async function cmdInfo(url, flags, color) {
|
|
|
71
72
|
spin.start();
|
|
72
73
|
let items;
|
|
73
74
|
try {
|
|
74
|
-
items = await engine.fetchInfo(url);
|
|
75
|
+
items = await engine.fetchInfo(url, { cookiesPath: process.env.WAVESCONVERTER_COOKIES || '' });
|
|
75
76
|
spin.succeed(`${items.length} element(ów)`);
|
|
76
77
|
} catch (e) {
|
|
77
78
|
spin.fail(e.message);
|
|
@@ -100,8 +101,8 @@ async function cmdInfo(url, flags, color) {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
async function cmdDownload(url, flags, color) {
|
|
103
|
-
if (!engine.
|
|
104
|
-
throw new Error('
|
|
104
|
+
if (!engine.isSupportedMediaUrl(url)) {
|
|
105
|
+
throw new Error('Nieobsługiwany URL. Użyj linku YouTube lub Instagram (post / Reel / Stories).');
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
const setupSpin = new ui.Spinner('Przygotowanie yt-dlp', color);
|
|
@@ -124,7 +125,7 @@ async function cmdDownload(url, flags, color) {
|
|
|
124
125
|
const metaSpin = new ui.Spinner('Wczytywanie metadanych', color);
|
|
125
126
|
metaSpin.start();
|
|
126
127
|
try {
|
|
127
|
-
const items = await engine.fetchInfo(url);
|
|
128
|
+
const items = await engine.fetchInfo(url, { cookiesPath: process.env.WAVESCONVERTER_COOKIES || '' });
|
|
128
129
|
if (items[0]) title = items[0].title || items[0].id || title;
|
|
129
130
|
metaSpin.succeed(title.slice(0, 52) + (title.length > 52 ? '…' : ''));
|
|
130
131
|
} catch (_) {
|
|
@@ -135,6 +136,8 @@ async function cmdDownload(url, flags, color) {
|
|
|
135
136
|
id: 'cli-' + Date.now(),
|
|
136
137
|
title,
|
|
137
138
|
url,
|
|
139
|
+
platform: engine.getMediaPlatform(url),
|
|
140
|
+
cookiesPath: process.env.WAVESCONVERTER_COOKIES || '',
|
|
138
141
|
audioOnly,
|
|
139
142
|
outputFormat: format,
|
|
140
143
|
quality: flags.quality || 'best',
|
|
@@ -228,9 +231,22 @@ async function run(argv) {
|
|
|
228
231
|
}
|
|
229
232
|
|
|
230
233
|
const cmd = positional[0];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
|
|
235
|
+
// wavesconv — tryb interaktywny (wklej linki w konsoli)
|
|
236
|
+
if (!cmd || cmd === 'interactive' || cmd === 'i') {
|
|
237
|
+
if (!process.stdin.isTTY) {
|
|
238
|
+
ui.error(color, 'Tryb interaktywny wymaga terminala. Użyj: wavesconv download <url>');
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
return runInteractive(
|
|
242
|
+
{
|
|
243
|
+
download: cmdDownload,
|
|
244
|
+
info: cmdInfo,
|
|
245
|
+
toolsInstall: cmdToolsInstall,
|
|
246
|
+
},
|
|
247
|
+
VERSION,
|
|
248
|
+
color
|
|
249
|
+
);
|
|
234
250
|
}
|
|
235
251
|
|
|
236
252
|
if (color && cmd !== 'tools') {
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const engine = require('./engine');
|
|
7
|
+
const ui = require('./cli-ui');
|
|
8
|
+
|
|
9
|
+
function defaultSession() {
|
|
10
|
+
return {
|
|
11
|
+
output: engine.getDefaultDownloadDir(),
|
|
12
|
+
format: 'mp4',
|
|
13
|
+
audioOnly: false,
|
|
14
|
+
quality: 'best',
|
|
15
|
+
bitrate: '',
|
|
16
|
+
filename: '%(title)s',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sessionToFlags(session) {
|
|
21
|
+
return {
|
|
22
|
+
output: session.output,
|
|
23
|
+
format: session.format,
|
|
24
|
+
audio: session.audioOnly,
|
|
25
|
+
quality: session.quality,
|
|
26
|
+
bitrate: session.bitrate,
|
|
27
|
+
filename: session.filename,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractYouTubeUrls(text) {
|
|
32
|
+
return engine.extractMediaUrls(text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printSession(color, session) {
|
|
36
|
+
const mode = session.audioOnly ? 'audio' : 'wideo';
|
|
37
|
+
const fmt = session.format;
|
|
38
|
+
ui.info(
|
|
39
|
+
color,
|
|
40
|
+
`Tryb: ${mode} · ${fmt} · ${session.quality} · folder: ${session.output}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printInteractiveHelp(color) {
|
|
45
|
+
console.log('');
|
|
46
|
+
ui.info(color, 'Wklej link YouTube i naciśnij Enter — pobieranie startuje automatycznie.');
|
|
47
|
+
console.log(ui.c(color, ui.palette.dim, ' Komendy:'));
|
|
48
|
+
const cmds = [
|
|
49
|
+
['/help', 'Ta pomoc'],
|
|
50
|
+
['/info <url>', 'Podgląd bez pobierania'],
|
|
51
|
+
['/audio', 'Tryb audio (mp3 domyślnie)'],
|
|
52
|
+
['/video', 'Tryb wideo (mp4 domyślnie)'],
|
|
53
|
+
['/fmt mp3', 'Format: mp4, mp3, webm, mkv…'],
|
|
54
|
+
['/q 1080p', 'Jakość: best, 1080p, 720p…'],
|
|
55
|
+
['/folder', 'Zmień folder pobierania'],
|
|
56
|
+
['/folder C:\\Videos', 'Ustaw folder od razu'],
|
|
57
|
+
['/status', 'Narzędzia yt-dlp / ffmpeg'],
|
|
58
|
+
['/install', 'Zainstaluj yt-dlp'],
|
|
59
|
+
['/quit', 'Wyjście (lub Ctrl+C)'],
|
|
60
|
+
];
|
|
61
|
+
for (const [cmd, desc] of cmds) {
|
|
62
|
+
console.log(' ' + ui.c(color, ui.palette.cyan, cmd.padEnd(22)) + ui.c(color, ui.palette.dim, desc));
|
|
63
|
+
}
|
|
64
|
+
console.log('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function runInteractive(handlers, version, color) {
|
|
68
|
+
const session = defaultSession();
|
|
69
|
+
let ytDlpReady = false;
|
|
70
|
+
|
|
71
|
+
ui.printBanner(version, color);
|
|
72
|
+
printSession(color, session);
|
|
73
|
+
printInteractiveHelp(color);
|
|
74
|
+
|
|
75
|
+
const promptText = color
|
|
76
|
+
? ui.c(true, ui.palette.pink, '🔗 ') + ui.c(true, ui.palette.bold, 'Wklej link') + ui.c(true, ui.palette.dim, ' › ')
|
|
77
|
+
: 'link> ';
|
|
78
|
+
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
terminal: true,
|
|
83
|
+
historySize: 100,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const ensureTools = async () => {
|
|
87
|
+
if (ytDlpReady && engine.findYtDlp()) return;
|
|
88
|
+
const spin = new ui.Spinner('Przygotowanie yt-dlp', color);
|
|
89
|
+
spin.start();
|
|
90
|
+
try {
|
|
91
|
+
await engine.ensureYtDlp(msg => spin.draw(msg));
|
|
92
|
+
spin.succeed('Gotowe');
|
|
93
|
+
ytDlpReady = true;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
spin.fail(e.message);
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const askLine = (question) => new Promise(resolve => {
|
|
101
|
+
rl.question(question, answer => resolve(answer.trim()));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const processSlashCommand = async (line) => {
|
|
105
|
+
const parts = line.slice(1).trim().split(/\s+/);
|
|
106
|
+
const cmd = (parts[0] || '').toLowerCase();
|
|
107
|
+
const arg = parts.slice(1).join(' ');
|
|
108
|
+
|
|
109
|
+
switch (cmd) {
|
|
110
|
+
case 'help':
|
|
111
|
+
case 'h':
|
|
112
|
+
case '?':
|
|
113
|
+
printInteractiveHelp(color);
|
|
114
|
+
return;
|
|
115
|
+
case 'quit':
|
|
116
|
+
case 'exit':
|
|
117
|
+
case 'q':
|
|
118
|
+
ui.info(color, 'Do zobaczenia! 🌊');
|
|
119
|
+
rl.close();
|
|
120
|
+
return 'exit';
|
|
121
|
+
case 'audio':
|
|
122
|
+
case 'a':
|
|
123
|
+
session.audioOnly = true;
|
|
124
|
+
if (!['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'].includes(session.format)) {
|
|
125
|
+
session.format = 'mp3';
|
|
126
|
+
}
|
|
127
|
+
ui.success(color, `Tryb audio · format: ${session.format}`);
|
|
128
|
+
printSession(color, session);
|
|
129
|
+
return;
|
|
130
|
+
case 'video':
|
|
131
|
+
case 'v':
|
|
132
|
+
session.audioOnly = false;
|
|
133
|
+
if (['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'].includes(session.format)) {
|
|
134
|
+
session.format = 'mp4';
|
|
135
|
+
}
|
|
136
|
+
ui.success(color, `Tryb wideo · format: ${session.format}`);
|
|
137
|
+
printSession(color, session);
|
|
138
|
+
return;
|
|
139
|
+
case 'fmt':
|
|
140
|
+
case 'format':
|
|
141
|
+
case 'f':
|
|
142
|
+
if (!arg) {
|
|
143
|
+
ui.warn(color, 'Użyj: /fmt mp3 lub /fmt mp4');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
session.format = arg.toLowerCase();
|
|
147
|
+
if (['mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg'].includes(session.format)) {
|
|
148
|
+
session.audioOnly = true;
|
|
149
|
+
}
|
|
150
|
+
ui.success(color, `Format: ${session.format}`);
|
|
151
|
+
printSession(color, session);
|
|
152
|
+
return;
|
|
153
|
+
case 'q':
|
|
154
|
+
case 'quality':
|
|
155
|
+
if (!arg) {
|
|
156
|
+
ui.warn(color, 'Użyj: /q 1080p lub /q best');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
session.quality = arg;
|
|
160
|
+
ui.success(color, `Jakość: ${session.quality}`);
|
|
161
|
+
printSession(color, session);
|
|
162
|
+
return;
|
|
163
|
+
case 'folder':
|
|
164
|
+
case 'o':
|
|
165
|
+
case 'output':
|
|
166
|
+
if (arg) {
|
|
167
|
+
const dir = path.resolve(arg);
|
|
168
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
169
|
+
session.output = dir;
|
|
170
|
+
ui.success(color, 'Folder: ' + dir);
|
|
171
|
+
} else {
|
|
172
|
+
const entered = await askLine(
|
|
173
|
+
color ? ui.c(true, ui.palette.cyan, 'Ścieżka folderu: ') : 'Folder: '
|
|
174
|
+
);
|
|
175
|
+
if (entered) {
|
|
176
|
+
const dir = path.resolve(entered);
|
|
177
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
178
|
+
session.output = dir;
|
|
179
|
+
ui.success(color, 'Folder: ' + dir);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
printSession(color, session);
|
|
183
|
+
return;
|
|
184
|
+
case 'status':
|
|
185
|
+
ui.kv(color, 'yt-dlp', engine.findYtDlp() || 'BRAK', !!engine.findYtDlp());
|
|
186
|
+
ui.kv(color, 'ffmpeg', engine.findFfmpeg() || 'BRAK', !!engine.findFfmpeg());
|
|
187
|
+
return;
|
|
188
|
+
case 'install':
|
|
189
|
+
await handlers.toolsInstall(color);
|
|
190
|
+
ytDlpReady = true;
|
|
191
|
+
return;
|
|
192
|
+
case 'info':
|
|
193
|
+
case 'i': {
|
|
194
|
+
const url = arg || '';
|
|
195
|
+
if (!url || !engine.isSupportedMediaUrl(url)) {
|
|
196
|
+
ui.warn(color, 'Użyj: /info <url> (YouTube lub Instagram)');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await handlers.info(url, {}, color);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
case 'clear':
|
|
203
|
+
console.clear();
|
|
204
|
+
ui.printBanner(version, color);
|
|
205
|
+
printSession(color, session);
|
|
206
|
+
return;
|
|
207
|
+
default:
|
|
208
|
+
ui.warn(color, `Nieznana komenda: /${cmd} — wpisz /help`);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
rl.setPrompt(promptText);
|
|
213
|
+
|
|
214
|
+
return new Promise(resolve => {
|
|
215
|
+
rl.on('close', () => resolve(0));
|
|
216
|
+
|
|
217
|
+
const loop = () => {
|
|
218
|
+
rl.prompt();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
rl.on('line', async line => {
|
|
222
|
+
const trimmed = line.trim();
|
|
223
|
+
rl.pause();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
if (!trimmed) {
|
|
227
|
+
rl.resume();
|
|
228
|
+
loop();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (trimmed.startsWith('/')) {
|
|
233
|
+
const result = await processSlashCommand(trimmed);
|
|
234
|
+
if (result === 'exit') {
|
|
235
|
+
resolve(0);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
rl.resume();
|
|
239
|
+
loop();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const urls = extractYouTubeUrls(trimmed);
|
|
244
|
+
if (!urls.length) {
|
|
245
|
+
ui.warn(color, 'Nie wykryto linku YouTube/Instagram. Wklej URL lub wpisz /help');
|
|
246
|
+
rl.resume();
|
|
247
|
+
loop();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await ensureTools();
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < urls.length; i++) {
|
|
254
|
+
if (urls.length > 1) {
|
|
255
|
+
ui.info(color, `Pobieranie ${i + 1}/${urls.length}`);
|
|
256
|
+
}
|
|
257
|
+
await handlers.download(urls[i], sessionToFlags(session), color);
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
ui.error(color, e.message || String(e));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
rl.resume();
|
|
264
|
+
loop();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
rl.on('SIGINT', () => {
|
|
268
|
+
console.log('');
|
|
269
|
+
ui.info(color, 'Wyjście (Ctrl+C)');
|
|
270
|
+
rl.close();
|
|
271
|
+
resolve(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
loop();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { runInteractive, defaultSession };
|
package/lib/cli-ui.js
CHANGED
|
@@ -66,6 +66,7 @@ function printHelp(version, enabled) {
|
|
|
66
66
|
[' convert <plik>', 'Konwertuj plik lokalny'],
|
|
67
67
|
[' tools install', 'Zainstaluj yt-dlp'],
|
|
68
68
|
[' tools status', 'Status yt-dlp i ffmpeg'],
|
|
69
|
+
[' (bez argumentów)', 'Tryb interaktywny — wklej linki w konsoli'],
|
|
69
70
|
['', ''],
|
|
70
71
|
['Opcje download', ''],
|
|
71
72
|
[' -o, --output <dir>', 'Folder docelowy'],
|
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) {
|
|
@@ -343,6 +374,10 @@ module.exports = {
|
|
|
343
374
|
runDownload,
|
|
344
375
|
runConvert,
|
|
345
376
|
isYouTubeUrl,
|
|
377
|
+
isSupportedMediaUrl,
|
|
378
|
+
getMediaPlatform,
|
|
379
|
+
extractMediaUrls,
|
|
380
|
+
getInstagramContentKind: urls.getInstagramContentKind,
|
|
346
381
|
parseDeepLink,
|
|
347
382
|
findDeepLinkInArgv,
|
|
348
383
|
};
|
package/lib/urls.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
function extractMediaUrls(text) {
|
|
44
|
+
const found = new Set();
|
|
45
|
+
const raw = text || '';
|
|
46
|
+
const re = /https?:\/\/[^\s<>"']+/gi;
|
|
47
|
+
let m;
|
|
48
|
+
while ((m = re.exec(raw)) !== null) {
|
|
49
|
+
const cleaned = normalizeUrl(m[0]);
|
|
50
|
+
if (isSupportedMediaUrl(cleaned)) found.add(cleaned);
|
|
51
|
+
}
|
|
52
|
+
raw.split(/\s+/).filter(Boolean).forEach(part => {
|
|
53
|
+
const cleaned = normalizeUrl(part);
|
|
54
|
+
if (isSupportedMediaUrl(cleaned)) found.add(cleaned);
|
|
55
|
+
});
|
|
56
|
+
return [...found];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
normalizeUrl,
|
|
61
|
+
getMediaPlatform,
|
|
62
|
+
isSupportedMediaUrl,
|
|
63
|
+
isYouTubeUrl,
|
|
64
|
+
isInstagramUrl,
|
|
65
|
+
getInstagramContentKind,
|
|
66
|
+
extractMediaUrls,
|
|
67
|
+
};
|