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/config-template.js
CHANGED
|
@@ -1,171 +1,224 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// "
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
// "
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// "
|
|
25
|
-
// "
|
|
26
|
-
// "
|
|
27
|
-
// "
|
|
28
|
-
//
|
|
29
|
-
// "
|
|
30
|
-
// "
|
|
31
|
-
// "
|
|
32
|
-
//
|
|
33
|
-
// "
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// "
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { translateLegacyConfigComments } from './legacy-config-comments.js';
|
|
3
|
+
|
|
4
|
+
export const TEMPLATE_MARKER = '// veo: commented configuration template';
|
|
5
|
+
|
|
6
|
+
export const SOURCE_OPTIONS_GUIDE = String.raw`// veo: source discovery options v1
|
|
7
|
+
// Copy these settings into the global object or a named profile if desired.
|
|
8
|
+
// "deepScan": true, // Verify every observed media candidate instead of the first 30
|
|
9
|
+
// "timeout": "2m", // Discovery deadline: 5s to 10m; plain numbers mean seconds
|
|
10
|
+
// "listSources": false, // true = list sources and exit for single-URL calls
|
|
11
|
+
// "autoListSources": false, // true = search after extraction fails, without a yes/no prompt
|
|
12
|
+
// Command-line flags override these values, including --no-deep-scan and --no-list-sources.
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const PROFILE_OPTIONS_MARKER = '// veo: profile download options v4';
|
|
16
|
+
export const PROFILE_OPTIONS_GUIDE = String.raw`${PROFILE_OPTIONS_MARKER}
|
|
17
|
+
// Any global option can also be placed inside a profile.
|
|
18
|
+
// Copy the settings you want into "profiles" > "default" or a named profile.
|
|
19
|
+
// Remove the leading // on copied settings; separate entries with commas.
|
|
20
|
+
// Speed without reducing the selected video quality:
|
|
21
|
+
// "quality": "best",
|
|
22
|
+
// "concurrentDownloads": 2, // Parallel URLs/batch items: 1-4; default 2
|
|
23
|
+
// "adaptiveConcurrency": true, // Reduce parallelism and retry transient errors
|
|
24
|
+
// "concurrentFragments": 8, // DASH/HLS fragments: 1-16; default 8
|
|
25
|
+
// "deepScan": true, // Check every observed source candidate
|
|
26
|
+
// "timeout": "2m", // Source-search deadline, 5s to 10m
|
|
27
|
+
// "listSources": false, // List sources and exit for one URL
|
|
28
|
+
// "autoListSources": true, // Search after extraction fails, without asking
|
|
29
|
+
// "playlist": true, // Enable only for playlist downloads
|
|
30
|
+
// "playlistConcurrency": 2, // Simultaneous entries: 1-4; default 2
|
|
31
|
+
// "checkSpace": true, // Estimate free cache/output space first
|
|
32
|
+
// "timings": true, // Show time spent in each phase
|
|
33
|
+
// "color": true, // Gray details and colored results; false = plain text
|
|
34
|
+
// Applies to veo's own terminal output, including stats/history/doctor.
|
|
35
|
+
// Each profile can set color independently; NO_COLOR always disables styling.
|
|
36
|
+
// Example inside profiles: "plain": { "color": false }
|
|
37
|
+
// Use: veo stats --profile plain or veo URL --profile plain
|
|
38
|
+
// "folderTemplate": "{channel}/{year}",
|
|
39
|
+
// "filenameTemplate": "{index} - {title}", // Without extension; do not combine with rename
|
|
40
|
+
// "resume": true,
|
|
41
|
+
// "skipExisting": true
|
|
42
|
+
// Lossless container change (the selected codecs must fit the container):
|
|
43
|
+
// "format": "mkv",
|
|
44
|
+
// "compatible": false, // Opt-in H.264/AAC MP4; converts only when needed
|
|
45
|
+
// "recode": false
|
|
46
|
+
// Compatibility profile to add inside profiles:
|
|
47
|
+
// "kompatibel": { "audio": false, "format": "mp4", "compatible": true, "recode": false }
|
|
48
|
+
// Use: veo URL --profile kompatibel (or simply veo URL --compatible).
|
|
49
|
+
// Optional conversion instead: "format": "webm", "recode": true
|
|
50
|
+
// Conversion can be slower and lose quality; recode is for video only.
|
|
51
|
+
// Sequential downloads: "concurrentDownloads": 1, "concurrentFragments": 1, "playlistConcurrency": 1
|
|
52
|
+
// Example named profile inside the profiles object:
|
|
53
|
+
// "fast": { "quality": "best", "concurrentFragments": 8, "playlistConcurrency": 2, "resume": true }
|
|
54
|
+
// Use with: veo URL --profile fast (add --playlist for a playlist URL).
|
|
55
|
+
// Inspect: veo config show --profile fast | Validate: veo config check
|
|
56
|
+
// Template fields: {title}, {id}, {channel}, {year}, {playlist}, {index}.
|
|
57
|
+
// Missing values use Unknown channel/year, No playlist, unknown ID, index 001.
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const TEMPLATE_SOURCE = String.raw`${TEMPLATE_MARKER}
|
|
61
|
+
// Save and close the editor. Settings apply the next time you run veo.
|
|
62
|
+
// Comments using // or /* ... */ are supported. Command-line options take priority.
|
|
63
|
+
// To enable an option, remove its leading // and adjust the value.
|
|
64
|
+
// Separate active entries with commas. Do not add a comma after the last entry.
|
|
65
|
+
{
|
|
66
|
+
// Output directory: use / or double backslashes, for example "D:\\Videos".
|
|
67
|
+
// Without output, downloads are saved in the current working directory.
|
|
68
|
+
// "output": "D:/Videos",
|
|
69
|
+
// Filename without extension. Every * inserts the original video title.
|
|
70
|
+
// "rename": "movie_*",
|
|
71
|
+
|
|
72
|
+
// Maximum video resolution: best, 2160p, 1440p, 1080p, 720p, ...
|
|
73
|
+
// "quality": "1080p",
|
|
74
|
+
// "closestQuality": false, // Allow the nearest available resolution
|
|
75
|
+
// Video format: mp4, mkv, webm, mov. Lossless remux; codecs must fit the container.
|
|
76
|
+
// "format": "mp4",
|
|
77
|
+
// "recode": false, // Explicit video conversion; may lose quality
|
|
78
|
+
// "playlistConcurrency": 2, // Concurrent playlist entries: 1 to 4
|
|
79
|
+
// "playlistItems": "1-5", // Download only these playlist entries
|
|
80
|
+
// "section": "*10:00-12:00", // Download only a time range
|
|
81
|
+
// "open": false, // Open the completed file automatically
|
|
82
|
+
// "resume": true, // Resume interrupted downloads
|
|
83
|
+
// "skipExisting": true, // Skip previously saved downloads
|
|
84
|
+
// "concurrentFragments": 8, // Concurrent fragments: 1 to 16; default 8
|
|
85
|
+
// "cookies": "D:/cookies.txt", // Netscape-format cookies file
|
|
86
|
+
// "cookiesFromBrowser": "firefox", // Use your browser's cookies
|
|
87
|
+
// "sponsorblockRemove": "sponsor", // Remove matching SponsorBlock segments
|
|
88
|
+
// "json": false, // Print machine-readable output for downloads
|
|
89
|
+
|
|
90
|
+
${SOURCE_OPTIONS_GUIDE}
|
|
91
|
+
|
|
92
|
+
// Subtitles and additional information:
|
|
93
|
+
// "subs": true, // Download subtitles; defaults to English
|
|
94
|
+
// "subLangs": "de,en", // Enable subtitles for these languages
|
|
95
|
+
// "embedSubs": true, // Embed subtitles in the video
|
|
96
|
+
// "embedMetadata": true, // Embed the title, date and other metadata
|
|
97
|
+
// "embedThumbnail": true, // Embed the thumbnail
|
|
98
|
+
|
|
99
|
+
// The default profile is used automatically unless you select another profile.
|
|
100
|
+
${PROFILE_OPTIONS_GUIDE}
|
|
101
|
+
// Select other profiles with --profile NAME or in the interactive wizard.
|
|
102
|
+
// Example: veo "https://example.com/video.mp4" --profile music
|
|
103
|
+
"profiles": {
|
|
104
|
+
"default": {
|
|
105
|
+
// "color": true, // Set false to disable terminal styling for this profile.
|
|
106
|
+
// Add everyday defaults here, for example: "quality": "1080p"
|
|
107
|
+
// Empty means use the global settings above and veo's built-in defaults.
|
|
108
|
+
},
|
|
109
|
+
"kompatibel": {
|
|
110
|
+
"audio": false,
|
|
111
|
+
"format": "mp4",
|
|
112
|
+
"compatible": true,
|
|
113
|
+
"recode": false
|
|
114
|
+
},
|
|
115
|
+
"music": {
|
|
116
|
+
// Audio only. Formats: mp3, m4a, aac, opus, flac, wav.
|
|
117
|
+
"audio": true,
|
|
118
|
+
"format": "mp3"
|
|
119
|
+
},
|
|
120
|
+
"archive": {
|
|
121
|
+
"quality": "1080p",
|
|
122
|
+
"embedMetadata": true,
|
|
123
|
+
"subLangs": "de,en"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
// The revision follows the template content, so future examples refresh without
|
|
130
|
+
// maintaining a separate version number.
|
|
131
|
+
const TEMPLATE_REVISION = createHash('sha256').update(TEMPLATE_SOURCE).digest('hex').slice(0, 12);
|
|
132
|
+
const REVISION_LINE = `// veo: template revision ${TEMPLATE_REVISION}`;
|
|
133
|
+
const REFERENCE_BEGIN = '// veo: template reference begin';
|
|
134
|
+
const REFERENCE_END = '// veo: template reference end';
|
|
135
|
+
export const CONFIG_TEMPLATE = TEMPLATE_SOURCE.replace(TEMPLATE_MARKER, `${TEMPLATE_MARKER}\n${REVISION_LINE}`);
|
|
136
|
+
|
|
137
|
+
function currentReference() {
|
|
138
|
+
const examples = CONFIG_TEMPLATE.split('\n').slice(2).map(line => `// ${line}`).join('\n');
|
|
139
|
+
return `${REFERENCE_BEGIN} ${TEMPLATE_REVISION}\n// This generated reference is refreshed when the template changes. Keep personal notes below it.\n// Current veo options: copy the settings you need below.\n${examples}\n${REFERENCE_END}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Remove comments only outside JSON strings. Whitespace replacement preserves
|
|
143
|
+
// token boundaries, so malformed input cannot become valid by joining tokens.
|
|
144
|
+
export function stripConfigComments(text) {
|
|
145
|
+
text = text.replace(/^\uFEFF/, '');
|
|
146
|
+
let result = '', quoted = false;
|
|
147
|
+
for (let i = 0; i < text.length; i++) {
|
|
148
|
+
const char = text[i];
|
|
149
|
+
if (quoted) {
|
|
150
|
+
result += char;
|
|
151
|
+
if (char === '\\' && i + 1 < text.length) result += text[++i];
|
|
152
|
+
else if (char === '"') quoted = false;
|
|
153
|
+
} else if (char === '"') { quoted = true; result += char; }
|
|
154
|
+
else if (char === '/' && text[i + 1] === '/') {
|
|
155
|
+
result += ' '; i++;
|
|
156
|
+
while (i + 1 < text.length && !'\r\n'.includes(text[i + 1])) { result += ' '; i++; }
|
|
157
|
+
} else if (char === '/' && text[i + 1] === '*') {
|
|
158
|
+
const commentStart = i;
|
|
159
|
+
result += ' '; i++;
|
|
160
|
+
let closed = false;
|
|
161
|
+
while (++i < text.length) {
|
|
162
|
+
if (text[i] === '*' && text[i + 1] === '/') { result += ' '; i++; closed = true; break; }
|
|
163
|
+
result += '\r\n'.includes(text[i]) ? text[i] : ' ';
|
|
164
|
+
}
|
|
165
|
+
if (!closed) throw Object.assign(new Error('Unterminated block comment; add */ to close it.'), { position: commentStart });
|
|
166
|
+
} else result += char;
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function withConfigTemplate(text) {
|
|
172
|
+
text = translateLegacyConfigComments(text);
|
|
173
|
+
text = addDefaultProfile(text);
|
|
174
|
+
if (!text.replace(/^\uFEFF/, '').trim()) return CONFIG_TEMPLATE;
|
|
175
|
+
text = text.replace(/^\uFEFF/, '');
|
|
176
|
+
const reference = currentReference();
|
|
177
|
+
const begin = text.indexOf(REFERENCE_BEGIN);
|
|
178
|
+
const end = begin < 0 ? -1 : text.indexOf(REFERENCE_END, begin);
|
|
179
|
+
if (begin >= 0 && end < 0) return text; // Never replace an incomplete user-edited block.
|
|
180
|
+
if (begin >= 0 && end >= 0) {
|
|
181
|
+
if (text.slice(begin, end).startsWith(`${REFERENCE_BEGIN} ${TEMPLATE_REVISION}\n`)) return text;
|
|
182
|
+
const endOfLine = text.indexOf('\n', end);
|
|
183
|
+
const after = endOfLine < 0 ? text.length : endOfLine;
|
|
184
|
+
return text.slice(0, begin) + reference + text.slice(after);
|
|
185
|
+
}
|
|
186
|
+
if (text.startsWith(TEMPLATE_MARKER) && text.slice(0, 200).includes(REVISION_LINE)) return text;
|
|
187
|
+
if (text.startsWith(TEMPLATE_MARKER)) {
|
|
188
|
+
// Replace the older generated reference prefix, if present. Everything
|
|
189
|
+
// below the user's settings marker stays exactly as it was.
|
|
190
|
+
const settingsMarker = '// Your existing settings:';
|
|
191
|
+
const settingsAt = text.indexOf(settingsMarker);
|
|
192
|
+
if (text.includes('// Reference: copy any examples') && settingsAt >= 0) {
|
|
193
|
+
return `${TEMPLATE_MARKER}\n${reference}\n${text.slice(settingsAt)}`;
|
|
194
|
+
}
|
|
195
|
+
return text.replace(TEMPLATE_MARKER, `${TEMPLATE_MARKER}\n${reference}`);
|
|
196
|
+
}
|
|
197
|
+
return `${TEMPLATE_MARKER}\n${reference}\n// Your existing settings:\n${text}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function addDefaultProfile(text) {
|
|
201
|
+
// Insert only the missing profile, preserving user comments and formatting.
|
|
202
|
+
let clean, config;
|
|
203
|
+
try { clean = stripConfigComments(text); config = JSON.parse(clean); } catch { return text; }
|
|
204
|
+
if (!config || Array.isArray(config) || typeof config !== 'object') return text;
|
|
205
|
+
if (config.profiles && Object.hasOwn(config.profiles, 'default')) return text;
|
|
206
|
+
if (config.profiles !== undefined && (!config.profiles || Array.isArray(config.profiles) || typeof config.profiles !== 'object')) return text;
|
|
207
|
+
text = text.replace(/^\uFEFF/, '');
|
|
208
|
+
// Track depth to find the top-level profiles object, never a profile named profiles.
|
|
209
|
+
let depth = 0;
|
|
210
|
+
const tokens = /"(?:\\.|[^"\\])*"|[{}]/g;
|
|
211
|
+
for (const token of clean.matchAll(tokens)) {
|
|
212
|
+
if (token[0] === '{') depth++;
|
|
213
|
+
else if (token[0] === '}') depth--;
|
|
214
|
+
else if (depth === 1 && token[0] === '"profiles"') {
|
|
215
|
+
const after = token.index + token[0].length;
|
|
216
|
+
const match = /^\s*:\s*\{/.exec(clean.slice(after));
|
|
217
|
+
if (!match) continue;
|
|
218
|
+
const index = after + match[0].length;
|
|
219
|
+
return text.slice(0, index) + '\n // Used automatically when no other profile is selected.\n "default": {}' + (Object.keys(config.profiles).length ? ',' : '') + '\n' + text.slice(index);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const index = clean.indexOf('{') + 1;
|
|
223
|
+
return text.slice(0, index) + '\n // Used automatically when no other profile is selected.\n "profiles": { "default": {} }' + (Object.keys(config).length ? ',' : '') + '\n' + text.slice(index);
|
|
224
|
+
}
|