veodl 1.12.0 → 1.12.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/src/cli.js CHANGED
@@ -624,7 +624,7 @@ async function runMain(args, { config }) {
624
624
  if (cookieWarning) stderr.write(`veo: ${cookieWarning}\n`);
625
625
 
626
626
  // For a single page, fall back to media requests observed while its player loads.
627
- // Explicit --source always rediscovers: signed media URLs can expire between runs.
627
+ // Explicit --source rediscovers unless a completed local transfer is usable.
628
628
  if (!retryItems && options.urls.length === 1 && (!options.playlist || options.listSources)) {
629
629
  const discovery = await resolvePageSource(options, { signal: controller.signal, stderr, reporter,
630
630
  onBackend: backend => preparedBackends.set(options.url, backend) });
@@ -727,8 +727,12 @@ async function askSourceQuestion(prompt, signal) {
727
727
  }
728
728
 
729
729
  export async function resolvePageSource(options, { signal, stderr, reporter, onBackend, inspect, discover, ask = askSourceQuestion,
730
- backendResolver, interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !options.json) }) {
731
- const { fetchMetadata, prepareBackend, runBackend } = await import('./downloader.js');
730
+ backendResolver, localRoot, interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY && !options.json) }) {
731
+ const { fetchMetadata, prepareBackend, readReadyTransfer, runBackend } = await import('./downloader.js');
732
+ // Source validation remains necessary for new downloads and metadata actions;
733
+ // a completed local transfer can finish while its original source is offline.
734
+ if (!options.incognito && !options.dryRun && !options.listSources && !options.listFormats
735
+ && !options.listQualities && !options.listSubs && await readReadyTransfer(options, { localRoot, signal })) return {};
732
736
  const { discoverSources, formatSource, shouldOfferSourceDiscovery } = await import('./source-discovery.js');
733
737
  const backend = inspect ? null : await (backendResolver || prepareBackend)(options, { signal, reporter });
734
738
  if (backend) onBackend?.(backend);
@@ -1,39 +1,39 @@
1
- import path from 'node:path';
2
- import { rename, rm } from 'node:fs/promises';
3
-
4
- export async function ensureCompatibility(file, { backend, runner, signal, convert = false, reporter }) {
5
- const executable = name => path.join(backend.ffmpegLocation, name + (process.platform === 'win32' ? '.exe' : ''));
6
- let streams;
7
- try {
8
- const raw = await runner(executable('ffprobe'), ['-v', 'error', '-show_streams', '-of', 'json', file], { signal });
9
- streams = JSON.parse(raw).streams;
10
- if (!Array.isArray(streams)) throw new Error('No stream metadata');
11
- } catch (error) {
12
- if (signal?.aborted || convert) throw error;
13
- reporter?.status('Playback compatibility could not be checked; original media retained.');
14
- return file;
15
- }
16
- const video = streams.find(stream => stream.codec_type === 'video' && !stream.disposition?.attached_pic);
17
- const audio = streams.filter(stream => stream.codec_type === 'audio');
18
- if (!video) return file;
19
- const videoOK = video.codec_name === 'h264' && video.pix_fmt === 'yuv420p';
20
- const audioOK = audio.every(stream => stream.codec_name === 'aac');
21
- if (!convert) {
22
- if (!videoOK || !audioOK) reporter?.status(`Playback note: ${video.codec_name}/${audio.map(stream => stream.codec_name).join(',') || 'no audio'} may require additional player codecs. Original quality retained; use --compatible for H.264/AAC conversion.`);
23
- return file;
24
- }
25
- if (videoOK && audioOK && path.extname(file) === '.mp4') return file;
26
- if (!videoOK && (video.color_transfer === 'smpte2084' || video.color_transfer === 'arib-std-b67')) throw new Error('HDR conversion needs tone mapping; original download retained. Use an HDR-capable player or disable --compatible.');
27
- reporter?.status(videoOK && audioOK ? 'Preparing compatible MP4 without re-encoding…' : 'Converting to compatible H.264/AAC; this takes time and may lose quality…');
28
- const temporary = path.join(path.dirname(file), 'compatible-output.mp4');
29
- const target = path.join(path.dirname(file), 'media.mp4');
30
- try {
31
- await runner(executable('ffmpeg'), ['-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-i', file,
32
- '-map', '0:v:0', '-map', '0:a?', '-map_metadata', '0',
33
- '-c:v', videoOK ? 'copy' : 'libx264', ...(!videoOK ? ['-preset', 'fast', '-crf', '18', '-pix_fmt', 'yuv420p'] : []),
34
- '-c:a', audioOK ? 'copy' : 'aac', ...(!audioOK ? ['-b:a', '192k'] : []), '-movflags', '+faststart', temporary], { signal });
35
- await rename(temporary, target);
36
- if (file !== target) await rm(file);
37
- return target;
38
- } finally { await rm(temporary, { force: true }); }
1
+ import path from 'node:path';
2
+ import { rename, rm } from 'node:fs/promises';
3
+
4
+ export async function ensureCompatibility(file, { backend, runner, signal, convert = false, reporter }) {
5
+ const executable = name => path.join(backend.ffmpegLocation, name + (process.platform === 'win32' ? '.exe' : ''));
6
+ let streams;
7
+ try {
8
+ const raw = await runner(executable('ffprobe'), ['-v', 'error', '-show_streams', '-of', 'json', file], { signal });
9
+ streams = JSON.parse(raw).streams;
10
+ if (!Array.isArray(streams)) throw new Error('No stream metadata');
11
+ } catch (error) {
12
+ if (signal?.aborted || convert) throw error;
13
+ reporter?.status('Playback compatibility could not be checked; original media retained.');
14
+ return file;
15
+ }
16
+ const video = streams.find(stream => stream.codec_type === 'video' && !stream.disposition?.attached_pic);
17
+ const audio = streams.filter(stream => stream.codec_type === 'audio');
18
+ if (!video) return file;
19
+ const videoOK = video.codec_name === 'h264' && video.pix_fmt === 'yuv420p';
20
+ const audioOK = audio.every(stream => stream.codec_name === 'aac');
21
+ if (!convert) {
22
+ if (!videoOK || !audioOK) reporter?.status(`Playback note: ${video.codec_name}/${audio.map(stream => stream.codec_name).join(',') || 'no audio'} may require additional player codecs. Original quality retained; use --compatible for H.264/AAC conversion.`);
23
+ return file;
24
+ }
25
+ if (videoOK && audioOK && path.extname(file) === '.mp4') return file;
26
+ if (!videoOK && (video.color_transfer === 'smpte2084' || video.color_transfer === 'arib-std-b67')) throw new Error('HDR conversion needs tone mapping; original download retained. Use an HDR-capable player or disable --compatible.');
27
+ reporter?.status(videoOK && audioOK ? 'Preparing compatible MP4 without re-encoding…' : 'Converting to compatible H.264/AAC; this takes time and may lose quality…');
28
+ const temporary = path.join(path.dirname(file), 'compatible-output.mp4');
29
+ const target = path.join(path.dirname(file), 'media.mp4');
30
+ try {
31
+ await runner(executable('ffmpeg'), ['-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-i', file,
32
+ '-map', '0:v:0', '-map', '0:a?', '-map_metadata', '0',
33
+ '-c:v', videoOK ? 'copy' : 'libx264', ...(!videoOK ? ['-preset', 'fast', '-crf', '18', '-pix_fmt', 'yuv420p'] : []),
34
+ '-c:a', audioOK ? 'copy' : 'aac', ...(!audioOK ? ['-b:a', '192k'] : []), '-movflags', '+faststart', temporary], { signal });
35
+ await rename(temporary, target);
36
+ if (file !== target) await rm(file);
37
+ return target;
38
+ } finally { await rm(temporary, { force: true }); }
39
39
  }
@@ -1,67 +1,67 @@
1
- import { spawn } from 'node:child_process';
2
-
3
- const MAX_TEXT = 2 * 1024 * 1024;
4
-
5
- // Prefer the clipboard for the active display server, then try the other common
6
- // Linux tools. A terminal reached over SSH may have neither display available.
7
- export function clipboardCommands(operation, { platform = process.platform, env = process.env } = {}) {
8
- if (platform === 'win32') {
9
- const setup = 'Add-Type -AssemblyName System.Windows.Forms; [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ';
10
- return [['powershell.exe', ['-NoProfile', '-NonInteractive', '-Sta', '-Command', setup + (operation === 'copy'
11
- ? '[System.Windows.Forms.Clipboard]::SetText([Console]::In.ReadToEnd())'
12
- : '[Console]::Out.Write([System.Windows.Forms.Clipboard]::GetText())')]]];
13
- }
14
- if (platform === 'darwin') return [[operation === 'copy' ? 'pbcopy' : 'pbpaste', []]];
15
- if (platform === 'android') return [[operation === 'copy' ? 'termux-clipboard-set' : 'termux-clipboard-get', []]];
16
- const wayland = [operation === 'copy' ? 'wl-copy' : 'wl-paste', operation === 'copy' ? [] : ['--no-newline']];
17
- const xclip = ['xclip', ['-selection', 'clipboard', operation === 'copy' ? '-in' : '-out']];
18
- const xsel = ['xsel', ['--clipboard', operation === 'copy' ? '--input' : '--output']];
19
- const x11 = [xclip, xsel];
20
- return env.WAYLAND_DISPLAY ? [wayland, ...x11] : env.DISPLAY ? [...x11, wayland] : [wayland, ...x11];
21
- }
22
-
23
- function runCommand([program, args], operation, value) {
24
- return new Promise((resolve, reject) => {
25
- // Clipboard owners such as xclip and wl-copy may continue in the background.
26
- // Do not keep their inherited output pipes open after the command exits.
27
- const child = spawn(program, args, {
28
- shell: false, windowsHide: true,
29
- stdio: operation === 'copy' ? ['pipe', 'ignore', 'ignore'] : ['ignore', 'pipe', 'ignore'],
30
- });
31
- const timer = setTimeout(() => child.kill(), 5000);
32
- const chunks = [];
33
- let size = 0, failed = false;
34
- child.stdout?.on('data', chunk => {
35
- size += chunk.length;
36
- if (size > MAX_TEXT) child.kill();
37
- else chunks.push(chunk);
38
- });
39
- child.stdin?.on('error', () => {});
40
- child.on('error', () => { failed = true; });
41
- child.on('close', code => {
42
- clearTimeout(timer);
43
- if (failed || code !== 0 || size > MAX_TEXT) reject(new Error('Clipboard command failed.'));
44
- else resolve(operation === 'paste' ? Buffer.concat(chunks).toString('utf8') : undefined);
45
- });
46
- child.stdin?.end(value);
47
- });
48
- }
49
-
50
- export async function clipboardText(operation, value = '', options = {}) {
51
- if (Buffer.byteLength(value, 'utf8') > MAX_TEXT) throw new Error('Selection is too large for the editor clipboard.');
52
- const candidates = clipboardCommands(operation, options);
53
- for (const candidate of candidates) {
54
- try { return await (options.run || runCommand)(candidate, operation, value); }
55
- catch { /* Try the next available display-server clipboard. */ }
56
- }
57
- const platform = options.platform || process.platform;
58
- const hint = platform === 'linux'
59
- ? ' Install wl-clipboard, xclip or xsel and use a graphical session.'
60
- : platform === 'android' ? ' Install the Termux:API app and pkg install termux-api.' : '';
61
- throw new Error(`System clipboard is unavailable.${hint}`);
62
- }
63
-
64
- export const configClipboard = {
65
- copy: value => clipboardText('copy', value),
66
- paste: () => clipboardText('paste'),
67
- };
1
+ import { spawn } from 'node:child_process';
2
+
3
+ const MAX_TEXT = 2 * 1024 * 1024;
4
+
5
+ // Prefer the clipboard for the active display server, then try the other common
6
+ // Linux tools. A terminal reached over SSH may have neither display available.
7
+ export function clipboardCommands(operation, { platform = process.platform, env = process.env } = {}) {
8
+ if (platform === 'win32') {
9
+ const setup = 'Add-Type -AssemblyName System.Windows.Forms; [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false); [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ';
10
+ return [['powershell.exe', ['-NoProfile', '-NonInteractive', '-Sta', '-Command', setup + (operation === 'copy'
11
+ ? '[System.Windows.Forms.Clipboard]::SetText([Console]::In.ReadToEnd())'
12
+ : '[Console]::Out.Write([System.Windows.Forms.Clipboard]::GetText())')]]];
13
+ }
14
+ if (platform === 'darwin') return [[operation === 'copy' ? 'pbcopy' : 'pbpaste', []]];
15
+ if (platform === 'android') return [[operation === 'copy' ? 'termux-clipboard-set' : 'termux-clipboard-get', []]];
16
+ const wayland = [operation === 'copy' ? 'wl-copy' : 'wl-paste', operation === 'copy' ? [] : ['--no-newline']];
17
+ const xclip = ['xclip', ['-selection', 'clipboard', operation === 'copy' ? '-in' : '-out']];
18
+ const xsel = ['xsel', ['--clipboard', operation === 'copy' ? '--input' : '--output']];
19
+ const x11 = [xclip, xsel];
20
+ return env.WAYLAND_DISPLAY ? [wayland, ...x11] : env.DISPLAY ? [...x11, wayland] : [wayland, ...x11];
21
+ }
22
+
23
+ function runCommand([program, args], operation, value) {
24
+ return new Promise((resolve, reject) => {
25
+ // Clipboard owners such as xclip and wl-copy may continue in the background.
26
+ // Do not keep their inherited output pipes open after the command exits.
27
+ const child = spawn(program, args, {
28
+ shell: false, windowsHide: true,
29
+ stdio: operation === 'copy' ? ['pipe', 'ignore', 'ignore'] : ['ignore', 'pipe', 'ignore'],
30
+ });
31
+ const timer = setTimeout(() => child.kill(), 5000);
32
+ const chunks = [];
33
+ let size = 0, failed = false;
34
+ child.stdout?.on('data', chunk => {
35
+ size += chunk.length;
36
+ if (size > MAX_TEXT) child.kill();
37
+ else chunks.push(chunk);
38
+ });
39
+ child.stdin?.on('error', () => {});
40
+ child.on('error', () => { failed = true; });
41
+ child.on('close', code => {
42
+ clearTimeout(timer);
43
+ if (failed || code !== 0 || size > MAX_TEXT) reject(new Error('Clipboard command failed.'));
44
+ else resolve(operation === 'paste' ? Buffer.concat(chunks).toString('utf8') : undefined);
45
+ });
46
+ child.stdin?.end(value);
47
+ });
48
+ }
49
+
50
+ export async function clipboardText(operation, value = '', options = {}) {
51
+ if (Buffer.byteLength(value, 'utf8') > MAX_TEXT) throw new Error('Selection is too large for the editor clipboard.');
52
+ const candidates = clipboardCommands(operation, options);
53
+ for (const candidate of candidates) {
54
+ try { return await (options.run || runCommand)(candidate, operation, value); }
55
+ catch { /* Try the next available display-server clipboard. */ }
56
+ }
57
+ const platform = options.platform || process.platform;
58
+ const hint = platform === 'linux'
59
+ ? ' Install wl-clipboard, xclip or xsel and use a graphical session.'
60
+ : platform === 'android' ? ' Install the Termux:API app and pkg install termux-api.' : '';
61
+ throw new Error(`System clipboard is unavailable.${hint}`);
62
+ }
63
+
64
+ export const configClipboard = {
65
+ copy: value => clipboardText('copy', value),
66
+ paste: () => clipboardText('paste'),
67
+ };