veodl 1.12.1 → 2.0.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 +15 -0
- package/LICENSE +21 -21
- package/README.md +178 -97
- package/bin/veo.js +47 -2
- package/docs/AGENT_GUIDE.md +1 -1
- package/native/android-arm64/veo +0 -0
- package/native/darwin-arm64/veo +0 -0
- package/native/darwin-x64/veo +0 -0
- package/native/linux-arm64/veo +0 -0
- package/native/linux-x64/veo +0 -0
- package/native/win32-arm64/veo.exe +0 -0
- package/native/win32-x64/veo.exe +0 -0
- package/package.json +53 -57
- package/src/alias.js +0 -258
- package/src/backend-update.js +0 -191
- package/src/backend.js +0 -572
- package/src/changes.js +0 -278
- package/src/cli.js +0 -785
- package/src/compatibility.js +0 -39
- package/src/config-clipboard.js +0 -67
- package/src/config-diagnostics.js +0 -155
- package/src/config-editor.js +0 -404
- package/src/config-errors.js +0 -64
- package/src/config-reset.js +0 -39
- package/src/config-template.js +0 -227
- package/src/config.js +0 -307
- package/src/disk-space.js +0 -68
- package/src/doctor.js +0 -306
- package/src/download-cache.js +0 -31
- package/src/downloader.js +0 -704
- package/src/execution.js +0 -68
- package/src/experimental-fragments.js +0 -29
- package/src/flush.js +0 -66
- package/src/history.js +0 -173
- package/src/inline-region.js +0 -72
- package/src/inspect-media.js +0 -179
- package/src/interactive.js +0 -120
- package/src/jobs.js +0 -218
- package/src/legacy-config-comments.js +0 -106
- package/src/media-assets.js +0 -272
- package/src/naming.js +0 -54
- package/src/open-file.js +0 -15
- package/src/option-suggestions.js +0 -43
- package/src/output.js +0 -81
- package/src/path-links.js +0 -53
- package/src/paths.js +0 -26
- package/src/playlist.js +0 -31
- package/src/progress.js +0 -341
- package/src/run-archive.js +0 -148
- package/src/runs.js +0 -346
- package/src/source-discovery.js +0 -305
- package/src/state.js +0 -27
- package/src/stats.js +0 -63
- package/src/terminal-size.js +0 -8
- package/src/terminal-title.js +0 -19
- package/src/tool-setup.js +0 -289
- package/src/uninstall.js +0 -222
- package/src/updater.js +0 -229
- package/src/utils.js +0 -208
- package/src/version.js +0 -38
package/src/interactive.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
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, selectedProfileName } 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 selected = selectedProfileName(config);
|
|
29
|
-
const choices = selected ? profiles : [...profiles, 'none'];
|
|
30
|
-
const fallback = selected || '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 === false) throw originalError;
|
|
57
|
-
const inspectSource = (mediaUrl, _page, { signal: scanSignal = signal } = {}) => inspect
|
|
58
|
-
? inspect({ ...inspectionOptions, playlist: false, mediaUrl }, scanSignal)
|
|
59
|
-
: fetchMetadata({ ...inspectionOptions, playlist: false, mediaUrl }, { signal: scanSignal, backend, runner: runBackend });
|
|
60
|
-
const sources = await (discover || discoverSources)(url, { signal, inspect: inspectSource,
|
|
61
|
-
deepScan: defaults.deepScan, timeoutMs: defaults.timeout ? parseSourceTimeout(defaults.timeout) : undefined }).catch(() => []);
|
|
62
|
-
if (!sources.length) throw originalError;
|
|
63
|
-
let selection = sources[0];
|
|
64
|
-
if (sources.length > 1) {
|
|
65
|
-
for (const source of sources) output.write(`${formatSource(source)}\n`);
|
|
66
|
-
selection = null;
|
|
67
|
-
while (!selection) {
|
|
68
|
-
const answer = (await question('Source number: ')).trim();
|
|
69
|
-
const index = Number(answer);
|
|
70
|
-
if (/^[1-9]\d*$/.test(answer) && sources[index - 1]) selection = sources[index - 1];
|
|
71
|
-
else output.write(`Choose a number from 1 to ${sources.length}.\n`);
|
|
72
|
-
}
|
|
73
|
-
} else output.write(`Found video source: ${formatSource(selection)}\n`);
|
|
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);
|
|
108
|
-
if (defaults.incognito) {
|
|
109
|
-
args.push('--no-resume', '--no-skip-existing');
|
|
110
|
-
} else {
|
|
111
|
-
args.push('--resume');
|
|
112
|
-
const skipDefault = defaults.skipExisting === false ? 'n' : 'y';
|
|
113
|
-
const skip = await choose(`Skip previously downloaded videos? (y/n) [${skipDefault}]: `, ['y', 'n'], skipDefault);
|
|
114
|
-
args.push(skip === 'y' ? '--skip-existing' : '--no-skip-existing');
|
|
115
|
-
}
|
|
116
|
-
output.write(`Ready: ${type}, ${directory}\n`);
|
|
117
|
-
if (await choose('Start download? (y/n) [y]: ', ['y', 'n'], 'y') === 'n') return null;
|
|
118
|
-
return args;
|
|
119
|
-
} finally { rl?.close(); }
|
|
120
|
-
}
|
package/src/jobs.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { runPool, serialQueue, reducedLimit, formatTimings } from './execution.js';
|
|
2
|
-
import { styleText, terminalText } from './progress.js';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { lstat, mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
5
|
-
import os from 'node:os';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { cacheBase } from './paths.js';
|
|
8
|
-
import { publicOptions, readJson, writeJson } from './state.js';
|
|
9
|
-
import { cleanText, readableError, validateUrl } from './utils.js';
|
|
10
|
-
import { RUN_ARCHIVE_ID } from './run-archive.js';
|
|
11
|
-
import { folderLink } from './path-links.js';
|
|
12
|
-
|
|
13
|
-
export async function retryOptions(file, root = cacheBase()) {
|
|
14
|
-
if (RUN_ARCHIVE_ID.test(file)) file = await retryJobForRun(file, root);
|
|
15
|
-
const job = await readJson(path.resolve(file), null);
|
|
16
|
-
if (job?.version !== 1 || !Array.isArray(job.items)) throw new Error('Invalid retry job file.');
|
|
17
|
-
const pending = job.items.filter(item => !['saved', 'skipped'].includes(item.status));
|
|
18
|
-
if (!pending.length) throw new Error('This job has no failed or unfinished downloads.');
|
|
19
|
-
return pending.map(item => {
|
|
20
|
-
validateUrl(item.url);
|
|
21
|
-
const entries = item.entries || [];
|
|
22
|
-
// After interruption, retry the original selection; resume history skips completed entries.
|
|
23
|
-
const failures = entries.filter(entry => entry.status === 'failed');
|
|
24
|
-
return { ...job.options, ...item.options, url: item.url, resume: true,
|
|
25
|
-
...(item.status === 'failed' && failures.length && item.finished ? { playlistItems: failures.map(entry => entry.index).join(',') } : {}) };
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Resolve a persistent run ID using its original retry job, including older runs. */
|
|
30
|
-
async function retryJobForRun(id, root) {
|
|
31
|
-
const { listRuns } = await import('./runs.js');
|
|
32
|
-
const runs = await listRuns(root);
|
|
33
|
-
if (runs.some(run => run.id === id && run.alive)) throw new Error(`Run ${id} is still active. Stop it before retrying.`);
|
|
34
|
-
const directory = path.join(path.resolve(root), 'jobs');
|
|
35
|
-
const entries = await readdir(directory, { withFileTypes: true }).catch(error => {
|
|
36
|
-
if (error.code === 'ENOENT') return [];
|
|
37
|
-
throw error;
|
|
38
|
-
});
|
|
39
|
-
for (const entry of entries.filter(entry => entry.isFile() && /^\d{13}-[a-f0-9-]{36}\.json$/.test(entry.name)).sort((a, b) => b.name.localeCompare(a.name))) {
|
|
40
|
-
const file = path.join(directory, entry.name);
|
|
41
|
-
const job = await readJson(file, null).catch(error => { if (error instanceof SyntaxError) return null; throw error; });
|
|
42
|
-
if (job?.runId === id) return file;
|
|
43
|
-
}
|
|
44
|
-
throw new Error(`No retry job found for run ${id}. Retry jobs may have been removed by veo flush.`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Location of one run's job file. It is created before the first download starts
|
|
49
|
-
* so `veo runs <id>` can report per-item progress from it.
|
|
50
|
-
*/
|
|
51
|
-
export function jobFilePath(root = cacheBase()) {
|
|
52
|
-
return path.join(root, 'jobs', `${Date.now()}-${randomUUID()}.json`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Find the newest retryable job, skipping finished jobs and active runs. */
|
|
56
|
-
export async function latestFailedJob(root = cacheBase()) {
|
|
57
|
-
const directory = path.join(path.resolve(root), 'jobs');
|
|
58
|
-
const info = await lstat(directory).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
59
|
-
if (!info) throw new Error('No retry jobs found. Run a download first.');
|
|
60
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`Invalid retry job directory: ${directory}`);
|
|
61
|
-
const { listRuns } = await import('./runs.js');
|
|
62
|
-
const active = new Set((await listRuns(root)).filter(run => run.alive && run.job).map(run => path.resolve(run.job)));
|
|
63
|
-
const names = (await readdir(directory, { withFileTypes: true }))
|
|
64
|
-
.filter(entry => entry.isFile() && /^\d{13}-[a-f0-9-]{36}\.json$/.test(entry.name))
|
|
65
|
-
.map(entry => entry.name).sort().reverse();
|
|
66
|
-
for (const name of names) {
|
|
67
|
-
const file = path.join(directory, name);
|
|
68
|
-
if (active.has(file)) continue;
|
|
69
|
-
const job = await readJson(file, null).catch(error => { if (error instanceof SyntaxError) return null; throw error; });
|
|
70
|
-
if (job?.version === 1 && job.options && typeof job.options === 'object' && !Array.isArray(job.options)
|
|
71
|
-
&& Array.isArray(job.items) && job.items.every(item => item && typeof item.url === 'string' && typeof item.status === 'string')
|
|
72
|
-
&& job.items.some(item => !['saved', 'skipped'].includes(item.status))) return file;
|
|
73
|
-
}
|
|
74
|
-
throw new Error('No failed or unfinished retry job found.');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function runJob(options, { download, reporter, signal, openFile, stdout = process.stdout, stderr = process.stderr, jobFile, items, recordStats, recordHistory, runId, archiveRoot } = {}) {
|
|
78
|
-
if (options.incognito) {
|
|
79
|
-
runId = undefined;
|
|
80
|
-
recordStats = undefined;
|
|
81
|
-
recordHistory = undefined;
|
|
82
|
-
}
|
|
83
|
-
const concurrency = options.concurrentDownloads ?? 2;
|
|
84
|
-
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 4) throw new Error('Concurrent downloads must be between 1 and 4.');
|
|
85
|
-
const file = jobFile || jobFilePath();
|
|
86
|
-
const requests = items || options.urls.map(url => ({ ...options, url }));
|
|
87
|
-
const job = { version: 1, ...(runId ? { runId } : {}), startedAt: new Date().toISOString(), options: publicOptions({ ...options, output: path.resolve(options.output) }),
|
|
88
|
-
items: requests.map(item => ({ url: item.url, status: 'pending', entries: [] })) };
|
|
89
|
-
// Each retry item can have its own playlist selection.
|
|
90
|
-
job.items.forEach((item, index) => { item.options = publicOptions(requests[index]); });
|
|
91
|
-
const { cleanupDownloadCache } = await import('./download-cache.js');
|
|
92
|
-
if (!options.incognito) await cleanupDownloadCache();
|
|
93
|
-
const privateRoot = options.incognito ? await mkdtemp(path.join(os.tmpdir(), 'veo-incognito-')) : null;
|
|
94
|
-
const queue = serialQueue();
|
|
95
|
-
const persist = () => options.incognito ? Promise.resolve() : queue(() => writeJson(file, job));
|
|
96
|
-
await persist();
|
|
97
|
-
const activeTargets = new Map();
|
|
98
|
-
const totals = { saved: 0, skipped: 0, failed: 0 };
|
|
99
|
-
const adaptiveState = { divisor: 1 };
|
|
100
|
-
const archiveTools = runId ? await import('./run-archive.js') : null;
|
|
101
|
-
try {
|
|
102
|
-
await runPool(requests, () => reducedLimit(concurrency, adaptiveState), async (request, index) => {
|
|
103
|
-
const targetKey = JSON.stringify([request.url, path.resolve(request.output)]);
|
|
104
|
-
const previous = activeTargets.get(targetKey);
|
|
105
|
-
let release;
|
|
106
|
-
const current = new Promise(resolve => { release = resolve; });
|
|
107
|
-
activeTargets.set(targetKey, current);
|
|
108
|
-
try {
|
|
109
|
-
await previous;
|
|
110
|
-
signal?.throwIfAborted();
|
|
111
|
-
let saved = 0, skipped = 0, failed = 0;
|
|
112
|
-
const itemReporter = concurrency > 1 && requests.length > 1 ? reporter.scoped?.(index + 1, requests.length, request.url) || reporter : reporter;
|
|
113
|
-
const item = job.items[index];
|
|
114
|
-
const captureFiles = async files => {
|
|
115
|
-
if (!archiveTools) return;
|
|
116
|
-
item.fingerprints ||= {};
|
|
117
|
-
for (const target of files || []) {
|
|
118
|
-
if (!archiveTools.isMediaFile(target) || item.fingerprints[target]) continue;
|
|
119
|
-
item.fingerprints[target] = await archiveTools.fileFingerprint(target).catch(() => null);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
const started = performance.now();
|
|
123
|
-
const published = new Set();
|
|
124
|
-
let opened = false;
|
|
125
|
-
let title;
|
|
126
|
-
const publish = async files => {
|
|
127
|
-
if (!options.json) for (const target of files) {
|
|
128
|
-
if (!published.has(target)) {
|
|
129
|
-
const display = terminalText(stdout, `Saved: ${cleanText(target)}`);
|
|
130
|
-
stdout.write(styleText(stdout, display.startsWith('Saved: ') ? `Saved: ${folderLink(stdout, target, display.slice(7))}` : display, 'success', options.color !== false) + '\n');
|
|
131
|
-
}
|
|
132
|
-
published.add(target);
|
|
133
|
-
}
|
|
134
|
-
if (request.open && !opened && files.length) {
|
|
135
|
-
opened = true;
|
|
136
|
-
try { await openFile(files[0]); }
|
|
137
|
-
catch (error) { stderr.write(`veo: File saved, but could not launch the default app: ${readableError(error)}\n`); }
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
if (itemReporter === reporter) reporter.item?.(index + 1, requests.length, request.url);
|
|
141
|
-
try {
|
|
142
|
-
item.status = 'running';
|
|
143
|
-
await persist();
|
|
144
|
-
const result = await download(request, { reporter: itemReporter, signal, adaptiveState, skipCacheCleanup: true,
|
|
145
|
-
...(privateRoot ? { localRoot: privateRoot } : {}), onEntry: async entry => {
|
|
146
|
-
await captureFiles(entry.files);
|
|
147
|
-
item.entries.push(entry);
|
|
148
|
-
await persist();
|
|
149
|
-
if (entry.status === 'saved') saved++;
|
|
150
|
-
if (entry.status === 'skipped') skipped++;
|
|
151
|
-
if (entry.status === 'failed') failed++;
|
|
152
|
-
await publish(entry.files || []);
|
|
153
|
-
} });
|
|
154
|
-
await captureFiles(result.files);
|
|
155
|
-
Object.assign(item, { status: result.status || 'saved', files: result.files, timings: result.timings, entryTimings: result.entryTimings, finished: true });
|
|
156
|
-
title = result.title;
|
|
157
|
-
if (!item.entries.length) {
|
|
158
|
-
saved += result.saved ?? (item.status === 'saved' ? 1 : 0);
|
|
159
|
-
skipped += result.skipped ?? 0;
|
|
160
|
-
failed += result.failures?.length ?? (item.status === 'failed' ? 1 : 0);
|
|
161
|
-
}
|
|
162
|
-
if (options.json) stdout.write(`${JSON.stringify({ ...result, status: item.status, profile: request.profile || null, ...(runId ? { runId } : {}) })}\n`);
|
|
163
|
-
await publish(result.files);
|
|
164
|
-
} catch (error) {
|
|
165
|
-
item.status = signal?.aborted ? 'cancelled' : 'failed';
|
|
166
|
-
item.error = readableError(error);
|
|
167
|
-
if (!signal?.aborted) failed++;
|
|
168
|
-
item.files = [...new Set([...item.entries.flatMap(entry => entry.files || []), ...(error.files || [])])];
|
|
169
|
-
if (options.json) stdout.write(`${JSON.stringify({ url: request.url, status: item.status, profile: request.profile || null, error: item.error, files: item.files, ...(runId ? { runId } : {}) })}\n`);
|
|
170
|
-
else stderr.write(styleText(stderr, terminalText(stderr, `veo: ${item.error} (${request.url})`), 'error', options.color !== false) + '\n');
|
|
171
|
-
if (error.verificationFailed && !options.json) for (const file of item.files) stderr.write(`Saved file retained: ${cleanText(file)}\n`);
|
|
172
|
-
}
|
|
173
|
-
if (recordStats) {
|
|
174
|
-
try {
|
|
175
|
-
await recordStats({ videos: request.audio ? 0 : saved,
|
|
176
|
-
audio: request.audio ? saved : 0, failed: failed,
|
|
177
|
-
skipped: skipped, cancelled: item.status === 'cancelled' ? 1 : 0,
|
|
178
|
-
elapsedMs: Math.round(performance.now() - started) });
|
|
179
|
-
} catch (error) { stderr.write(`veo: Could not save statistics: ${readableError(error)}\n`); }
|
|
180
|
-
}
|
|
181
|
-
// History is written per finished item, so `veo history` never reports an
|
|
182
|
-
// attempt that is still running.
|
|
183
|
-
if (recordHistory) {
|
|
184
|
-
try {
|
|
185
|
-
await recordHistory({ url: request.url, title, status: item.status, runId, job: ['failed', 'cancelled'].includes(item.status) ? file : null, audio: Boolean(request.audio),
|
|
186
|
-
quality: request.quality, format: request.format, files: item.files || [], error: item.error,
|
|
187
|
-
elapsedMs: Math.round(performance.now() - started) });
|
|
188
|
-
} catch (error) { stderr.write(`veo: Could not save download history: ${readableError(error)}\n`); }
|
|
189
|
-
}
|
|
190
|
-
await persist();
|
|
191
|
-
totals.saved += saved; totals.skipped += skipped; totals.failed += failed;
|
|
192
|
-
} finally { release(); if (activeTargets.get(targetKey) === current) activeTargets.delete(targetKey); }
|
|
193
|
-
}, { signal }).catch(error => { if (!signal?.aborted) throw error; });
|
|
194
|
-
} finally {
|
|
195
|
-
if (privateRoot) await rm(privateRoot, { recursive: true, force: true });
|
|
196
|
-
}
|
|
197
|
-
const { saved, skipped, failed } = totals;
|
|
198
|
-
if (runId) {
|
|
199
|
-
try {
|
|
200
|
-
await archiveTools.archiveRun(job, { ...(archiveRoot ? { root: archiveRoot } : {}) });
|
|
201
|
-
} catch (error) { stderr.write(`veo: Could not archive run ${runId}: ${readableError(error)}\n`); }
|
|
202
|
-
stderr.write(`Run ID: ${runId}\n`);
|
|
203
|
-
}
|
|
204
|
-
const unfinished = job.items.some(item => !['saved', 'skipped'].includes(item.status));
|
|
205
|
-
const downloadCount = job.items.reduce((count, item) => count + Math.max(1, item.entries?.length || 0), 0);
|
|
206
|
-
if (downloadCount > 1) stderr.write(styleText(stderr, `Summary: ${saved} saved, ${skipped} skipped, ${failed} failed${signal?.aborted ? ', cancelled' : ''}.`, unfinished ? 'error' : 'success', options.color !== false && !options.json) + '\n');
|
|
207
|
-
if (options.timings !== false) {
|
|
208
|
-
const totals = {};
|
|
209
|
-
for (const item of job.items) for (const timings of [item.timings, ...(item.entryTimings || []).map(entry => entry.timings)]) {
|
|
210
|
-
if (!timings) continue;
|
|
211
|
-
for (const [phase, ms] of Object.entries(timings)) totals[phase] = (totals[phase] || 0) + ms;
|
|
212
|
-
}
|
|
213
|
-
if (Object.keys(totals).length) stderr.write(styleText(stderr, formatTimings(totals), 'muted', options.color !== false && !options.json) + '\n');
|
|
214
|
-
}
|
|
215
|
-
if (unfinished && !options.incognito) stderr.write(`Retry failed/unfinished downloads: veo --retry-failed ${runId || `"${file}"`}\n`);
|
|
216
|
-
if (unfinished) reporter.fail(Boolean(signal?.aborted)); else reporter.complete();
|
|
217
|
-
return signal?.aborted ? 130 : unfinished ? 1 : 0;
|
|
218
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// Compatibility data for comments generated by the former German template.
|
|
2
|
-
// These strings are recognized during migration, never emitted as new UI text.
|
|
3
|
-
const translations = new Map([
|
|
4
|
-
[
|
|
5
|
-
"// veo: kommentierte Konfigurationsvorlage",
|
|
6
|
-
"// veo: commented configuration template"
|
|
7
|
-
],
|
|
8
|
-
[
|
|
9
|
-
"// Speichern und den Editor schliessen. Beim naechsten Aufruf gelten die Einstellungen.",
|
|
10
|
-
"// Save and close the editor. Settings apply the next time you run veo."
|
|
11
|
-
],
|
|
12
|
-
[
|
|
13
|
-
"// Kommentare mit // oder /* ... */ sind erlaubt. CLI-Optionen haben Vorrang.",
|
|
14
|
-
"// Comments using // or /* ... */ are supported. Command-line options take priority."
|
|
15
|
-
],
|
|
16
|
-
[
|
|
17
|
-
"// Option aktivieren: die beiden // davor entfernen und den Wert anpassen.",
|
|
18
|
-
"// To enable an option, remove its leading // and adjust the value."
|
|
19
|
-
],
|
|
20
|
-
[
|
|
21
|
-
"// Zwischen aktiven Eintraegen steht ein Komma, nach dem letzten keines.",
|
|
22
|
-
"// Separate active entries with commas. Do not add a comma after the last entry."
|
|
23
|
-
],
|
|
24
|
-
[
|
|
25
|
-
"// Zielordner: / oder doppelte Backslashes verwenden, z.B. \"D:\\\\Videos\".",
|
|
26
|
-
"// Output directory: use / or double backslashes, for example \"D:\\\\Videos\"."
|
|
27
|
-
],
|
|
28
|
-
[
|
|
29
|
-
"// Ohne output wird im aktuellen Arbeitsordner gespeichert.",
|
|
30
|
-
"// Without output, downloads are saved in the current working directory."
|
|
31
|
-
],
|
|
32
|
-
[
|
|
33
|
-
"// Maximale Videoaufloesung: best, 2160p, 1440p, 1080p, 720p, ...",
|
|
34
|
-
"// Maximum video resolution: best, 2160p, 1440p, 1080p, 720p, ..."
|
|
35
|
-
],
|
|
36
|
-
[
|
|
37
|
-
"// Videoformat: mp4, mkv, webm, mov. Konvertierung kann Zeit brauchen.",
|
|
38
|
-
"// Video format: mp4, mkv, webm, mov. Conversion can take time."
|
|
39
|
-
],
|
|
40
|
-
[
|
|
41
|
-
"// \"open\": false, // Fertige Datei automatisch oeffnen",
|
|
42
|
-
"// \"open\": false, // Open the completed file automatically"
|
|
43
|
-
],
|
|
44
|
-
[
|
|
45
|
-
"// \"resume\": true, // Abgebrochene Downloads fortsetzen",
|
|
46
|
-
"// \"resume\": true, // Resume interrupted downloads"
|
|
47
|
-
],
|
|
48
|
-
[
|
|
49
|
-
"// \"skipExisting\": true, // Bereits gespeicherte Downloads ueberspringen",
|
|
50
|
-
"// \"skipExisting\": true, // Skip previously saved downloads"
|
|
51
|
-
],
|
|
52
|
-
[
|
|
53
|
-
"// \"concurrentFragments\": 4, // Gleichzeitige Fragmente: 1 bis 16",
|
|
54
|
-
"// \"concurrentFragments\": 4, // Concurrent fragments: 1 to 16"
|
|
55
|
-
],
|
|
56
|
-
[
|
|
57
|
-
"// Untertitel und Zusatzinformationen:",
|
|
58
|
-
"// Subtitles and additional information:"
|
|
59
|
-
],
|
|
60
|
-
[
|
|
61
|
-
"// \"subLangs\": \"de,en\", // Aktiviert Untertitel fuer diese Sprachen",
|
|
62
|
-
"// \"subLangs\": \"de,en\", // Enable subtitles for these languages"
|
|
63
|
-
],
|
|
64
|
-
[
|
|
65
|
-
"// \"embedSubs\": true, // Untertitel ins Video einbetten",
|
|
66
|
-
"// \"embedSubs\": true, // Embed subtitles in the video"
|
|
67
|
-
],
|
|
68
|
-
[
|
|
69
|
-
"// \"embedMetadata\": true, // Titel, Datum usw. einbetten",
|
|
70
|
-
"// \"embedMetadata\": true, // Embed the title, date and other metadata"
|
|
71
|
-
],
|
|
72
|
-
[
|
|
73
|
-
"// \"embedThumbnail\": true, // Vorschaubild einbetten",
|
|
74
|
-
"// \"embedThumbnail\": true, // Embed the thumbnail"
|
|
75
|
-
],
|
|
76
|
-
[
|
|
77
|
-
"// Profile werden nur mit --profile NAME oder im interaktiven Dialog aktiviert.",
|
|
78
|
-
"// Profiles are activated only with --profile NAME or in the interactive wizard."
|
|
79
|
-
],
|
|
80
|
-
[
|
|
81
|
-
"// Beispiel: veo \"https://example.com/video.mp4\" --profile musik",
|
|
82
|
-
"// Example: veo \"https://example.com/video.mp4\" --profile music"
|
|
83
|
-
],
|
|
84
|
-
[
|
|
85
|
-
"// Nur Audio. Formate: mp3, m4a, aac, opus, flac, wav.",
|
|
86
|
-
"// Audio only. Formats: mp3, m4a, aac, opus, flac, wav."
|
|
87
|
-
],
|
|
88
|
-
[
|
|
89
|
-
"// Referenz: Beispiele bei Bedarf in die bestehende Konfiguration unten uebernehmen.",
|
|
90
|
-
"// Reference: copy any examples you need into your existing configuration below."
|
|
91
|
-
],
|
|
92
|
-
[
|
|
93
|
-
"// Deine bestehenden Einstellungen:",
|
|
94
|
-
"// Your existing settings:"
|
|
95
|
-
]
|
|
96
|
-
]);
|
|
97
|
-
|
|
98
|
-
export function translateLegacyConfigComments(text) {
|
|
99
|
-
if (!text.replace(/^\uFEFF/, '').startsWith('// veo: kommentierte Konfigurationsvorlage')) return text;
|
|
100
|
-
return text.replace(/^([ \t]*(?:\/\/[ \t]*)?)(\/\/[^\r\n]*)/gm, (line, prefix, comment) => {
|
|
101
|
-
const translated = translations.get(comment);
|
|
102
|
-
if (translated) return prefix + translated.replace('--profile music', '--profile NAME');
|
|
103
|
-
// Profile names in the commented reference are examples, not active settings.
|
|
104
|
-
return prefix + comment.replace(/^(\/\/[ \t]*")musik("\s*:)/, '$1music$2').replace(/^(\/\/[ \t]*")archiv("\s*:)/, '$1archive$2');
|
|
105
|
-
});
|
|
106
|
-
}
|