@larkup/workspace-server 0.2.2 → 0.2.6
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.
|
@@ -34,7 +34,7 @@ export interface UrlInspection {
|
|
|
34
34
|
isYouTube: boolean;
|
|
35
35
|
}
|
|
36
36
|
/** Download a public URL after applying the same DNS and redirect checks as media imports. */
|
|
37
|
-
export declare function readPublicUrl(url: string, maxBytes?: number): Promise<{
|
|
37
|
+
export declare function readPublicUrl(url: string, maxBytes?: number, init?: RequestInit): Promise<{
|
|
38
38
|
bytes: Uint8Array;
|
|
39
39
|
mimeType?: string;
|
|
40
40
|
finalUrl: string;
|
|
@@ -57,6 +57,21 @@ interface YouTubeInspectionData {
|
|
|
57
57
|
duration?: number;
|
|
58
58
|
}>;
|
|
59
59
|
}
|
|
60
|
+
interface YouTubePlayerData {
|
|
61
|
+
videoDetails?: {
|
|
62
|
+
videoId?: string;
|
|
63
|
+
title?: string;
|
|
64
|
+
lengthSeconds?: string | number;
|
|
65
|
+
};
|
|
66
|
+
streamingData?: {
|
|
67
|
+
formats?: Array<{
|
|
68
|
+
approxDurationMs?: string | number;
|
|
69
|
+
}>;
|
|
70
|
+
adaptiveFormats?: Array<{
|
|
71
|
+
approxDurationMs?: string | number;
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
60
75
|
export declare function inspectYouTubeMetadata(url: string, data: YouTubeInspectionData): UrlInspection;
|
|
61
76
|
/**
|
|
62
77
|
* A public watch URL is enough to stage one video even when an interactive
|
|
@@ -65,6 +80,8 @@ export declare function inspectYouTubeMetadata(url: string, data: YouTubeInspect
|
|
|
65
80
|
* fallback does not claim a title, duration, or byte size it could not verify.
|
|
66
81
|
*/
|
|
67
82
|
export declare function inspectYouTubeWatchUrl(url: string): UrlInspection | undefined;
|
|
83
|
+
export declare function inspectYouTubeWatchPage(url: string, html: string): UrlInspection | undefined;
|
|
84
|
+
export declare function inspectYouTubePlayerMetadata(url: string, data: YouTubePlayerData): UrlInspection | undefined;
|
|
68
85
|
export interface MediaProbe {
|
|
69
86
|
durationSecs: number;
|
|
70
87
|
containerDurationSecs: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-utils.d.ts","sourceRoot":"","sources":["../../../src/lib/media/source-utils.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,eAAe,CAAC;IAC3B,gBAAgB,CAAC,EAAE;QACjB,MAAM,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACpE,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE;YAAE,IAAI,EAAE,iBAAiB,CAAC;YAAC,QAAQ,EAAE,SAAS,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7E,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,eAAe,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,8FAA8F;AAC9F,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,QAAQ,SAAmB,
|
|
1
|
+
{"version":3,"file":"source-utils.d.ts","sourceRoot":"","sources":["../../../src/lib/media/source-utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -43,8 +43,9 @@ export interface UrlInspection {
|
|
|
43
43
|
export async function readPublicUrl(
|
|
44
44
|
url: string,
|
|
45
45
|
maxBytes = 25 * 1024 * 1024,
|
|
46
|
+
init: RequestInit = {},
|
|
46
47
|
): Promise<{ bytes: Uint8Array; mimeType?: string; finalUrl: string }> {
|
|
47
|
-
const response = await fetchPublic(url);
|
|
48
|
+
const response = await fetchPublic(url, init);
|
|
48
49
|
if (!response.ok || !response.body) {
|
|
49
50
|
await response.body?.cancel();
|
|
50
51
|
throw new Error(`Media download failed (${response.status}).`);
|
|
@@ -98,6 +99,18 @@ interface YouTubeInspectionData {
|
|
|
98
99
|
}>;
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
interface YouTubePlayerData {
|
|
103
|
+
videoDetails?: {
|
|
104
|
+
videoId?: string;
|
|
105
|
+
title?: string;
|
|
106
|
+
lengthSeconds?: string | number;
|
|
107
|
+
};
|
|
108
|
+
streamingData?: {
|
|
109
|
+
formats?: Array<{ approxDurationMs?: string | number }>;
|
|
110
|
+
adaptiveFormats?: Array<{ approxDurationMs?: string | number }>;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
function positiveDuration(value: unknown): number | undefined {
|
|
102
115
|
const duration = Number(value);
|
|
103
116
|
return Number.isFinite(duration) && duration > 0 ? duration : undefined;
|
|
@@ -170,6 +183,107 @@ export function inspectYouTubeWatchUrl(url: string): UrlInspection | undefined {
|
|
|
170
183
|
};
|
|
171
184
|
}
|
|
172
185
|
|
|
186
|
+
export function inspectYouTubeWatchPage(url: string, html: string): UrlInspection | undefined {
|
|
187
|
+
const details = jsonObjectAfterMarker(html, '"videoDetails":');
|
|
188
|
+
if (details) {
|
|
189
|
+
try {
|
|
190
|
+
const inspection = inspectYouTubePlayerMetadata(url, {
|
|
191
|
+
videoDetails: JSON.parse(details) as YouTubePlayerData['videoDetails'],
|
|
192
|
+
});
|
|
193
|
+
if (inspection) return inspection;
|
|
194
|
+
} catch {
|
|
195
|
+
// Continue to the page's public schema.org duration metadata.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const requestedId = youtubeVideoId(url);
|
|
199
|
+
const durationSecs = youtubePageDurationSecs(html);
|
|
200
|
+
return requestedId && durationSecs
|
|
201
|
+
? youtubeInspection(url, requestedId, undefined, durationSecs)
|
|
202
|
+
: undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function inspectYouTubePlayerMetadata(
|
|
206
|
+
url: string,
|
|
207
|
+
data: YouTubePlayerData,
|
|
208
|
+
): UrlInspection | undefined {
|
|
209
|
+
const requestedId = youtubeVideoId(url);
|
|
210
|
+
const details = data.videoDetails;
|
|
211
|
+
const declaredDurationSecs = positiveDuration(details?.lengthSeconds);
|
|
212
|
+
if (requestedId && declaredDurationSecs && details?.videoId === requestedId) {
|
|
213
|
+
return youtubeInspection(url, requestedId, details.title, declaredDurationSecs);
|
|
214
|
+
}
|
|
215
|
+
// Some serverless requests receive a reduced but still valid player payload:
|
|
216
|
+
// `videoDetails` is omitted while the signed selected-stream metadata remains.
|
|
217
|
+
// It is tied to the requested ID in this player request, so use a rounded
|
|
218
|
+
// positive stream duration only when the response does not claim another ID.
|
|
219
|
+
const streamDurationSecs = playerStreamDurationSecs(data);
|
|
220
|
+
if (!requestedId || data.videoDetails?.videoId || !streamDurationSecs) return undefined;
|
|
221
|
+
return youtubeInspection(url, requestedId, data.videoDetails?.title, streamDurationSecs);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function youtubeInspection(
|
|
225
|
+
url: string,
|
|
226
|
+
requestedId: string,
|
|
227
|
+
title: string | undefined,
|
|
228
|
+
durationSecs: number,
|
|
229
|
+
): UrlInspection {
|
|
230
|
+
return {
|
|
231
|
+
originalUrl: url,
|
|
232
|
+
...(title ? { title } : {}),
|
|
233
|
+
durationSecs,
|
|
234
|
+
singleItemDurationSecs: durationSecs,
|
|
235
|
+
singleItemUrl: `https://www.youtube.com/watch?v=${encodeURIComponent(requestedId)}`,
|
|
236
|
+
entryCount: 1,
|
|
237
|
+
mediaType: 'video',
|
|
238
|
+
isYouTube: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function playerStreamDurationSecs(data: YouTubePlayerData): number | undefined {
|
|
243
|
+
const durations = [
|
|
244
|
+
...(data.streamingData?.formats ?? []),
|
|
245
|
+
...(data.streamingData?.adaptiveFormats ?? []),
|
|
246
|
+
]
|
|
247
|
+
.map((format) => positiveDuration(Number(format.approxDurationMs) / 1_000))
|
|
248
|
+
.filter((duration): duration is number => duration !== undefined);
|
|
249
|
+
return durations.length ? Math.round(Math.max(...durations)) : undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function youtubePageDurationSecs(html: string): number | undefined {
|
|
253
|
+
const durationTag = html.match(
|
|
254
|
+
/<meta\b(?=[^>]*\bitemprop=["']duration["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/i,
|
|
255
|
+
);
|
|
256
|
+
const durationText = durationTag?.[1];
|
|
257
|
+
if (!durationText) return undefined;
|
|
258
|
+
const match = durationText.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i);
|
|
259
|
+
if (!match) return undefined;
|
|
260
|
+
return positiveDuration(
|
|
261
|
+
Number(match[1] ?? 0) * 3_600 + Number(match[2] ?? 0) * 60 + Number(match[3] ?? 0),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function jsonObjectAfterMarker(source: string, marker: string): string | undefined {
|
|
266
|
+
const markerIndex = source.indexOf(marker);
|
|
267
|
+
const start = markerIndex < 0 ? -1 : source.indexOf('{', markerIndex + marker.length);
|
|
268
|
+
if (start < 0) return undefined;
|
|
269
|
+
let depth = 0;
|
|
270
|
+
let inString = false;
|
|
271
|
+
let escaped = false;
|
|
272
|
+
for (let index = start; index < source.length; index++) {
|
|
273
|
+
const character = source[index];
|
|
274
|
+
if (inString) {
|
|
275
|
+
if (escaped) escaped = false;
|
|
276
|
+
else if (character === '\\') escaped = true;
|
|
277
|
+
else if (character === '"') inString = false;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (character === '"') inString = true;
|
|
281
|
+
else if (character === '{') depth += 1;
|
|
282
|
+
else if (character === '}' && --depth === 0) return source.slice(start, index + 1);
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
173
287
|
export interface MediaProbe {
|
|
174
288
|
durationSecs: number;
|
|
175
289
|
containerDurationSecs: number;
|
|
@@ -313,6 +427,48 @@ export function hasInflatedPlaybackTimeline(probe: MediaProbe) {
|
|
|
313
427
|
return longestReported - videoDuration > Math.max(5, videoDuration * 0.03);
|
|
314
428
|
}
|
|
315
429
|
|
|
430
|
+
async function inspectYouTubePlayer(url: string): Promise<UrlInspection | undefined> {
|
|
431
|
+
const videoId = youtubeVideoId(url);
|
|
432
|
+
if (!videoId) return undefined;
|
|
433
|
+
const response = await fetchPublic(
|
|
434
|
+
'https://www.youtube.com/youtubei/v1/player?prettyPrint=false',
|
|
435
|
+
{
|
|
436
|
+
method: 'POST',
|
|
437
|
+
headers: {
|
|
438
|
+
'content-type': 'application/json',
|
|
439
|
+
'user-agent': 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
|
|
440
|
+
},
|
|
441
|
+
body: JSON.stringify({
|
|
442
|
+
context: {
|
|
443
|
+
client: {
|
|
444
|
+
clientName: 'ANDROID',
|
|
445
|
+
clientVersion: '20.10.38',
|
|
446
|
+
androidSdkVersion: 30,
|
|
447
|
+
hl: 'en',
|
|
448
|
+
gl: 'US',
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
videoId,
|
|
452
|
+
contentCheckOk: true,
|
|
453
|
+
racyCheckOk: true,
|
|
454
|
+
}),
|
|
455
|
+
signal: AbortSignal.timeout(MEDIA_URL_INSPECTION_TIMEOUT_MS),
|
|
456
|
+
},
|
|
457
|
+
);
|
|
458
|
+
if (!response.ok) {
|
|
459
|
+
await response.body?.cancel();
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
const declared = Number(response.headers.get('content-length'));
|
|
463
|
+
if (Number.isFinite(declared) && declared > 3 * 1024 * 1024) {
|
|
464
|
+
await response.body?.cancel();
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const body = await response.text();
|
|
468
|
+
if (body.length > 3 * 1024 * 1024) return undefined;
|
|
469
|
+
return inspectYouTubePlayerMetadata(url, JSON.parse(body) as YouTubePlayerData);
|
|
470
|
+
}
|
|
471
|
+
|
|
316
472
|
export async function inspectMediaUrl(url: string): Promise<UrlInspection> {
|
|
317
473
|
const parsed = validHttpUrl(url);
|
|
318
474
|
await assertPublicHost(parsed.hostname);
|
|
@@ -321,6 +477,21 @@ export async function inspectMediaUrl(url: string): Promise<UrlInspection> {
|
|
|
321
477
|
try {
|
|
322
478
|
output = await runYtDlp(['--dump-single-json', '--simulate', '--flat-playlist', url]);
|
|
323
479
|
} catch (error) {
|
|
480
|
+
const playerInspection = await inspectYouTubePlayer(url).catch(() => undefined);
|
|
481
|
+
if (playerInspection) return playerInspection;
|
|
482
|
+
try {
|
|
483
|
+
const page = await readPublicUrl(url, 3 * 1024 * 1024, {
|
|
484
|
+
headers: {
|
|
485
|
+
// YouTube otherwise serves a reduced challenge page to serverless
|
|
486
|
+
// fetch clients, which excludes its public schema.org duration.
|
|
487
|
+
'user-agent': 'Mozilla/5.0 (compatible; Larkup/1.0; +https://larkup.de)',
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
const inspection = inspectYouTubeWatchPage(url, new TextDecoder().decode(page.bytes));
|
|
491
|
+
if (inspection) return inspection;
|
|
492
|
+
} catch {
|
|
493
|
+
// A public watch URL can still be staged for durable inspection below.
|
|
494
|
+
}
|
|
324
495
|
const fallback = inspectYouTubeWatchUrl(url);
|
|
325
496
|
if (fallback) return fallback;
|
|
326
497
|
throw error;
|