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.
@@ -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
- cdp.send('Network.enable', {}, attached).catch(() => {});
163
- return;
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
- await delay(Math.min(observeMs / 2, 4000), undefined, { signal });
191
- await abortable(cdp.send('Runtime.evaluate', { expression: `(() => {
192
- for (const video of document.querySelectorAll('video')) video.play().catch(() => {});
193
- const buttons = [...document.querySelectorAll('button,[role="button"]')];
194
- const play = buttons.find(node => /^(play|watch|abspielen|ansehen|start)(?:\\s+(?:video|movie|film))?$/i.test((node.getAttribute('aria-label') || node.textContent || '').trim()));
195
- if (play) play.click();
196
- })()`, returnByValue: true }, attached.sessionId).catch(() => {}), signal);
197
- await delay(Math.max(1000, observeMs / 2), undefined, { signal });
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
- Object.defineProperty(sources, 'timedOut', { value: timeout.signal.aborted });
241
- return sources;
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
  }