veodl 1.10.0 → 1.11.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/AGENTS.md +3 -3
- package/CHANGELOG.md +29 -0
- package/README.md +128 -67
- package/docs/AGENT_GUIDE.md +23 -9
- package/package.json +61 -58
- package/src/cli.js +227 -133
- package/src/config-diagnostics.js +20 -20
- package/src/config-editor.js +21 -17
- package/src/config-template.js +79 -66
- package/src/config.js +117 -110
- package/src/downloader.js +103 -42
- package/src/experimental-fragments.js +29 -0
- package/src/history.js +1 -1
- package/src/inline-region.js +72 -0
- package/src/inspect-media.js +11 -0
- package/src/interactive.js +9 -4
- package/src/jobs.js +51 -14
- package/src/naming.js +4 -0
- package/src/output.js +77 -60
- package/src/path-links.js +53 -0
- package/src/progress.js +341 -208
- package/src/terminal-size.js +8 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONFIG_KEYS, RESERVED_PROFILE_NAMES, parseConfigText, effectiveConfig, applyProfile } from './config.js';
|
|
1
|
+
import { CONFIG_KEYS, RESERVED_PROFILE_NAMES, parseConfigText, effectiveConfig, applyProfile } from './config.js';
|
|
2
2
|
import { stripConfigComments } from './config-template.js';
|
|
3
3
|
import { QUALITIES, AUDIO_FORMATS, VIDEO_FORMATS } from './utils.js';
|
|
4
4
|
|
|
@@ -64,7 +64,7 @@ function valueChoices(key, config = {}) {
|
|
|
64
64
|
if (key === 'format') return config.audio ? AUDIO_FORMATS : VIDEO_FORMATS;
|
|
65
65
|
if (key === 'timeout') return ['30s', '45s', '1m', '2m', '5m'];
|
|
66
66
|
if (['concurrentDownloads', 'playlistConcurrency'].includes(key)) return [1, 2, 3, 4];
|
|
67
|
-
if (key === 'concurrentFragments') return Array.from({ length:
|
|
67
|
+
if (key === 'concurrentFragments') return Array.from({ length: 64 }, (_, i) => i + 1);
|
|
68
68
|
return [];
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -75,7 +75,7 @@ export async function semanticIssue(text) {
|
|
|
75
75
|
if (!object(data)) return located(text, node([]), 'The config must contain a JSON object.');
|
|
76
76
|
const check = (settings, path) => {
|
|
77
77
|
for (const [key, value] of Object.entries(settings)) {
|
|
78
|
-
if (!path.length && ['$schema', 'profiles', 'activeProfile'].includes(key)) continue;
|
|
78
|
+
if (!path.length && ['$schema', 'profiles', 'activeProfile'].includes(key)) continue;
|
|
79
79
|
const target = node([...path, key]);
|
|
80
80
|
if (!Object.hasOwn(CONFIG_KEYS, key)) {
|
|
81
81
|
const proposed = suggestion(key, path.length ? keys : [...keys, 'profiles', '$schema']);
|
|
@@ -90,22 +90,22 @@ export async function semanticIssue(text) {
|
|
|
90
90
|
};
|
|
91
91
|
let issue = check(data, []);
|
|
92
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.`);
|
|
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
100
|
issue = check(profile, ['profiles', name]); if (issue) return issue;
|
|
101
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
|
-
}
|
|
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
109
|
const { config } = parseConfigText(text);
|
|
110
110
|
for (const profile of [undefined, ...Object.keys(config.profiles || {})]) {
|
|
111
111
|
try { await effectiveConfig(config, profile); }
|
|
@@ -114,7 +114,7 @@ export async function semanticIssue(text) {
|
|
|
114
114
|
let key = /Invalid quality/.test(message) ? 'quality' : /Invalid (?:audio|video) format/.test(message) ? 'format' :
|
|
115
115
|
/output directory/.test(message) ? 'output' : /custom filename/.test(message) ? 'rename' :
|
|
116
116
|
/--([a-z-]+)/.exec(message)?.[1]?.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
117
|
-
const name = profile ?? config.activeProfile ?? (config.profiles?.default ? 'default' : undefined);
|
|
117
|
+
const name = profile ?? config.activeProfile ?? (config.profiles?.default ? 'default' : undefined);
|
|
118
118
|
const selected = applyProfile(config, profile);
|
|
119
119
|
// Template errors do not name the CLI option. Locate the failing template.
|
|
120
120
|
if (!key) {
|
|
@@ -143,8 +143,8 @@ export function editorCompletions(text, cursor) {
|
|
|
143
143
|
const key = path.at(-1), property = cursor >= item.keyStart && cursor <= item.keyEnd;
|
|
144
144
|
if (!property && !(cursor >= item.start && cursor <= item.end)) continue;
|
|
145
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));
|
|
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
148
|
if (!choices.length) return null;
|
|
149
149
|
const proposed = property ? suggestion(key, choices) : null;
|
|
150
150
|
return { start: property ? item.keyStart : item.start, end: property ? item.keyEnd : item.end,
|
package/src/config-editor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { parseConfigText } from './config.js';
|
|
|
6
6
|
import { semanticIssue, editorCompletions } from './config-diagnostics.js';
|
|
7
7
|
import { configClipboard } from './config-clipboard.js';
|
|
8
8
|
import { addConfigGuide, configGuideState, removeConfigGuide } from './config-template.js';
|
|
9
|
+
import { folderLink } from './path-links.js';
|
|
9
10
|
|
|
10
11
|
const safe = text => text.replace(/[\x00-\x1f\x7f-\x9f]/g, ' ');
|
|
11
12
|
|
|
@@ -118,19 +119,18 @@ export async function editConfig(file, { input = process.stdin, output = process
|
|
|
118
119
|
let status = guideState === 'outdated' ? 'Updated config guide available. Press F3 to choose.' :
|
|
119
120
|
guideState === 'current' ? 'Generated guide in file. Press F3 to remove it if duplicated.' : '', revision = 0;
|
|
120
121
|
let viewport, dragging = false, completion = null;
|
|
121
|
-
const wasRaw = input.isRaw, wasPaused = input.isPaused();
|
|
122
|
+
const wasRaw = input.isRaw, wasPaused = input.isPaused(), wasFlowing = input.readableFlowing;
|
|
122
123
|
const render = () => {
|
|
123
124
|
if (closed) return;
|
|
124
125
|
const width = Math.max(1, (output.columns || 80) - 1);
|
|
125
126
|
const diagnosis = issue ? `${issue.line ? `Line ${issue.line}, column ${issue.column}: ` : ''}${issue.message}` : '';
|
|
126
127
|
const detail = safe(completion ? `Options (${completion.index + 1}/${completion.choices.length}): ${JSON.stringify(completion.choices[completion.index])} | Up/Down choose, Enter apply, Esc cancel` :
|
|
127
128
|
status ? `${status}${diagnosis ? ` | ${diagnosis}` : ''}` : diagnosis || 'Config OK');
|
|
128
|
-
const detailRows = Math.min(3, Math.max(1, Math.ceil(detail.length / width)));
|
|
129
129
|
const helpLines = question ? ['Discard unsaved changes? Y = discard, N / Esc = keep editing'] :
|
|
130
130
|
guidePrompt ? ['Guide: A add/update | R remove | Esc cancel'] :
|
|
131
131
|
['F3 Guide | F2 Options | Ctrl+S Save | Ctrl+Z Undo | Ctrl+Y Redo',
|
|
132
132
|
'Ctrl+A All | Ctrl+C Copy | Ctrl+V Paste | Esc / Ctrl+Q Exit'];
|
|
133
|
-
const height = Math.max(1, (output.rows || 24) -
|
|
133
|
+
const height = Math.max(1, (output.rows || 24) - 3);
|
|
134
134
|
const lines = buffer.text.split('\n'), { row, col } = buffer.position;
|
|
135
135
|
const gutter = Math.min(width - 1, String(lines.length).length + 2), available = Math.max(1, width - gutter);
|
|
136
136
|
if (followCursor) {
|
|
@@ -142,7 +142,9 @@ export async function editConfig(file, { input = process.stdin, output = process
|
|
|
142
142
|
viewport = { height, gutter, width };
|
|
143
143
|
const [selectionStart, selectionEnd] = buffer.selection;
|
|
144
144
|
let offset = lines.slice(0, top).reduce((sum, line) => sum + line.length + 1, 0);
|
|
145
|
-
const
|
|
145
|
+
const heading = safe(`veo config | ${file}${buffer.text !== saved ? ' *' : ''}`).slice(0, width);
|
|
146
|
+
const pathLabel = heading.slice(13, Math.min(heading.length, 13 + safe(file).length));
|
|
147
|
+
const screen = [heading.slice(0, 13) + folderLink(output, file, pathLabel) + heading.slice(13 + pathLabel.length)];
|
|
146
148
|
for (let index = top; index < top + height; index++) {
|
|
147
149
|
const line = lines[index];
|
|
148
150
|
const prefix = line === undefined ? '' : `${index + 1 === issue?.line ? '!' : ' '}${String(index + 1).padStart(Math.max(0, gutter - 2))} `;
|
|
@@ -157,18 +159,18 @@ export async function editConfig(file, { input = process.stdin, output = process
|
|
|
157
159
|
offset += (line || '').length + 1;
|
|
158
160
|
}
|
|
159
161
|
const position = `Ln ${row + 1}, Col ${col + 1}`;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
const tone = completion ? 33 : issue ? 31 : status ? 32 : 90;
|
|
163
|
+
const styledDetail = value => color ? `\x1b[${tone}m${value}\x1b[0m` : value;
|
|
164
|
+
const styledHelp = value => color ? value.replace(/Ctrl\+[A-Z]|F[23]|Esc|\b[A,R]\b/g, match => `\x1b[1;36m${match}\x1b[0m`) : value;
|
|
165
|
+
const prompt = question || guidePrompt;
|
|
166
|
+
const longDetail = prompt || detail.length + position.length + 3 > Math.max(30, Math.floor(width / 2));
|
|
167
|
+
screen.push(prompt ? styledHelp(safe(helpLines[0]).slice(0, width)) :
|
|
168
|
+
longDetail ? styledDetail(detail.slice(0, width)) : styledHelp(safe(helpLines[0]).slice(0, width)));
|
|
169
|
+
const right = (longDetail ? '' : `${detail} | `) + position;
|
|
170
|
+
const helpWidth = Math.max(0, width - right.length - 1);
|
|
171
|
+
const help = safe(helpLines[1] || '').slice(0, helpWidth);
|
|
172
|
+
const padding = ' '.repeat(Math.max(0, width - help.length - right.length));
|
|
173
|
+
screen.push(styledHelp(help) + padding + (longDetail ? position : styledDetail(detail) + ' | ' + position));
|
|
172
174
|
const cursorVisible = row >= top && row < top + height;
|
|
173
175
|
output.write('\x1b[?25l\x1b[H' + screen.map(line => line + '\x1b[K').join('\r\n') +
|
|
174
176
|
(cursorVisible ? `\x1b[${row - top + 2};${gutter + col - left + 1}H\x1b[?25h` : ''));
|
|
@@ -253,7 +255,9 @@ export async function editConfig(file, { input = process.stdin, output = process
|
|
|
253
255
|
closed = true; clearTimeout(timer); clearTimeout(mouseTimer); stopDrag();
|
|
254
256
|
input.off('data', onData); keyboard.off('keypress', onKey); keyboard.destroy();
|
|
255
257
|
input.off('keypress', onKey); input.off('end', onEnd); input.off('error', onError); output.off('resize', render);
|
|
256
|
-
input.setRawMode(Boolean(wasRaw));
|
|
258
|
+
input.setRawMode(Boolean(wasRaw));
|
|
259
|
+
// A fresh stdin has no explicit pause, but resume() still keeps its handle alive.
|
|
260
|
+
if (wasPaused || wasFlowing !== true) input.pause();
|
|
257
261
|
output.write('\x1b[?1002l\x1b[?1006l\x1b[0m\x1b[?25h\x1b[?1049l');
|
|
258
262
|
error ? reject(error) : resolve();
|
|
259
263
|
};
|
package/src/config-template.js
CHANGED
|
@@ -7,11 +7,11 @@ export const SOURCE_OPTIONS_GUIDE = String.raw`// veo: source discovery options
|
|
|
7
7
|
// "deepScan": true, // Verify every observed media candidate instead of the first 30
|
|
8
8
|
// "timeout": "2m", // Discovery deadline: 5s to 10m; plain numbers mean seconds
|
|
9
9
|
// "listSources": false, // true = list sources and exit for single-URL calls
|
|
10
|
-
// "autoListSources": true, // Search after extraction fails; false disables fallback
|
|
10
|
+
// "autoListSources": true, // Search after extraction fails; false disables fallback
|
|
11
11
|
// Command-line flags override these values, including --no-deep-scan and --no-list-sources.
|
|
12
12
|
`;
|
|
13
13
|
|
|
14
|
-
const PROFILE_OPTIONS_MARKER = '// veo: profile download options
|
|
14
|
+
const PROFILE_OPTIONS_MARKER = '// veo: profile download options v5';
|
|
15
15
|
export const PROFILE_OPTIONS_GUIDE = String.raw`${PROFILE_OPTIONS_MARKER}
|
|
16
16
|
// Any global option can also be placed inside a profile.
|
|
17
17
|
// Copy the settings you want into "profiles" > "default" or a named profile.
|
|
@@ -20,11 +20,14 @@ export const PROFILE_OPTIONS_GUIDE = String.raw`${PROFILE_OPTIONS_MARKER}
|
|
|
20
20
|
// "quality": "best",
|
|
21
21
|
// "concurrentDownloads": 2, // Parallel URLs/batch items: 1-4; default 2
|
|
22
22
|
// "adaptiveConcurrency": true, // Reduce parallelism and retry transient errors
|
|
23
|
-
// "concurrentFragments": 8, // DASH/HLS fragments: 1-
|
|
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.
|
|
24
27
|
// "deepScan": true, // Check every observed source candidate
|
|
25
28
|
// "timeout": "2m", // Source-search deadline, 5s to 10m
|
|
26
29
|
// "listSources": false, // List sources and exit for one URL
|
|
27
|
-
// "autoListSources": true, // Search after extraction fails (default)
|
|
30
|
+
// "autoListSources": true, // Search after extraction fails (default)
|
|
28
31
|
// "playlist": true, // Enable only for playlist downloads
|
|
29
32
|
// "playlistConcurrency": 2, // Simultaneous entries: 1-4; default 2
|
|
30
33
|
// "checkSpace": true, // Estimate free cache/output space first
|
|
@@ -37,7 +40,10 @@ export const PROFILE_OPTIONS_GUIDE = String.raw`${PROFILE_OPTIONS_MARKER}
|
|
|
37
40
|
// "folderTemplate": "{channel}/{year}",
|
|
38
41
|
// "filenameTemplate": "{index} - {title}", // Without extension; do not combine with rename
|
|
39
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
|
|
40
45
|
// "skipExisting": true
|
|
46
|
+
// "verify": false, // Probe saved media with FFprobe; no full decode
|
|
41
47
|
// Lossless container change (the selected codecs must fit the container):
|
|
42
48
|
// "format": "mkv",
|
|
43
49
|
// "compatible": false, // Opt-in H.264/AAC MP4; converts only when needed
|
|
@@ -79,8 +85,13 @@ const TEMPLATE_SOURCE = String.raw`${TEMPLATE_MARKER}
|
|
|
79
85
|
// "section": "*10:00-12:00", // Download only a time range
|
|
80
86
|
// "open": false, // Open the completed file automatically
|
|
81
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
|
|
82
90
|
// "skipExisting": true, // Skip previously saved downloads
|
|
83
|
-
// "
|
|
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
|
|
84
95
|
// "cookies": "D:/cookies.txt", // Netscape-format cookies file
|
|
85
96
|
// "cookiesFromBrowser": "firefox", // Use your browser's cookies
|
|
86
97
|
// "sponsorblockRemove": "sponsor", // Remove matching SponsorBlock segments
|
|
@@ -90,15 +101,17 @@ ${SOURCE_OPTIONS_GUIDE}
|
|
|
90
101
|
|
|
91
102
|
// Subtitles and additional information:
|
|
92
103
|
// "subs": true, // Download subtitles; defaults to English
|
|
104
|
+
// "autoSubs": false, // Include automatically generated subtitles
|
|
93
105
|
// "subLangs": "de,en", // Enable subtitles for these languages
|
|
106
|
+
// "subFormat": "srt/vtt/best", // Prefer these available subtitle formats
|
|
94
107
|
// "embedSubs": true, // Embed subtitles in the video
|
|
95
108
|
// "embedMetadata": true, // Embed the title, date and other metadata
|
|
96
109
|
// "embedThumbnail": true, // Embed the thumbnail
|
|
97
110
|
|
|
98
|
-
// The default profile is used automatically unless you select another profile.
|
|
99
|
-
// Use veo profile NAME to make any named profile the default for future commands.
|
|
100
|
-
// The active choice is stored at the top level: "activeProfile": "music",
|
|
101
|
-
${PROFILE_OPTIONS_GUIDE}
|
|
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}
|
|
102
115
|
// Select other profiles with --profile NAME or in the interactive wizard.
|
|
103
116
|
// Example: veo "https://example.com/video.mp4" --profile music
|
|
104
117
|
"profiles": {
|
|
@@ -127,62 +140,62 @@ ${PROFILE_OPTIONS_GUIDE}
|
|
|
127
140
|
}
|
|
128
141
|
`;
|
|
129
142
|
|
|
130
|
-
// The guide revision follows its content so the editor can offer an update.
|
|
143
|
+
// The guide revision follows its content so the editor can offer an update.
|
|
131
144
|
const TEMPLATE_REVISION = createHash('sha256').update(TEMPLATE_SOURCE).digest('hex').slice(0, 12);
|
|
132
|
-
const REFERENCE_BEGIN = '// veo: template reference begin';
|
|
133
|
-
const REFERENCE_END = '// veo: template reference end';
|
|
134
|
-
export const CONFIG_TEMPLATE = `{
|
|
135
|
-
"profiles": {
|
|
136
|
-
"default": {},
|
|
137
|
-
"kompatibel": { "audio": false, "format": "mp4", "compatible": true, "recode": false },
|
|
138
|
-
"music": { "audio": true, "format": "mp3" },
|
|
139
|
-
"archive": { "quality": "1080p", "embedMetadata": true, "subLangs": "de,en" }
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
`;
|
|
143
|
-
|
|
144
|
-
// Kept in the program. It is inserted into a config only when requested.
|
|
145
|
-
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`;
|
|
146
|
-
|
|
147
|
-
function generatedRange(text) {
|
|
148
|
-
const begin = text.indexOf(REFERENCE_BEGIN);
|
|
149
|
-
const end = begin < 0 ? -1 : text.indexOf(REFERENCE_END, begin);
|
|
150
|
-
if (begin < 0) {
|
|
151
|
-
const legacyHeader = /^\uFEFF?\/\/ veo: commented configuration template\r?\n\/\/ Reference: copy any examples/;
|
|
152
|
-
const settingsMarker = '// Your existing settings:';
|
|
153
|
-
const markerAt = legacyHeader.test(text) ? text.indexOf(settingsMarker) : -1;
|
|
154
|
-
if (markerAt < 0) return null;
|
|
155
|
-
const lineEnd = text.indexOf('\n', markerAt);
|
|
156
|
-
return { start: 0, stop: lineEnd < 0 ? text.length : lineEnd + 1, revision: 'legacy' };
|
|
157
|
-
}
|
|
158
|
-
if (end < 0) return null;
|
|
159
|
-
let start = begin;
|
|
160
|
-
const before = text.slice(0, begin);
|
|
161
|
-
if (/^\uFEFF?\/\/ veo: commented configuration template\r?\n$/.test(before)) start = 0;
|
|
162
|
-
const lineEnd = text.indexOf('\n', end);
|
|
163
|
-
let stop = lineEnd < 0 ? text.length : lineEnd + 1;
|
|
164
|
-
if (text.slice(stop).startsWith('// Your existing settings:\n')) stop += '// Your existing settings:\n'.length;
|
|
165
|
-
return { start, stop, revision: text.slice(begin, text.indexOf('\n', begin) < 0 ? text.length : text.indexOf('\n', begin)) };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function configGuideState(text) {
|
|
169
|
-
const range = generatedRange(text);
|
|
170
|
-
if (!range) return text.includes(REFERENCE_BEGIN) || text.includes('// Reference: copy any examples') ? 'incomplete' : 'absent';
|
|
171
|
-
return range.revision === `${REFERENCE_BEGIN} ${TEMPLATE_REVISION}` ? 'current' : 'outdated';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function addConfigGuide(text) {
|
|
175
|
-
const range = generatedRange(text);
|
|
176
|
-
if (!range && configGuideState(text) === 'incomplete') return text;
|
|
177
|
-
const bom = text.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
178
|
-
if (range) return (range.start === 0 ? bom : text.slice(0, range.start)) + CONFIG_GUIDE + text.slice(range.stop);
|
|
179
|
-
return bom + CONFIG_GUIDE + text.slice(bom.length);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function removeConfigGuide(text) {
|
|
183
|
-
const range = generatedRange(text);
|
|
184
|
-
return range ? (range.start === 0 && text.startsWith('\uFEFF') ? '\uFEFF' : text.slice(0, range.start)) + text.slice(range.stop) : text;
|
|
185
|
-
}
|
|
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
|
+
}
|
|
186
199
|
|
|
187
200
|
// Remove comments only outside JSON strings. Whitespace replacement preserves
|
|
188
201
|
// token boundaries, so malformed input cannot become valid by joining tokens.
|
|
@@ -210,5 +223,5 @@ export function stripConfigComments(text) {
|
|
|
210
223
|
if (!closed) throw Object.assign(new Error('Unterminated block comment; add */ to close it.'), { position: commentStart });
|
|
211
224
|
} else result += char;
|
|
212
225
|
}
|
|
213
|
-
return result;
|
|
214
|
-
}
|
|
226
|
+
return result;
|
|
227
|
+
}
|