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/config-template.js
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
export const TEMPLATE_MARKER = '// veo: commented configuration template';
|
|
4
|
-
|
|
5
|
-
export const SOURCE_OPTIONS_GUIDE = String.raw`// veo: source discovery options v1
|
|
6
|
-
// Copy these settings into the global object or a named profile if desired.
|
|
7
|
-
// "deepScan": true, // Verify every observed media candidate instead of the first 30
|
|
8
|
-
// "timeout": "2m", // Discovery deadline: 5s to 10m; plain numbers mean seconds
|
|
9
|
-
// "listSources": false, // true = list sources and exit for single-URL calls
|
|
10
|
-
// "autoListSources": true, // Search after extraction fails; false disables fallback
|
|
11
|
-
// Command-line flags override these values, including --no-deep-scan and --no-list-sources.
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
|
-
const PROFILE_OPTIONS_MARKER = '// veo: profile download options v5';
|
|
15
|
-
export const PROFILE_OPTIONS_GUIDE = String.raw`${PROFILE_OPTIONS_MARKER}
|
|
16
|
-
// Any global option can also be placed inside a profile.
|
|
17
|
-
// Copy the settings you want into "profiles" > "default" or a named profile.
|
|
18
|
-
// Remove the leading // on copied settings; separate entries with commas.
|
|
19
|
-
// Speed without reducing the selected video quality:
|
|
20
|
-
// "quality": "best",
|
|
21
|
-
// "concurrentDownloads": 2, // Parallel URLs/batch items: 1-4; default 2
|
|
22
|
-
// "adaptiveConcurrency": true, // Reduce parallelism and retry transient errors
|
|
23
|
-
// "concurrentFragments": 8, // DASH/HLS fragments: 1-64; default 8
|
|
24
|
-
// Values 17-64 are experimental and require terminal confirmation before download.
|
|
25
|
-
// "experimentalFragments": false, // true explicitly accepts 17-64, including in scripts
|
|
26
|
-
// More connections may trigger server limits and do not guarantee better speed.
|
|
27
|
-
// "deepScan": true, // Check every observed source candidate
|
|
28
|
-
// "timeout": "2m", // Source-search deadline, 5s to 10m
|
|
29
|
-
// "listSources": false, // List sources and exit for one URL
|
|
30
|
-
// "autoListSources": true, // Search after extraction fails (default)
|
|
31
|
-
// "playlist": true, // Enable only for playlist downloads
|
|
32
|
-
// "playlistConcurrency": 2, // Simultaneous entries: 1-4; default 2
|
|
33
|
-
// "checkSpace": true, // Estimate free cache/output space first
|
|
34
|
-
// "timings": true, // Show time spent in each phase
|
|
35
|
-
// "color": true, // Gray details and colored results; false = plain text
|
|
36
|
-
// Applies to veo's own terminal output, including stats/history/doctor.
|
|
37
|
-
// Each profile can set color independently; NO_COLOR always disables styling.
|
|
38
|
-
// Example inside profiles: "plain": { "color": false }
|
|
39
|
-
// Use: veo stats --profile plain or veo URL --profile plain
|
|
40
|
-
// "folderTemplate": "{channel}/{year}",
|
|
41
|
-
// "filenameTemplate": "{index} - {title}", // Without extension; do not combine with rename
|
|
42
|
-
// "resume": true,
|
|
43
|
-
// "incognito": false, // Save only media; no run ID, history, stats or retry data
|
|
44
|
-
// "neutralFilename": false, // Optional random filenames; ignores rename and naming templates
|
|
45
|
-
// "skipExisting": true
|
|
46
|
-
// "verify": false, // Probe saved media with FFprobe; no full decode
|
|
47
|
-
// Lossless container change (the selected codecs must fit the container):
|
|
48
|
-
// "format": "mkv",
|
|
49
|
-
// "compatible": false, // Opt-in H.264/AAC MP4; converts only when needed
|
|
50
|
-
// "recode": false
|
|
51
|
-
// Compatibility profile to add inside profiles:
|
|
52
|
-
// "kompatibel": { "audio": false, "format": "mp4", "compatible": true, "recode": false }
|
|
53
|
-
// Use: veo URL --profile kompatibel (or simply veo URL --compatible).
|
|
54
|
-
// Optional conversion instead: "format": "webm", "recode": true
|
|
55
|
-
// Conversion can be slower and lose quality; recode is for video only.
|
|
56
|
-
// Sequential downloads: "concurrentDownloads": 1, "concurrentFragments": 1, "playlistConcurrency": 1
|
|
57
|
-
// Example named profile inside the profiles object:
|
|
58
|
-
// "fast": { "quality": "best", "concurrentFragments": 8, "playlistConcurrency": 2, "resume": true }
|
|
59
|
-
// Use with: veo URL --profile fast (add --playlist for a playlist URL).
|
|
60
|
-
// Inspect: veo config show --profile fast | Validate: veo config check
|
|
61
|
-
// Template fields: {title}, {id}, {channel}, {year}, {playlist}, {index}.
|
|
62
|
-
// Missing values use Unknown channel/year, No playlist, unknown ID, index 001.
|
|
63
|
-
`;
|
|
64
|
-
|
|
65
|
-
const TEMPLATE_SOURCE = String.raw`${TEMPLATE_MARKER}
|
|
66
|
-
// Save and close the editor. Settings apply the next time you run veo.
|
|
67
|
-
// Comments using // or /* ... */ are supported. Command-line options take priority.
|
|
68
|
-
// To enable an option, remove its leading // and adjust the value.
|
|
69
|
-
// Separate active entries with commas. Do not add a comma after the last entry.
|
|
70
|
-
{
|
|
71
|
-
// Output directory: use / or double backslashes, for example "D:\\Videos".
|
|
72
|
-
// Without output, downloads are saved in the current working directory.
|
|
73
|
-
// "output": "D:/Videos",
|
|
74
|
-
// Filename without extension. Every * inserts the original video title.
|
|
75
|
-
// "rename": "movie_*",
|
|
76
|
-
|
|
77
|
-
// Maximum video resolution: best, 2160p, 1440p, 1080p, 720p, ...
|
|
78
|
-
// "quality": "1080p",
|
|
79
|
-
// "closestQuality": false, // Allow the nearest available resolution
|
|
80
|
-
// Video format: mp4, mkv, webm, mov. Lossless remux; codecs must fit the container.
|
|
81
|
-
// "format": "mp4",
|
|
82
|
-
// "recode": false, // Explicit video conversion; may lose quality
|
|
83
|
-
// "playlistConcurrency": 2, // Concurrent playlist entries: 1 to 4
|
|
84
|
-
// "playlistItems": "1-5", // Download only these playlist entries
|
|
85
|
-
// "section": "*10:00-12:00", // Download only a time range
|
|
86
|
-
// "open": false, // Open the completed file automatically
|
|
87
|
-
// "resume": true, // Resume interrupted downloads
|
|
88
|
-
// "incognito": true, // Save only media; no run ID, history, stats or retry data
|
|
89
|
-
// "neutralFilename": false, // Set true for random filenames without title or template folders
|
|
90
|
-
// "skipExisting": true, // Skip previously saved downloads
|
|
91
|
-
// "verify": false, // Check saved media container and tracks with FFprobe
|
|
92
|
-
// "concurrentFragments": 8, // Concurrent fragments: 1 to 64; default 8
|
|
93
|
-
// Values above 16 require confirmation or the explicit opt-in below.
|
|
94
|
-
// "experimentalFragments": false, // true accepts experimental values 17-64
|
|
95
|
-
// "cookies": "D:/cookies.txt", // Netscape-format cookies file
|
|
96
|
-
// "cookiesFromBrowser": "firefox", // Use your browser's cookies
|
|
97
|
-
// "sponsorblockRemove": "sponsor", // Remove matching SponsorBlock segments
|
|
98
|
-
// "json": false, // Print machine-readable output for downloads
|
|
99
|
-
|
|
100
|
-
${SOURCE_OPTIONS_GUIDE}
|
|
101
|
-
|
|
102
|
-
// Subtitles and additional information:
|
|
103
|
-
// "subs": true, // Download subtitles; defaults to English
|
|
104
|
-
// "autoSubs": false, // Include automatically generated subtitles
|
|
105
|
-
// "subLangs": "de,en", // Enable subtitles for these languages
|
|
106
|
-
// "subFormat": "srt/vtt/best", // Prefer these available subtitle formats
|
|
107
|
-
// "embedSubs": true, // Embed subtitles in the video
|
|
108
|
-
// "embedMetadata": true, // Embed the title, date and other metadata
|
|
109
|
-
// "embedThumbnail": true, // Embed the thumbnail
|
|
110
|
-
|
|
111
|
-
// The default profile is used automatically unless you select another profile.
|
|
112
|
-
// Use veo profile NAME to make any named profile the default for future commands.
|
|
113
|
-
// The active choice is stored at the top level: "activeProfile": "music",
|
|
114
|
-
${PROFILE_OPTIONS_GUIDE}
|
|
115
|
-
// Select other profiles with --profile NAME or in the interactive wizard.
|
|
116
|
-
// Example: veo "https://example.com/video.mp4" --profile music
|
|
117
|
-
"profiles": {
|
|
118
|
-
"default": {
|
|
119
|
-
// "color": true, // Set false to disable terminal styling for this profile.
|
|
120
|
-
// Add everyday defaults here, for example: "quality": "1080p"
|
|
121
|
-
// Empty means use the global settings above and veo's built-in defaults.
|
|
122
|
-
},
|
|
123
|
-
"kompatibel": {
|
|
124
|
-
"audio": false,
|
|
125
|
-
"format": "mp4",
|
|
126
|
-
"compatible": true,
|
|
127
|
-
"recode": false
|
|
128
|
-
},
|
|
129
|
-
"music": {
|
|
130
|
-
// Audio only. Formats: mp3, m4a, aac, opus, flac, wav.
|
|
131
|
-
"audio": true,
|
|
132
|
-
"format": "mp3"
|
|
133
|
-
},
|
|
134
|
-
"archive": {
|
|
135
|
-
"quality": "1080p",
|
|
136
|
-
"embedMetadata": true,
|
|
137
|
-
"subLangs": "de,en"
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
`;
|
|
142
|
-
|
|
143
|
-
// The guide revision follows its content so the editor can offer an update.
|
|
144
|
-
const TEMPLATE_REVISION = createHash('sha256').update(TEMPLATE_SOURCE).digest('hex').slice(0, 12);
|
|
145
|
-
const REFERENCE_BEGIN = '// veo: template reference begin';
|
|
146
|
-
const REFERENCE_END = '// veo: template reference end';
|
|
147
|
-
export const CONFIG_TEMPLATE = `{
|
|
148
|
-
"profiles": {
|
|
149
|
-
"default": {},
|
|
150
|
-
"kompatibel": { "audio": false, "format": "mp4", "compatible": true, "recode": false },
|
|
151
|
-
"music": { "audio": true, "format": "mp3" },
|
|
152
|
-
"archive": { "quality": "1080p", "embedMetadata": true, "subLangs": "de,en" }
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
`;
|
|
156
|
-
|
|
157
|
-
// Kept in the program. It is inserted into a config only when requested.
|
|
158
|
-
export const CONFIG_GUIDE = `${REFERENCE_BEGIN} ${TEMPLATE_REVISION}\n// Current veo options. Copy only the settings you want into the JSON below.\n${TEMPLATE_SOURCE.split('\n').slice(1).map(line => line.startsWith('//') ? line : `// ${line}`).join('\n').trimEnd()}\n${REFERENCE_END}\n`;
|
|
159
|
-
|
|
160
|
-
function generatedRange(text) {
|
|
161
|
-
const begin = text.indexOf(REFERENCE_BEGIN);
|
|
162
|
-
const end = begin < 0 ? -1 : text.indexOf(REFERENCE_END, begin);
|
|
163
|
-
if (begin < 0) {
|
|
164
|
-
const legacyHeader = /^\uFEFF?\/\/ veo: commented configuration template\r?\n\/\/ Reference: copy any examples/;
|
|
165
|
-
const settingsMarker = '// Your existing settings:';
|
|
166
|
-
const markerAt = legacyHeader.test(text) ? text.indexOf(settingsMarker) : -1;
|
|
167
|
-
if (markerAt < 0) return null;
|
|
168
|
-
const lineEnd = text.indexOf('\n', markerAt);
|
|
169
|
-
return { start: 0, stop: lineEnd < 0 ? text.length : lineEnd + 1, revision: 'legacy' };
|
|
170
|
-
}
|
|
171
|
-
if (end < 0) return null;
|
|
172
|
-
let start = begin;
|
|
173
|
-
const before = text.slice(0, begin);
|
|
174
|
-
if (/^\uFEFF?\/\/ veo: commented configuration template\r?\n$/.test(before)) start = 0;
|
|
175
|
-
const lineEnd = text.indexOf('\n', end);
|
|
176
|
-
let stop = lineEnd < 0 ? text.length : lineEnd + 1;
|
|
177
|
-
if (text.slice(stop).startsWith('// Your existing settings:\n')) stop += '// Your existing settings:\n'.length;
|
|
178
|
-
return { start, stop, revision: text.slice(begin, text.indexOf('\n', begin) < 0 ? text.length : text.indexOf('\n', begin)) };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function configGuideState(text) {
|
|
182
|
-
const range = generatedRange(text);
|
|
183
|
-
if (!range) return text.includes(REFERENCE_BEGIN) || text.includes('// Reference: copy any examples') ? 'incomplete' : 'absent';
|
|
184
|
-
return range.revision === `${REFERENCE_BEGIN} ${TEMPLATE_REVISION}` ? 'current' : 'outdated';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function addConfigGuide(text) {
|
|
188
|
-
const range = generatedRange(text);
|
|
189
|
-
if (!range && configGuideState(text) === 'incomplete') return text;
|
|
190
|
-
const bom = text.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
191
|
-
if (range) return (range.start === 0 ? bom : text.slice(0, range.start)) + CONFIG_GUIDE + text.slice(range.stop);
|
|
192
|
-
return bom + CONFIG_GUIDE + text.slice(bom.length);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function removeConfigGuide(text) {
|
|
196
|
-
const range = generatedRange(text);
|
|
197
|
-
return range ? (range.start === 0 && text.startsWith('\uFEFF') ? '\uFEFF' : text.slice(0, range.start)) + text.slice(range.stop) : text;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Remove comments only outside JSON strings. Whitespace replacement preserves
|
|
201
|
-
// token boundaries, so malformed input cannot become valid by joining tokens.
|
|
202
|
-
export function stripConfigComments(text) {
|
|
203
|
-
text = text.replace(/^\uFEFF/, '');
|
|
204
|
-
let result = '', quoted = false;
|
|
205
|
-
for (let i = 0; i < text.length; i++) {
|
|
206
|
-
const char = text[i];
|
|
207
|
-
if (quoted) {
|
|
208
|
-
result += char;
|
|
209
|
-
if (char === '\\' && i + 1 < text.length) result += text[++i];
|
|
210
|
-
else if (char === '"') quoted = false;
|
|
211
|
-
} else if (char === '"') { quoted = true; result += char; }
|
|
212
|
-
else if (char === '/' && text[i + 1] === '/') {
|
|
213
|
-
result += ' '; i++;
|
|
214
|
-
while (i + 1 < text.length && !'\r\n'.includes(text[i + 1])) { result += ' '; i++; }
|
|
215
|
-
} else if (char === '/' && text[i + 1] === '*') {
|
|
216
|
-
const commentStart = i;
|
|
217
|
-
result += ' '; i++;
|
|
218
|
-
let closed = false;
|
|
219
|
-
while (++i < text.length) {
|
|
220
|
-
if (text[i] === '*' && text[i + 1] === '/') { result += ' '; i++; closed = true; break; }
|
|
221
|
-
result += '\r\n'.includes(text[i]) ? text[i] : ' ';
|
|
222
|
-
}
|
|
223
|
-
if (!closed) throw Object.assign(new Error('Unterminated block comment; add */ to close it.'), { position: commentStart });
|
|
224
|
-
} else result += char;
|
|
225
|
-
}
|
|
226
|
-
return result;
|
|
227
|
-
}
|
package/src/config.js
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import { commandOutput } from './output.js';
|
|
2
|
-
import { folderLink } from './path-links.js';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { configBase } from './paths.js';
|
|
7
|
-
import { configSyntaxError } from './config-errors.js';
|
|
8
|
-
import { CONFIG_TEMPLATE, CONFIG_GUIDE, stripConfigComments } from './config-template.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Keys accepted in the config file, with the CLI flag they act as a default for.
|
|
12
|
-
* Everything here is a default only: an explicit command-line flag always wins.
|
|
13
|
-
*/
|
|
14
|
-
export const CONFIG_KEYS = Object.freeze({
|
|
15
|
-
concurrentDownloads: 'number',
|
|
16
|
-
adaptiveConcurrency: 'boolean',
|
|
17
|
-
filenameTemplate: 'string',
|
|
18
|
-
folderTemplate: 'string',
|
|
19
|
-
checkSpace: 'boolean',
|
|
20
|
-
timings: 'boolean',
|
|
21
|
-
color: 'boolean',
|
|
22
|
-
output: 'string',
|
|
23
|
-
quality: 'string',
|
|
24
|
-
format: 'string',
|
|
25
|
-
compatible: 'boolean',
|
|
26
|
-
recode: 'boolean',
|
|
27
|
-
playlistConcurrency: 'number',
|
|
28
|
-
rename: 'string',
|
|
29
|
-
audio: 'boolean',
|
|
30
|
-
open: 'boolean',
|
|
31
|
-
resume: 'boolean',
|
|
32
|
-
incognito: 'boolean',
|
|
33
|
-
neutralFilename: 'boolean',
|
|
34
|
-
closestQuality: 'boolean',
|
|
35
|
-
cookies: 'string',
|
|
36
|
-
cookiesFromBrowser: 'string',
|
|
37
|
-
playlist: 'boolean',
|
|
38
|
-
concurrentFragments: 'number',
|
|
39
|
-
experimentalFragments: 'boolean',
|
|
40
|
-
subs: 'boolean',
|
|
41
|
-
autoSubs: 'boolean',
|
|
42
|
-
subLangs: 'string',
|
|
43
|
-
subFormat: 'string',
|
|
44
|
-
verify: 'boolean',
|
|
45
|
-
embedSubs: 'boolean',
|
|
46
|
-
embedMetadata: 'boolean',
|
|
47
|
-
embedThumbnail: 'boolean',
|
|
48
|
-
sponsorblockRemove: 'string',
|
|
49
|
-
section: 'string',
|
|
50
|
-
json: 'boolean',
|
|
51
|
-
skipExisting: 'boolean',
|
|
52
|
-
playlistItems: 'string',
|
|
53
|
-
deepScan: 'boolean',
|
|
54
|
-
timeout: 'string',
|
|
55
|
-
listSources: 'boolean',
|
|
56
|
-
autoListSources: 'boolean',
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
export const RESERVED_PROFILE_NAMES = Object.freeze(['list', 'reset']);
|
|
60
|
-
|
|
61
|
-
export function configFile({ env = process.env, platform = process.platform } = {}) {
|
|
62
|
-
const override = env.VEO_CONFIG;
|
|
63
|
-
if (typeof override === 'string' && override.trim()) {
|
|
64
|
-
if (override.includes('\0')) throw new Error('VEO_CONFIG must be a filesystem path.');
|
|
65
|
-
return path.resolve(override.trim());
|
|
66
|
-
}
|
|
67
|
-
return path.join(configBase({ env, platform }), 'config.json');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function typeOf(value) {
|
|
71
|
-
if (value === null) return 'null';
|
|
72
|
-
if (Array.isArray(value)) return 'array';
|
|
73
|
-
return typeof value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Read optional user defaults. A missing file is not an error; a malformed one
|
|
78
|
-
* is reported loudly, because silently ignoring a typo would be worse.
|
|
79
|
-
*/
|
|
80
|
-
export async function loadConfig({ env = process.env, file } = {}) {
|
|
81
|
-
const target = file || configFile({ env });
|
|
82
|
-
let text;
|
|
83
|
-
try {
|
|
84
|
-
text = await readFile(target, 'utf8');
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (error.code === 'ENOENT') return { file: target, exists: false, config: {}, warnings: [] };
|
|
87
|
-
throw new Error(`Cannot read the veo config file ${target}: ${error.message}`);
|
|
88
|
-
}
|
|
89
|
-
return parseConfigText(text, target);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function parseConfigText(text, target = 'config') {
|
|
93
|
-
let data;
|
|
94
|
-
let clean = text.replace(/^\uFEFF/, '');
|
|
95
|
-
try {
|
|
96
|
-
clean = stripConfigComments(text);
|
|
97
|
-
data = JSON.parse(clean);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
throw configSyntaxError(clean, target, error);
|
|
100
|
-
}
|
|
101
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
102
|
-
throw new Error(`The veo config file must contain a JSON object: ${target}`);
|
|
103
|
-
}
|
|
104
|
-
const warnings = [];
|
|
105
|
-
const config = {};
|
|
106
|
-
for (const [key, value] of Object.entries(data)) {
|
|
107
|
-
if (key === '$schema') continue;
|
|
108
|
-
if (key === 'activeProfile') {
|
|
109
|
-
if (typeOf(value) !== 'string' || !value) throw new Error(`Config key "activeProfile" must be a non-empty string (${target}).`);
|
|
110
|
-
config.activeProfile = value;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (key === 'profiles') {
|
|
114
|
-
if (!value || typeOf(value) !== 'object') throw new Error('Config profiles must be an object.');
|
|
115
|
-
config.profiles = Object.create(null);
|
|
116
|
-
for (const [name, profile] of Object.entries(value)) {
|
|
117
|
-
if (RESERVED_PROFILE_NAMES.includes(name)) throw new Error(`Profile name "${name}" is reserved for veo profile ${name}. Rename it in ${target}.`);
|
|
118
|
-
if (!profile || typeOf(profile) !== 'object') throw new Error(`Profile "${name}" must be an object.`);
|
|
119
|
-
const checked = {};
|
|
120
|
-
for (const [setting, item] of Object.entries(profile)) {
|
|
121
|
-
if (!Object.hasOwn(CONFIG_KEYS, setting)) throw new Error(`Unknown setting "${setting}" in profile "${name}".`);
|
|
122
|
-
if (typeOf(item) !== CONFIG_KEYS[setting]) throw new Error(`Profile "${name}": "${setting}" must be a ${CONFIG_KEYS[setting]}.`);
|
|
123
|
-
checked[setting] = item;
|
|
124
|
-
}
|
|
125
|
-
config.profiles[name] = checked;
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
const expected = CONFIG_KEYS[key];
|
|
130
|
-
if (!expected) {
|
|
131
|
-
warnings.push(`Unknown config key "${key}" in ${target} was ignored.`);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (typeOf(value) !== expected) {
|
|
135
|
-
throw new Error(`Config key "${key}" must be a ${expected}, not a ${typeOf(value)} (${target}).`);
|
|
136
|
-
}
|
|
137
|
-
config[key] = value;
|
|
138
|
-
}
|
|
139
|
-
if (config.activeProfile && !Object.hasOwn(config.profiles || {}, config.activeProfile)) {
|
|
140
|
-
throw new Error(`Unknown active profile "${config.activeProfile}" in ${target}. Available: ${Object.keys(config.profiles || {}).join(', ') || 'none'}.`);
|
|
141
|
-
}
|
|
142
|
-
return { file: target, exists: true, config, warnings };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function selectedProfileName(config, name) {
|
|
146
|
-
return name || config.activeProfile || (Object.hasOwn(config.profiles || {}, 'default') ? 'default' : undefined);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function applyProfile(config, name) {
|
|
150
|
-
const { profiles, activeProfile, ...defaults } = config;
|
|
151
|
-
name = selectedProfileName(config, name);
|
|
152
|
-
if (!name) return defaults;
|
|
153
|
-
if (!profiles || !Object.hasOwn(profiles, name)) throw new Error(`Unknown profile "${name}". Available: ${Object.keys(profiles || {}).join(', ') || 'none'}. Use veo config edit.`);
|
|
154
|
-
return { ...defaults, ...profiles[name] };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function profileMain(args, { file = configFile(), output = process.stdout } = {}) {
|
|
158
|
-
if (args.length > 1 || args[0]?.startsWith('-')) throw new Error('Usage: veo profile [list|reset|NAME]');
|
|
159
|
-
const loaded = await loadConfig({ file });
|
|
160
|
-
if (!args.length) {
|
|
161
|
-
output.write(`Active profile: ${selectedProfileName(loaded.config) || 'global (no profile)'}\n`);
|
|
162
|
-
output.write(`Available: ${Object.keys(loaded.config.profiles || {}).join(', ') || 'none'}\n`);
|
|
163
|
-
return 0;
|
|
164
|
-
}
|
|
165
|
-
if (args[0] === 'list' && args.length === 1) {
|
|
166
|
-
const names = Object.keys(loaded.config.profiles || {});
|
|
167
|
-
if (!names.length) output.write('No profiles configured. Use veo config edit.\n');
|
|
168
|
-
else {
|
|
169
|
-
const active = selectedProfileName(loaded.config);
|
|
170
|
-
output.write(`Profiles:\n${active ? '' : '* global (no profile)\n'}${names.map(name => `${name === active ? '* ' : ' '}${name}${name === active ? ' (active)' : ''}`).join('\n')}\n`);
|
|
171
|
-
}
|
|
172
|
-
return 0;
|
|
173
|
-
}
|
|
174
|
-
const reset = args[0] === 'reset' && args.length === 1;
|
|
175
|
-
const name = args[0];
|
|
176
|
-
if (!reset) {
|
|
177
|
-
applyProfile(loaded.config, name);
|
|
178
|
-
await effectiveConfig(loaded.config, name);
|
|
179
|
-
}
|
|
180
|
-
if (!loaded.exists) {
|
|
181
|
-
output.write('Active profile: global (no profile)\n');
|
|
182
|
-
return 0;
|
|
183
|
-
}
|
|
184
|
-
const original = await readFile(file, 'utf8');
|
|
185
|
-
// Token locations come from the comment-aware parser, preserving hand-written notes.
|
|
186
|
-
const { configSource } = await import('./config-diagnostics.js');
|
|
187
|
-
const { nodes } = configSource(original);
|
|
188
|
-
const property = nodes.get(JSON.stringify(['activeProfile']));
|
|
189
|
-
let updated;
|
|
190
|
-
if (reset) {
|
|
191
|
-
if (!property) updated = original;
|
|
192
|
-
else {
|
|
193
|
-
const clean = (original.startsWith('\uFEFF') ? ' ' : '') + stripConfigComments(original);
|
|
194
|
-
let start = property.keyStart, end = property.end;
|
|
195
|
-
while (/\s/.test(clean[end] || '')) end++;
|
|
196
|
-
if (clean[end] === ',') end++;
|
|
197
|
-
else {
|
|
198
|
-
let before = start - 1;
|
|
199
|
-
while (before >= 0 && /\s/.test(clean[before])) before--;
|
|
200
|
-
if (clean[before] === ',') start = before;
|
|
201
|
-
end = property.end;
|
|
202
|
-
}
|
|
203
|
-
updated = original.slice(0, start) + original.slice(end);
|
|
204
|
-
}
|
|
205
|
-
} else if (property) updated = original.slice(0, property.start) + JSON.stringify(name) + original.slice(property.end);
|
|
206
|
-
else {
|
|
207
|
-
const root = nodes.get('[]');
|
|
208
|
-
const clean = (original.startsWith('\uFEFF') ? ' ' : '') + stripConfigComments(original);
|
|
209
|
-
const first = root.start + 1 + clean.slice(root.start + 1, root.end - 1).search(/\S/);
|
|
210
|
-
const empty = first === root.start;
|
|
211
|
-
const newline = original.includes('\r\n') ? '\r\n' : '\n';
|
|
212
|
-
const position = empty ? root.end - 1 : first;
|
|
213
|
-
const indent = empty ? ' ' : /^[ \t]*$/.test(original.slice(original.lastIndexOf('\n', position - 1) + 1, position))
|
|
214
|
-
? original.slice(original.lastIndexOf('\n', position - 1) + 1, position) : '';
|
|
215
|
-
updated = original.slice(0, position) + (empty ? `${newline}${indent}` : '') +
|
|
216
|
-
`"activeProfile": ${JSON.stringify(name)}${empty ? `${newline}` : `,${newline}${indent}`}` + original.slice(position);
|
|
217
|
-
}
|
|
218
|
-
parseConfigText(updated, file);
|
|
219
|
-
if (updated !== original) {
|
|
220
|
-
if (await readFile(file, 'utf8') !== original) throw new Error('Config changed while switching profiles. Try again.');
|
|
221
|
-
await writeFile(file, updated, 'utf8');
|
|
222
|
-
}
|
|
223
|
-
output.write(`Active profile: ${reset ? selectedProfileName(parseConfigText(updated, file).config) || 'global (no profile)' : name}\n`);
|
|
224
|
-
return 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export async function prepareConfigEdit(file) {
|
|
228
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
229
|
-
try { await writeFile(file, CONFIG_TEMPLATE, { flag: 'wx', mode: 0o600 }); }
|
|
230
|
-
catch (error) {
|
|
231
|
-
if (error.code !== 'EEXIST') throw error;
|
|
232
|
-
const original = await readFile(file, 'utf8');
|
|
233
|
-
if (!original.replace(/^\uFEFF/, '').trim()) await writeFile(file, CONFIG_TEMPLATE, 'utf8');
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export async function configMain(args) {
|
|
238
|
-
const editorColor = !args.includes('--no-color') && !Object.hasOwn(process.env, 'NO_COLOR');
|
|
239
|
-
let stdout;
|
|
240
|
-
[args, stdout] = commandOutput(args, process.stdout);
|
|
241
|
-
if (args[0] === 'show' || args[0] === 'guide') stdout = process.stdout;
|
|
242
|
-
const file = configFile();
|
|
243
|
-
if (args[0] === 'reset') {
|
|
244
|
-
if (args.length !== 1) throw new Error('Usage: veo config reset');
|
|
245
|
-
return (await import('./config-reset.js')).resetConfig(file, { output: stdout });
|
|
246
|
-
}
|
|
247
|
-
if (['check', 'show'].includes(args[0])) {
|
|
248
|
-
const { parseArgs } = await import('node:util');
|
|
249
|
-
const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { profile: { type: 'string' } } });
|
|
250
|
-
if (positionals.length) throw new Error('Usage: veo config check|show [--profile NAME]');
|
|
251
|
-
const loaded = await loadConfig({ file });
|
|
252
|
-
if (loaded.warnings.length) throw new Error(loaded.warnings.join('\n'));
|
|
253
|
-
if (args[0] === 'show') {
|
|
254
|
-
const effective = await effectiveConfig(loaded.config, values.profile);
|
|
255
|
-
stdout.write(`${JSON.stringify(effective, null, 2)}\n`);
|
|
256
|
-
} else {
|
|
257
|
-
const names = values.profile ? [values.profile] : [undefined, ...Object.keys(loaded.config.profiles || {})];
|
|
258
|
-
for (const name of names) {
|
|
259
|
-
try { await effectiveConfig(loaded.config, name); }
|
|
260
|
-
catch (error) { throw new Error(`Profile ${name || 'default/global'}: ${error.message}`); }
|
|
261
|
-
}
|
|
262
|
-
stdout.write(`Config OK: ${file} (${names.length} effective configurations checked).\n`);
|
|
263
|
-
}
|
|
264
|
-
return 0;
|
|
265
|
-
}
|
|
266
|
-
let editorMode;
|
|
267
|
-
if (args[0] === 'edit') {
|
|
268
|
-
if (args.length > 2 || (args[1] && !['--external', '--terminal'].includes(args[1]))) throw new Error('Usage: veo config edit [--external|--terminal]');
|
|
269
|
-
editorMode = args[1]?.slice(2) || process.env.VEO_CONFIG_EDITOR || 'auto';
|
|
270
|
-
if (!['auto', 'external', 'terminal'].includes(editorMode)) throw new Error('VEO_CONFIG_EDITOR must be auto, external or terminal.');
|
|
271
|
-
} else if (args.length !== 1 || !['path', 'profiles', 'guide'].includes(args[0])) throw new Error('Usage: veo config edit|path|profiles|guide|check|show|reset');
|
|
272
|
-
if (args[0] === 'path') { stdout.write(`${folderLink(stdout, file)}\n`); return 0; }
|
|
273
|
-
if (args[0] === 'guide') { stdout.write(CONFIG_GUIDE); return 0; }
|
|
274
|
-
if (args[0] === 'profiles') {
|
|
275
|
-
const loaded = await loadConfig();
|
|
276
|
-
stdout.write(`${Object.keys(loaded.config.profiles || {}).join('\n') || 'No profiles configured. Use veo config edit.'}\n`);
|
|
277
|
-
return 0;
|
|
278
|
-
}
|
|
279
|
-
await prepareConfigEdit(file);
|
|
280
|
-
if (editorMode === 'terminal' || (editorMode === 'auto' && !process.env.VISUAL && !process.env.EDITOR && process.stdin.isTTY && process.stdout.isTTY)) {
|
|
281
|
-
const { editConfig } = await import('./config-editor.js');
|
|
282
|
-
await editConfig(file, { color: editorColor });
|
|
283
|
-
return 0;
|
|
284
|
-
}
|
|
285
|
-
// Treat the editor as an executable path, never as shell code.
|
|
286
|
-
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === 'win32' ? 'notepad.exe' : 'vi');
|
|
287
|
-
await new Promise((resolve, reject) => {
|
|
288
|
-
const child = spawn(editor, [file], { shell: false, stdio: 'inherit', windowsHide: true });
|
|
289
|
-
child.on('error', () => reject(new Error(`Could not launch editor. Set EDITOR to an executable path or edit ${file}.`)));
|
|
290
|
-
child.on('close', code => code === 0 ? resolve() : reject(new Error(`Editor exited with ${code}. Config: ${file}`)));
|
|
291
|
-
});
|
|
292
|
-
await loadConfig();
|
|
293
|
-
return 0;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export async function effectiveConfig(config, profile) {
|
|
297
|
-
const { parseCli } = await import('./cli.js');
|
|
298
|
-
const selected = applyProfile(config, profile);
|
|
299
|
-
// Inspection is offline and never prints credential file paths or browser profiles.
|
|
300
|
-
const { cookies, cookiesFromBrowser, ...safe } = selected;
|
|
301
|
-
const parsed = parseCli(['https://example.invalid/config-check'], { config: safe });
|
|
302
|
-
const result = Object.fromEntries(Object.keys(CONFIG_KEYS).filter(key => parsed[key] !== undefined).map(key => [key, parsed[key]]));
|
|
303
|
-
result.output = path.resolve(result.output);
|
|
304
|
-
if (cookies !== undefined) result.cookies = '[configured]';
|
|
305
|
-
if (cookiesFromBrowser !== undefined) result.cookiesFromBrowser = '[configured]';
|
|
306
|
-
return result;
|
|
307
|
-
}
|
package/src/disk-space.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { stat, statfs } from 'node:fs/promises';
|
|
3
|
-
|
|
4
|
-
const reservations = new Map();
|
|
5
|
-
export function estimateMediaBytes(metadata, options = {}) {
|
|
6
|
-
const size = format => {
|
|
7
|
-
const direct = format.filesize || format.filesize_approx;
|
|
8
|
-
if (Number.isFinite(direct) && direct > 0) return direct;
|
|
9
|
-
return Number.isFinite(format.tbr) && metadata.duration > 0 ? format.tbr * 1000 / 8 * metadata.duration : 0;
|
|
10
|
-
};
|
|
11
|
-
if (size(metadata)) return Math.ceil(size(metadata));
|
|
12
|
-
const target = options.quality && options.quality !== 'best' ? parseInt(options.quality, 10) : Infinity;
|
|
13
|
-
const formats = (metadata.formats || []).filter(format => !format.has_drm && (options.closestQuality || !format.height || format.height <= target));
|
|
14
|
-
const video = formats.filter(format => format.vcodec !== 'none');
|
|
15
|
-
const audio = formats.filter(format => format.vcodec === 'none' && format.acodec !== 'none');
|
|
16
|
-
const bestVideo = Math.max(0, ...video.map(size));
|
|
17
|
-
const bestAudio = Math.max(0, ...audio.map(size));
|
|
18
|
-
if (options.audio) return bestAudio ? Math.ceil(bestAudio) : null;
|
|
19
|
-
if (!bestVideo) return null;
|
|
20
|
-
// Separate video needs an audio estimate too; otherwise the total is unknown.
|
|
21
|
-
if (video.some(format => format.acodec === 'none') && !bestAudio) return null;
|
|
22
|
-
return Math.ceil(bestVideo + bestAudio);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function volume(directory) {
|
|
26
|
-
let current = path.resolve(directory);
|
|
27
|
-
while (true) {
|
|
28
|
-
try {
|
|
29
|
-
const info = await stat(current);
|
|
30
|
-
if (info.isDirectory()) {
|
|
31
|
-
const fs = await statfs(current);
|
|
32
|
-
return { key: String(info.dev), free: fs.bavail * fs.bsize };
|
|
33
|
-
}
|
|
34
|
-
} catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error.code)) throw error; }
|
|
35
|
-
const parent = path.dirname(current);
|
|
36
|
-
if (parent === current) throw new Error('No existing parent directory.');
|
|
37
|
-
current = parent;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function reserveSpace({ cache, destination, bytes, cached = false, reporter, inspect = volume } = {}) {
|
|
42
|
-
let cacheVolume, outputVolume;
|
|
43
|
-
try { [cacheVolume, outputVolume] = await Promise.all([inspect(cache), inspect(destination)]); }
|
|
44
|
-
catch { reporter?.status('Free-space check unavailable on this filesystem; continuing.'); return () => {}; }
|
|
45
|
-
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
46
|
-
reporter?.status(`Size unknown; free space: cache ${(cacheVolume.free / 1073741824).toFixed(1)} GiB, destination ${(outputVolume.free / 1073741824).toFixed(1)} GiB.`);
|
|
47
|
-
return () => {};
|
|
48
|
-
}
|
|
49
|
-
// Allow space for temporary merge output and destination copy. Conservative even
|
|
50
|
-
// when the filesystem can hard-link; conversion sizes remain estimates.
|
|
51
|
-
const needs = new Map();
|
|
52
|
-
for (const [volume, amount] of [[cacheVolume, cached ? 0 : bytes * 2], [outputVolume, bytes]]) {
|
|
53
|
-
const entry = needs.get(volume.key) || { free: volume.free, bytes: 0 };
|
|
54
|
-
entry.free = Math.min(entry.free, volume.free); entry.bytes += amount;
|
|
55
|
-
needs.set(volume.key, entry);
|
|
56
|
-
}
|
|
57
|
-
for (const [key, need] of needs) {
|
|
58
|
-
const available = need.free - (reservations.get(key) || 0);
|
|
59
|
-
if (available < need.bytes) throw new Error(`Not enough disk space: approximately ${(need.bytes / 1048576).toFixed(1)} MiB needed including temporary files; ${(Math.max(0, available) / 1048576).toFixed(1)} MiB available after active downloads.`);
|
|
60
|
-
}
|
|
61
|
-
for (const [key, need] of needs) reservations.set(key, (reservations.get(key) || 0) + need.bytes);
|
|
62
|
-
let released = false;
|
|
63
|
-
return () => {
|
|
64
|
-
if (released) return;
|
|
65
|
-
released = true;
|
|
66
|
-
for (const [key, need] of needs) { const remaining = (reservations.get(key) || 0) - need.bytes; if (remaining > 0) reservations.set(key, remaining); else reservations.delete(key); }
|
|
67
|
-
};
|
|
68
|
-
}
|