eva-md 0.6.1 → 0.7.3
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/eva.js +343 -185
- package/package.json +3 -3
package/eva.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* EVA.md v0.
|
|
3
|
+
* EVA.md v0.7.3 — EVA Media Downloader by .maxon
|
|
4
4
|
* Pure Node.js Interactive TUI (Zero external npm dependencies!)
|
|
5
5
|
* Works just like Gemini CLI (`node eva.js` or `./eva.js`), with full mouse & keyboard support.
|
|
6
6
|
*
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
* --write-subs Download subtitles file
|
|
23
23
|
* --embed-subs Embed subtitles into media container
|
|
24
24
|
* --write-thumbnail Save thumbnail as a separate image file
|
|
25
|
-
* File Naming
|
|
25
|
+
* File Naming (Именование файлов — во вкладке Settings):
|
|
26
26
|
* -o "<template>" Output template (e.g. "%(playlist_index)s. %(title)s.%(ext)s")
|
|
27
|
-
* --download-archive <file> Record downloaded track IDs in archive.txt
|
|
28
27
|
* Playlists (Плейлисты):
|
|
29
28
|
* --playlist-items <range> Download specific items (e.g. "1-5")
|
|
30
29
|
* --playlist-reverse Download playlist in reverse order
|
|
30
|
+
* --download-archive <file> Record downloaded track IDs in archive.txt
|
|
31
31
|
* Network & Bypass (Сеть и обход блокировок — во вкладке Settings):
|
|
32
32
|
* -4 Force IPv4 connection (fixes DNS issues)
|
|
33
33
|
* --proxy <url> Use HTTP/SOCKS proxy (e.g. http://127.0.0.1:8080)
|
|
@@ -46,7 +46,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
46
46
|
// Constants & Configuration
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
|
|
49
|
-
const APP_VERSION = 'v0.
|
|
49
|
+
const APP_VERSION = 'v0.7.3 (Node.js)';
|
|
50
50
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'eva_md', 'config.json');
|
|
51
51
|
const MAX_LOG_LINES = 200;
|
|
52
52
|
const MAX_LOG_LINE_LENGTH = 500;
|
|
@@ -83,14 +83,21 @@ const NAMING_PRESETS = [
|
|
|
83
83
|
];
|
|
84
84
|
|
|
85
85
|
const ASCII_LOGO = [
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
|
|
91
|
-
'
|
|
86
|
+
' :::::::::: ::: ::: ::: ⡌⣔⣥⣔⣪⣯⣿⣿⣮⣳⣾⣻⣾⣽⢮⣷⢿⣦',
|
|
87
|
+
' :+: :+: :+: :+: :+: ⠨⣔⢺⢿⡿⠟⠛⠋⠩⠥⠄⠀⠌⠉⠝⠻⠿⣿⣿⣷⣿⣿⣳',
|
|
88
|
+
' +:+ +:+ +:+ +:+ +:+ ⢳⣭⢯⡆⠁⠠⠀⢀⣠⣤⠀⠀⠈⠀⠀⠈⡑⢬⢻⣿⣿⣿⣿⣧',
|
|
89
|
+
' +#++:++# +#+ +:+ +#++:++#++: ⠈⠲⣽⣻⣽⢦⠀⠠⠝⣿⣷⡀⠀⠶⠂⠀⣸⣿⣟⠕⢼⣿⣿⣿⢿',
|
|
90
|
+
" +#+ +#+ +#+ +#+ +#+ ⢣⣼⣻⣿⣟⢇⠀⡜⠤⠻⢿⣄⣀⣀⠼⢟⡻⣃⠀⠘⣿⣿⠟",
|
|
91
|
+
' #+# #+#+#+# #+# #+# ⠐⢮⡿⣽⣿⣿⣞⡵⢢⣌⣦⣕⣢⣬⣧⣺⣿⣿⣿⣿⠝',
|
|
92
|
+
"########## ## ### ### ⠲⠕⠻⢿⣿⡻⣿⢯⢟⡿⢯⠟⠛⠻⠝⠋",
|
|
92
93
|
];
|
|
93
94
|
|
|
95
|
+
const STRETCHED_ASCII_LOGO = ASCII_LOGO.map((line) => {
|
|
96
|
+
const chars = Array.from(line);
|
|
97
|
+
const width = Math.round(chars.length * 1.2);
|
|
98
|
+
return Array.from({ length: width }, (_, index) => chars[Math.floor(index / 1.2)]).join('');
|
|
99
|
+
});
|
|
100
|
+
|
|
94
101
|
// ANSI Styling Helpers
|
|
95
102
|
const CSI = '\x1b[';
|
|
96
103
|
const ANSI = {
|
|
@@ -152,6 +159,16 @@ function formatBytesToMB(bytes) {
|
|
|
152
159
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
153
160
|
}
|
|
154
161
|
|
|
162
|
+
function formatEta(eta) {
|
|
163
|
+
const parts = String(eta || '').split(':');
|
|
164
|
+
if (!parts.length || parts.length > 3 || parts.some((part) => !/^\d+$/.test(part))) return '00:00';
|
|
165
|
+
|
|
166
|
+
const seconds = parts.reduce((total, part) => total * 60 + Number(part), 0);
|
|
167
|
+
const minutes = Math.floor(seconds / 60);
|
|
168
|
+
const remainingSeconds = seconds % 60;
|
|
169
|
+
return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
155
172
|
function sanitizeLogLine(line) {
|
|
156
173
|
return String(line)
|
|
157
174
|
.replace(ANSI_ESCAPE_RE, '')
|
|
@@ -344,15 +361,11 @@ function buildYtdlpArgs(opts, targetUrl = null) {
|
|
|
344
361
|
if (opts.embedSubs) args.push('--embed-subs');
|
|
345
362
|
if (opts.writeThumbnail) args.push('--write-thumbnail');
|
|
346
363
|
|
|
347
|
-
// File Naming
|
|
364
|
+
// File Naming
|
|
348
365
|
const outDir = resolveOutputDir(opts.outputDir);
|
|
349
366
|
const tpl = opts.usePlaylistTemplate ? opts.outputTemplate : '%(title)s.%(ext)s';
|
|
350
367
|
args.push('-o', path.join(outDir, tpl));
|
|
351
368
|
|
|
352
|
-
if (opts.useDownloadArchive && opts.archiveFile.trim()) {
|
|
353
|
-
args.push('--download-archive', path.join(outDir, opts.archiveFile.trim()));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
369
|
// Playlists
|
|
357
370
|
if (opts.usePlaylistItems && opts.playlistItems.trim()) {
|
|
358
371
|
args.push('--playlist-items', opts.playlistItems.trim());
|
|
@@ -360,6 +373,9 @@ function buildYtdlpArgs(opts, targetUrl = null) {
|
|
|
360
373
|
if (opts.playlistReverse) {
|
|
361
374
|
args.push('--playlist-reverse');
|
|
362
375
|
}
|
|
376
|
+
if (opts.useDownloadArchive && opts.archiveFile.trim()) {
|
|
377
|
+
args.push('--download-archive', path.join(outDir, opts.archiveFile.trim()));
|
|
378
|
+
}
|
|
363
379
|
|
|
364
380
|
// Network & Bypass
|
|
365
381
|
if (opts.forceIpv4) {
|
|
@@ -405,6 +421,71 @@ function detectSource(url, defaultId = 1) {
|
|
|
405
421
|
return { id: found.id, name: found.name };
|
|
406
422
|
}
|
|
407
423
|
|
|
424
|
+
function isVideoUrl(url) {
|
|
425
|
+
try {
|
|
426
|
+
const parsed = new URL(url);
|
|
427
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
428
|
+
const pathname = parsed.pathname.toLowerCase();
|
|
429
|
+
|
|
430
|
+
if (/\.(mp4|m4v|mkv|mov|webm|avi)$/i.test(pathname)) return true;
|
|
431
|
+
if (host === 'youtu.be') return pathname.length > 1;
|
|
432
|
+
if (host === 'youtube.com' || host.endsWith('.youtube.com')) {
|
|
433
|
+
if (host === 'music.youtube.com') return false;
|
|
434
|
+
return (
|
|
435
|
+
(pathname === '/watch' && parsed.searchParams.has('v')) ||
|
|
436
|
+
/^\/(shorts|live|embed|v)\//.test(pathname)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (host === 'vkvideo.ru') return /^\/video/.test(pathname);
|
|
440
|
+
if (host === 'vk.com') return /^\/video/.test(pathname) || pathname === '/video_ext.php';
|
|
441
|
+
return false;
|
|
442
|
+
} catch (_) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function isAudioUrl(url) {
|
|
448
|
+
try {
|
|
449
|
+
const parsed = new URL(url);
|
|
450
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
451
|
+
const pathname = parsed.pathname.toLowerCase();
|
|
452
|
+
|
|
453
|
+
if (/\.(mp3|m4a|m4b|m4p|aac|flac|wav|ogg|opus|oga|wma)$/i.test(pathname)) return true;
|
|
454
|
+
if (host === 'music.youtube.com') return true;
|
|
455
|
+
if (
|
|
456
|
+
host === 'bandcamp.com' ||
|
|
457
|
+
host.endsWith('.bandcamp.com') ||
|
|
458
|
+
host === 'soundcloud.com' ||
|
|
459
|
+
host.endsWith('.soundcloud.com') ||
|
|
460
|
+
host === 'mixcloud.com' ||
|
|
461
|
+
host.endsWith('.mixcloud.com') ||
|
|
462
|
+
host === 'audiomack.com' ||
|
|
463
|
+
host.endsWith('.audiomack.com') ||
|
|
464
|
+
host === 'audius.co' ||
|
|
465
|
+
host.endsWith('.audius.co')
|
|
466
|
+
) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
return host === 'vk.com' && /^\/audio/.test(pathname);
|
|
470
|
+
} catch (_) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function applyVideoModeForUrl(opts, url) {
|
|
476
|
+
if (!isVideoUrl(url)) return false;
|
|
477
|
+
opts.bestVideoMerge = true;
|
|
478
|
+
opts.extractAudio = false;
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function applyAudioModeForUrl(opts, url) {
|
|
483
|
+
if (!isAudioUrl(url)) return false;
|
|
484
|
+
opts.bestVideoMerge = false;
|
|
485
|
+
opts.extractAudio = true;
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
408
489
|
function isYouTubePlaylistUrl(url) {
|
|
409
490
|
try {
|
|
410
491
|
const parsed = new URL(url);
|
|
@@ -1065,6 +1146,9 @@ class EvaNodeTUI {
|
|
|
1065
1146
|
this.selectedLibraryFile = null;
|
|
1066
1147
|
this.lastLibraryClick = null;
|
|
1067
1148
|
this.statusBanner = '';
|
|
1149
|
+
this.cliMarqueeTimer = null;
|
|
1150
|
+
this.cliMarqueeText = '';
|
|
1151
|
+
this.cliMarqueeOffset = 0;
|
|
1068
1152
|
|
|
1069
1153
|
this.modal = null;
|
|
1070
1154
|
}
|
|
@@ -1087,10 +1171,51 @@ class EvaNodeTUI {
|
|
|
1087
1171
|
}
|
|
1088
1172
|
|
|
1089
1173
|
quit() {
|
|
1174
|
+
if (this.cliMarqueeTimer) {
|
|
1175
|
+
clearInterval(this.cliMarqueeTimer);
|
|
1176
|
+
this.cliMarqueeTimer = null;
|
|
1177
|
+
}
|
|
1090
1178
|
process.stdout.write(ANSI.mouseOff + ANSI.showCursor + ANSI.altScreenOff + ANSI.reset + '\n');
|
|
1091
1179
|
process.exit(0);
|
|
1092
1180
|
}
|
|
1093
1181
|
|
|
1182
|
+
_stopCliMarquee() {
|
|
1183
|
+
if (this.cliMarqueeTimer) {
|
|
1184
|
+
clearInterval(this.cliMarqueeTimer);
|
|
1185
|
+
this.cliMarqueeTimer = null;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
_renderCliPreview(scr, x, y, width, text) {
|
|
1190
|
+
if (text !== this.cliMarqueeText) {
|
|
1191
|
+
this.cliMarqueeText = text;
|
|
1192
|
+
this.cliMarqueeOffset = 0;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if (text.length <= width) {
|
|
1196
|
+
this._stopCliMarquee();
|
|
1197
|
+
scr.putStr(y, x, text, ANSI.fgYellow);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const gap = ' ';
|
|
1202
|
+
const stream = text + gap + text;
|
|
1203
|
+
const loopLength = text.length + gap.length;
|
|
1204
|
+
const start = this.cliMarqueeOffset % loopLength;
|
|
1205
|
+
scr.putStr(y, x, stream.slice(start, start + width).padEnd(width), ANSI.fgYellow);
|
|
1206
|
+
|
|
1207
|
+
if (!this.cliMarqueeTimer && !this.modal) {
|
|
1208
|
+
this.cliMarqueeTimer = setInterval(() => {
|
|
1209
|
+
if (this.activeTab !== 1 || this.modal) {
|
|
1210
|
+
this._stopCliMarquee();
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
this.cliMarqueeOffset = (this.cliMarqueeOffset + 1) % loopLength;
|
|
1214
|
+
this.render();
|
|
1215
|
+
}, 175);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1094
1219
|
registerWidget(scr, id, y, x, label, action, { checked = null, radio = null, isCta = false, maxWidth = 0 } = {}) {
|
|
1095
1220
|
this.focusOrder.push(id);
|
|
1096
1221
|
const isFocused = this.focusId === id;
|
|
@@ -1144,6 +1269,10 @@ class EvaNodeTUI {
|
|
|
1144
1269
|
scr.putStr(y, textX + folderLabelIndex, folderLabel, ANSI.fgWhite);
|
|
1145
1270
|
const afterFolderIndex = folderLabelIndex + folderLabel.length;
|
|
1146
1271
|
scr.putStr(y, textX + afterFolderIndex, visibleLabel.slice(afterFolderIndex, editMarkerIndex), style);
|
|
1272
|
+
} else if (id === 'edit_outdir' && visibleLabel.startsWith('🗁')) {
|
|
1273
|
+
const iconLength = '🗁'.length;
|
|
1274
|
+
scr.putStr(y, textX, visibleLabel.slice(0, iconLength), style + ANSI.bold);
|
|
1275
|
+
scr.putStr(y, textX + iconLength, visibleLabel.slice(iconLength, editMarkerIndex), style);
|
|
1147
1276
|
} else {
|
|
1148
1277
|
scr.putStr(y, textX, visibleLabel.slice(0, editMarkerIndex), style);
|
|
1149
1278
|
}
|
|
@@ -1186,6 +1315,8 @@ class EvaNodeTUI {
|
|
|
1186
1315
|
// -------------------------------------------------------------------------
|
|
1187
1316
|
|
|
1188
1317
|
render() {
|
|
1318
|
+
if (this.activeTab !== 1 || this.modal) this._stopCliMarquee();
|
|
1319
|
+
|
|
1189
1320
|
if (this.selectedQueueItem) {
|
|
1190
1321
|
const selectedIndex = this.manager.items.indexOf(this.selectedQueueItem);
|
|
1191
1322
|
if (selectedIndex >= 0) this.selectedQueueIdx = selectedIndex;
|
|
@@ -1220,15 +1351,22 @@ class EvaNodeTUI {
|
|
|
1220
1351
|
}
|
|
1221
1352
|
|
|
1222
1353
|
_renderHeader(scr, cols) {
|
|
1223
|
-
|
|
1354
|
+
const logoRight = 2 + Math.max(...STRETCHED_ASCII_LOGO.map((line) => line.length));
|
|
1355
|
+
STRETCHED_ASCII_LOGO.forEach((line, i) => {
|
|
1224
1356
|
scr.putStr(1 + i, 2, line, ANSI.fgWhite + ANSI.bold);
|
|
1225
1357
|
});
|
|
1226
1358
|
scr.putStr(7, 2, 'EVA MEDIA DOWNLOADER', ANSI.fgCyan);
|
|
1227
|
-
|
|
1359
|
+
const versionX = cols - APP_VERSION.length - 3;
|
|
1360
|
+
if (versionX > logoRight + 1) {
|
|
1361
|
+
scr.putStr(2, versionX, APP_VERSION, ANSI.fgWhite);
|
|
1362
|
+
}
|
|
1228
1363
|
if (cols >= 90) {
|
|
1229
1364
|
const tagline = 'Music. Videos. Your collection.';
|
|
1230
|
-
|
|
1231
|
-
|
|
1365
|
+
const taglineX = cols - tagline.length - 3;
|
|
1366
|
+
if (taglineX > logoRight + 1) {
|
|
1367
|
+
scr.putStr(5, taglineX, tagline, ANSI.fgWhite);
|
|
1368
|
+
scr.putStr(6, taglineX, '─'.repeat(tagline.length), ANSI.fgCyan);
|
|
1369
|
+
}
|
|
1232
1370
|
}
|
|
1233
1371
|
}
|
|
1234
1372
|
|
|
@@ -1292,17 +1430,10 @@ class EvaNodeTUI {
|
|
|
1292
1430
|
const activeItems = items.filter((item) => item.status === 'Downloading');
|
|
1293
1431
|
const doneCnt = items.filter((i) => i.status === 'Done').length;
|
|
1294
1432
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const sum = activeItems.reduce((acc, i) => acc + (i.progress || 0), 0);
|
|
1298
|
-
pct = Math.round(sum / activeItems.length);
|
|
1299
|
-
} else if (items.length > 0 && doneCnt === items.length) {
|
|
1300
|
-
pct = 100;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const queueStatus = `│ ${activeItems.length}↓ ${doneCnt}✓ ${pct}%`;
|
|
1433
|
+
const eta = formatEta(activeItems[0] && activeItems[0].etaStr);
|
|
1434
|
+
const queueStatus = `│ ${activeItems.length}↓ ${doneCnt}✓ ${eta}`;
|
|
1304
1435
|
const queueStyle = activeItems.length > 0 ? ANSI.fgGreen + ANSI.bold : ANSI.fgWhite;
|
|
1305
|
-
const queueStartX = cols - queueStatus.length;
|
|
1436
|
+
const queueStartX = cols - queueStatus.length - 1;
|
|
1306
1437
|
scr.putStr(rows - 1, queueStartX, queueStatus, queueStyle);
|
|
1307
1438
|
|
|
1308
1439
|
const statusX = queueStartX + queueStatus.length + 2;
|
|
@@ -1364,96 +1495,95 @@ class EvaNodeTUI {
|
|
|
1364
1495
|
y += 7;
|
|
1365
1496
|
|
|
1366
1497
|
// --- Box 2: Download Options & yt-dlp Flags ---
|
|
1367
|
-
const box2H =
|
|
1498
|
+
const box2H = 12;
|
|
1368
1499
|
scr.clipRight = previousClipRight;
|
|
1369
1500
|
scr.drawBox(y, x, box2H, w, 'Download Options Flags');
|
|
1370
1501
|
scr.clipRight = x + w - 2;
|
|
1371
1502
|
const c1 = x + 2;
|
|
1372
1503
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1504
|
+
const renderAudioFormat = (row) => {
|
|
1505
|
+
scr.putStr(row, c1, 'ФОРМАТ:', ANSI.fgCyan + ANSI.bold);
|
|
1506
|
+
let fx = c1 + 8;
|
|
1507
|
+
AUDIO_FORMATS.forEach((fmt) => {
|
|
1508
|
+
fx =
|
|
1509
|
+
this.registerWidget(
|
|
1510
|
+
scr,
|
|
1511
|
+
`fmt_${fmt}`,
|
|
1512
|
+
row,
|
|
1513
|
+
fx,
|
|
1514
|
+
fmt,
|
|
1515
|
+
() => {
|
|
1516
|
+
this.opts.audioFormat = fmt;
|
|
1517
|
+
this.opts.extractAudio = true;
|
|
1518
|
+
this.opts.bestVideoMerge = false;
|
|
1519
|
+
this.statusBanner = `Формат: --audio-format ${fmt}`;
|
|
1520
|
+
this.render();
|
|
1521
|
+
},
|
|
1522
|
+
{ radio: this.opts.audioFormat === fmt }
|
|
1523
|
+
) + 2;
|
|
1524
|
+
});
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
const renderAudioQuality = (row) => {
|
|
1528
|
+
scr.putStr(row, c1, 'БИТРЕЙТ:', ANSI.fgCyan + ANSI.bold);
|
|
1529
|
+
let qx = c1 + 9;
|
|
1530
|
+
AUDIO_QUALITIES.forEach((q) => {
|
|
1531
|
+
qx =
|
|
1532
|
+
this.registerWidget(
|
|
1533
|
+
scr,
|
|
1534
|
+
`qual_${q.val}`,
|
|
1535
|
+
row,
|
|
1536
|
+
qx,
|
|
1537
|
+
q.label,
|
|
1538
|
+
() => {
|
|
1539
|
+
this.opts.audioQuality = q.val;
|
|
1540
|
+
this.statusBanner = `Качество: --audio-quality ${q.val}`;
|
|
1541
|
+
this.render();
|
|
1542
|
+
},
|
|
1543
|
+
{ radio: this.opts.audioQuality === q.val }
|
|
1544
|
+
) + 2;
|
|
1545
|
+
});
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
{
|
|
1549
|
+
const videoX = c1 + Math.floor((w - 4) / 2);
|
|
1550
|
+
const videoRight = x + w - 2;
|
|
1551
|
+
|
|
1552
|
+
scr.putStr(y + 1, c1, 'АУДИО:', ANSI.fgCyan + ANSI.bold);
|
|
1377
1553
|
this.registerWidget(
|
|
1378
1554
|
scr,
|
|
1379
1555
|
'flag_x',
|
|
1380
1556
|
y + 1,
|
|
1381
|
-
|
|
1557
|
+
c1 + 7,
|
|
1382
1558
|
'Только аудио',
|
|
1383
1559
|
() => this._toggle('extractAudio'),
|
|
1384
|
-
{ checked: this.opts.extractAudio }
|
|
1385
|
-
)
|
|
1386
|
-
|
|
1560
|
+
{ checked: this.opts.extractAudio, maxWidth: videoX - c1 - 8 }
|
|
1561
|
+
);
|
|
1562
|
+
this.registerWidget(
|
|
1563
|
+
scr,
|
|
1564
|
+
'flag_thumb',
|
|
1565
|
+
y + 2,
|
|
1566
|
+
c1 + 7,
|
|
1567
|
+
'Встроить обложку',
|
|
1568
|
+
() => this._toggle('embedThumbnail'),
|
|
1569
|
+
{ checked: this.opts.embedThumbnail, maxWidth: videoX - c1 - 8 }
|
|
1570
|
+
);
|
|
1387
1571
|
this.registerWidget(
|
|
1388
1572
|
scr,
|
|
1389
1573
|
'flag_meta',
|
|
1390
|
-
y +
|
|
1391
|
-
|
|
1574
|
+
y + 3,
|
|
1575
|
+
c1 + 7,
|
|
1392
1576
|
'Встроить метаданные',
|
|
1393
1577
|
() => this._toggle('embedMetadata'),
|
|
1394
|
-
{ checked: this.opts.embedMetadata }
|
|
1395
|
-
)
|
|
1396
|
-
this.registerWidget(
|
|
1397
|
-
scr,
|
|
1398
|
-
'flag_thumb',
|
|
1399
|
-
y + 1,
|
|
1400
|
-
ax,
|
|
1401
|
-
'Встроить обложку',
|
|
1402
|
-
() => this._toggle('embedThumbnail'),
|
|
1403
|
-
{ checked: this.opts.embedThumbnail, maxWidth: x + w - ax - 2 }
|
|
1404
|
-
);
|
|
1405
|
-
|
|
1406
|
-
// Row 2: Format (--audio-format)
|
|
1407
|
-
scr.putStr(y + 2, c1, 'ФОРМАТ:', ANSI.fgCyan + ANSI.bold);
|
|
1408
|
-
let fx = c1 + 8;
|
|
1409
|
-
AUDIO_FORMATS.forEach((fmt) => {
|
|
1410
|
-
fx =
|
|
1411
|
-
this.registerWidget(
|
|
1412
|
-
scr,
|
|
1413
|
-
`fmt_${fmt}`,
|
|
1414
|
-
y + 2,
|
|
1415
|
-
fx,
|
|
1416
|
-
fmt,
|
|
1417
|
-
() => {
|
|
1418
|
-
this.opts.audioFormat = fmt;
|
|
1419
|
-
this.opts.extractAudio = true;
|
|
1420
|
-
this.opts.bestVideoMerge = false;
|
|
1421
|
-
this.statusBanner = `Формат: --audio-format ${fmt}`;
|
|
1422
|
-
this.render();
|
|
1423
|
-
},
|
|
1424
|
-
{ radio: this.opts.audioFormat === fmt }
|
|
1425
|
-
) + 2;
|
|
1426
|
-
});
|
|
1427
|
-
|
|
1428
|
-
// Row 3: Quality (--audio-quality)
|
|
1429
|
-
scr.putStr(y + 3, c1, 'БИТРЕЙТ:', ANSI.fgCyan + ANSI.bold);
|
|
1430
|
-
let qx = c1 + 9;
|
|
1431
|
-
AUDIO_QUALITIES.forEach((q) => {
|
|
1432
|
-
qx =
|
|
1433
|
-
this.registerWidget(
|
|
1434
|
-
scr,
|
|
1435
|
-
`qual_${q.val}`,
|
|
1436
|
-
y + 3,
|
|
1437
|
-
qx,
|
|
1438
|
-
q.label,
|
|
1439
|
-
() => {
|
|
1440
|
-
this.opts.audioQuality = q.val;
|
|
1441
|
-
this.statusBanner = `Качество: --audio-quality ${q.val}`;
|
|
1442
|
-
this.render();
|
|
1443
|
-
},
|
|
1444
|
-
{ radio: this.opts.audioQuality === q.val }
|
|
1445
|
-
) + 2;
|
|
1446
|
-
});
|
|
1578
|
+
{ checked: this.opts.embedMetadata, maxWidth: videoX - c1 - 8 }
|
|
1579
|
+
);
|
|
1447
1580
|
|
|
1448
|
-
|
|
1449
|
-
scr.putStr(y + 4, c1, 'ВИДЕО:', ANSI.fgCyan + ANSI.bold);
|
|
1450
|
-
let vx = c1 + 7;
|
|
1451
|
-
vx =
|
|
1581
|
+
scr.putStr(y + 1, videoX, 'ВИДЕО:', ANSI.fgCyan + ANSI.bold);
|
|
1452
1582
|
this.registerWidget(
|
|
1453
1583
|
scr,
|
|
1454
1584
|
'flag_bvbab',
|
|
1455
|
-
y +
|
|
1456
|
-
|
|
1585
|
+
y + 1,
|
|
1586
|
+
videoX + 7,
|
|
1457
1587
|
'Видео+Аудио',
|
|
1458
1588
|
() => {
|
|
1459
1589
|
this.opts.bestVideoMerge = !this.opts.bestVideoMerge;
|
|
@@ -1461,123 +1591,116 @@ class EvaNodeTUI {
|
|
|
1461
1591
|
this.statusBanner = this.opts.bestVideoMerge ? 'Режим видео -f "bv*+ba/b"' : 'Режим аудио -x';
|
|
1462
1592
|
this.render();
|
|
1463
1593
|
},
|
|
1464
|
-
{ checked: this.opts.bestVideoMerge }
|
|
1465
|
-
)
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
checked: this.opts.usePlaylistTemplate,
|
|
1494
|
-
maxWidth: templateMaxWidth,
|
|
1495
|
-
}
|
|
1496
|
-
);
|
|
1497
|
-
const templateEditEnd = this.registerWidget(scr, 'edit_otpl', y + 6, templateEnd + 1, '[✎ ]', () => {
|
|
1498
|
-
return this.openPrompt('Шаблон имени (-o)', this.opts.outputTemplate, (v) => {
|
|
1499
|
-
this.opts.outputTemplate = v || '%(title)s.%(ext)s';
|
|
1500
|
-
});
|
|
1501
|
-
}, { maxWidth: 6 });
|
|
1594
|
+
{ checked: this.opts.bestVideoMerge, maxWidth: videoRight - videoX - 6 }
|
|
1595
|
+
);
|
|
1596
|
+
this.registerWidget(
|
|
1597
|
+
scr,
|
|
1598
|
+
'flag_wthumb',
|
|
1599
|
+
y + 2,
|
|
1600
|
+
videoX + 7,
|
|
1601
|
+
'Скачать обложку',
|
|
1602
|
+
() => this._toggle('writeThumbnail'),
|
|
1603
|
+
{ checked: this.opts.writeThumbnail, maxWidth: videoRight - videoX - 6 }
|
|
1604
|
+
);
|
|
1605
|
+
this.registerWidget(
|
|
1606
|
+
scr,
|
|
1607
|
+
'flag_wsubs',
|
|
1608
|
+
y + 3,
|
|
1609
|
+
videoX + 7,
|
|
1610
|
+
'Файл субтитров',
|
|
1611
|
+
() => this._toggle('writeSubs'),
|
|
1612
|
+
{ checked: this.opts.writeSubs, maxWidth: videoRight - videoX - 6 }
|
|
1613
|
+
);
|
|
1614
|
+
this.registerWidget(
|
|
1615
|
+
scr,
|
|
1616
|
+
'flag_esubs',
|
|
1617
|
+
y + 4,
|
|
1618
|
+
videoX + 7,
|
|
1619
|
+
'Встроить субтитры',
|
|
1620
|
+
() => this._toggle('embedSubs'),
|
|
1621
|
+
{ checked: this.opts.embedSubs, maxWidth: videoRight - videoX - 6 }
|
|
1622
|
+
);
|
|
1502
1623
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
scr,
|
|
1507
|
-
'flag_archive',
|
|
1508
|
-
y + 6,
|
|
1509
|
-
archiveX,
|
|
1510
|
-
`История загрузки ${this.opts.archiveFile}`,
|
|
1511
|
-
() => this._toggle('useDownloadArchive'),
|
|
1512
|
-
{
|
|
1513
|
-
checked: this.opts.useDownloadArchive,
|
|
1514
|
-
maxWidth: archiveMaxWidth,
|
|
1515
|
-
}
|
|
1516
|
-
);
|
|
1517
|
-
this.registerWidget(scr, 'edit_archive', y + 6, archiveEnd + 1, '[✎ ]', () => {
|
|
1518
|
-
return this.openPrompt('Файл архива (--download-archive)', this.opts.archiveFile, (v) => {
|
|
1519
|
-
this.opts.archiveFile = v || 'archive.txt';
|
|
1520
|
-
});
|
|
1521
|
-
}, { maxWidth: 6 });
|
|
1624
|
+
renderAudioQuality(y + 5);
|
|
1625
|
+
renderAudioFormat(y + 6);
|
|
1626
|
+
}
|
|
1522
1627
|
|
|
1523
|
-
// Row
|
|
1524
|
-
scr.putStr(y +
|
|
1628
|
+
// Row 8: Playlists (--playlist-items, --playlist-reverse & --download-archive)
|
|
1629
|
+
scr.putStr(y + 8, c1, 'ПЛЕЙЛИСТ:', ANSI.fgCyan + ANSI.bold);
|
|
1630
|
+
const rowRight = x + w - 2;
|
|
1525
1631
|
let px =
|
|
1526
1632
|
this.registerWidget(
|
|
1527
1633
|
scr,
|
|
1528
1634
|
'flag_plitems',
|
|
1529
|
-
y +
|
|
1530
|
-
c1 +
|
|
1531
|
-
|
|
1635
|
+
y + 8,
|
|
1636
|
+
c1 + 10,
|
|
1637
|
+
`Скачать № ${this.opts.playlistItems}`,
|
|
1532
1638
|
() => this._toggle('usePlaylistItems'),
|
|
1533
|
-
{ checked: this.opts.usePlaylistItems }
|
|
1639
|
+
{ checked: this.opts.usePlaylistItems, maxWidth: 18 }
|
|
1534
1640
|
) + 1;
|
|
1535
1641
|
px =
|
|
1536
|
-
this.registerWidget(scr, 'edit_plitems', y +
|
|
1642
|
+
this.registerWidget(scr, 'edit_plitems', y + 8, px, '[✎ ]', () =>
|
|
1537
1643
|
this.openPrompt('Номера треков (--playlist-items, напр. 1-5)', this.opts.playlistItems, (v) => {
|
|
1538
1644
|
this.opts.playlistItems = v || '1-5';
|
|
1539
1645
|
})
|
|
1540
|
-
) +
|
|
1646
|
+
) + 2;
|
|
1541
1647
|
|
|
1542
|
-
|
|
1648
|
+
px =
|
|
1649
|
+
this.registerWidget(
|
|
1650
|
+
scr,
|
|
1651
|
+
'flag_plrev',
|
|
1652
|
+
y + 8,
|
|
1653
|
+
px,
|
|
1654
|
+
'С конца',
|
|
1655
|
+
() => this._toggle('playlistReverse'),
|
|
1656
|
+
{ checked: this.opts.playlistReverse, maxWidth: Math.max(5, rowRight - px - 1) }
|
|
1657
|
+
) + 2;
|
|
1658
|
+
|
|
1659
|
+
const archiveEditWidth = '[✎ ]'.length;
|
|
1660
|
+
const archiveFlagWidth = Math.max(5, rowRight - px - archiveEditWidth);
|
|
1661
|
+
const archiveEnd = this.registerWidget(
|
|
1543
1662
|
scr,
|
|
1544
|
-
'
|
|
1545
|
-
y +
|
|
1663
|
+
'flag_archive',
|
|
1664
|
+
y + 8,
|
|
1546
1665
|
px,
|
|
1547
|
-
|
|
1548
|
-
() => this._toggle('
|
|
1549
|
-
{ checked: this.opts.
|
|
1666
|
+
`История ${this.opts.archiveFile}`,
|
|
1667
|
+
() => this._toggle('useDownloadArchive'),
|
|
1668
|
+
{ checked: this.opts.useDownloadArchive, maxWidth: archiveFlagWidth }
|
|
1550
1669
|
);
|
|
1670
|
+
if (archiveEnd + 1 <= rowRight) {
|
|
1671
|
+
this.registerWidget(scr, 'edit_archive', y + 8, archiveEnd + 1, '[✎ ]', () => {
|
|
1672
|
+
return this.openPrompt('Файл архива (--download-archive)', this.opts.archiveFile, (v) => {
|
|
1673
|
+
this.opts.archiveFile = v || 'archive.txt';
|
|
1674
|
+
});
|
|
1675
|
+
}, { maxWidth: Math.min(6, rowRight - archiveEnd) });
|
|
1676
|
+
}
|
|
1551
1677
|
|
|
1552
|
-
//
|
|
1678
|
+
// Keep the output controls on the last inner row of the options box.
|
|
1679
|
+
const outputControlsRow = y + box2H - 2;
|
|
1553
1680
|
const maxDirLen = Math.max(16, w - 48);
|
|
1554
1681
|
const shortDir =
|
|
1555
1682
|
this.opts.outputDir.length > maxDirLen
|
|
1556
1683
|
? '…' + this.opts.outputDir.slice(-(maxDirLen - 1))
|
|
1557
1684
|
: this.opts.outputDir;
|
|
1558
1685
|
let dx =
|
|
1559
|
-
this.registerWidget(scr, 'edit_outdir',
|
|
1686
|
+
this.registerWidget(scr, 'edit_outdir', outputControlsRow, c1, `📁: ${shortDir} [✎ ]`, () =>
|
|
1560
1687
|
this.openPrompt('Папка сохранения', this.opts.outputDir, (v) => {
|
|
1561
1688
|
this.opts.outputDir = v || path.join(os.homedir(), 'Downloads');
|
|
1562
1689
|
})
|
|
1563
1690
|
) + 3;
|
|
1564
1691
|
|
|
1565
|
-
const ctaLabel = 'Скачать
|
|
1692
|
+
const ctaLabel = '[A] Скачать';
|
|
1566
1693
|
const ctaX = Math.max(dx, x + w - ctaLabel.length - 3);
|
|
1567
|
-
this.registerWidget(scr, 'btn_add_queue',
|
|
1694
|
+
this.registerWidget(scr, 'btn_add_queue', outputControlsRow, ctaX, ctaLabel, () => this._actionAddToQueue(), {
|
|
1568
1695
|
isCta: true,
|
|
1569
1696
|
});
|
|
1570
1697
|
|
|
1571
1698
|
y += box2H;
|
|
1572
1699
|
scr.clipRight = previousClipRight;
|
|
1573
1700
|
|
|
1574
|
-
//
|
|
1701
|
+
// Scroll long command previews horizontally so they remain readable in compact windows.
|
|
1575
1702
|
const fullCliText = `CLI: ${buildCommandPreview(this.opts)}`;
|
|
1576
|
-
|
|
1577
|
-
const availCliRows = Math.max(1, rows - 2 - y);
|
|
1578
|
-
wrappedCli.slice(0, availCliRows).forEach((line, idx) => {
|
|
1579
|
-
scr.putStr(y + idx, x + 1, line, ANSI.fgYellow);
|
|
1580
|
-
});
|
|
1703
|
+
this._renderCliPreview(scr, x + 1, y, w - 2, fullCliText);
|
|
1581
1704
|
}
|
|
1582
1705
|
|
|
1583
1706
|
// -------------------------------------------------------------------------
|
|
@@ -1914,7 +2037,28 @@ class EvaNodeTUI {
|
|
|
1914
2037
|
this.render();
|
|
1915
2038
|
}, { isCta: true });
|
|
1916
2039
|
|
|
1917
|
-
|
|
2040
|
+
const namingHeading = 'ШАБЛОНЫ ИМЕНОВАНИЯ ФАЙЛОВ';
|
|
2041
|
+
scr.putStr(15, c1, namingHeading, ANSI.fgCyan + ANSI.bold);
|
|
2042
|
+
const namingTemplateX = c1 + namingHeading.length + 1;
|
|
2043
|
+
const namingRight = x + w - 2;
|
|
2044
|
+
const namingTemplateEnd = this.registerWidget(
|
|
2045
|
+
scr,
|
|
2046
|
+
'flag_otpl',
|
|
2047
|
+
15,
|
|
2048
|
+
namingTemplateX,
|
|
2049
|
+
`-o "${this.opts.outputTemplate}"`,
|
|
2050
|
+
() => this._toggle('usePlaylistTemplate'),
|
|
2051
|
+
{
|
|
2052
|
+
checked: this.opts.usePlaylistTemplate,
|
|
2053
|
+
maxWidth: Math.max(5, namingRight - namingTemplateX - '[✎ ]'.length),
|
|
2054
|
+
}
|
|
2055
|
+
);
|
|
2056
|
+
this.registerWidget(scr, 'edit_otpl', 15, namingTemplateEnd + 1, '[✎ ]', () => {
|
|
2057
|
+
return this.openPrompt('Шаблон имени (-o)', this.opts.outputTemplate, (v) => {
|
|
2058
|
+
this.opts.outputTemplate = v || '%(title)s.%(ext)s';
|
|
2059
|
+
});
|
|
2060
|
+
}, { maxWidth: 6 });
|
|
2061
|
+
|
|
1918
2062
|
NAMING_PRESETS.forEach((p, idx) => {
|
|
1919
2063
|
this.registerWidget(
|
|
1920
2064
|
scr,
|
|
@@ -1992,8 +2136,8 @@ class EvaNodeTUI {
|
|
|
1992
2136
|
'Интегрированные флаги yt-dlp:',
|
|
1993
2137
|
' • Аудио: -x, --audio-format (mp3/flac/m4a/wav), --audio-quality 0, --embed-metadata, --embed-thumbnail',
|
|
1994
2138
|
' • Видео: -f "bv*+ba/b", --write-subs, --embed-subs, --write-thumbnail, --merge-output-format mp4',
|
|
1995
|
-
' •
|
|
1996
|
-
' • Плейлисты: --playlist-items 1-5, --playlist-reverse',
|
|
2139
|
+
' • Файлы (вкладка Settings): -o "%(playlist_index)s. %(title)s.%(ext)s"',
|
|
2140
|
+
' • Плейлисты: --playlist-items 1-5, --playlist-reverse, --download-archive archive.txt',
|
|
1997
2141
|
' • Сеть (вкладка Settings): -4, --proxy http://127.0.0.1:8080, --cookies-from-browser chrome',
|
|
1998
2142
|
` `,
|
|
1999
2143
|
` `,
|
|
@@ -2026,11 +2170,17 @@ class EvaNodeTUI {
|
|
|
2026
2170
|
_setUrl(val) {
|
|
2027
2171
|
this.opts.url = val;
|
|
2028
2172
|
if (val) {
|
|
2173
|
+
const videoMode = applyVideoModeForUrl(this.opts, val);
|
|
2174
|
+
const audioMode = !videoMode && applyAudioModeForUrl(this.opts, val);
|
|
2029
2175
|
const detected = detectSource(val, this.opts.sourceId);
|
|
2030
2176
|
if (this.opts.sourceId !== 1) {
|
|
2031
2177
|
this.opts.sourceId = detected.id;
|
|
2032
2178
|
}
|
|
2033
|
-
this.statusBanner =
|
|
2179
|
+
this.statusBanner = videoMode
|
|
2180
|
+
? `Видео+Аудио (${detected.name})`
|
|
2181
|
+
: audioMode
|
|
2182
|
+
? `Только аудио (${detected.name})`
|
|
2183
|
+
: `URL (${detected.name})`;
|
|
2034
2184
|
}
|
|
2035
2185
|
this.render();
|
|
2036
2186
|
}
|
|
@@ -2228,7 +2378,10 @@ function main() {
|
|
|
2228
2378
|
const urlArg = argv.find((a) => !a.startsWith('-')) || '';
|
|
2229
2379
|
|
|
2230
2380
|
const opts = loadSavedOptions();
|
|
2231
|
-
if (urlArg)
|
|
2381
|
+
if (urlArg) {
|
|
2382
|
+
opts.url = urlArg;
|
|
2383
|
+
if (!applyVideoModeForUrl(opts, urlArg)) applyAudioModeForUrl(opts, urlArg);
|
|
2384
|
+
}
|
|
2232
2385
|
|
|
2233
2386
|
if (isPrintCmd) {
|
|
2234
2387
|
console.log(buildCommandPreview(opts));
|
|
@@ -2283,11 +2436,16 @@ module.exports = {
|
|
|
2283
2436
|
buildPlaylistDiscoveryArgs,
|
|
2284
2437
|
buildCommandPreview,
|
|
2285
2438
|
detectSource,
|
|
2439
|
+
isVideoUrl,
|
|
2440
|
+
isAudioUrl,
|
|
2441
|
+
applyVideoModeForUrl,
|
|
2442
|
+
applyAudioModeForUrl,
|
|
2286
2443
|
isYouTubePlaylistUrl,
|
|
2287
2444
|
playlistEntryUrl,
|
|
2288
2445
|
ScreenBuffer,
|
|
2289
2446
|
DownloadManager,
|
|
2290
2447
|
formatBytesToMB,
|
|
2448
|
+
formatEta,
|
|
2291
2449
|
fitTextToCellWidth,
|
|
2292
2450
|
wrapTextByWords,
|
|
2293
2451
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eva-md",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.3",
|
|
4
|
+
"description": "Eva Media Downloader CLI",
|
|
5
5
|
"main": "eva.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"eva": "
|
|
7
|
+
"eva": "eva.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"eva.js",
|