veodl 1.9.0 → 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 +28 -0
- package/README.md +57 -37
- package/docs/AGENT_GUIDE.md +8 -5
- package/package.json +1 -1
- package/src/backend.js +64 -36
- package/src/cli.js +120 -56
- 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/option-suggestions.js +43 -0
- package/src/progress.js +68 -12
- package/src/runs.js +8 -5
- package/src/source-discovery.js +82 -30
package/src/source-discovery.js
CHANGED
|
@@ -5,8 +5,44 @@ import path from 'node:path';
|
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
6
|
import { cleanText, validateUrl } from './utils.js';
|
|
7
7
|
|
|
8
|
-
const MEDIA_URL = /\.(?:m3u8|mpd|mp4|webm|mov)(?:[?#]|$)/i;
|
|
9
|
-
const MEDIA_TYPE = /(?:mpegurl|dash\+xml|video\/(?:mp4|webm|quicktime))/i;
|
|
8
|
+
const MEDIA_URL = /\.(?:m3u8|mpd|mp4|webm|mov)(?:[?#]|$)/i;
|
|
9
|
+
const MEDIA_TYPE = /(?:mpegurl|dash\+xml|video\/(?:mp4|webm|quicktime))/i;
|
|
10
|
+
const PLAY_CONTROL = String.raw`(() => {
|
|
11
|
+
const visited = new WeakSet();
|
|
12
|
+
const state = globalThis[Symbol.for('veo.playControls')] ||= { visited, clicks: 0 };
|
|
13
|
+
const labelMatches = node => {
|
|
14
|
+
const label = [node.getAttribute('aria-label'), node.getAttribute('title'), node.getAttribute('data-testid'),
|
|
15
|
+
node.getAttribute('value'), node.textContent].find(value => value && value.trim())?.trim() || '';
|
|
16
|
+
return /^(?:play|watch|start|replay|abspielen|ansehen|wiedergabe)(?:\s+(?:video|movie|film|now))?$/i.test(label)
|
|
17
|
+
|| /(?:^|[-_\s])(?:big[-_])?play(?:[-_\s]|$)/i.test(node.className?.baseVal || node.className || '');
|
|
18
|
+
};
|
|
19
|
+
const documents = [document];
|
|
20
|
+
for (let index = 0; index < documents.length && index < 16; index++) {
|
|
21
|
+
for (const frame of documents[index].querySelectorAll('iframe')) {
|
|
22
|
+
try { if (frame.contentDocument && !documents.includes(frame.contentDocument)) documents.push(frame.contentDocument); } catch {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const doc of documents) {
|
|
26
|
+
const controls = doc.querySelectorAll('button,[role="button"],[aria-label],[title],[data-testid],input[type="button"],input[type="image"],[class*="play"],[class*="Play"]');
|
|
27
|
+
for (const node of controls) {
|
|
28
|
+
if (state.clicks >= 8) return false;
|
|
29
|
+
if (state.visited.has(node) || node.disabled || node.getAttribute('aria-disabled') === 'true' || !labelMatches(node)) continue;
|
|
30
|
+
const style = doc.defaultView.getComputedStyle(node);
|
|
31
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.pointerEvents === 'none' || !node.getClientRects().length) continue;
|
|
32
|
+
state.visited.add(node);
|
|
33
|
+
state.clicks++;
|
|
34
|
+
node.click();
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
for (const video of doc.querySelectorAll('video')) {
|
|
38
|
+
if (state.visited.has(video) || !video.paused) continue;
|
|
39
|
+
state.visited.add(video);
|
|
40
|
+
video.play().catch(() => {});
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
})()`;
|
|
10
46
|
|
|
11
47
|
function abortable(promise, signal) {
|
|
12
48
|
if (!signal) return promise;
|
|
@@ -149,19 +185,22 @@ export async function captureBrowserMedia(pageUrl, { signal, browserPaths = brow
|
|
|
149
185
|
socket.addEventListener('open', resolve, { once: true });
|
|
150
186
|
socket.addEventListener('error', reject, { once: true });
|
|
151
187
|
}), signal);
|
|
152
|
-
const cdp = createCdp(socket);
|
|
153
|
-
const responses = new Map();
|
|
154
|
-
const bodyReads = [];
|
|
188
|
+
const cdp = createCdp(socket);
|
|
189
|
+
const responses = new Map();
|
|
190
|
+
const bodyReads = [];
|
|
191
|
+
const sessions = new Set();
|
|
155
192
|
const collect = (value, mime = '') => {
|
|
156
193
|
if (!MEDIA_URL.test(value) && !MEDIA_TYPE.test(mime)) return;
|
|
157
194
|
try { urls.add(validateUrl(value)); } catch {}
|
|
158
195
|
};
|
|
159
196
|
cdp.on(message => {
|
|
160
|
-
if (message.method === 'Target.attachedToTarget') {
|
|
161
|
-
const attached = message.params.sessionId;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
197
|
+
if (message.method === 'Target.attachedToTarget') {
|
|
198
|
+
const attached = message.params.sessionId;
|
|
199
|
+
sessions.add(attached);
|
|
200
|
+
cdp.send('Network.enable', {}, attached).catch(() => {});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (message.method === 'Target.detachedFromTarget') sessions.delete(message.params.sessionId);
|
|
165
204
|
if (message.method === 'Network.requestWillBeSent') collect(message.params.request?.url);
|
|
166
205
|
if (message.method === 'Network.responseReceived') {
|
|
167
206
|
const response = message.params.response;
|
|
@@ -182,19 +221,22 @@ export async function captureBrowserMedia(pageUrl, { signal, browserPaths = brow
|
|
|
182
221
|
}
|
|
183
222
|
});
|
|
184
223
|
await abortable(cdp.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }), signal);
|
|
185
|
-
const target = await abortable(cdp.send('Target.createTarget', { url: 'about:blank' }), signal);
|
|
186
|
-
const attached = await abortable(cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true }), signal);
|
|
224
|
+
const target = await abortable(cdp.send('Target.createTarget', { url: 'about:blank' }), signal);
|
|
225
|
+
const attached = await abortable(cdp.send('Target.attachToTarget', { targetId: target.targetId, flatten: true }), signal);
|
|
226
|
+
sessions.add(attached.sessionId);
|
|
187
227
|
await abortable(cdp.send('Network.enable', {}, attached.sessionId), signal);
|
|
188
228
|
await abortable(cdp.send('Page.enable', {}, attached.sessionId), signal);
|
|
189
|
-
await abortable(cdp.send('Page.navigate', { url: pageUrl }, attached.sessionId), signal);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
229
|
+
await abortable(cdp.send('Page.navigate', { url: pageUrl }, attached.sessionId), signal);
|
|
230
|
+
const observeUntil = Date.now() + observeMs;
|
|
231
|
+
while (Date.now() < observeUntil) {
|
|
232
|
+
signal?.throwIfAborted();
|
|
233
|
+
await Promise.allSettled([...sessions].map(sessionId => abortable(cdp.send('Runtime.evaluate', {
|
|
234
|
+
expression: PLAY_CONTROL, returnByValue: true,
|
|
235
|
+
}, sessionId), signal)));
|
|
236
|
+
if (!deepScan && urls.size > 0) break;
|
|
237
|
+
const remaining = observeUntil - Date.now();
|
|
238
|
+
if (remaining > 0) await delay(Math.min(300, remaining), undefined, { signal });
|
|
239
|
+
}
|
|
198
240
|
await Promise.race([Promise.allSettled(bodyReads), delay(1000, undefined, { signal }).catch(() => {})]);
|
|
199
241
|
return [...urls];
|
|
200
242
|
} catch (error) {
|
|
@@ -208,14 +250,19 @@ export async function captureBrowserMedia(pageUrl, { signal, browserPaths = brow
|
|
|
208
250
|
}
|
|
209
251
|
|
|
210
252
|
/** Verify candidates with yt-dlp before offering them to the user. */
|
|
211
|
-
export async function discoverSources(pageUrl, { signal, capture = captureBrowserMedia, inspect, limit = 30,
|
|
212
|
-
deepScan = false, timeoutMs = deepScan ? 120000 : 45000 } = {}) {
|
|
253
|
+
export async function discoverSources(pageUrl, { signal, capture = captureBrowserMedia, inspect, limit = 30,
|
|
254
|
+
deepScan = false, timeoutMs = deepScan ? 120000 : 45000 } = {}) {
|
|
213
255
|
signal?.throwIfAborted();
|
|
214
256
|
const timeout = new AbortController();
|
|
215
257
|
const timer = setTimeout(() => timeout.abort(new DOMException('Source scan timed out.', 'TimeoutError')), timeoutMs);
|
|
216
258
|
const scanSignal = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
|
|
217
|
-
const sources = [];
|
|
218
|
-
const seen = new Set();
|
|
259
|
+
const sources = [];
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
const formatReferences = new Map();
|
|
262
|
+
const mediaKey = value => {
|
|
263
|
+
try { const address = new URL(value); return `${address.origin}${address.pathname}`; }
|
|
264
|
+
catch { return null; }
|
|
265
|
+
};
|
|
219
266
|
try {
|
|
220
267
|
const observeMs = deepScan ? Math.min(30000, Math.floor(timeoutMs * 0.6)) : Math.min(8000, Math.floor(timeoutMs * 0.5));
|
|
221
268
|
let urls;
|
|
@@ -231,14 +278,19 @@ export async function discoverSources(pageUrl, { signal, capture = captureBrowse
|
|
|
231
278
|
|| !metadata.formats?.length && !metadata.url) continue;
|
|
232
279
|
const address = new URL(url);
|
|
233
280
|
const key = `${address.origin}${address.pathname}:${metadata.id || ''}`;
|
|
234
|
-
if (seen.has(key)) continue;
|
|
235
|
-
seen.add(key);
|
|
236
|
-
sources.push({ ...summarizeSource(metadata, url, sources.length + 1, pageUrl), url });
|
|
281
|
+
if (seen.has(key)) continue;
|
|
282
|
+
seen.add(key);
|
|
283
|
+
sources.push({ ...summarizeSource(metadata, url, sources.length + 1, pageUrl), url });
|
|
284
|
+
formatReferences.set(url, new Set((metadata.formats || []).map(format => mediaKey(format.url)).filter(Boolean)));
|
|
237
285
|
} catch { /* A request alone is not proof of downloadable media. */ }
|
|
238
286
|
}
|
|
239
287
|
signal?.throwIfAborted();
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
// A master playlist and its variant streams describe the same video.
|
|
289
|
+
const distinct = sources.filter(source => !sources.some(other => other !== source
|
|
290
|
+
&& formatReferences.get(other.url)?.has(mediaKey(source.url))));
|
|
291
|
+
distinct.forEach((source, index) => { source.index = index + 1; });
|
|
292
|
+
Object.defineProperty(distinct, 'timedOut', { value: timeout.signal.aborted });
|
|
293
|
+
return distinct;
|
|
242
294
|
} finally {
|
|
243
295
|
clearTimeout(timer);
|
|
244
296
|
}
|