veodl 1.8.1

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/bin/veo.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ process.exitCode = await main();
@@ -0,0 +1,66 @@
1
+ # veo for agents
2
+
3
+ veo is a command-line video downloader. Use it only for content the user is authorized to download. This guide covers the stable command flow and the difference between download state, media tracks and measured audio signal.
4
+
5
+ ## Fast path
6
+
7
+ ```sh
8
+ veo "https://example.com/video" --dry-run --json
9
+ veo "https://example.com/video" --json --output ./downloads
10
+ veo inspect "./downloads/video.mp4" --check-audio --json
11
+ ```
12
+
13
+ Set `VEO_NO_UPDATE_CHECK=1` when a background update notice would distract a script. Use an explicit output directory to avoid depending on the agent's current working directory. `--dry-run` plans the download without saving media. It can still inspect the source and prepare backend tools on first use.
14
+
15
+ ## Output and failures
16
+
17
+ Download commands write **one JSON object per URL** to stdout with `--json`. Several URLs produce newline-delimited JSON, not a JSON array. Progress, summaries and errors go to stderr. A failed item can still contain saved files, especially within a playlist.
18
+
19
+ Example success:
20
+
21
+ ```json
22
+ {"url":"https://example.com/video","status":"saved","title":"Example","files":["/downloads/Example.mp4"],"runId":"abc123"}
23
+ ```
24
+
25
+ Example failure:
26
+
27
+ ```json
28
+ {"url":"https://example.com/video","status":"failed","error":"HTTP Error 429","files":[]}
29
+ ```
30
+
31
+ These are abbreviated examples; fields such as timing and playlist counts may also be present. Exit code `0` means the command succeeded, `1` means at least one item failed, and `130` means cancellation. Validate both the exit code and each result's `status`; a command can fail before producing any stdout JSON. Never infer success from the presence of a file path alone.
32
+
33
+ ## Inspect runs and results
34
+
35
+ ```sh
36
+ veo runs --json
37
+ veo runs k3f9qa --json
38
+ veo history --failed --limit 20 --json
39
+ veo retry --last --json
40
+ ```
41
+
42
+ `runs` reports **active** runs, including their ID, state, options, job path and per-item progress. Completed runs disappear from this list. Download JSON and `history` retain the `runId`. `history` reports finished attempts with their outcome and saved file paths. A failed history entry may contain a `job` path, which can be passed to `veo --retry-failed "<job-path>"`; `veo retry --last` finds the newest retryable job automatically. Running jobs are excluded from `retry --last`.
43
+
44
+ ## Inspect saved media and audio
45
+
46
+ ```sh
47
+ veo inspect "/downloads/Example.mp4" --json
48
+ veo inspect "/downloads/Example.mp4" --check-audio --json
49
+ veo inspect run abc123 --check-audio --json
50
+ veo inspect run abc123 --search "/moved/videos" --json
51
+ ```
52
+
53
+ `inspect` uses local FFprobe/FFmpeg and never downloads a URL. Its JSON contains `format`, `durationSeconds`, `sizeBytes`, `streams`, `hasAudioTrack` and `hasVideoTrack`. Without `--check-audio`, `audioCheck` is `null`.
54
+
55
+ With `--check-audio`, veo decodes every audio track from start to end. `audioCheck.hasSignal` is true when at least one track's peak exceeds **−60 dBFS**. `audioCheck.tracks` lists each track's `streamIndex`, `maxDbfs` and `hasSignal`; digital silence may have `maxDbfs: null` or a very low level after conversion. A present audio track does not prove audible content. This is a signal-level check, not a listening or speech-quality test. Decoding long media can take time; if FFmpeg fails, the command exits nonzero instead of reporting silence.
56
+
57
+ `inspect run` reads the saved run record even after the download finishes. It checks whether each file still exists **before** probing it. If a media file was renamed, veo searches the original output tree using file identity, size and a three-part SHA-256 fingerprint. `--search` adds one more directory after a move. Missing or ambiguous matches are reported without probing a different file; missing files make the command exit with code `1`. The fingerprint lives in veo's run history, so this works across media formats without rewriting the media. Runs created before this feature do not have a persistent run record.
58
+
59
+ The run record is stored in veo's per-user cache on the device that ran the download. `inspect` works on Windows, macOS, Linux and Termux with locally available FFmpeg/FFprobe; set `VEO_FFMPEG_PATH` to their directory when they are not on the normal search path. Moving only the media to another device does not transfer the run record.
60
+
61
+ ## Operational notes
62
+
63
+ - Run `veo doctor --offline` to check the local setup without network repair. `veo doctor fix` may install missing tools.
64
+ - Use `--resume` for interrupted downloads and `--skip-existing` when repeated saves should be avoided.
65
+ - `veo flush` removes temporary downloads and retry jobs; do not run it as routine maintenance in an agent workflow.
66
+ - Do not call bare `veo` in an interactive terminal from automation: it opens a wizard. Use explicit URLs and flags.
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "veodl",
3
+ "version": "1.8.1",
4
+ "description": "A clean, simple video downloader powered by yt-dlp.",
5
+ "type": "module",
6
+ "bin": {
7
+ "veo": "bin/veo.js",
8
+ "veod": "bin/veo.js",
9
+ "veodl": "bin/veo.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "src/",
14
+ "docs/",
15
+ "AGENTS.md",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "scripts": {
24
+ "test": "node --test",
25
+ "test:smoke": "node scripts/smoke.js",
26
+ "test:open": "node scripts/smoke-open.js",
27
+ "check:package": "node scripts/check-package.js",
28
+ "prepublishOnly": "npm test",
29
+ "test:workflows": "node scripts/smoke-workflows.js"
30
+ },
31
+ "keywords": [
32
+ "cli",
33
+ "video",
34
+ "downloader",
35
+ "yt-dlp",
36
+ "audio",
37
+ "youtube",
38
+ "playlist",
39
+ "subtitles"
40
+ ],
41
+ "license": "MIT",
42
+ "author": "Mailo037",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/Mailo037/veo.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/Mailo037/veo/issues"
49
+ },
50
+ "homepage": "https://github.com/Mailo037/veo#readme",
51
+ "preferGlobal": true,
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "optionalDependencies": {
56
+ "ffmpeg-static": "5.3.0",
57
+ "ffprobe-static": "3.1.0"
58
+ }
59
+ }
@@ -0,0 +1,191 @@
1
+ import { commandOutput } from './output.js';
2
+ import path from 'node:path';
3
+ import { rm } from 'node:fs/promises';
4
+ import {
5
+ RELEASE, activeOverride, backendCacheDirectory, clearBackendOverride, installBackend,
6
+ isValidRelease, readBackendOverride, releaseUrl, selectAsset, writeBackendOverride,
7
+ } from './backend.js';
8
+ import { compareVersions } from './version.js';
9
+ import { readableError, cleanText } from './utils.js';
10
+
11
+ const API_LATEST = 'https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest';
12
+ const CHECKSUMS = 'SHA2-256SUMS';
13
+
14
+ export const BACKEND_HELP = `veo backend - manage the yt-dlp backend
15
+
16
+ Usage:
17
+ veo backend update Install the newest yt-dlp release
18
+ veo backend update --check Only report whether a newer release exists
19
+ veo backend reset Forget the installed release and use the pinned one
20
+ veo backend reset --keep-files
21
+ Remove the pointer but keep the downloaded files
22
+ veo backend --help Show this help
23
+
24
+ veo ships a pinned, hash-verified yt-dlp release. \`veo backend update\` is an
25
+ opt-in escape hatch for when a website changed and the pinned release is too old:
26
+ it installs the newest official release after verifying its SHA-256 against that
27
+ release's own SHA2-256SUMS file over HTTPS.
28
+
29
+ That checksum does not ship with veo, so this weakens the pinned-hash guarantee:
30
+ it then rests on HTTPS and GitHub alone. \`veo backend reset\` returns to the
31
+ release veo was built and tested against. VEO_YT_DLP_PATH always wins over both.
32
+ `;
33
+
34
+ export async function fetchLatestBackendRelease({ fetchImpl = fetch, timeoutMs = 10_000, signal } = {}) {
35
+ const response = await fetchImpl(API_LATEST, {
36
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
37
+ headers: { accept: 'application/vnd.github+json', 'user-agent': 'veo' },
38
+ });
39
+ if (!response.ok) {
40
+ await response.body?.cancel();
41
+ throw new Error(`Could not query the latest yt-dlp release: HTTP ${response.status}.`);
42
+ }
43
+ const data = await response.json();
44
+ const tag = data?.tag_name;
45
+ if (!isValidRelease(tag)) throw new Error(`The release host returned an unexpected version: ${cleanText(String(tag))}`);
46
+ return tag;
47
+ }
48
+
49
+ /** Parse an official SHA2-256SUMS file into a name -> hash map. */
50
+ export function parseChecksums(text) {
51
+ const hashes = new Map();
52
+ for (const line of String(text).split('\n')) {
53
+ const match = /^([a-f0-9]{64})\s+\*?(.+?)\s*$/i.exec(line.trim());
54
+ if (match) hashes.set(path.basename(match[2]), match[1].toLowerCase());
55
+ }
56
+ if (!hashes.size) throw new Error('The release checksum list could not be parsed.');
57
+ return hashes;
58
+ }
59
+
60
+ export async function fetchReleaseChecksum({ release, asset, fetchImpl = fetch, timeoutMs = 15_000, signal }) {
61
+ if (!isValidRelease(release)) throw new Error(`Invalid yt-dlp release: ${release}`);
62
+ const response = await fetchImpl(releaseUrl(release, CHECKSUMS), {
63
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
64
+ });
65
+ if (!response.ok) {
66
+ await response.body?.cancel();
67
+ throw new Error(`Could not download ${CHECKSUMS} for yt-dlp ${release}: HTTP ${response.status}.`);
68
+ }
69
+ const hash = parseChecksums(await response.text()).get(asset);
70
+ if (!hash) throw new Error(`yt-dlp ${release} does not publish a checksum for ${asset}.`);
71
+ return hash;
72
+ }
73
+
74
+ export function muslDetected() {
75
+ return process.platform === 'linux' && !process.report.getReport().header.glibcVersionRuntime;
76
+ }
77
+
78
+ export async function checkBackend({
79
+ platform = process.platform,
80
+ arch = process.arch,
81
+ musl = muslDetected(),
82
+ signal,
83
+ stateFile,
84
+ fetchImpl = fetch,
85
+ installed,
86
+ latest,
87
+ } = {}) {
88
+ const asset = selectAsset(platform, arch, musl);
89
+ const active = installed !== undefined ? installed : await activeOverride(asset, { signal, stateFile });
90
+ return {
91
+ asset,
92
+ active,
93
+ current: active?.release ?? RELEASE,
94
+ latest: latest ?? await fetchLatestBackendRelease({ fetchImpl, signal }),
95
+ };
96
+ }
97
+
98
+ export async function backendUpdateMain(args = [], {
99
+ stdout = process.stdout,
100
+ stderr = process.stderr,
101
+ env = process.env,
102
+ signal,
103
+ stateFile,
104
+ platform = process.platform,
105
+ arch = process.arch,
106
+ fetchImpl = fetch,
107
+ check = checkBackend,
108
+ install = installBackend,
109
+ reset = clearBackendOverride,
110
+ removeFiles = directory => rm(directory, { recursive: true, force: true }),
111
+ } = {}) {
112
+ [args, stdout, stderr] = commandOutput(args, stdout, stderr);
113
+ const [command, ...rest] = args;
114
+ if (command === undefined || command === '-h' || command === '--help' || rest.includes('-h') || rest.includes('--help')) {
115
+ stdout.write(BACKEND_HELP);
116
+ return 0;
117
+ }
118
+ if (command === 'reset') {
119
+ const unknown = rest.filter(token => token !== '--keep-files');
120
+ if (unknown.length) throw new Error(`Unknown option for veo backend reset: ${unknown.join(' ')}. Use: veo backend reset [--keep-files]`);
121
+ if (platform === 'android') {
122
+ stdout.write('Android/Termux uses system yt-dlp or VEO_YT_DLP_PATH. There is no managed backend to reset.\n');
123
+ return 0;
124
+ }
125
+ const state = await readBackendOverride(stateFile);
126
+ if (!await reset(stateFile)) {
127
+ stdout.write(`No installed backend to remove. veo uses the pinned release ${RELEASE}.\n`);
128
+ return 0;
129
+ }
130
+ if (state?.release && !rest.includes('--keep-files')) await removeFiles(backendCacheDirectory(state.release)).catch(() => {});
131
+ const removed = state?.release ? ` yt-dlp ${state.release}` : ' the installed backend';
132
+ stdout.write(`Removed${removed}. veo uses the pinned release ${RELEASE} again.\n`);
133
+ return 0;
134
+ }
135
+ if (command !== 'update') {
136
+ throw new Error(`Unknown backend command: ${cleanText(command)}. Use: veo backend update [--check] | veo backend reset`);
137
+ }
138
+ const checkOnly = rest.includes('--check');
139
+ const unknown = rest.filter(token => token !== '--check');
140
+ if (unknown.length) throw new Error(`Unknown option for veo backend update: ${unknown.join(' ')}. Use: veo backend update [--check]`);
141
+ if (platform === 'android') {
142
+ stdout.write('Android/Termux uses system yt-dlp or VEO_YT_DLP_PATH, not a pinned release. Update the Termux package with: pkg upgrade python-yt-dlp yt-dlp-ejs\n');
143
+ return 0;
144
+ }
145
+ if (env.VEO_YT_DLP_PATH) stderr.write('veo: VEO_YT_DLP_PATH is set and always takes precedence over the managed backend.\n');
146
+
147
+ let info;
148
+ try {
149
+ info = await check({ platform, arch, signal, stateFile, fetchImpl });
150
+ } catch (error) {
151
+ stderr.write(`veo: ${readableError(error)}\n`);
152
+ return 1;
153
+ }
154
+ const { asset, current, latest } = info;
155
+ if (!asset) {
156
+ stderr.write(`veo: no standalone yt-dlp is published for ${platform}/${arch}. Set VEO_YT_DLP_PATH to a trusted executable.\n`);
157
+ return 1;
158
+ }
159
+ if (compareVersions(latest, current) <= 0) {
160
+ stdout.write(`yt-dlp ${current} is up to date${current === RELEASE ? ' (pinned release, hash verified at build time)' : ' (installed release)'}.\n`);
161
+ return 0;
162
+ }
163
+ if (checkOnly) {
164
+ stdout.write(`Newer yt-dlp release available: ${latest} (using ${current}). Run: veo backend update\n`);
165
+ return 0;
166
+ }
167
+
168
+ let sha256;
169
+ try {
170
+ sha256 = await fetchReleaseChecksum({ release: latest, asset, fetchImpl, signal });
171
+ } catch (error) {
172
+ stderr.write(`veo: ${readableError(error)}\nNothing was installed. The pinned release ${RELEASE} is still in use.\n`);
173
+ return 1;
174
+ }
175
+ stdout.write(`Installing yt-dlp ${latest} for ${platform}/${asset}…\n`);
176
+ try {
177
+ await install({ release: latest, asset, sha256, signal, status: message => stdout.write(`${message}\n`) });
178
+ } catch (error) {
179
+ stderr.write(`veo: ${readableError(error)}\nThe pinned release ${RELEASE} is still in use.\n`);
180
+ return 1;
181
+ }
182
+ try {
183
+ await writeBackendOverride({ release: latest, asset, sha256, platform, arch, installed: new Date().toISOString() }, stateFile);
184
+ } catch (error) {
185
+ stderr.write(`veo: ${readableError(error)}\nThe download succeeded, but veo could not record it; the pinned release stays in use.\n`);
186
+ return 1;
187
+ }
188
+ stdout.write(`yt-dlp ${latest} installed and will be used for the next download.\n`);
189
+ stderr.write(`veo: note: yt-dlp ${latest} was verified against the checksum published with that release over HTTPS, not against a hash shipped with veo. Run "veo backend reset" to return to the pinned release.\n`);
190
+ return 0;
191
+ }