veodl 1.8.2 → 1.9.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/CHANGELOG.md +28 -0
- package/README.md +796 -725
- package/docs/AGENT_GUIDE.md +82 -66
- package/package.json +1 -2
- package/src/alias.js +258 -0
- package/src/cli.js +613 -454
- package/src/config-diagnostics.js +145 -144
- package/src/config-editor.js +285 -281
- package/src/config-template.js +224 -171
- package/src/config.js +217 -213
- package/src/doctor.js +1 -1
- package/src/downloader.js +601 -600
- package/src/interactive.js +115 -82
- package/src/output.js +1 -1
- package/src/runs.js +343 -333
- package/src/source-discovery.js +251 -0
- package/src/state.js +27 -27
- package/src/uninstall.js +222 -0
- package/src/updater.js +5 -3
package/src/interactive.js
CHANGED
|
@@ -1,82 +1,115 @@
|
|
|
1
|
-
import { outputStream, formatOutput } from './output.js';
|
|
2
|
-
import { createInterface } from 'node:readline/promises';
|
|
3
|
-
import { availableHeights, cleanText, validateUrl } from './utils.js';
|
|
4
|
-
import { applyProfile } from './config.js';
|
|
5
|
-
import { fetchMetadata, prepareBackend, runBackend } from './downloader.js';
|
|
6
|
-
import { describeEstimate, selectedEntries, sizeEstimate, validateItems } from './playlist.js';
|
|
7
|
-
|
|
8
|
-
export async function interactiveArgs(config, { signal, input = process.stdin, output = process.stderr, ask, inspect } = {}) {
|
|
9
|
-
const terminalOutput = output;
|
|
10
|
-
output = outputStream(output);
|
|
11
|
-
const rl = ask ? null : createInterface({ input, output: terminalOutput });
|
|
12
|
-
const controller = new AbortController();
|
|
13
|
-
signal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
14
|
-
rl?.on('SIGINT', () => controller.abort());
|
|
15
|
-
const question = ask || (prompt => rl.question(formatOutput(prompt, terminalOutput), { signal }));
|
|
16
|
-
const choose = async (prompt, allowed, fallback) => {
|
|
17
|
-
while (true) {
|
|
18
|
-
const answer = (await question(prompt)).trim() || fallback;
|
|
19
|
-
if (allowed.includes(answer)) return answer;
|
|
20
|
-
output.write(`Choose: ${allowed.join(', ')}\n`);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
try {
|
|
24
|
-
const args = [];
|
|
25
|
-
const profiles = Object.keys(config.profiles || {});
|
|
26
|
-
let defaults = applyProfile(config);
|
|
27
|
-
if (profiles.length) {
|
|
28
|
-
const hasDefault = profiles.includes('default');
|
|
29
|
-
const choices = hasDefault ? profiles : [...profiles, 'none'];
|
|
30
|
-
const fallback = hasDefault ? 'default' : 'none';
|
|
31
|
-
const profile = await choose(`Profile (${choices.join(', ')}) [${fallback}]: `, choices, fallback);
|
|
32
|
-
if (profile !== 'none') { args.push('--profile', profile); defaults = applyProfile(config, profile); }
|
|
33
|
-
}
|
|
34
|
-
const url = validateUrl((await question('Video or playlist URL: ')).trim());
|
|
35
|
-
args.push(url);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
import { outputStream, formatOutput } from './output.js';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { availableHeights, cleanText, validateUrl } from './utils.js';
|
|
4
|
+
import { applyProfile } from './config.js';
|
|
5
|
+
import { fetchMetadata, prepareBackend, runBackend } from './downloader.js';
|
|
6
|
+
import { describeEstimate, selectedEntries, sizeEstimate, validateItems } from './playlist.js';
|
|
7
|
+
|
|
8
|
+
export async function interactiveArgs(config, { signal, input = process.stdin, output = process.stderr, ask, inspect, discover } = {}) {
|
|
9
|
+
const terminalOutput = output;
|
|
10
|
+
output = outputStream(output);
|
|
11
|
+
const rl = ask ? null : createInterface({ input, output: terminalOutput });
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
signal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
14
|
+
rl?.on('SIGINT', () => controller.abort());
|
|
15
|
+
const question = ask || (prompt => rl.question(formatOutput(prompt, terminalOutput), { signal }));
|
|
16
|
+
const choose = async (prompt, allowed, fallback) => {
|
|
17
|
+
while (true) {
|
|
18
|
+
const answer = (await question(prompt)).trim() || fallback;
|
|
19
|
+
if (allowed.includes(answer)) return answer;
|
|
20
|
+
output.write(`Choose: ${allowed.join(', ')}\n`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
const args = [];
|
|
25
|
+
const profiles = Object.keys(config.profiles || {});
|
|
26
|
+
let defaults = applyProfile(config);
|
|
27
|
+
if (profiles.length) {
|
|
28
|
+
const hasDefault = profiles.includes('default');
|
|
29
|
+
const choices = hasDefault ? profiles : [...profiles, 'none'];
|
|
30
|
+
const fallback = hasDefault ? 'default' : 'none';
|
|
31
|
+
const profile = await choose(`Profile (${choices.join(', ')}) [${fallback}]: `, choices, fallback);
|
|
32
|
+
if (profile !== 'none') { args.push('--profile', profile); defaults = applyProfile(config, profile); }
|
|
33
|
+
}
|
|
34
|
+
const url = validateUrl((await question('Video or playlist URL: ')).trim());
|
|
35
|
+
args.push(url);
|
|
36
|
+
if (defaults.listSources) {
|
|
37
|
+
output.write('Listing page sources…\n');
|
|
38
|
+
args.push('--list-sources');
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
const type = await choose(`Download (video/audio) [${defaults.audio ? 'audio' : 'video'}]: `, ['video', 'audio'], defaults.audio ? 'audio' : 'video');
|
|
42
|
+
args.push(type === 'audio' ? '--audio' : '--no-audio');
|
|
43
|
+
output.write('Reading available media…\n');
|
|
44
|
+
// Inspect the collection first so ordinary video links do not need a playlist prompt.
|
|
45
|
+
const inspectionOptions = { ...defaults, url, playlist: true };
|
|
46
|
+
const backend = inspect ? null : await prepareBackend(inspectionOptions, { signal });
|
|
47
|
+
let metadata;
|
|
48
|
+
try {
|
|
49
|
+
metadata = inspect ? await inspect(inspectionOptions) : await fetchMetadata(inspectionOptions, { signal, backend, runner: runBackend });
|
|
50
|
+
if (Array.isArray(metadata.formats) && !metadata.formats.length && !metadata.entries?.length && !metadata.url) {
|
|
51
|
+
throw new Error('No downloadable video formats found.');
|
|
52
|
+
}
|
|
53
|
+
} catch (originalError) {
|
|
54
|
+
const { discoverSources, formatSource, parseSourceTimeout, shouldOfferSourceDiscovery } = await import('./source-discovery.js');
|
|
55
|
+
if (!shouldOfferSourceDiscovery(originalError)) throw originalError;
|
|
56
|
+
if (!defaults.autoListSources) {
|
|
57
|
+
const answer = (await question('No downloadable video found. Search this page for other sources? (y/N): ')).trim().toLowerCase();
|
|
58
|
+
if (!['y', 'yes'].includes(answer)) throw originalError;
|
|
59
|
+
}
|
|
60
|
+
const inspectSource = (mediaUrl, _page, { signal: scanSignal = signal } = {}) => inspect
|
|
61
|
+
? inspect({ ...inspectionOptions, playlist: false, mediaUrl }, scanSignal)
|
|
62
|
+
: fetchMetadata({ ...inspectionOptions, playlist: false, mediaUrl }, { signal: scanSignal, backend, runner: runBackend });
|
|
63
|
+
const sources = await (discover || discoverSources)(url, { signal, inspect: inspectSource,
|
|
64
|
+
deepScan: defaults.deepScan, timeoutMs: defaults.timeout ? parseSourceTimeout(defaults.timeout) : undefined }).catch(() => []);
|
|
65
|
+
if (!sources.length) throw originalError;
|
|
66
|
+
for (const source of sources) output.write(`${formatSource(source)}\n`);
|
|
67
|
+
let selection;
|
|
68
|
+
while (!selection) {
|
|
69
|
+
const answer = (await question('Source number: ')).trim();
|
|
70
|
+
const index = Number(answer);
|
|
71
|
+
if (/^[1-9]\d*$/.test(answer) && sources[index - 1]) selection = sources[index - 1];
|
|
72
|
+
else output.write(`Choose a number from 1 to ${sources.length}.\n`);
|
|
73
|
+
}
|
|
74
|
+
args.push('--source', String(selection.index));
|
|
75
|
+
metadata = await inspectSource(selection.url);
|
|
76
|
+
}
|
|
77
|
+
const isCollection = metadata._type === 'playlist' || Boolean(metadata.entries);
|
|
78
|
+
const collection = isCollection
|
|
79
|
+
? await choose(`Download the playlist? (y/n) [${defaults.playlist === false ? 'n' : 'y'}]: `,
|
|
80
|
+
['y', 'n'], defaults.playlist === false ? 'n' : 'y')
|
|
81
|
+
: 'n';
|
|
82
|
+
args.push(collection === 'y' ? '--playlist' : '--no-playlist');
|
|
83
|
+
output.write(`${cleanText(metadata.title || metadata.id || 'Media')}\n`);
|
|
84
|
+
if (collection === 'y' && metadata.entries) {
|
|
85
|
+
for (const { entry, index } of selectedEntries(metadata)) output.write(`${index}. ${cleanText(entry?.title || 'Unavailable entry')}\n`);
|
|
86
|
+
let selection;
|
|
87
|
+
while (true) {
|
|
88
|
+
const preferred = defaults.playlistItems || '';
|
|
89
|
+
selection = (await question(`Entries (e.g. 1,3-5; Enter = ${preferred || 'all'}): `)).trim() || preferred;
|
|
90
|
+
try { if (selection) validateItems(selection); selectedEntries(metadata, selection); break; }
|
|
91
|
+
catch (error) { output.write(`${error.message}\n`); }
|
|
92
|
+
}
|
|
93
|
+
if (selection) args.push('--playlist-items', selection);
|
|
94
|
+
output.write(`${describeEstimate(sizeEstimate(selectedEntries(metadata, selection)))}\n`);
|
|
95
|
+
}
|
|
96
|
+
if (type === 'video') {
|
|
97
|
+
const heights = availableHeights(metadata.formats).sort((a, b) => b - a).map(height => `${height}p`);
|
|
98
|
+
const choices = ['best', ...heights];
|
|
99
|
+
if (!heights.length) output.write('Resolution metadata unavailable; using best. Set a quality cap with -q in command mode.\n');
|
|
100
|
+
const fallback = choices.includes(defaults.quality) ? defaults.quality : 'best';
|
|
101
|
+
args.push('-q', await choose(`Quality (${choices.join(', ')}) [${fallback}]: `, choices, fallback));
|
|
102
|
+
}
|
|
103
|
+
// Switching the media type must not leave an incompatible format from a profile.
|
|
104
|
+
if (type === 'video' && ['mp3', 'm4a', 'aac', 'opus', 'flac', 'wav'].includes(defaults.format)) args.push('--format', 'mp4');
|
|
105
|
+
if (type === 'audio' && ['mp4', 'mkv', 'webm', 'mov'].includes(defaults.format)) args.push('--format', 'mp3');
|
|
106
|
+
const directory = (await question(`Output directory [${defaults.output || process.cwd()}]: `)).trim() || defaults.output || process.cwd();
|
|
107
|
+
args.push('-o', directory, '--resume');
|
|
108
|
+
const skipDefault = defaults.skipExisting === false ? 'n' : 'y';
|
|
109
|
+
const skip = await choose(`Skip previously downloaded videos? (y/n) [${skipDefault}]: `, ['y', 'n'], skipDefault);
|
|
110
|
+
args.push(skip === 'y' ? '--skip-existing' : '--no-skip-existing');
|
|
111
|
+
output.write(`Ready: ${type}, ${directory}\n`);
|
|
112
|
+
if (await choose('Start download? (y/n) [y]: ', ['y', 'n'], 'y') === 'n') return null;
|
|
113
|
+
return args;
|
|
114
|
+
} finally { rl?.close(); }
|
|
115
|
+
}
|
package/src/output.js
CHANGED
|
@@ -27,7 +27,7 @@ export function formatOutput(text, stream, enabled = settings.getStore()?.enable
|
|
|
27
27
|
if (!line.trim() || /\x1b/.test(line)) return line;
|
|
28
28
|
const value = line.trim();
|
|
29
29
|
let role = 'muted';
|
|
30
|
-
if (/^(?:veo (?:stats|history|runs|stop|doctor|flush|update|backend)\b|veo \d[^ ]* doctor|ID\s+PID\s+STATE|Usage:|Options:|Commands:|Examples:|\d+\. )/.test(value)) role = 'title';
|
|
30
|
+
if (/^(?:veo (?:stats|history|runs|stop|doctor|flush|update|backend|alias|uninstall)\b|veo \d[^ ]* doctor|ID\s+PID\s+STATE|Usage:|Options:|Commands:|Examples:|\d+\. )/.test(value)) role = 'title';
|
|
31
31
|
if (/^(?:\[?OK\]?\s|Saved:|Flushed:|Stopped\b|Removed\b|Config OK:|Statistics reset|Managed tools are ready)|\bis up to date\b|\bupdated to\b|\binstalled and will be used\b/i.test(value)) role = 'success';
|
|
32
32
|
if (/^(?:\[?FAIL\]?\s|Error:|veo: (?!note:))|Status:\s*(?:failed|cancelled)/i.test(value)) role = 'error';
|
|
33
33
|
if (/Status:\s*saved/.test(value)) role = 'success';
|