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.
@@ -1,171 +1,224 @@
1
- import { translateLegacyConfigComments } from './legacy-config-comments.js';
2
-
3
- export const TEMPLATE_MARKER = '// veo: commented configuration template';
4
-
5
- const PROFILE_OPTIONS_MARKER = '// veo: profile download options v4';
6
- export const PROFILE_OPTIONS_GUIDE = String.raw`${PROFILE_OPTIONS_MARKER}
7
- // Any global option can also be placed inside a profile.
8
- // Copy the settings you want into "profiles" > "default" or a named profile.
9
- // Remove the leading // on copied settings; separate entries with commas.
10
- // Speed without reducing the selected video quality:
11
- // "quality": "best",
12
- // "concurrentDownloads": 2, // Parallel URLs/batch items: 1-4; default 2
13
- // "adaptiveConcurrency": true, // Reduce parallelism and retry transient errors
14
- // "concurrentFragments": 8, // DASH/HLS fragments: 1-16; default 8
15
- // "playlist": true, // Enable only for playlist downloads
16
- // "playlistConcurrency": 2, // Simultaneous entries: 1-4; default 2
17
- // "checkSpace": true, // Estimate free cache/output space first
18
- // "timings": true, // Show time spent in each phase
19
- // "color": true, // Gray details and colored results; false = plain text
20
- // Applies to veo's own terminal output, including stats/history/doctor.
21
- // Each profile can set color independently; NO_COLOR always disables styling.
22
- // Example inside profiles: "plain": { "color": false }
23
- // Use: veo stats --profile plain or veo URL --profile plain
24
- // "folderTemplate": "{channel}/{year}",
25
- // "filenameTemplate": "{index} - {title}", // Without extension; do not combine with rename
26
- // "resume": true,
27
- // "skipExisting": true
28
- // Lossless container change (the selected codecs must fit the container):
29
- // "format": "mkv",
30
- // "compatible": false, // Opt-in H.264/AAC MP4; converts only when needed
31
- // "recode": false
32
- // Compatibility profile to add inside profiles:
33
- // "kompatibel": { "audio": false, "format": "mp4", "compatible": true, "recode": false }
34
- // Use: veo URL --profile kompatibel (or simply veo URL --compatible).
35
- // Optional conversion instead: "format": "webm", "recode": true
36
- // Conversion can be slower and lose quality; recode is for video only.
37
- // Sequential downloads: "concurrentDownloads": 1, "concurrentFragments": 1, "playlistConcurrency": 1
38
- // Example named profile inside the profiles object:
39
- // "fast": { "quality": "best", "concurrentFragments": 8, "playlistConcurrency": 2, "resume": true }
40
- // Use with: veo URL --profile fast (add --playlist for a playlist URL).
41
- // Inspect: veo config show --profile fast | Validate: veo config check
42
- // Template fields: {title}, {id}, {channel}, {year}, {playlist}, {index}.
43
- // Missing values use Unknown channel/year, No playlist, unknown ID, index 001.
44
- `;
45
-
46
- export const CONFIG_TEMPLATE = String.raw`${TEMPLATE_MARKER}
47
- // Save and close the editor. Settings apply the next time you run veo.
48
- // Comments using // or /* ... */ are supported. Command-line options take priority.
49
- // To enable an option, remove its leading // and adjust the value.
50
- // Separate active entries with commas. Do not add a comma after the last entry.
51
- {
52
- // Output directory: use / or double backslashes, for example "D:\\Videos".
53
- // Without output, downloads are saved in the current working directory.
54
- // "output": "D:/Videos",
55
- // Filename without extension. Every * inserts the original video title.
56
- // "rename": "movie_*",
57
-
58
- // Maximum video resolution: best, 2160p, 1440p, 1080p, 720p, ...
59
- // "quality": "1080p",
60
- // Video format: mp4, mkv, webm, mov. Lossless remux; codecs must fit the container.
61
- // "format": "mp4",
62
- // "recode": false, // Explicit video conversion; may lose quality
63
- // "playlistConcurrency": 2, // Concurrent playlist entries: 1 to 4
64
- // "open": false, // Open the completed file automatically
65
- // "resume": true, // Resume interrupted downloads
66
- // "skipExisting": true, // Skip previously saved downloads
67
- // "concurrentFragments": 8, // Concurrent fragments: 1 to 16; default 8
68
-
69
- // Subtitles and additional information:
70
- // "subLangs": "de,en", // Enable subtitles for these languages
71
- // "embedSubs": true, // Embed subtitles in the video
72
- // "embedMetadata": true, // Embed the title, date and other metadata
73
- // "embedThumbnail": true, // Embed the thumbnail
74
-
75
- // The default profile is used automatically unless you select another profile.
76
- ${PROFILE_OPTIONS_GUIDE}
77
- // Select other profiles with --profile NAME or in the interactive wizard.
78
- // Example: veo "https://example.com/video.mp4" --profile music
79
- "profiles": {
80
- "default": {
81
- // "color": true, // Set false to disable terminal styling for this profile.
82
- // Add everyday defaults here, for example: "quality": "1080p"
83
- // Empty means use the global settings above and veo's built-in defaults.
84
- },
85
- "kompatibel": {
86
- "audio": false,
87
- "format": "mp4",
88
- "compatible": true,
89
- "recode": false
90
- },
91
- "music": {
92
- // Audio only. Formats: mp3, m4a, aac, opus, flac, wav.
93
- "audio": true,
94
- "format": "mp3"
95
- },
96
- "archive": {
97
- "quality": "1080p",
98
- "embedMetadata": true,
99
- "subLangs": "de,en"
100
- }
101
- }
102
- }
103
- `;
104
-
105
- // Remove comments only outside JSON strings. Whitespace replacement preserves
106
- // token boundaries, so malformed input cannot become valid by joining tokens.
107
- export function stripConfigComments(text) {
108
- text = text.replace(/^\uFEFF/, '');
109
- let result = '', quoted = false;
110
- for (let i = 0; i < text.length; i++) {
111
- const char = text[i];
112
- if (quoted) {
113
- result += char;
114
- if (char === '\\' && i + 1 < text.length) result += text[++i];
115
- else if (char === '"') quoted = false;
116
- } else if (char === '"') { quoted = true; result += char; }
117
- else if (char === '/' && text[i + 1] === '/') {
118
- result += ' '; i++;
119
- while (i + 1 < text.length && !'\r\n'.includes(text[i + 1])) { result += ' '; i++; }
120
- } else if (char === '/' && text[i + 1] === '*') {
121
- const commentStart = i;
122
- result += ' '; i++;
123
- let closed = false;
124
- while (++i < text.length) {
125
- if (text[i] === '*' && text[i + 1] === '/') { result += ' '; i++; closed = true; break; }
126
- result += '\r\n'.includes(text[i]) ? text[i] : ' ';
127
- }
128
- if (!closed) throw Object.assign(new Error('Unterminated block comment; add */ to close it.'), { position: commentStart });
129
- } else result += char;
130
- }
131
- return result;
132
- }
133
-
134
- export function withConfigTemplate(text) {
135
- text = translateLegacyConfigComments(text);
136
- text = addDefaultProfile(text);
137
- if (!text.replace(/^\uFEFF/, '').trim()) return CONFIG_TEMPLATE;
138
- if (text.replace(/^\uFEFF/, '').startsWith(TEMPLATE_MARKER)) {
139
- // Upgrade the commented reference in older configs without changing settings.
140
- if (!text.includes(PROFILE_OPTIONS_MARKER)) return text.replace(TEMPLATE_MARKER, `${TEMPLATE_MARKER}\n${PROFILE_OPTIONS_GUIDE}`);
141
- return text;
142
- }
143
- // Keep existing settings and formatting byte-for-byte after a commented guide.
144
- return `${TEMPLATE_MARKER}\n// Reference: copy any examples you need into your existing configuration below.\n${CONFIG_TEMPLATE.split('\n').slice(1).map(line => `// ${line}`).join('\n')}\n// Your existing settings:\n${text.replace(/^\uFEFF/, '')}`;
145
- }
146
-
147
- function addDefaultProfile(text) {
148
- // Insert only the missing profile, preserving user comments and formatting.
149
- let clean, config;
150
- try { clean = stripConfigComments(text); config = JSON.parse(clean); } catch { return text; }
151
- if (!config || Array.isArray(config) || typeof config !== 'object') return text;
152
- if (config.profiles && Object.hasOwn(config.profiles, 'default')) return text;
153
- if (config.profiles !== undefined && (!config.profiles || Array.isArray(config.profiles) || typeof config.profiles !== 'object')) return text;
154
- text = text.replace(/^\uFEFF/, '');
155
- // Track depth to find the top-level profiles object, never a profile named profiles.
156
- let depth = 0;
157
- const tokens = /"(?:\\.|[^"\\])*"|[{}]/g;
158
- for (const token of clean.matchAll(tokens)) {
159
- if (token[0] === '{') depth++;
160
- else if (token[0] === '}') depth--;
161
- else if (depth === 1 && token[0] === '"profiles"') {
162
- const after = token.index + token[0].length;
163
- const match = /^\s*:\s*\{/.exec(clean.slice(after));
164
- if (!match) continue;
165
- const index = after + match[0].length;
166
- 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);
167
- }
168
- }
169
- const index = clean.indexOf('{') + 1;
170
- 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);
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
+ }