veodl 1.9.1 → 1.10.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 +18 -0
- package/README.md +50 -33
- package/docs/AGENT_GUIDE.md +6 -4
- package/package.json +1 -1
- package/src/backend.js +64 -36
- package/src/cli.js +70 -48
- package/src/config-diagnostics.js +19 -9
- package/src/config-editor.js +181 -66
- package/src/config-template.js +63 -73
- package/src/config.js +110 -27
- package/src/downloader.js +28 -21
- package/src/interactive.js +19 -19
- package/src/jobs.js +13 -4
- package/src/progress.js +68 -12
- package/src/runs.js +8 -5
- package/src/source-discovery.js +20 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to veo. This project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 1.10.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Persistent Profile Management**: Added `veo profile [NAME|list|reset]` commands to view the active configuration profile, inspect available profiles, switch defaults, or reset back to default without manual configuration edits.
|
|
10
|
+
- **On-demand Config Guide**: Added `veo config guide` to display full configuration guidance, supported options, and templates in the terminal without opening the interactive editor.
|
|
11
|
+
- **Interactive Config Editor enhancements**:
|
|
12
|
+
- Full Undo (`Ctrl+Z`) and Redo (`Ctrl+Y`) history support when modifying settings.
|
|
13
|
+
- Select All (`Ctrl+A`) shortcut to easily highlight and overwrite lines or entire sections.
|
|
14
|
+
- Interactive F3 guide management prompt (`A` to add/refresh, `R` to strip documentation lines).
|
|
15
|
+
- Mouse edge drag auto-scrolling when dragging selections past viewport bounds.
|
|
16
|
+
- **Automatic Browser Source Discovery**: Enabled automatic source discovery fallback by default when direct extraction fails, automatically selecting and proceeding if a single verified media stream is found.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Streamlined source discovery observation polling to immediately proceed once candidate streams are detected.
|
|
21
|
+
- Enhanced progress output styling and ANSI handling across terminal status reporting.
|
|
22
|
+
|
|
5
23
|
## 1.9.1
|
|
6
24
|
|
|
7
25
|
### Added
|
package/README.md
CHANGED
|
@@ -193,9 +193,11 @@ example through `veo config edit`. Enable it with `veo URL --profile kompatibel`
|
|
|
193
193
|
reserved by concurrent downloads in the same process. Unknown sizes are reported as
|
|
194
194
|
unknown; conversion sizes and other processes' disk usage cannot be predicted exactly.
|
|
195
195
|
Use `--no-check-space` to disable this conservative check.
|
|
196
|
-
- `--timings` shows
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
- `--timings` shows phase times at the end of the run. Single downloads show
|
|
197
|
+
timing without a summary; multiple downloads show timing below the summary.
|
|
198
|
+
The phases cover setup, metadata, download/backend, processing, saving and retry waits.
|
|
199
|
+
Download/backend includes the backend's own startup and extraction overhead;
|
|
200
|
+
these are summed per-download times, not a diagnosis of network speed.
|
|
199
201
|
JSON results include `timings` in milliseconds, or `entryTimings` for playlists.
|
|
200
202
|
Use `--no-timings` to hide/omit them.
|
|
201
203
|
- Terminal details are gray, titles bold, saves green and errors red. The same styling
|
|
@@ -230,7 +232,7 @@ veo config show --profile fast
|
|
|
230
232
|
Config checks validate every effective profile (or the selected one), including unknown
|
|
231
233
|
settings, ranges and conflicting options. Show prints merged built-in, global and profile
|
|
232
234
|
settings as JSON, with credential paths/browser profiles redacted. Neither command contacts
|
|
233
|
-
video sites. All new options are also documented
|
|
235
|
+
video sites. All new options are also documented in `veo config guide`.
|
|
234
236
|
|
|
235
237
|
### Files, names and resume
|
|
236
238
|
|
|
@@ -322,11 +324,12 @@ download fails because a login is required, the error message points at these fl
|
|
|
322
324
|
is reached, verified sources found so far are shown with a timeout notice.
|
|
323
325
|
A deep scan covers observed requests; it cannot discover sources that the
|
|
324
326
|
page never loads. The timeout does not stop a subsequent download.
|
|
325
|
-
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
- For an unsupported single page URL, veo searches for media automatically and
|
|
328
|
+
uses the only verified source when there is one. If several sources are found,
|
|
329
|
+
the terminal asks which one to use; scripts receive a numbered source list and
|
|
330
|
+
a nonzero exit. `veo <page-url> --source 2 --dry-run --json` previews a chosen
|
|
331
|
+
source, and `veo <page-url> --source 2 --json` downloads it. Scripts never
|
|
332
|
+
receive an interactive prompt.
|
|
330
333
|
Retry jobs retain the page URL and selected number and discover a fresh media
|
|
331
334
|
URL, because player links may expire. Browser discovery uses a temporary
|
|
332
335
|
profile without copying browser cookies, and cannot unlock DRM content.
|
|
@@ -391,10 +394,11 @@ silently ignoring a typo would be worse.
|
|
|
391
394
|
Source discovery defaults are `deepScan` (boolean), `timeout` (string such as
|
|
392
395
|
`"30s"` or `"2m"`), `listSources` (boolean) and `autoListSources` (boolean).
|
|
393
396
|
`listSources: true` makes a single URL list sources and exit; use
|
|
394
|
-
`--no-list-sources` to download for one invocation. `autoListSources
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
397
|
+
`--no-list-sources` to download for one invocation. `autoListSources` defaults
|
|
398
|
+
to `true` and searches when ordinary extraction finds no downloadable video.
|
|
399
|
+
Set it to `false` or pass `--no-auto-list-sources` to disable that fallback.
|
|
400
|
+
A single verified source is selected automatically; multiple sources require
|
|
401
|
+
a choice. `deepScan` and
|
|
398
402
|
`timeout` apply to either kind of source search.
|
|
399
403
|
|
|
400
404
|
Additional defaults are `skipExisting` and `playlistItems`. Boolean defaults can be disabled
|
|
@@ -403,11 +407,16 @@ disables stored subtitle languages and subtitle embedding for that invocation.
|
|
|
403
407
|
|
|
404
408
|
### Named profiles
|
|
405
409
|
|
|
406
|
-
If no profile is selected, `profiles.default` is applied automatically.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
`veo
|
|
410
|
-
|
|
410
|
+
If no profile is selected, `profiles.default` is applied automatically. `veo profile music`
|
|
411
|
+
selects another named profile for future commands and stores it as `activeProfile` in the
|
|
412
|
+
config; `veo profile` shows the current choice, and `veo profile list` marks it in
|
|
413
|
+
the list. `veo profile reset` removes the stored choice and returns to `default`.
|
|
414
|
+
You can also use `veo profile default` to select it explicitly.
|
|
415
|
+
The names `list` and `reset` are reserved for these commands and cannot be used as profile names.
|
|
416
|
+
`--profile NAME` overrides this choice for one command. Named profiles use global defaults
|
|
417
|
+
rather than inheriting `default`. The wizard preselects the active profile.
|
|
418
|
+
New configurations include an empty `default` profile and the example profiles. Existing configurations are
|
|
419
|
+
left unchanged when opened. An empty profile does not change download behavior.
|
|
411
420
|
|
|
412
421
|
```json
|
|
413
422
|
{
|
|
@@ -420,13 +429,15 @@ preserving existing settings. An empty profile does not change download behavior
|
|
|
420
429
|
}
|
|
421
430
|
```
|
|
422
431
|
|
|
423
|
-
`veo <url> --profile music` merges global defaults, then the selected profile, then explicit
|
|
424
|
-
CLI flags. `veo config profiles` lists profile names; `veo config path` prints the file path.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
432
|
+
`veo <url> --profile music` merges global defaults, then the selected profile, then explicit
|
|
433
|
+
CLI flags. `veo config profiles` lists profile names; `veo config path` prints the file path.
|
|
434
|
+
The download status shows `Profile: NAME` (or `global (no profile)` when none is configured).
|
|
435
|
+
`veo config edit` creates a small, valid configuration for new or empty files. Opening an
|
|
436
|
+
existing configuration does not add or update comments. The current option guide is kept in
|
|
437
|
+
the program; `veo config guide` prints it without changing the file. In the built-in editor,
|
|
438
|
+
F3 offers **A** to add or update the guide at the top and **R** to remove a previously
|
|
439
|
+
generated guide. Ctrl+S saves that choice. This can also clean up the long duplicate block
|
|
440
|
+
inserted by older versions. Personal settings and notes below a generated block are preserved.
|
|
430
441
|
In an interactive terminal, it opens the built-in editor
|
|
431
442
|
unless `VISUAL` or `EDITOR` is configured (an executable path, without shell arguments).
|
|
432
443
|
The editor provides syntax colors, line numbers and live validation. Syntax errors mark
|
|
@@ -435,11 +446,13 @@ spelling suggestions, expected types and allowed values in the status area. Vali
|
|
|
435
446
|
the same rules as the CLI, including profile overrides and audio/video formats.
|
|
436
447
|
Press F2 on a property or value to open suggestions, use Up/Down to choose, Enter to apply
|
|
437
448
|
or Esc to cancel. Suggestions require valid JSON syntax; free-text values and custom numeric
|
|
438
|
-
resolutions such as `900p` remain supported. Ctrl+S validates and saves;
|
|
439
|
-
|
|
449
|
+
resolutions such as `900p` remain supported. Ctrl+S validates and saves;
|
|
450
|
+
Ctrl+Z undoes edits, Ctrl+Y redoes them, and Ctrl+A selects the whole file.
|
|
451
|
+
Esc or Ctrl+Q exits, asking before discarding changes. Use arrows, Home/End and PageUp/PageDown
|
|
440
452
|
to navigate. Invalid configurations cannot be saved. `--no-color` disables syntax colors.
|
|
441
453
|
In terminals supporting SGR mouse reporting, left-click positions the cursor and dragging
|
|
442
|
-
selects text, including across lines.
|
|
454
|
+
selects text, including across lines. Holding the pointer at the top or bottom while dragging
|
|
455
|
+
scrolls the file and extends the selection. Backspace/Delete removes the selection; typing replaces
|
|
443
456
|
it. The mouse wheel scrolls without changing the editing position; Ctrl+Up/Down also scrolls.
|
|
444
457
|
Ctrl+C copies selected text to the system clipboard, and Ctrl+V pastes clipboard text at the
|
|
445
458
|
cursor or replaces the selection. Ctrl+Q or Esc exits the editor. The editor disables mouse
|
|
@@ -464,9 +477,8 @@ $env:VEO_CONFIG_EDITOR = 'external'
|
|
|
464
477
|
|
|
465
478
|
Non-interactive sessions use the external editor unless `--terminal` is explicitly selected,
|
|
466
479
|
in which case an interactive-terminal error is reported. The wizard also offers configured profiles.
|
|
467
|
-
Generated
|
|
468
|
-
|
|
469
|
-
paths and custom comments are preserved.
|
|
480
|
+
Generated guides and app messages are in English. Existing comments, profile names and
|
|
481
|
+
settings are preserved when opening the editor.
|
|
470
482
|
|
|
471
483
|
### Terminal output
|
|
472
484
|
|
|
@@ -502,9 +514,14 @@ paths and custom comments are preserved.
|
|
|
502
514
|
```bash
|
|
503
515
|
veo version # installed version (also: veo --version or veo -v)
|
|
504
516
|
veo # interactive download wizard (terminal only)
|
|
505
|
-
veo config edit # create/open config
|
|
517
|
+
veo config edit # create/open config; F3 offers the optional guide
|
|
518
|
+
veo config guide # print the current option guide
|
|
506
519
|
veo config reset # confirm with y; back up config and restore the current template
|
|
507
|
-
veo config profiles # list available profiles
|
|
520
|
+
veo config profiles # list available profiles
|
|
521
|
+
veo profile # show the currently selected profile
|
|
522
|
+
veo profile list # list all profiles and mark the active one
|
|
523
|
+
veo profile music # use music as the default for future commands
|
|
524
|
+
veo profile reset # return to default (or global settings if absent)
|
|
508
525
|
veo config path # show the config file location
|
|
509
526
|
veo doctor # diagnose the local setup; exit 1 if a check fails
|
|
510
527
|
veo doctor fix # restore missing or damaged managed tools
|
package/docs/AGENT_GUIDE.md
CHANGED
|
@@ -10,10 +10,12 @@ veo "https://example.com/video" --json --output ./downloads
|
|
|
10
10
|
veo inspect "./downloads/video.mp4" --check-audio --json
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
For a page with an embedded player that the normal extractor cannot identify,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
For a page with an embedded player that the normal extractor cannot identify,
|
|
14
|
+
`veo "<page-url>" --dry-run --json` searches for playable media automatically.
|
|
15
|
+
It uses a single verified source without a second command. If several sources
|
|
16
|
+
are found, run `veo "<page-url>" --list-sources --json` and choose an `index`,
|
|
17
|
+
then use `veo "<page-url>" --source <index> --dry-run --json` before downloading.
|
|
18
|
+
Source discovery opens a temporary headless Chromium-based
|
|
17
19
|
browser, repeatedly checks for Play controls, and observes media requests after
|
|
18
20
|
interaction. It needs Chrome, Edge or Chromium,
|
|
19
21
|
or `VEO_BROWSER_PATH`. A found source is verified by the backend; size may be
|
package/package.json
CHANGED
package/src/backend.js
CHANGED
|
@@ -291,11 +291,19 @@ async function downloadBackend({ asset, release, expected, directory, signal, st
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
async function acquire(asset, directory, signal, status) {
|
|
294
|
+
async function acquire(asset, directory, signal, status, statusDone = () => {}) {
|
|
295
295
|
const destination = path.join(directory, `yt-dlp${exeSuffix()}`);
|
|
296
296
|
const expected = HASHES[asset];
|
|
297
297
|
status('Checking yt-dlp…');
|
|
298
|
-
|
|
298
|
+
let cached;
|
|
299
|
+
try {
|
|
300
|
+
cached = await matches(destination, expected, signal);
|
|
301
|
+
statusDone('Checking yt-dlp…', 'done');
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (!signal?.aborted && error?.name !== 'AbortError') statusDone('Checking yt-dlp…', 'failed');
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
if (cached) {
|
|
299
307
|
if (process.platform !== 'win32') await chmod(destination, 0o755);
|
|
300
308
|
return destination;
|
|
301
309
|
}
|
|
@@ -330,7 +338,6 @@ async function managedMediaTools({ signal, status, directory }) {
|
|
|
330
338
|
} catch (cause) {
|
|
331
339
|
return `the bundled static media tools are unusable (${cause.message})`;
|
|
332
340
|
}
|
|
333
|
-
status('Preparing ffmpeg and ffprobe…');
|
|
334
341
|
const suffix = exeSuffix();
|
|
335
342
|
await stage(ffmpeg, path.join(directory, `ffmpeg${suffix}`), signal);
|
|
336
343
|
await stage(ffprobe, path.join(directory, `ffprobe${suffix}`), signal);
|
|
@@ -343,38 +350,58 @@ async function managedMediaTools({ signal, status, directory }) {
|
|
|
343
350
|
* then a system installation (which is what Windows on ARM needs, because
|
|
344
351
|
* ffmpeg-static and ffprobe-static ship no arm64 Windows binaries).
|
|
345
352
|
*/
|
|
346
|
-
export async function resolveMediaTools({ signal, status = () => {}, directory, offline = false,
|
|
353
|
+
export async function resolveMediaTools({ signal, status = () => {}, statusDone = () => {}, directory, offline = false,
|
|
347
354
|
find = findOnPath, managed = managedMediaTools, install = installMediaTools } = {}) {
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
355
|
+
const preparing = 'Preparing ffmpeg and ffprobe…';
|
|
356
|
+
let currentStatus;
|
|
357
|
+
const report = message => { currentStatus = message; status(message); };
|
|
358
|
+
const finish = outcome => {
|
|
359
|
+
if (currentStatus !== preparing) report(preparing);
|
|
360
|
+
statusDone(preparing, outcome);
|
|
361
|
+
};
|
|
362
|
+
report(preparing);
|
|
363
|
+
try {
|
|
364
|
+
const suffix = exeSuffix();
|
|
365
|
+
const override = envPath('VEO_FFMPEG_PATH');
|
|
366
|
+
if (override) {
|
|
367
|
+
await executable(path.join(override, `ffmpeg${suffix}`), 'VEO_FFMPEG_PATH ffmpeg');
|
|
368
|
+
await executable(path.join(override, `ffprobe${suffix}`), 'VEO_FFMPEG_PATH ffprobe');
|
|
369
|
+
finish('done');
|
|
370
|
+
report('Using VEO_FFMPEG_PATH override.');
|
|
371
|
+
return override;
|
|
372
|
+
}
|
|
373
|
+
const problem = await managed({ signal, status: report, directory });
|
|
374
|
+
if (!problem) { finish('done'); return directory; }
|
|
375
|
+
// A previous run may already have staged a working pair into the cache.
|
|
376
|
+
if (await isExecutable(path.join(directory, `ffmpeg${suffix}`))
|
|
377
|
+
&& await isExecutable(path.join(directory, `ffprobe${suffix}`))) { finish('done'); return directory; }
|
|
378
|
+
const ffmpeg = await find(['ffmpeg']);
|
|
379
|
+
const ffprobe = await find(['ffprobe']);
|
|
380
|
+
if (!ffmpeg || !ffprobe) {
|
|
381
|
+
if (!offline) {
|
|
382
|
+
const location = await install({ signal, status: report, directory, find, stage });
|
|
383
|
+
finish('done');
|
|
384
|
+
return location;
|
|
385
|
+
}
|
|
386
|
+
const missing = [!ffmpeg && 'ffmpeg', !ffprobe && 'ffprobe'].filter(Boolean).join(' and ');
|
|
387
|
+
throw new Error(`Cannot load media tools: ${problem}, and no ${missing} was found on PATH. `
|
|
388
|
+
+ 'Run veo doctor fix without --offline to install them automatically, or set VEO_FFMPEG_PATH to a directory containing both binaries.');
|
|
389
|
+
}
|
|
390
|
+
// Same directory: yt-dlp can use it in place instead of duplicating ~150 MB.
|
|
391
|
+
if (path.dirname(ffmpeg) === path.dirname(ffprobe)) {
|
|
392
|
+
finish('done');
|
|
393
|
+
report(`Using the system ffmpeg in ${path.dirname(ffmpeg)}.`);
|
|
394
|
+
return path.dirname(ffmpeg);
|
|
395
|
+
}
|
|
396
|
+
report('Copying system ffmpeg and ffprobe into the backend cache…');
|
|
397
|
+
await stage(ffmpeg, path.join(directory, `ffmpeg${suffix}`), signal);
|
|
398
|
+
await stage(ffprobe, path.join(directory, `ffprobe${suffix}`), signal);
|
|
399
|
+
finish('done');
|
|
400
|
+
return directory;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (!signal?.aborted && error?.name !== 'AbortError') finish('failed');
|
|
403
|
+
throw error;
|
|
373
404
|
}
|
|
374
|
-
status('Copying system ffmpeg and ffprobe into the backend cache…');
|
|
375
|
-
await stage(ffmpeg, path.join(directory, `ffmpeg${suffix}`), signal);
|
|
376
|
-
await stage(ffprobe, path.join(directory, `ffprobe${suffix}`), signal);
|
|
377
|
-
return directory;
|
|
378
405
|
}
|
|
379
406
|
|
|
380
407
|
/**
|
|
@@ -457,12 +484,13 @@ export async function inspectBackend({ signal, platform = process.platform, arch
|
|
|
457
484
|
* cannot serve the current platform (notably Windows on ARM).
|
|
458
485
|
* onStatus receives plain strings. Throws on cancellation or acquisition failure.
|
|
459
486
|
*/
|
|
460
|
-
export async function resolveBackend({ signal, onStatus, offline = false, platform = process.platform, find = findOnPath,
|
|
487
|
+
export async function resolveBackend({ signal, onStatus, onStatusDone, offline = false, platform = process.platform, find = findOnPath,
|
|
461
488
|
setupTermux = installTermuxTools, hasEjs = hasTermuxEjs, run = runSetup } = {}) {
|
|
462
489
|
if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal must be an AbortSignal.');
|
|
463
490
|
if (onStatus !== undefined && typeof onStatus !== 'function') throw new TypeError('onStatus must be a function.');
|
|
464
491
|
signal?.throwIfAborted();
|
|
465
492
|
const status = onStatus || (() => {});
|
|
493
|
+
const statusDone = onStatusDone || (() => {});
|
|
466
494
|
const ytOverride = envPath('VEO_YT_DLP_PATH');
|
|
467
495
|
if (platform === 'android') {
|
|
468
496
|
const mediaOverride = envPath('VEO_FFMPEG_PATH');
|
|
@@ -508,13 +536,13 @@ export async function resolveBackend({ signal, onStatus, offline = false, platfo
|
|
|
508
536
|
const installed = ytDlp ? null : await activeOverride(asset, { signal });
|
|
509
537
|
const directory = cacheDirectory();
|
|
510
538
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
511
|
-
const ffmpegLocation = await resolveMediaTools({ signal, status, directory, offline });
|
|
539
|
+
const ffmpegLocation = await resolveMediaTools({ signal, status, statusDone, directory, offline });
|
|
512
540
|
if (ytDlp) status('Using VEO_YT_DLP_PATH override.');
|
|
513
541
|
else if (installed) status(`Using the installed yt-dlp ${installed.release}.`);
|
|
514
542
|
if (offline && !ytDlp && !installed && !await matches(path.join(directory, `yt-dlp${exeSuffix()}`), HASHES[asset], signal)) {
|
|
515
543
|
throw new Error('yt-dlp is missing or damaged. Run veo doctor fix without --offline to download it.');
|
|
516
544
|
}
|
|
517
|
-
const backend = ytDlp || installed?.path || await acquire(asset, directory, signal, status);
|
|
545
|
+
const backend = ytDlp || installed?.path || await acquire(asset, directory, signal, status, statusDone);
|
|
518
546
|
signal?.throwIfAborted();
|
|
519
547
|
return { ytDlp: backend, ffmpegLocation };
|
|
520
548
|
}
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
7
7
|
import { createReporter } from './progress.js';
|
|
8
8
|
import { openFile } from './open-file.js';
|
|
9
9
|
import { maybeUpdateNotice, updateMain, UPDATE_HELP, defaultRegistry, packageVersion } from './updater.js';
|
|
10
|
-
import { applyProfile, configMain, loadConfig } from './config.js';
|
|
10
|
+
import { applyProfile, configMain, loadConfig, profileMain, selectedProfileName } from './config.js';
|
|
11
11
|
import { validateItems, describeEstimate } from './playlist.js';
|
|
12
12
|
import { parseSourceTimeout } from './source-discovery.js';
|
|
13
13
|
import { explainUnknownOption, optionSpellings, suggestOption } from './option-suggestions.js';
|
|
@@ -58,7 +58,7 @@ Options:
|
|
|
58
58
|
--resume Keep partial data and continue an interrupted download
|
|
59
59
|
--list-formats Show the available formats and exit
|
|
60
60
|
--list-sources Find video sources loaded by a web page and exit
|
|
61
|
-
--auto-list-sources Search
|
|
61
|
+
--auto-list-sources Search after no video is found (default: on)
|
|
62
62
|
--source <n> Select source number from that page (also for scripts)
|
|
63
63
|
--deep-scan Check every media candidate found during source search
|
|
64
64
|
--timeout <duration> Source search deadline, e.g. 30, 30s or 2m (5s-10m)
|
|
@@ -89,7 +89,8 @@ Commands:
|
|
|
89
89
|
veo alias list|add|remove Manage extra command names (wrappers calling veo)
|
|
90
90
|
veo uninstall [-p <name>] Remove one alias, or everything with --yes
|
|
91
91
|
veo version Show the installed version
|
|
92
|
-
veo config edit|path|profiles|check|show|reset Manage defaults and named profiles
|
|
92
|
+
veo config edit|path|profiles|guide|check|show|reset Manage defaults and named profiles
|
|
93
|
+
veo profile [list|reset|name] Show, list or change the default profile
|
|
93
94
|
veo config edit [--external|--terminal] Choose the configuration editor
|
|
94
95
|
|
|
95
96
|
Run veo without arguments in a terminal for interactive setup.
|
|
@@ -198,7 +199,7 @@ export function optionDefaults(config = {}) {
|
|
|
198
199
|
timeout: config.timeout,
|
|
199
200
|
'deep-scan': config.deepScan ?? false,
|
|
200
201
|
'list-sources': config.listSources ?? false,
|
|
201
|
-
'auto-list-sources': config.autoListSources ??
|
|
202
|
+
'auto-list-sources': config.autoListSources ?? true,
|
|
202
203
|
};
|
|
203
204
|
}
|
|
204
205
|
|
|
@@ -226,6 +227,7 @@ const COMMAND_FLAGS = {
|
|
|
226
227
|
alias: ['--json', '--force', '--bin-dir'],
|
|
227
228
|
uninstall: ['--prefix', '-p', '--yes', '--keep-cache', '--keep-config', '--keep-aliases', '--json', '--bin-dir'],
|
|
228
229
|
config: ['--profile', '--external', '--terminal'],
|
|
230
|
+
profile: [],
|
|
229
231
|
};
|
|
230
232
|
const COMMAND_NAMES = [...Object.keys(COMMAND_FLAGS), 'retry', 'help', 'version'];
|
|
231
233
|
|
|
@@ -259,6 +261,7 @@ export function parseCli(args, { config = {} } = {}) {
|
|
|
259
261
|
let preliminary;
|
|
260
262
|
try { preliminary = parseArgs({ args, allowPositionals: true, strict: true, allowNegative: true, options: cliOptions() }); }
|
|
261
263
|
catch (error) { throw explainUnknownOption(error, cliOptions()); }
|
|
264
|
+
const profileName = selectedProfileName(config, preliminary.values.profile);
|
|
262
265
|
config = applyProfile(config, preliminary.values.profile);
|
|
263
266
|
let parsed;
|
|
264
267
|
try { parsed = parseArgs({ args, tokens: true, allowNegative: true, allowPositionals: true, strict: true, options: cliOptions(config) }); }
|
|
@@ -340,12 +343,13 @@ export function parseCli(args, { config = {} } = {}) {
|
|
|
340
343
|
if (options.rename && options.filenameTemplate) throw new Error('--rename and --filename-template cannot be combined.');
|
|
341
344
|
const disableSubs = values.subs === false && tokens.some(token => token.name === 'subs');
|
|
342
345
|
if (options.subLangs && !disableSubs) options.subs = true;
|
|
343
|
-
if (disableSubs) {
|
|
344
|
-
options.subLangs = undefined;
|
|
345
|
-
options.embedSubs = false;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
346
|
+
if (disableSubs) {
|
|
347
|
+
options.subLangs = undefined;
|
|
348
|
+
options.embedSubs = false;
|
|
349
|
+
}
|
|
350
|
+
options.profile = profileName;
|
|
351
|
+
return options;
|
|
352
|
+
}
|
|
349
353
|
|
|
350
354
|
export async function main(args = process.argv.slice(2), { config } = {}) {
|
|
351
355
|
if (args[0] === 'help') args = ['--help', ...args.slice(1)];
|
|
@@ -417,7 +421,7 @@ async function runMain(args, { config }) {
|
|
|
417
421
|
try { return await (await import('./runs.js')).stopMain(args.slice(1)); }
|
|
418
422
|
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
419
423
|
}
|
|
420
|
-
if (args[0] === 'retry') {
|
|
424
|
+
if (args[0] === 'retry') {
|
|
421
425
|
if (args[1] !== '--last') {
|
|
422
426
|
stderr.write('veo: Usage: veo retry --last [download options]\n');
|
|
423
427
|
return 1;
|
|
@@ -426,8 +430,12 @@ async function runMain(args, { config }) {
|
|
|
426
430
|
const { latestFailedJob } = await import('./jobs.js');
|
|
427
431
|
args = ['--retry-failed', await latestFailedJob(), ...args.slice(2)];
|
|
428
432
|
} catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
429
|
-
}
|
|
430
|
-
|
|
433
|
+
}
|
|
434
|
+
if (args[0] === 'profile') {
|
|
435
|
+
try { return await profileMain(args.slice(1)); }
|
|
436
|
+
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
437
|
+
}
|
|
438
|
+
const { cleanupDownloadCache } = await import('./download-cache.js');
|
|
431
439
|
await cleanupDownloadCache().catch(error => stderr.write(`veo: Could not clean expired local downloads: ${readableError(error)}\n`));
|
|
432
440
|
// update/upgrade/up/check subcommands are handled before URL validation.
|
|
433
441
|
if (['update', 'up', 'upgrade', 'check'].includes(args[0])) {
|
|
@@ -438,10 +446,10 @@ async function runMain(args, { config }) {
|
|
|
438
446
|
}
|
|
439
447
|
return updateMain(args, { registry: defaultRegistry() });
|
|
440
448
|
}
|
|
441
|
-
if (args[0] === 'config') {
|
|
442
|
-
try { return await configMain(args.slice(1)); }
|
|
443
|
-
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
444
|
-
}
|
|
449
|
+
if (args[0] === 'config') {
|
|
450
|
+
try { return await configMain(args.slice(1)); }
|
|
451
|
+
catch (error) { stderr.write(`veo: ${readableError(error)}\n`); return 1; }
|
|
452
|
+
}
|
|
445
453
|
if (args[0] === 'doctor') {
|
|
446
454
|
try {
|
|
447
455
|
const { doctorMain } = await import('./doctor.js');
|
|
@@ -507,7 +515,14 @@ async function runMain(args, { config }) {
|
|
|
507
515
|
retryItems = await retryOptions(options.retryFailed);
|
|
508
516
|
// Revalidate stored options and apply only flags explicitly supplied now.
|
|
509
517
|
const retryArgs = args.filter((arg, index) => arg !== '--retry-failed' && args[index - 1] !== '--retry-failed' && !arg.startsWith('--retry-failed='));
|
|
510
|
-
|
|
518
|
+
const overrideProfile = retryArgs.some(arg => arg === '--profile' || arg.startsWith('--profile='));
|
|
519
|
+
retryItems = retryItems.map(item => {
|
|
520
|
+
const resumed = parseCli([item.url, ...retryArgs], { config: overrideProfile
|
|
521
|
+
? { ...item, profiles: loaded.config?.profiles }
|
|
522
|
+
: item });
|
|
523
|
+
if (!overrideProfile) resumed.profile = item.profile;
|
|
524
|
+
return resumed;
|
|
525
|
+
});
|
|
511
526
|
options = { ...retryItems[0], urls: retryItems.map(item => item.url) };
|
|
512
527
|
}
|
|
513
528
|
if (options.batchFile) {
|
|
@@ -521,17 +536,22 @@ async function runMain(args, { config }) {
|
|
|
521
536
|
// The job file is created before the first download, so another terminal can
|
|
522
537
|
// follow this run's per-item progress with `veo runs <id>`.
|
|
523
538
|
const jobFile = options.listFormats || options.listSources || options.dryRun ? null : jobFilePath();
|
|
524
|
-
await run?.describe({ urls: options.urls, output: options.output ? path.resolve(options.output) : null,
|
|
525
|
-
audio: options.audio, quality: options.quality, format: options.format,
|
|
526
|
-
|
|
527
|
-
reporter.
|
|
528
|
-
|
|
539
|
+
await run?.describe({ urls: options.urls, output: options.output ? path.resolve(options.output) : null,
|
|
540
|
+
audio: options.audio, quality: options.quality, format: options.format, profile: options.profile || null,
|
|
541
|
+
playlist: options.playlist, job: jobFile });
|
|
542
|
+
reporter.configure?.({ color: options.color && !options.json });
|
|
543
|
+
reporter.start(options.rename);
|
|
544
|
+
if (!options.dryRun && !options.listFormats && !options.listSources) reporter.profile(options.profile);
|
|
545
|
+
const preparedBackends = new Map();
|
|
546
|
+
const reuseBackend = request => preparedBackends.has(request.url) ? async () => preparedBackends.get(request.url) : undefined;
|
|
547
|
+
const cookieWarning = cookieFileWarning(options.cookies);
|
|
529
548
|
if (cookieWarning) stderr.write(`veo: ${cookieWarning}\n`);
|
|
530
549
|
|
|
531
550
|
// For a single page, fall back to media requests observed while its player loads.
|
|
532
551
|
// Explicit --source always rediscovers: signed media URLs can expire between runs.
|
|
533
552
|
if (!retryItems && options.urls.length === 1 && (!options.playlist || options.listSources)) {
|
|
534
|
-
const discovery = await resolvePageSource(options, { signal: controller.signal, stderr
|
|
553
|
+
const discovery = await resolvePageSource(options, { signal: controller.signal, stderr, reporter,
|
|
554
|
+
onBackend: backend => preparedBackends.set(options.url, backend) });
|
|
535
555
|
if (options.listSources) {
|
|
536
556
|
if (options.json) stdout.write(`${JSON.stringify({ url: options.url, status: 'sources', timedOut: Boolean(discovery.sources.timedOut), sources: discovery.sources.map(({ url, pageUrl, ...safe }) => safe) })}\n`);
|
|
537
557
|
else for (const source of discovery.sources) stdout.write(`${discovery.formatSource(source)}\n`);
|
|
@@ -554,20 +574,21 @@ async function runMain(args, { config }) {
|
|
|
554
574
|
}
|
|
555
575
|
if (retryItems) for (const item of retryItems) {
|
|
556
576
|
if (!item.source) continue;
|
|
557
|
-
const discovery = await resolvePageSource(item, { signal: controller.signal, stderr
|
|
577
|
+
const discovery = await resolvePageSource(item, { signal: controller.signal, stderr, reporter,
|
|
578
|
+
onBackend: backend => preparedBackends.set(item.url, backend) });
|
|
558
579
|
item.mediaUrl = discovery.mediaUrl;
|
|
559
580
|
}
|
|
560
581
|
|
|
561
582
|
if (options.listFormats) {
|
|
562
583
|
const { listFormats } = await import('./downloader.js');
|
|
563
|
-
stdout.write(await listFormats(options, { signal: controller.signal, reporter }));
|
|
584
|
+
stdout.write(await listFormats(options, { signal: controller.signal, reporter, backendResolver: reuseBackend(options) }));
|
|
564
585
|
return 0;
|
|
565
586
|
}
|
|
566
587
|
if (options.dryRun) {
|
|
567
588
|
const { planDownload } = await import('./downloader.js');
|
|
568
589
|
for (const request of retryItems || options.urls.map(url => ({ ...options, url }))) {
|
|
569
590
|
const { url } = request;
|
|
570
|
-
const plan = await planDownload(request, { signal: controller.signal, reporter });
|
|
591
|
+
const plan = await planDownload(request, { signal: controller.signal, reporter, backendResolver: reuseBackend(request) });
|
|
571
592
|
if (options.json) stdout.write(`${JSON.stringify({ url, status: 'planned', ...plan })}\n`);
|
|
572
593
|
else {
|
|
573
594
|
stdout.write(`URL: ${url}\n`);
|
|
@@ -581,7 +602,8 @@ async function runMain(args, { config }) {
|
|
|
581
602
|
const { download } = await import('./downloader.js');
|
|
582
603
|
const { createStatsRecorder } = await import('./stats.js');
|
|
583
604
|
const { createHistoryRecorder } = await import('./history.js');
|
|
584
|
-
const result = await runJob(options, { download
|
|
605
|
+
const result = await runJob(options, { download: (request, dependencies) => download(request, { ...dependencies, backendResolver: reuseBackend(request) }),
|
|
606
|
+
reporter, signal: controller.signal, openFile, items: retryItems, jobFile: jobFile || undefined, runId: run?.id, recordStats: createStatsRecorder(), recordHistory: createHistoryRecorder() });
|
|
585
607
|
return result;
|
|
586
608
|
} catch (error) {
|
|
587
609
|
reporter.fail(controller.signal.aborted);
|
|
@@ -600,11 +622,12 @@ async function askSourceQuestion(prompt, signal) {
|
|
|
600
622
|
finally { rl.close(); }
|
|
601
623
|
}
|
|
602
624
|
|
|
603
|
-
export async function resolvePageSource(options, { signal, stderr, inspect, discover, ask = askSourceQuestion,
|
|
604
|
-
backendResolver, interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !options.json) }) {
|
|
625
|
+
export async function resolvePageSource(options, { signal, stderr, reporter, onBackend, inspect, discover, ask = askSourceQuestion,
|
|
626
|
+
backendResolver, interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !options.json) }) {
|
|
605
627
|
const { fetchMetadata, prepareBackend, runBackend } = await import('./downloader.js');
|
|
606
628
|
const { discoverSources, formatSource, shouldOfferSourceDiscovery } = await import('./source-discovery.js');
|
|
607
|
-
const backend = inspect ? null : await (backendResolver || prepareBackend)(options, { signal });
|
|
629
|
+
const backend = inspect ? null : await (backendResolver || prepareBackend)(options, { signal, reporter });
|
|
630
|
+
if (backend) onBackend?.(backend);
|
|
608
631
|
const inspectMedia = (request, requestSignal = signal) => inspect ? inspect(request, requestSignal) : fetchMetadata(request, { signal: requestSignal, backend, runner: runBackend });
|
|
609
632
|
let originalError;
|
|
610
633
|
if (!options.source && !options.listSources) {
|
|
@@ -615,13 +638,10 @@ export async function resolvePageSource(options, { signal, stderr, inspect, disc
|
|
|
615
638
|
}
|
|
616
639
|
return {};
|
|
617
640
|
}
|
|
618
|
-
catch (error) { originalError = error; }
|
|
619
|
-
if (!shouldOfferSourceDiscovery(originalError)) throw originalError;
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
const answer = (await ask('No downloadable video found. Search this page for other sources? (y/N): ', signal)).trim().toLowerCase();
|
|
623
|
-
if (!['y', 'yes'].includes(answer)) throw originalError;
|
|
624
|
-
}
|
|
641
|
+
catch (error) { originalError = error; }
|
|
642
|
+
if (!shouldOfferSourceDiscovery(originalError)) throw originalError;
|
|
643
|
+
if (options.autoListSources === false) throw new Error(`${originalError.message} Use --list-sources to search this page, then --source <number> to select one.`);
|
|
644
|
+
stderr.write('veo: No direct video found; searching the page for a playable source…\n');
|
|
625
645
|
}
|
|
626
646
|
let sources;
|
|
627
647
|
try {
|
|
@@ -636,16 +656,18 @@ export async function resolvePageSource(options, { signal, stderr, inspect, disc
|
|
|
636
656
|
}
|
|
637
657
|
if (options.listSources) return { sources, formatSource };
|
|
638
658
|
if (sources.timedOut) stderr.write(`veo: Source search timed out after ${options.timeout || (options.deepScan ? '120s' : '45s')}; showing verified sources found so far.\n`);
|
|
639
|
-
if (!sources.length) {
|
|
640
|
-
if (originalError) throw originalError;
|
|
641
|
-
throw new Error('No downloadable video sources were found on this page.');
|
|
642
|
-
}
|
|
643
|
-
let index = Number(options.source);
|
|
644
|
-
if (!index) {
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
659
|
+
if (!sources.length) {
|
|
660
|
+
if (originalError) throw originalError;
|
|
661
|
+
throw new Error('No downloadable video sources were found on this page.');
|
|
662
|
+
}
|
|
663
|
+
let index = Number(options.source);
|
|
664
|
+
if (!index) {
|
|
665
|
+
if (sources.length === 1) index = 1;
|
|
666
|
+
else if (!interactive) return { sources, formatSource, needsSelection: true };
|
|
667
|
+
}
|
|
668
|
+
if (!index) {
|
|
669
|
+
for (const source of sources) stderr.write(`${formatSource(source)}\n`);
|
|
670
|
+
const answer = await ask('Source number (Enter to cancel): ', signal);
|
|
649
671
|
if (!/^[1-9]\d*$/.test(answer.trim())) throw new Error('No source selected.');
|
|
650
672
|
index = Number(answer.trim());
|
|
651
673
|
}
|