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/compatibility.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { rename, rm } from 'node:fs/promises';
|
|
3
|
-
|
|
4
|
-
export async function ensureCompatibility(file, { backend, runner, signal, convert = false, reporter }) {
|
|
5
|
-
const executable = name => path.join(backend.ffmpegLocation, name + (process.platform === 'win32' ? '.exe' : ''));
|
|
6
|
-
let streams;
|
|
7
|
-
try {
|
|
8
|
-
const raw = await runner(executable('ffprobe'), ['-v', 'error', '-show_streams', '-of', 'json', file], { signal });
|
|
9
|
-
streams = JSON.parse(raw).streams;
|
|
10
|
-
if (!Array.isArray(streams)) throw new Error('No stream metadata');
|
|
11
|
-
} catch (error) {
|
|
12
|
-
if (signal?.aborted || convert) throw error;
|
|
13
|
-
reporter?.status('Playback compatibility could not be checked; original media retained.');
|
|
14
|
-
return file;
|
|
15
|
-
}
|
|
16
|
-
const video = streams.find(stream => stream.codec_type === 'video' && !stream.disposition?.attached_pic);
|
|
17
|
-
const audio = streams.filter(stream => stream.codec_type === 'audio');
|
|
18
|
-
if (!video) return file;
|
|
19
|
-
const videoOK = video.codec_name === 'h264' && video.pix_fmt === 'yuv420p';
|
|
20
|
-
const audioOK = audio.every(stream => stream.codec_name === 'aac');
|
|
21
|
-
if (!convert) {
|
|
22
|
-
if (!videoOK || !audioOK) reporter?.status(`Playback note: ${video.codec_name}/${audio.map(stream => stream.codec_name).join(',') || 'no audio'} may require additional player codecs. Original quality retained; use --compatible for H.264/AAC conversion.`);
|
|
23
|
-
return file;
|
|
24
|
-
}
|
|
25
|
-
if (videoOK && audioOK && path.extname(file) === '.mp4') return file;
|
|
26
|
-
if (!videoOK && (video.color_transfer === 'smpte2084' || video.color_transfer === 'arib-std-b67')) throw new Error('HDR conversion needs tone mapping; original download retained. Use an HDR-capable player or disable --compatible.');
|
|
27
|
-
reporter?.status(videoOK && audioOK ? 'Preparing compatible MP4 without re-encoding…' : 'Converting to compatible H.264/AAC; this takes time and may lose quality…');
|
|
28
|
-
const temporary = path.join(path.dirname(file), 'compatible-output.mp4');
|
|
29
|
-
const target = path.join(path.dirname(file), 'media.mp4');
|
|
30
|
-
try {
|
|
31
|
-
await runner(executable('ffmpeg'), ['-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-i', file,
|
|
32
|
-
'-map', '0:v:0', '-map', '0:a?', '-map_metadata', '0',
|
|
33
|
-
'-c:v', videoOK ? 'copy' : 'libx264', ...(!videoOK ? ['-preset', 'fast', '-crf', '18', '-pix_fmt', 'yuv420p'] : []),
|
|
34
|
-
'-c:a', audioOK ? 'copy' : 'aac', ...(!audioOK ? ['-b:a', '192k'] : []), '-movflags', '+faststart', temporary], { signal });
|
|
35
|
-
await rename(temporary, target);
|
|
36
|
-
if (file !== target) await rm(file);
|
|
37
|
-
return target;
|
|
38
|
-
} finally { await rm(temporary, { force: true }); }
|
|
39
|
-
}
|
package/src/config-clipboard.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
const MAX_TEXT = 2 * 1024 * 1024;
|
|
4
|
-
|
|
5
|
-
// Prefer the clipboard for the active display server, then try the other common
|
|
6
|
-
// Linux tools. A terminal reached over SSH may have neither display available.
|
|
7
|
-
export function clipboardCommands(operation, { platform = process.platform, env = process.env } = {}) {
|
|
8
|
-
if (platform === 'win32') {
|
|
9
|
-
const setup = 'Add-Type -AssemblyName System.Windows.Forms; [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ';
|
|
10
|
-
return [['powershell.exe', ['-NoProfile', '-NonInteractive', '-Sta', '-Command', setup + (operation === 'copy'
|
|
11
|
-
? '[System.Windows.Forms.Clipboard]::SetText([Console]::In.ReadToEnd())'
|
|
12
|
-
: '[Console]::Out.Write([System.Windows.Forms.Clipboard]::GetText())')]]];
|
|
13
|
-
}
|
|
14
|
-
if (platform === 'darwin') return [[operation === 'copy' ? 'pbcopy' : 'pbpaste', []]];
|
|
15
|
-
if (platform === 'android') return [[operation === 'copy' ? 'termux-clipboard-set' : 'termux-clipboard-get', []]];
|
|
16
|
-
const wayland = [operation === 'copy' ? 'wl-copy' : 'wl-paste', operation === 'copy' ? [] : ['--no-newline']];
|
|
17
|
-
const xclip = ['xclip', ['-selection', 'clipboard', operation === 'copy' ? '-in' : '-out']];
|
|
18
|
-
const xsel = ['xsel', ['--clipboard', operation === 'copy' ? '--input' : '--output']];
|
|
19
|
-
const x11 = [xclip, xsel];
|
|
20
|
-
return env.WAYLAND_DISPLAY ? [wayland, ...x11] : env.DISPLAY ? [...x11, wayland] : [wayland, ...x11];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function runCommand([program, args], operation, value) {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
// Clipboard owners such as xclip and wl-copy may continue in the background.
|
|
26
|
-
// Do not keep their inherited output pipes open after the command exits.
|
|
27
|
-
const child = spawn(program, args, {
|
|
28
|
-
shell: false, windowsHide: true,
|
|
29
|
-
stdio: operation === 'copy' ? ['pipe', 'ignore', 'ignore'] : ['ignore', 'pipe', 'ignore'],
|
|
30
|
-
});
|
|
31
|
-
const timer = setTimeout(() => child.kill(), 5000);
|
|
32
|
-
const chunks = [];
|
|
33
|
-
let size = 0, failed = false;
|
|
34
|
-
child.stdout?.on('data', chunk => {
|
|
35
|
-
size += chunk.length;
|
|
36
|
-
if (size > MAX_TEXT) child.kill();
|
|
37
|
-
else chunks.push(chunk);
|
|
38
|
-
});
|
|
39
|
-
child.stdin?.on('error', () => {});
|
|
40
|
-
child.on('error', () => { failed = true; });
|
|
41
|
-
child.on('close', code => {
|
|
42
|
-
clearTimeout(timer);
|
|
43
|
-
if (failed || code !== 0 || size > MAX_TEXT) reject(new Error('Clipboard command failed.'));
|
|
44
|
-
else resolve(operation === 'paste' ? Buffer.concat(chunks).toString('utf8') : undefined);
|
|
45
|
-
});
|
|
46
|
-
child.stdin?.end(value);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function clipboardText(operation, value = '', options = {}) {
|
|
51
|
-
if (Buffer.byteLength(value, 'utf8') > MAX_TEXT) throw new Error('Selection is too large for the editor clipboard.');
|
|
52
|
-
const candidates = clipboardCommands(operation, options);
|
|
53
|
-
for (const candidate of candidates) {
|
|
54
|
-
try { return await (options.run || runCommand)(candidate, operation, value); }
|
|
55
|
-
catch { /* Try the next available display-server clipboard. */ }
|
|
56
|
-
}
|
|
57
|
-
const platform = options.platform || process.platform;
|
|
58
|
-
const hint = platform === 'linux'
|
|
59
|
-
? ' Install wl-clipboard, xclip or xsel and use a graphical session.'
|
|
60
|
-
: platform === 'android' ? ' Install the Termux:API app and pkg install termux-api.' : '';
|
|
61
|
-
throw new Error(`System clipboard is unavailable.${hint}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const configClipboard = {
|
|
65
|
-
copy: value => clipboardText('copy', value),
|
|
66
|
-
paste: () => clipboardText('paste'),
|
|
67
|
-
};
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { CONFIG_KEYS, RESERVED_PROFILE_NAMES, parseConfigText, effectiveConfig, applyProfile } from './config.js';
|
|
2
|
-
import { stripConfigComments } from './config-template.js';
|
|
3
|
-
import { QUALITIES, AUDIO_FORMATS, VIDEO_FORMATS } from './utils.js';
|
|
4
|
-
|
|
5
|
-
// Parse only after JSON.parse succeeds. Keep paths and source spans, including
|
|
6
|
-
// escaped property names and repeated settings in different profiles.
|
|
7
|
-
export function configSource(text) {
|
|
8
|
-
const clean = stripConfigComments(text);
|
|
9
|
-
const data = JSON.parse(clean), nodes = new Map();
|
|
10
|
-
const tokens = [...clean.matchAll(/"(?:\\.|[^"\\])*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}\[\]:,]/g)];
|
|
11
|
-
let index = 0;
|
|
12
|
-
const bom = text.startsWith('\uFEFF') ? 1 : 0;
|
|
13
|
-
const visit = (path, keyToken) => {
|
|
14
|
-
const first = tokens[index++];
|
|
15
|
-
if (first[0] === '{') {
|
|
16
|
-
while (tokens[index][0] !== '}') {
|
|
17
|
-
const key = tokens[index++]; index++; // colon
|
|
18
|
-
visit([...path, JSON.parse(key[0])], key);
|
|
19
|
-
if (tokens[index][0] === ',') index++;
|
|
20
|
-
}
|
|
21
|
-
index++;
|
|
22
|
-
} else if (first[0] === '[') {
|
|
23
|
-
let item = 0;
|
|
24
|
-
while (tokens[index][0] !== ']') {
|
|
25
|
-
visit([...path, item++]);
|
|
26
|
-
if (tokens[index][0] === ',') index++;
|
|
27
|
-
}
|
|
28
|
-
index++;
|
|
29
|
-
}
|
|
30
|
-
const last = tokens[index - 1];
|
|
31
|
-
nodes.set(JSON.stringify(path), { path, start: first.index + bom, end: last.index + last[0].length + bom,
|
|
32
|
-
keyStart: keyToken?.index + bom, keyEnd: keyToken ? keyToken.index + keyToken[0].length + bom : undefined });
|
|
33
|
-
};
|
|
34
|
-
visit([]);
|
|
35
|
-
return { data, nodes };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function distance(a, b) {
|
|
39
|
-
let row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
40
|
-
for (let i = 0; i < a.length; i++) {
|
|
41
|
-
const next = [i + 1];
|
|
42
|
-
for (let j = 0; j < b.length; j++) next.push(Math.min(next[j] + 1, row[j + 1] + 1, row[j] + (a[i] !== b[j])));
|
|
43
|
-
row = next;
|
|
44
|
-
}
|
|
45
|
-
return row[b.length];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const keys = Object.keys(CONFIG_KEYS);
|
|
49
|
-
function suggestion(key, choices) {
|
|
50
|
-
const ranked = choices.map(value => [value, distance(key.toLowerCase(), value.toLowerCase())]).sort((a, b) => a[1] - b[1]);
|
|
51
|
-
return ranked[0]?.[1] <= 2 ? ranked[0][0] : null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function located(text, node, message, property = false, choices = []) {
|
|
55
|
-
const start = (property ? node?.keyStart : node?.start) ?? 0;
|
|
56
|
-
const end = (property ? node?.keyEnd : node?.end) ?? start + 1;
|
|
57
|
-
return { message, start, end, line: text.slice(0, start).split('\n').length,
|
|
58
|
-
column: start - text.lastIndexOf('\n', start - 1), choices };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function valueChoices(key, config = {}) {
|
|
62
|
-
if (CONFIG_KEYS[key] === 'boolean') return [true, false];
|
|
63
|
-
if (key === 'quality') return QUALITIES;
|
|
64
|
-
if (key === 'format') return config.audio ? AUDIO_FORMATS : VIDEO_FORMATS;
|
|
65
|
-
if (key === 'timeout') return ['30s', '45s', '1m', '2m', '5m'];
|
|
66
|
-
if (['concurrentDownloads', 'playlistConcurrency'].includes(key)) return [1, 2, 3, 4];
|
|
67
|
-
if (key === 'concurrentFragments') return Array.from({ length: 64 }, (_, i) => i + 1);
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function semanticIssue(text) {
|
|
72
|
-
const { data, nodes } = configSource(text);
|
|
73
|
-
const node = path => nodes.get(JSON.stringify(path));
|
|
74
|
-
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
75
|
-
if (!object(data)) return located(text, node([]), 'The config must contain a JSON object.');
|
|
76
|
-
const check = (settings, path) => {
|
|
77
|
-
for (const [key, value] of Object.entries(settings)) {
|
|
78
|
-
if (!path.length && ['$schema', 'profiles', 'activeProfile'].includes(key)) continue;
|
|
79
|
-
const target = node([...path, key]);
|
|
80
|
-
if (!Object.hasOwn(CONFIG_KEYS, key)) {
|
|
81
|
-
const proposed = suggestion(key, path.length ? keys : [...keys, 'profiles', '$schema']);
|
|
82
|
-
return located(text, target, `Unknown setting "${key}".${proposed ? ` Did you mean "${proposed}"?` : ''}`, true, proposed ? [proposed] : []);
|
|
83
|
-
}
|
|
84
|
-
if (typeof value !== CONFIG_KEYS[key]) {
|
|
85
|
-
const choices = valueChoices(key, settings);
|
|
86
|
-
return located(text, target, `"${key}" must be a ${CONFIG_KEYS[key]}.${choices.length ? ` Choose: ${choices.join(', ')}.` : ''}`, false, choices);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
};
|
|
91
|
-
let issue = check(data, []);
|
|
92
|
-
if (issue) return issue;
|
|
93
|
-
if (Object.hasOwn(data, 'profiles')) {
|
|
94
|
-
if (!object(data.profiles)) return located(text, node(['profiles']), 'Profiles must be an object.');
|
|
95
|
-
for (const [name, profile] of Object.entries(data.profiles)) {
|
|
96
|
-
if (RESERVED_PROFILE_NAMES.includes(name)) {
|
|
97
|
-
return located(text, node(['profiles', name]), `Profile name "${name}" is reserved for veo profile ${name}. Rename this profile.`, true);
|
|
98
|
-
}
|
|
99
|
-
if (!object(profile)) return located(text, node(['profiles', name]), `Profile "${name}" must be an object.`);
|
|
100
|
-
issue = check(profile, ['profiles', name]); if (issue) return issue;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (Object.hasOwn(data, 'activeProfile')) {
|
|
104
|
-
if (typeof data.activeProfile !== 'string' || !data.activeProfile) return located(text, node(['activeProfile']), '"activeProfile" must be a non-empty profile name.');
|
|
105
|
-
if (!object(data.profiles) || !Object.hasOwn(data.profiles, data.activeProfile)) {
|
|
106
|
-
return located(text, node(['activeProfile']), `Unknown active profile "${data.activeProfile}". Choose: ${Object.keys(data.profiles || {}).join(', ') || 'none'}.`, false, Object.keys(data.profiles || {}));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const { config } = parseConfigText(text);
|
|
110
|
-
for (const profile of [undefined, ...Object.keys(config.profiles || {})]) {
|
|
111
|
-
try { await effectiveConfig(config, profile); }
|
|
112
|
-
catch (error) {
|
|
113
|
-
const message = error.message;
|
|
114
|
-
let key = /Invalid quality/.test(message) ? 'quality' : /Invalid (?:audio|video) format/.test(message) ? 'format' :
|
|
115
|
-
/output directory/.test(message) ? 'output' : /custom filename/.test(message) ? 'rename' :
|
|
116
|
-
/--([a-z-]+)/.exec(message)?.[1]?.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
117
|
-
const name = profile ?? config.activeProfile ?? (config.profiles?.default ? 'default' : undefined);
|
|
118
|
-
const selected = applyProfile(config, profile);
|
|
119
|
-
// Template errors do not name the CLI option. Locate the failing template.
|
|
120
|
-
if (!key) {
|
|
121
|
-
const { validateTemplate } = await import('./naming.js');
|
|
122
|
-
for (const candidate of ['filenameTemplate', 'folderTemplate']) {
|
|
123
|
-
if (selected[candidate] !== undefined) {
|
|
124
|
-
try { validateTemplate(selected[candidate], { folders: candidate === 'folderTemplate' }); }
|
|
125
|
-
catch { key = candidate; break; }
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const target = name !== undefined && Object.hasOwn(config.profiles[name], key) ? node(['profiles', name, key]) : node([key]);
|
|
130
|
-
const value = ['quality', 'format'].includes(key) ? `"${key}": unsupported value ${JSON.stringify(selected[key])}. ` : '';
|
|
131
|
-
return located(text, target || node([]), `${name === undefined ? '' : `Profile "${name}": `}${value}${message}`, false, valueChoices(key, selected));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function editorCompletions(text, cursor) {
|
|
138
|
-
try {
|
|
139
|
-
const { data, nodes } = configSource(text);
|
|
140
|
-
for (const item of nodes.values()) {
|
|
141
|
-
const path = item.path;
|
|
142
|
-
if (!(path.length === 1 || (path.length === 3 && path[0] === 'profiles'))) continue;
|
|
143
|
-
const key = path.at(-1), property = cursor >= item.keyStart && cursor <= item.keyEnd;
|
|
144
|
-
if (!property && !(cursor >= item.start && cursor <= item.end)) continue;
|
|
145
|
-
const profile = path.length === 3 ? path[1] : undefined;
|
|
146
|
-
const choices = property ? (path.length === 1 ? [...keys, 'profiles', 'activeProfile', '$schema'] : keys) :
|
|
147
|
-
key === 'activeProfile' ? Object.keys(data.profiles || {}) : valueChoices(key, applyProfile(data, profile));
|
|
148
|
-
if (!choices.length) return null;
|
|
149
|
-
const proposed = property ? suggestion(key, choices) : null;
|
|
150
|
-
return { start: property ? item.keyStart : item.start, end: property ? item.keyEnd : item.end,
|
|
151
|
-
choices: proposed ? [proposed, ...choices.filter(value => value !== proposed)] : choices };
|
|
152
|
-
}
|
|
153
|
-
} catch { /* Syntax must be valid before offering safe token replacements. */ }
|
|
154
|
-
return null;
|
|
155
|
-
}
|