@postfetch/core 0.2.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/dist/index.js ADDED
@@ -0,0 +1,1089 @@
1
+ // src/internal.ts
2
+ class PostfetchError extends Error {
3
+ status;
4
+ constructor(status, message) {
5
+ super(message);
6
+ this.status = status;
7
+ this.name = "PostfetchError";
8
+ }
9
+ }
10
+ function createNet(baseFetch = globalThis.fetch) {
11
+ return async function net(url, init = {}, attempts = 3) {
12
+ let lastError;
13
+ for (let attempt = 1;attempt <= attempts; attempt += 1) {
14
+ const controller = new AbortController;
15
+ const timeout = setTimeout(() => controller.abort(), 30000);
16
+ try {
17
+ const response = await baseFetch(url, { ...init, signal: controller.signal });
18
+ if (!retryable(response.status) || attempt === attempts) {
19
+ return response;
20
+ }
21
+ await sleep(retryDelay(response, attempt));
22
+ } catch (error) {
23
+ lastError = error;
24
+ if (attempt === attempts) {
25
+ break;
26
+ }
27
+ await sleep(500 * 2 ** (attempt - 1));
28
+ } finally {
29
+ clearTimeout(timeout);
30
+ }
31
+ }
32
+ throw lastError instanceof Error ? lastError : new Error("request failed");
33
+ };
34
+ }
35
+ function object(value) {
36
+ return typeof value === "object" && value !== null && !Array.isArray(value);
37
+ }
38
+ function string(value) {
39
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
40
+ }
41
+ function number(value) {
42
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
43
+ }
44
+ function asUrl(value) {
45
+ try {
46
+ return new URL(value);
47
+ } catch {
48
+ throw new PostfetchError(400, "invalid url");
49
+ }
50
+ }
51
+ function filename(value) {
52
+ return value.replace(/[^A-Za-z0-9_.-]+/g, "_");
53
+ }
54
+ function retryable(status) {
55
+ return status === 408 || status === 429 || status >= 500;
56
+ }
57
+ function retryDelay(response, attempt) {
58
+ const retryAfter = response.headers.get("retry-after");
59
+ if (retryAfter) {
60
+ const seconds = Number(retryAfter);
61
+ if (Number.isFinite(seconds) && seconds >= 0) {
62
+ return seconds * 1000;
63
+ }
64
+ const time = Date.parse(retryAfter);
65
+ if (!Number.isNaN(time) && time > Date.now()) {
66
+ return time - Date.now();
67
+ }
68
+ }
69
+ return Math.min(500 * 2 ** (attempt - 1), 1e4);
70
+ }
71
+ function sleep(ms) {
72
+ return new Promise((resolve) => setTimeout(resolve, ms));
73
+ }
74
+
75
+ // src/fingerprint.ts
76
+ function pick(values) {
77
+ return values[Math.floor(Math.random() * values.length)];
78
+ }
79
+ var chromeVersions = ["128", "129", "130", "131", "132", "133"];
80
+ var firefoxVersions = ["140", "143", "146", "148"];
81
+ var desktopPlatforms = [
82
+ { chPlatform: "Windows", uaToken: "Windows NT 10.0; Win64; x64" },
83
+ { chPlatform: "macOS", uaToken: "Macintosh; Intel Mac OS X 10_15_7" },
84
+ { chPlatform: "Linux", uaToken: "X11; Linux x86_64" }
85
+ ];
86
+ var acceptLanguages = ["en-US,en;q=0.9", "en-GB,en;q=0.9", "en;q=0.9", "en-US,en;q=0.8"];
87
+ var instagramAppUserAgents = [
88
+ "Instagram 275.0.0.27.98 Android (33/13; 280dpi; 720x1423; Xiaomi; Redmi 7; onclite; qcom; en_US; 458229237)",
89
+ "Instagram 301.1.0.33.110 Android (34/14; 420dpi; 1080x2340; samsung; SM-G991B; o1s; exynos2100; en_US; 521879118)",
90
+ "Instagram 309.0.0.40.113 Android (33/13; 440dpi; 1080x2280; OnePlus; HD1913; OnePlus7TPro; qcom; en_US; 537291984)"
91
+ ];
92
+ function browserFingerprint() {
93
+ const version = pick(chromeVersions);
94
+ const platform = pick(desktopPlatforms);
95
+ return {
96
+ acceptLanguage: pick(acceptLanguages),
97
+ secChUa: `"Chromium";v="${version}", "Google Chrome";v="${version}", "Not_A Brand";v="24"`,
98
+ secChUaPlatform: `"${platform.chPlatform}"`,
99
+ userAgent: `Mozilla/5.0 (${platform.uaToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version}.0.0.0 Safari/537.36`
100
+ };
101
+ }
102
+ function navigationHeaders() {
103
+ const fingerprint = browserFingerprint();
104
+ return {
105
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
106
+ "accept-language": fingerprint.acceptLanguage,
107
+ "sec-ch-ua": fingerprint.secChUa,
108
+ "sec-ch-ua-mobile": "?0",
109
+ "sec-ch-ua-platform": fingerprint.secChUaPlatform,
110
+ "sec-fetch-dest": "document",
111
+ "sec-fetch-mode": "navigate",
112
+ "sec-fetch-site": "none",
113
+ "sec-fetch-user": "?1",
114
+ "upgrade-insecure-requests": "1",
115
+ "user-agent": fingerprint.userAgent
116
+ };
117
+ }
118
+ function browserUserAgent() {
119
+ return browserFingerprint().userAgent;
120
+ }
121
+ function firefoxUserAgent() {
122
+ const platform = pick(desktopPlatforms);
123
+ const version = pick(firefoxVersions);
124
+ return `Mozilla/5.0 (${platform.uaToken}; rv:${version}.0) Gecko/20100101 Firefox/${version}.0`;
125
+ }
126
+ function instagramAppUserAgent() {
127
+ return pick(instagramAppUserAgents);
128
+ }
129
+
130
+ // src/facebook.ts
131
+ async function resolveFacebook(input) {
132
+ const canonical = await canonicalUrl(input.net, input.url);
133
+ const id = facebookId(canonical) ?? facebookId(input.url) ?? "video";
134
+ const url = await embedVideo(input.net, canonical);
135
+ if (!url) {
136
+ throw new Error("Facebook video not found");
137
+ }
138
+ const item = {
139
+ filename: filename(`facebook_${id}.mp4`),
140
+ headers: { "user-agent": browserUserAgent() },
141
+ id,
142
+ kind: "video",
143
+ mime: "video/mp4",
144
+ platform: "facebook",
145
+ url
146
+ };
147
+ return { archiveFilename: filename(`facebook_${id}.zip`), id, items: [item], platform: "facebook" };
148
+ }
149
+ function navigationHeaders2() {
150
+ return {
151
+ "accept-language": "en-US,en;q=0.9",
152
+ "sec-fetch-mode": "navigate",
153
+ "user-agent": browserUserAgent()
154
+ };
155
+ }
156
+ async function canonicalUrl(net, input) {
157
+ const response = await net(input, { headers: navigationHeaders2() });
158
+ const resolved = asUrl(response.url);
159
+ return `${resolved.origin}${resolved.pathname}`;
160
+ }
161
+ async function embedVideo(net, canonical) {
162
+ const embed = `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(canonical)}`;
163
+ const response = await net(embed, { headers: navigationHeaders2() });
164
+ if (!response.ok) {
165
+ return null;
166
+ }
167
+ const html = await response.text();
168
+ return source(html, "hd_src") ?? source(html, "sd_src");
169
+ }
170
+ function source(html, key) {
171
+ const match = html.match(new RegExp(`"${key}":("(?:\\\\.|[^"\\\\])*")`));
172
+ if (!match) {
173
+ return null;
174
+ }
175
+ const decoded = JSON.parse(match[1]);
176
+ return typeof decoded === "string" && decoded.length > 0 ? decoded : null;
177
+ }
178
+ function facebookId(input) {
179
+ const url = asUrl(input);
180
+ const fromQuery = url.searchParams.get("v");
181
+ if (fromQuery && /^\d+$/.test(fromQuery)) {
182
+ return fromQuery;
183
+ }
184
+ const numeric = url.pathname.match(/\/(?:reel|videos?|watch)\/(\d+)/);
185
+ if (numeric) {
186
+ return numeric[1];
187
+ }
188
+ const token = url.pathname.match(/\/(?:share\/[a-z]\/|posts\/|videos\/)?([A-Za-z0-9]+)\/?$/);
189
+ return token ? token[1] : null;
190
+ }
191
+
192
+ // src/instagram.ts
193
+ var appId = "936619743392459";
194
+ function mobileHeaders() {
195
+ return {
196
+ "accept-language": "en-US",
197
+ "content-length": "0",
198
+ "user-agent": instagramAppUserAgent(),
199
+ "x-fb-client-ip": "True",
200
+ "x-fb-http-engine": "Liger",
201
+ "x-fb-server-cluster": "True",
202
+ "x-ig-app-locale": "en_US",
203
+ "x-ig-device-locale": "en_US",
204
+ "x-ig-mapped-locale": "en_US"
205
+ };
206
+ }
207
+ function embedHeaders() {
208
+ const fingerprint = browserFingerprint();
209
+ return {
210
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
211
+ "Accept-Language": fingerprint.acceptLanguage,
212
+ "Cache-Control": "max-age=0",
213
+ Dnt: "1",
214
+ Priority: "u=0, i",
215
+ "Sec-Ch-Ua": fingerprint.secChUa,
216
+ "Sec-Ch-Ua-Mobile": "?0",
217
+ "Sec-Ch-Ua-Platform": fingerprint.secChUaPlatform,
218
+ "Sec-Fetch-Dest": "document",
219
+ "Sec-Fetch-Mode": "navigate",
220
+ "Sec-Fetch-Site": "none",
221
+ "Sec-Fetch-User": "?1",
222
+ "Upgrade-Insecure-Requests": "1",
223
+ "User-Agent": fingerprint.userAgent
224
+ };
225
+ }
226
+ async function resolveInstagram(input) {
227
+ const code = shortcode(input.url);
228
+ const media = await pageMedia(input.net, code, input.preferredWidth) ?? await mobileMedia(input.net, code, input.preferredWidth) ?? await embedMedia(input.net, code) ?? await graphqlMedia(input.net, code);
229
+ if (!media) {
230
+ throw new Error("Instagram media not found");
231
+ }
232
+ const items = mediaItems(media, code, input.preferredWidth);
233
+ if (items.length === 0) {
234
+ throw new Error("Instagram media url not found");
235
+ }
236
+ return { archiveFilename: filename(`instagram_${code}.zip`), id: code, items, platform: "instagram" };
237
+ }
238
+ function shortcode(input) {
239
+ const path = asUrl(input).pathname.split("/").filter(Boolean);
240
+ const index = path.findIndex((part) => part === "p" || part === "reel" || part === "reels" || part === "tv");
241
+ const code = index >= 0 ? path[index + 1] : path[path.length - 1];
242
+ if (!code) {
243
+ throw new Error("Instagram shortcode not found");
244
+ }
245
+ return code;
246
+ }
247
+ async function pageMedia(net, code, preferredWidth) {
248
+ const response = await net(`https://www.instagram.com/p/${code}/`, { headers: navigationHeaders() });
249
+ if (!response.ok) {
250
+ return null;
251
+ }
252
+ const html = await response.text();
253
+ const media = inlineMedia(html, code);
254
+ return media && mediaItems(media, code, preferredWidth).length > 0 ? media : null;
255
+ }
256
+ function inlineMedia(html, code) {
257
+ for (const match of html.matchAll(/<script type="application\/json"[^>]*>(.*?)<\/script>/gs)) {
258
+ let parsed;
259
+ try {
260
+ parsed = JSON.parse(match[1]);
261
+ } catch {
262
+ continue;
263
+ }
264
+ const media = searchMedia(parsed, code);
265
+ if (media) {
266
+ return media;
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+ function searchMedia(node, code) {
272
+ if (Array.isArray(node)) {
273
+ for (const child of node) {
274
+ const media = searchMedia(child, code);
275
+ if (media) {
276
+ return media;
277
+ }
278
+ }
279
+ return null;
280
+ }
281
+ if (!object(node)) {
282
+ return null;
283
+ }
284
+ const hasMedia = Array.isArray(node.video_versions) || Array.isArray(node.carousel_media) || object(node.image_versions2) && Array.isArray(node.image_versions2.candidates);
285
+ if (hasMedia && node.code === code) {
286
+ return node;
287
+ }
288
+ for (const key in node) {
289
+ const media = searchMedia(node[key], code);
290
+ if (media) {
291
+ return media;
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+ async function mobileMedia(net, code, preferredWidth) {
297
+ const id = await mediaId(net, code);
298
+ if (!id) {
299
+ return null;
300
+ }
301
+ const media = await mobileInfo(net, id);
302
+ return media && mediaItems(media, code, preferredWidth).length > 0 ? media : null;
303
+ }
304
+ async function mediaId(net, code) {
305
+ const url = new URL("https://i.instagram.com/api/v1/oembed/");
306
+ url.searchParams.set("url", `https://www.instagram.com/p/${code}/`);
307
+ const response = await net(url.href, { headers: mobileHeaders() }, 1);
308
+ if (!response.ok) {
309
+ return null;
310
+ }
311
+ const payload = await response.json().catch(() => null);
312
+ return object(payload) ? string(payload.media_id) : null;
313
+ }
314
+ async function mobileInfo(net, mediaId2) {
315
+ const response = await net(`https://i.instagram.com/api/v1/media/${mediaId2}/info/`, {
316
+ headers: mobileHeaders()
317
+ }, 1);
318
+ if (!response.ok) {
319
+ return null;
320
+ }
321
+ const payload = await response.json().catch(() => null);
322
+ const items = object(payload) && Array.isArray(payload.items) ? payload.items : [];
323
+ const first = items[0];
324
+ return object(first) ? first : null;
325
+ }
326
+ async function embedMedia(net, code) {
327
+ const response = await net(`https://www.instagram.com/p/${code}/embed/captioned/`, {
328
+ headers: embedHeaders()
329
+ });
330
+ if (!response.ok) {
331
+ return null;
332
+ }
333
+ const html = await response.text();
334
+ const init = html.match(/"init",\[\],\[(.*?)\]\],/s)?.[1];
335
+ if (!init) {
336
+ return null;
337
+ }
338
+ const parsed = JSON.parse(init);
339
+ const contextJson = object(parsed) ? string(parsed.contextJSON) : null;
340
+ if (!contextJson) {
341
+ return null;
342
+ }
343
+ const context = JSON.parse(contextJson);
344
+ if (!object(context)) {
345
+ return null;
346
+ }
347
+ const embedded = object(context.context) && object(context.context.media) ? context.context.media : null;
348
+ const gqlMedia = gqlShortcodeMedia(context);
349
+ return embedded ?? gqlMedia;
350
+ }
351
+ async function graphqlMedia(net, code) {
352
+ const params = await graphqlParams(net, code);
353
+ if (!params) {
354
+ return null;
355
+ }
356
+ const body = new URLSearchParams({
357
+ ...params.body,
358
+ doc_id: "8845758582119845",
359
+ fb_api_caller_class: "RelayModern",
360
+ fb_api_req_friendly_name: "PolarisPostActionLoadPostQueryQuery",
361
+ server_timestamps: "true",
362
+ variables: JSON.stringify({
363
+ fetch_tagged_user_count: null,
364
+ hoisted_comment_id: null,
365
+ hoisted_reply_id: null,
366
+ shortcode: code
367
+ })
368
+ });
369
+ const response = await net("https://www.instagram.com/graphql/query", {
370
+ body,
371
+ headers: {
372
+ ...embedHeaders(),
373
+ ...params.headers,
374
+ "X-FB-Friendly-Name": "PolarisPostActionLoadPostQueryQuery",
375
+ "content-type": "application/x-www-form-urlencoded"
376
+ },
377
+ method: "POST"
378
+ });
379
+ if (!response.ok) {
380
+ return null;
381
+ }
382
+ const payload = await response.json().catch(() => null);
383
+ const data = object(payload) && object(payload.data) ? payload.data : null;
384
+ return data ? gqlShortcodeMedia(data) : null;
385
+ }
386
+ async function graphqlParams(net, code) {
387
+ const response = await net(`https://www.instagram.com/p/${code}/`, {
388
+ headers: embedHeaders()
389
+ });
390
+ if (!response.ok) {
391
+ return null;
392
+ }
393
+ const html = await response.text();
394
+ const site = entryObject("SiteData", html);
395
+ const polaris = entryObject("PolarisSiteData", html);
396
+ const web = entryObject("DGWWebConfig", html);
397
+ const push = entryObject("InstagramWebPushInfo", html);
398
+ const lsd = entryObject("LSD", html)?.token ?? randomToken();
399
+ const csrf = entryObject("InstagramSecurityConfig", html)?.csrf_token;
400
+ const cookie = [
401
+ csrf && `csrftoken=${csrf}`,
402
+ polaris?.device_id && `ig_did=${polaris.device_id}`,
403
+ polaris?.machine_id && `mid=${polaris.machine_id}`,
404
+ "wd=1280x720",
405
+ "dpr=2",
406
+ "ig_nrcb=1"
407
+ ].filter((value) => typeof value === "string" && value.length > 0).join("; ");
408
+ return {
409
+ headers: {
410
+ "X-CSRFToken": string(csrf) ?? "",
411
+ "X-FB-LSD": string(lsd) ?? randomToken(),
412
+ "X-Bloks-Version-Id": string(entryObject("WebBloksVersioningID", html)?.versioningID) ?? "",
413
+ cookie,
414
+ "x-asbd-id": "129477",
415
+ "x-ig-app-id": string(web?.appId) ?? appId
416
+ },
417
+ body: {
418
+ __a: "1",
419
+ __ccg: "EXCELLENT",
420
+ __comet_req: String(queryNumber("__comet_req", html) ?? 7),
421
+ __csr: randomToken(154),
422
+ __d: "www",
423
+ __dyn: randomToken(154),
424
+ __hs: string(site?.haste_session) ?? "20126.HYP:instagram_web_pkg.2.1...0",
425
+ __hsi: string(site?.hsi) ?? "7436540909012459023",
426
+ __req: "b",
427
+ __rev: string(push?.rollout_hash) ?? "1019933358",
428
+ __s: `::${Math.random().toString(36).replace(/\d/g, "").slice(2, 8)}`,
429
+ __spin_b: string(site?.__spin_b) ?? "trunk",
430
+ __spin_r: string(site?.__spin_r) ?? "1019933358",
431
+ __spin_t: String(number(site?.__spin_t) ?? Math.floor(Date.now() / 1000)),
432
+ __user: "0",
433
+ av: "0",
434
+ dpr: "2",
435
+ jazoest: String(queryNumber("jazoest", html) ?? Math.floor(Math.random() * 1e4)),
436
+ lsd: string(lsd) ?? randomToken()
437
+ }
438
+ };
439
+ }
440
+ function gqlShortcodeMedia(data) {
441
+ const media = data.gql_data && object(data.gql_data) ? data.gql_data.shortcode_media ?? data.gql_data.xdt_shortcode_media : data.shortcode_media ?? data.xdt_shortcode_media;
442
+ return object(media) ? media : null;
443
+ }
444
+ function mediaItems(media, code, preferredWidth) {
445
+ const sidecar = object(media.edge_sidecar_to_children) && Array.isArray(media.edge_sidecar_to_children.edges) ? media.edge_sidecar_to_children.edges : [];
446
+ const oldItems = sidecar.flatMap((edge, index) => {
447
+ const node = object(edge) && object(edge.node) ? edge.node : null;
448
+ return node ? instagramItem(node, code, index + 1, preferredWidth) : [];
449
+ });
450
+ if (oldItems.length > 0) {
451
+ return oldItems;
452
+ }
453
+ const carousel = Array.isArray(media.carousel_media) ? media.carousel_media.filter(object) : [];
454
+ const newItems = carousel.flatMap((item, index) => instagramItem(item, code, index + 1, preferredWidth));
455
+ if (newItems.length > 0) {
456
+ return newItems;
457
+ }
458
+ return instagramItem(media, code, null, preferredWidth);
459
+ }
460
+ function instagramItem(media, code, index, preferredWidth) {
461
+ const video = selectVersion(media.video_versions, preferredWidth) ?? string(media.video_url);
462
+ const suffix = index === null ? "" : `_${index}`;
463
+ if (video) {
464
+ return [{
465
+ filename: filename(`instagram_${code}${suffix}.mp4`),
466
+ headers: { "user-agent": browserUserAgent() },
467
+ id: code,
468
+ kind: "video",
469
+ mime: "video/mp4",
470
+ platform: "instagram",
471
+ url: video
472
+ }];
473
+ }
474
+ const image = selectImage(media);
475
+ return image ? [{
476
+ filename: filename(`instagram_${code}${suffix}.jpg`),
477
+ headers: { "user-agent": browserUserAgent() },
478
+ id: code,
479
+ kind: "image",
480
+ mime: "image/jpeg",
481
+ platform: "instagram",
482
+ url: image
483
+ }] : [];
484
+ }
485
+ function selectImage(media) {
486
+ const imageVersions = object(media.image_versions2) && Array.isArray(media.image_versions2.candidates) ? media.image_versions2.candidates.filter(object) : [];
487
+ const first = imageVersions[0] ? string(imageVersions[0].url) : null;
488
+ return first ?? string(media.display_url);
489
+ }
490
+ function selectVersion(value, preferredWidth) {
491
+ const versions = Array.isArray(value) ? value.filter(object) : [];
492
+ const best = versions.reduce((current, candidate) => {
493
+ const width = number(candidate.width);
494
+ const currentWidth = current ? number(current.width) : null;
495
+ if (width === null) {
496
+ return current;
497
+ }
498
+ if (currentWidth === null) {
499
+ return candidate;
500
+ }
501
+ return Math.abs(width - preferredWidth) < Math.abs(currentWidth - preferredWidth) ? candidate : current;
502
+ }, null);
503
+ const selected = best ?? versions[0] ?? null;
504
+ return selected ? string(selected.url) : null;
505
+ }
506
+ function entryObject(name, html) {
507
+ const raw = html.match(new RegExp(`\\\\["${name}",.*?,({.*?}),\\\\d+\\\\]`))?.[1];
508
+ if (!raw) {
509
+ return null;
510
+ }
511
+ const parsed = JSON.parse(raw);
512
+ return object(parsed) ? parsed : null;
513
+ }
514
+ function queryNumber(name, html) {
515
+ const raw = html.match(new RegExp(`${name}=(\\d+)`))?.[1];
516
+ const parsed = raw ? Number(raw) : NaN;
517
+ return Number.isFinite(parsed) ? parsed : null;
518
+ }
519
+ function randomToken(length = 8) {
520
+ return crypto.getRandomValues(new Uint8Array(length)).reduce((value, byte) => value + (byte % 36).toString(36), "");
521
+ }
522
+
523
+ // src/tiktok.ts
524
+ var marker = '<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">';
525
+ async function resolveTiktok(input) {
526
+ const userAgent = browserUserAgent();
527
+ const pageUrl = await followShortlink(input.net, userAgent, input.url);
528
+ const id = videoId(pageUrl);
529
+ if (!id) {
530
+ throw new Error("TikTok video id not found");
531
+ }
532
+ const page = await fetchVideoPage(input.net, id, userAgent).catch((error) => {
533
+ if (!recoverablePageError(error)) {
534
+ throw error;
535
+ }
536
+ return fetchVideoPage(input.net, id, firefoxUserAgent());
537
+ });
538
+ const user = author(page.item) ?? username(pageUrl) ?? "i";
539
+ const headers = {
540
+ referer: `https://www.tiktok.com/@${encodeURIComponent(user)}/video/${encodeURIComponent(id)}`,
541
+ "user-agent": userAgent
542
+ };
543
+ if (page.cookie) {
544
+ headers.cookie = page.cookie;
545
+ }
546
+ const items = mediaItems2(page.item, user, id, headers);
547
+ if (items.length === 0) {
548
+ throw new Error("TikTok media not found");
549
+ }
550
+ return { archiveFilename: filename(`tiktok_${user}_${id}.zip`), id, items, platform: "tiktok" };
551
+ }
552
+ async function fetchVideoPage(net, id, userAgent) {
553
+ const page = await net(`https://www.tiktok.com/@i/video/${id}`, {
554
+ headers: {
555
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
556
+ referer: "https://www.tiktok.com/",
557
+ "user-agent": userAgent
558
+ }
559
+ });
560
+ return { cookie: cookieHeader(page.headers), item: itemStruct(await page.text()) };
561
+ }
562
+ async function followShortlink(net, userAgent, input) {
563
+ const url = asUrl(input);
564
+ if (!url.hostname.includes("vt.tiktok.com")) {
565
+ return input;
566
+ }
567
+ const response = await net(input, { headers: { "user-agent": userAgent }, redirect: "manual" }, 1);
568
+ const location = response.headers.get("location");
569
+ if (location) {
570
+ return new URL(location, input).href;
571
+ }
572
+ const html = await response.text();
573
+ const href = html.match(/<a href="(https:\/\/[^"]+)"/)?.[1];
574
+ return href ? new URL(href.split("?")[0]).href : input;
575
+ }
576
+ function videoId(input) {
577
+ return asUrl(input).pathname.match(/video\/(\d+)/)?.[1] ?? null;
578
+ }
579
+ function itemStruct(html) {
580
+ if (html.includes("SlardarWAF") || html.includes("_wafchallengeid")) {
581
+ throw new Error("TikTok WAF challenge");
582
+ }
583
+ const start = html.indexOf(marker);
584
+ const end = start === -1 ? -1 : html.indexOf("</script>", start + marker.length);
585
+ if (start === -1 || end === -1) {
586
+ throw new Error("TikTok hydration not found");
587
+ }
588
+ const parsed = JSON.parse(html.slice(start + marker.length, end));
589
+ const scope = object(parsed) && object(parsed.__DEFAULT_SCOPE__) ? parsed.__DEFAULT_SCOPE__ : null;
590
+ const detail = scope && object(scope["webapp.video-detail"]) ? scope["webapp.video-detail"] : null;
591
+ const info = detail && object(detail.itemInfo) ? detail.itemInfo : null;
592
+ const item = info && object(info.itemStruct) ? info.itemStruct : null;
593
+ if (!item) {
594
+ throw new Error("TikTok itemStruct not found");
595
+ }
596
+ return item;
597
+ }
598
+ function recoverablePageError(error) {
599
+ const message = error instanceof Error ? error.message : "";
600
+ return message === "TikTok WAF challenge" || message === "TikTok hydration not found";
601
+ }
602
+ function downloadUrl(item) {
603
+ const video = object(item.video) ? item.video : null;
604
+ return video ? string(video.playAddr) ?? string(video.downloadAddr) : null;
605
+ }
606
+ function mediaItems2(item, user, id, headers) {
607
+ const images = imageItems(item, user, id, headers);
608
+ if (images.length > 0) {
609
+ const audio = audioItem(item, user, id, headers);
610
+ return audio ? [...images, audio] : images;
611
+ }
612
+ const url = downloadUrl(item);
613
+ return url ? [{
614
+ filename: filename(`tiktok_${user}_${id}.mp4`),
615
+ headers,
616
+ id,
617
+ kind: "video",
618
+ mime: "video/mp4",
619
+ platform: "tiktok",
620
+ url
621
+ }] : [];
622
+ }
623
+ function imageItems(item, user, id, headers) {
624
+ const imagePost = object(item.imagePost) ? item.imagePost : null;
625
+ const images = imagePost && Array.isArray(imagePost.images) ? imagePost.images.filter(object) : [];
626
+ return images.flatMap((image, index) => {
627
+ const imageUrl = object(image.imageURL) ? image.imageURL : null;
628
+ const list = imageUrl && Array.isArray(imageUrl.urlList) ? imageUrl.urlList : [];
629
+ const url = list.map(string).find((candidate) => Boolean(candidate)) ?? null;
630
+ return url ? [{
631
+ filename: filename(`tiktok_${user}_${id}_${index + 1}.jpg`),
632
+ headers,
633
+ id,
634
+ kind: "image",
635
+ mime: "image/jpeg",
636
+ platform: "tiktok",
637
+ url
638
+ }] : [];
639
+ });
640
+ }
641
+ function audioItem(item, user, id, headers) {
642
+ const video = object(item.video) ? item.video : null;
643
+ const music = object(item.music) ? item.music : null;
644
+ const url = video ? string(video.playAddr) : null;
645
+ const fallback = music ? string(music.playUrl) : null;
646
+ const selected = url ?? fallback;
647
+ if (!selected) {
648
+ return null;
649
+ }
650
+ const extension = selected.includes("mime_type=audio_mpeg") ? "mp3" : "m4a";
651
+ return {
652
+ filename: filename(`tiktok_${user}_${id}_audio.${extension}`),
653
+ headers,
654
+ id,
655
+ kind: "audio",
656
+ mime: extension === "mp3" ? "audio/mpeg" : "audio/mp4",
657
+ platform: "tiktok",
658
+ url: selected
659
+ };
660
+ }
661
+ function author(item) {
662
+ const user = object(item.author) ? item.author : null;
663
+ return user ? string(user.uniqueId) : null;
664
+ }
665
+ function username(input) {
666
+ return asUrl(input).pathname.match(/@([^/]+)/)?.[1] ?? null;
667
+ }
668
+ function cookieHeader(headers) {
669
+ const getter = headers.getSetCookie;
670
+ const setCookie = typeof getter === "function" ? getter.call(headers).join(",") : headers.get("set-cookie");
671
+ const cookies = setCookie?.split(/,(?=[^;]+?=)/).map((part) => part.split(";")[0]?.trim()).filter((part) => Boolean(part));
672
+ return cookies && cookies.length > 0 ? cookies.join("; ") : null;
673
+ }
674
+
675
+ // src/twitter.ts
676
+ async function resolveTwitter(input) {
677
+ const id = tweetId(input.url);
678
+ if (!id) {
679
+ throw new Error("Tweet id not found");
680
+ }
681
+ const tweet = await syndication(input.net, id);
682
+ if (string(tweet.__typename) === "TweetTombstone") {
683
+ throw new Error("Tweet is unavailable or age-restricted");
684
+ }
685
+ const media = Array.isArray(tweet.mediaDetails) ? tweet.mediaDetails.filter(object) : [];
686
+ const items = media.flatMap((entry, index) => twitterItem(entry, id, index + 1));
687
+ if (items.length === 0) {
688
+ throw new Error("Twitter media not found");
689
+ }
690
+ return { archiveFilename: filename(`twitter_${id}.zip`), id, items, platform: "twitter" };
691
+ }
692
+ async function syndication(net, id) {
693
+ const url = `https://cdn.syndication.twimg.com/tweet-result?id=${id}&lang=en&token=${syndicationToken(id)}`;
694
+ const response = await net(url, { headers: { accept: "application/json", "user-agent": browserUserAgent() } });
695
+ if (!response.ok) {
696
+ throw new Error(`Twitter syndication failed: ${response.status}`);
697
+ }
698
+ const payload = await response.json();
699
+ if (!object(payload)) {
700
+ throw new Error("Twitter response invalid");
701
+ }
702
+ return payload;
703
+ }
704
+ function syndicationToken(id) {
705
+ return (Number(id) / 1000000000000000 * Math.PI).toString(36).replace(/(0+|\.)/g, "");
706
+ }
707
+ function twitterItem(entry, id, index) {
708
+ const headers = { "user-agent": browserUserAgent() };
709
+ const type = string(entry.type);
710
+ if (type === "video" || type === "animated_gif") {
711
+ const url = bestVariant(entry);
712
+ return url ? [{ filename: filename(`twitter_${id}_${index}.mp4`), headers, id, kind: "video", mime: "video/mp4", platform: "twitter", url }] : [];
713
+ }
714
+ const photo = string(entry.media_url_https);
715
+ return photo ? [{ filename: filename(`twitter_${id}_${index}.jpg`), headers, id, kind: "image", mime: "image/jpeg", platform: "twitter", url: `${photo}?name=orig` }] : [];
716
+ }
717
+ function bestVariant(entry) {
718
+ const info = object(entry.video_info) ? entry.video_info : null;
719
+ const variants = info && Array.isArray(info.variants) ? info.variants.filter(object) : [];
720
+ const best = variants.filter((variant) => string(variant.content_type) === "video/mp4").reduce((current, variant) => {
721
+ const bitrate = number(variant.bitrate) ?? 0;
722
+ const currentBitrate = current ? number(current.bitrate) ?? 0 : -1;
723
+ return bitrate > currentBitrate ? variant : current;
724
+ }, null);
725
+ return best ? string(best.url) : null;
726
+ }
727
+ function tweetId(input) {
728
+ return asUrl(input).pathname.match(/\/status(?:es)?\/(\d+)/)?.[1] ?? null;
729
+ }
730
+
731
+ // src/youtube.ts
732
+ var androidVrClient = {
733
+ name: "ANDROID_VR",
734
+ number: "28",
735
+ userAgent: "com.google.android.apps.youtube.vr.oculus/1.65.10 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip",
736
+ version: "1.65.10"
737
+ };
738
+ var browserCookie = "PREF=hl=en&tz=UTC; SOCS=CAI";
739
+ async function resolveYoutube(input) {
740
+ const id = youtubeVideoId(input.url);
741
+ if (!id) {
742
+ throw new Error("YouTube video id not found");
743
+ }
744
+ const session = await youtubeSession(input.net, id);
745
+ const response = await input.net("https://www.youtube.com/youtubei/v1/player?prettyPrint=false", {
746
+ body: JSON.stringify(playerBody(id, session)),
747
+ headers: playerHeaders(session),
748
+ method: "POST"
749
+ });
750
+ if (!response.ok) {
751
+ throw new Error(`YouTube player failed: ${response.status}`);
752
+ }
753
+ const payload = await response.json();
754
+ const status = object(payload) && object(payload.playabilityStatus) ? string(payload.playabilityStatus.status) : null;
755
+ if (status !== "OK") {
756
+ const reason = object(payload) && object(payload.playabilityStatus) ? string(payload.playabilityStatus.reason) : null;
757
+ throw new Error(reason ?? "YouTube video unavailable");
758
+ }
759
+ const format = selectFormat(payload);
760
+ if (!format) {
761
+ throw new Error("YouTube progressive mp4 not found");
762
+ }
763
+ const title = object(payload) && object(payload.videoDetails) ? string(payload.videoDetails.title) : null;
764
+ const media = {
765
+ filename: filename(`youtube_${title ?? id}_${id}.mp4`),
766
+ headers: { "user-agent": androidVrClient.userAgent },
767
+ id,
768
+ kind: "video",
769
+ mime: "video/mp4",
770
+ platform: "youtube",
771
+ url: format
772
+ };
773
+ return { archiveFilename: filename(`youtube_${id}.zip`), id, items: [media], platform: "youtube" };
774
+ }
775
+ function playerBody(id, session) {
776
+ return {
777
+ contentCheckOk: true,
778
+ context: {
779
+ client: {
780
+ androidSdkVersion: 32,
781
+ clientName: androidVrClient.name,
782
+ clientVersion: androidVrClient.version,
783
+ deviceMake: "Oculus",
784
+ deviceModel: "Quest 3",
785
+ gl: "US",
786
+ hl: "en",
787
+ osName: "Android",
788
+ osVersion: "12L",
789
+ timeZone: "UTC",
790
+ userAgent: androidVrClient.userAgent,
791
+ utcOffsetMinutes: 0,
792
+ visitorData: session.visitorData
793
+ }
794
+ },
795
+ playbackContext: {
796
+ contentPlaybackContext: {
797
+ html5Preference: "HTML5_PREF_WANTS",
798
+ signatureTimestamp: session.signatureTimestamp
799
+ }
800
+ },
801
+ racyCheckOk: true,
802
+ videoId: id
803
+ };
804
+ }
805
+ async function youtubeSession(net, id) {
806
+ const response = await net(`https://www.youtube.com/watch?v=${id}&bpctr=9999999999&has_verified=1`, {
807
+ headers: {
808
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
809
+ "accept-language": "en-us,en;q=0.5",
810
+ cookie: browserCookie,
811
+ "sec-fetch-mode": "navigate",
812
+ "user-agent": browserUserAgent()
813
+ }
814
+ });
815
+ if (!response.ok) {
816
+ throw new Error(`YouTube page failed: ${response.status}`);
817
+ }
818
+ const page = await response.text();
819
+ const visitorData = string(page.match(/"visitorData":"([^"]+)"/)?.[1]);
820
+ if (!visitorData) {
821
+ throw new Error("YouTube visitor data not found");
822
+ }
823
+ const playerPath = string(page.match(/"jsUrl":"([^"]+)"/)?.[1]) ?? string(page.match(/"PLAYER_JS_URL":"([^"]+)"/)?.[1]);
824
+ if (!playerPath) {
825
+ throw new Error("YouTube player url not found");
826
+ }
827
+ return {
828
+ cookie: youtubeCookie(response.headers),
829
+ signatureTimestamp: await signatureTimestamp(net, playerPath),
830
+ visitorData
831
+ };
832
+ }
833
+ function playerHeaders(session) {
834
+ return new Headers({
835
+ "content-type": "application/json",
836
+ cookie: `${browserCookie}; ${session.cookie}`,
837
+ origin: "https://www.youtube.com",
838
+ "user-agent": androidVrClient.userAgent,
839
+ "x-goog-visitor-id": session.visitorData,
840
+ "x-youtube-client-name": androidVrClient.number,
841
+ "x-youtube-client-version": androidVrClient.version
842
+ });
843
+ }
844
+ function youtubeCookie(headers) {
845
+ const readable = headers;
846
+ const fallback = headers.get("set-cookie");
847
+ const cookieHeaders = readable.getSetCookie ? readable.getSetCookie() : fallback ? [fallback] : [];
848
+ return cookieHeaders.map((header) => string(header.split(";", 1)[0])).filter(string).join("; ");
849
+ }
850
+ async function signatureTimestamp(net, playerPath) {
851
+ const response = await net(new URL(playerPath, "https://www.youtube.com").toString(), {
852
+ headers: { "user-agent": browserUserAgent() }
853
+ });
854
+ if (!response.ok) {
855
+ throw new Error(`YouTube player failed: ${response.status}`);
856
+ }
857
+ const player = await response.text();
858
+ const timestamp = number(Number(player.match(/signatureTimestamp[:=](\d+)/)?.[1] ?? player.match(/sts[:=](\d+)/)?.[1]));
859
+ if (!timestamp) {
860
+ throw new Error("YouTube signature timestamp not found");
861
+ }
862
+ return timestamp;
863
+ }
864
+ function youtubeVideoId(input) {
865
+ const url = asUrl(input);
866
+ if (url.hostname === "youtu.be") {
867
+ return cleanId(url.pathname.split("/").filter(Boolean)[0]);
868
+ }
869
+ const fromQuery = cleanId(url.searchParams.get("v"));
870
+ if (fromQuery) {
871
+ return fromQuery;
872
+ }
873
+ const parts = url.pathname.split("/").filter(Boolean);
874
+ const index = parts.findIndex((part) => part === "shorts" || part === "live" || part === "embed");
875
+ return index >= 0 ? cleanId(parts[index + 1]) : null;
876
+ }
877
+ function cleanId(value) {
878
+ return typeof value === "string" && /^[A-Za-z0-9_-]{11}$/.test(value) ? value : null;
879
+ }
880
+ function selectFormat(payload) {
881
+ const root = object(payload) ? payload : null;
882
+ const streaming = root && object(root.streamingData) ? root.streamingData : null;
883
+ const formats = streaming && Array.isArray(streaming.formats) ? streaming.formats.filter(object) : [];
884
+ const mp4 = formats.filter((format) => string(format.url) && string(format.mimeType)?.startsWith("video/mp4")).sort((left, right) => height(right) - height(left));
885
+ return mp4[0] ? string(mp4[0].url) : null;
886
+ }
887
+ function height(format) {
888
+ return number(format.height) ?? Number(string(format.qualityLabel)?.match(/(\d+)p/)?.[1] ?? 0);
889
+ }
890
+
891
+ // src/postfetch.ts
892
+ async function postfetch(url, options = {}) {
893
+ const trimmed = url.trim();
894
+ if (trimmed.length === 0) {
895
+ throw new PostfetchError(400, "url is required");
896
+ }
897
+ const context = {
898
+ net: createNet(options.fetch ?? globalThis.fetch),
899
+ preferredWidth: options.preferredWidth ?? 720,
900
+ url: trimmed
901
+ };
902
+ switch (detect(trimmed)) {
903
+ case "facebook":
904
+ return resolveFacebook(context);
905
+ case "instagram":
906
+ return resolveInstagram(context);
907
+ case "tiktok":
908
+ return resolveTiktok(context);
909
+ case "twitter":
910
+ return resolveTwitter(context);
911
+ case "youtube":
912
+ return resolveYoutube(context);
913
+ }
914
+ }
915
+ function detect(url) {
916
+ const host = asUrl(url).hostname;
917
+ if (host.includes("tiktok.com")) {
918
+ return "tiktok";
919
+ }
920
+ if (host.includes("instagram.com")) {
921
+ return "instagram";
922
+ }
923
+ if (host.includes("youtube.com") || host === "youtu.be") {
924
+ return "youtube";
925
+ }
926
+ if (host.includes("facebook.com") || host === "fb.watch") {
927
+ return "facebook";
928
+ }
929
+ if (host === "x.com" || host.endsWith(".x.com") || host.includes("twitter.com")) {
930
+ return "twitter";
931
+ }
932
+ throw new PostfetchError(400, "only Facebook, Instagram, TikTok, X and YouTube URLs are supported");
933
+ }
934
+ // src/zip.ts
935
+ var encoder = new TextEncoder;
936
+ var table = new Uint32Array(256);
937
+ for (let value = 0;value < table.length; value += 1) {
938
+ let crc = value;
939
+ for (let bit = 0;bit < 8; bit += 1) {
940
+ crc = crc & 1 ? 3988292384 ^ crc >>> 1 : crc >>> 1;
941
+ }
942
+ table[value] = crc >>> 0;
943
+ }
944
+ function zip(entries) {
945
+ const files = entries.map((entry) => {
946
+ const name = encoder.encode(entry.name);
947
+ const crc = crc32(entry.data);
948
+ return { ...entry, crc, name };
949
+ });
950
+ const locals = files.map((file) => 30 + file.name.length + file.data.length);
951
+ const centrals = files.map((file) => 46 + file.name.length);
952
+ const localSize = locals.reduce((sum, size) => sum + size, 0);
953
+ const centralSize = centrals.reduce((sum, size) => sum + size, 0);
954
+ const result = new Uint8Array(localSize + centralSize + 22);
955
+ const view = new DataView(result.buffer);
956
+ let offset = 0;
957
+ const centralOffsets = [];
958
+ for (const file of files) {
959
+ centralOffsets.push(offset);
960
+ offset = writeLocal(view, result, offset, file);
961
+ }
962
+ const centralStart = offset;
963
+ for (let index = 0;index < files.length; index += 1) {
964
+ offset = writeCentral(view, result, offset, files[index], centralOffsets[index]);
965
+ }
966
+ writeEnd(view, offset, files.length, centralSize, centralStart);
967
+ return result;
968
+ }
969
+ function writeLocal(view, target, offset, file) {
970
+ view.setUint32(offset, 67324752, true);
971
+ view.setUint16(offset + 4, 20, true);
972
+ view.setUint16(offset + 6, 2048, true);
973
+ view.setUint16(offset + 8, 0, true);
974
+ writeDate(view, offset + 10);
975
+ view.setUint32(offset + 14, file.crc, true);
976
+ view.setUint32(offset + 18, file.data.length, true);
977
+ view.setUint32(offset + 22, file.data.length, true);
978
+ view.setUint16(offset + 26, file.name.length, true);
979
+ view.setUint16(offset + 28, 0, true);
980
+ target.set(file.name, offset + 30);
981
+ target.set(file.data, offset + 30 + file.name.length);
982
+ return offset + 30 + file.name.length + file.data.length;
983
+ }
984
+ function writeCentral(view, target, offset, file, localOffset) {
985
+ view.setUint32(offset, 33639248, true);
986
+ view.setUint16(offset + 4, 20, true);
987
+ view.setUint16(offset + 6, 20, true);
988
+ view.setUint16(offset + 8, 2048, true);
989
+ view.setUint16(offset + 10, 0, true);
990
+ writeDate(view, offset + 12);
991
+ view.setUint32(offset + 16, file.crc, true);
992
+ view.setUint32(offset + 20, file.data.length, true);
993
+ view.setUint32(offset + 24, file.data.length, true);
994
+ view.setUint16(offset + 28, file.name.length, true);
995
+ view.setUint16(offset + 30, 0, true);
996
+ view.setUint16(offset + 32, 0, true);
997
+ view.setUint16(offset + 34, 0, true);
998
+ view.setUint16(offset + 36, 0, true);
999
+ view.setUint32(offset + 38, 0, true);
1000
+ view.setUint32(offset + 42, localOffset, true);
1001
+ target.set(file.name, offset + 46);
1002
+ return offset + 46 + file.name.length;
1003
+ }
1004
+ function writeEnd(view, offset, count, centralSize, centralStart) {
1005
+ view.setUint32(offset, 101010256, true);
1006
+ view.setUint16(offset + 8, count, true);
1007
+ view.setUint16(offset + 10, count, true);
1008
+ view.setUint32(offset + 12, centralSize, true);
1009
+ view.setUint32(offset + 16, centralStart, true);
1010
+ }
1011
+ function writeDate(view, offset) {
1012
+ const date = new Date;
1013
+ const time = date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2);
1014
+ const day = date.getFullYear() - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate();
1015
+ view.setUint16(offset, time, true);
1016
+ view.setUint16(offset + 2, day, true);
1017
+ }
1018
+ function crc32(data) {
1019
+ let crc = 4294967295;
1020
+ for (const byte of data) {
1021
+ crc = table[(crc ^ byte) & 255] ^ crc >>> 8;
1022
+ }
1023
+ return (crc ^ 4294967295) >>> 0;
1024
+ }
1025
+
1026
+ // src/download.ts
1027
+ async function download(item, options = {}) {
1028
+ const net = createNet(options.fetch ?? globalThis.fetch);
1029
+ const response = await net(item.url, { headers: item.headers });
1030
+ if (!response.ok || !response.body) {
1031
+ throw new PostfetchError(502, `download failed: ${response.status}`);
1032
+ }
1033
+ return response;
1034
+ }
1035
+ async function archive(result, options = {}) {
1036
+ const net = createNet(options.fetch ?? globalThis.fetch);
1037
+ const files = await Promise.all(result.items.map(async (item) => {
1038
+ const response = await net(item.url, { headers: item.headers });
1039
+ if (!response.ok) {
1040
+ throw new PostfetchError(502, `download failed: ${response.status}`);
1041
+ }
1042
+ return { data: new Uint8Array(await response.arrayBuffer()), name: item.filename };
1043
+ }));
1044
+ return { bytes: zip(files), filename: result.archiveFilename, mime: "application/zip" };
1045
+ }
1046
+ async function toResponse(result, options = {}) {
1047
+ if (result.items.length === 1) {
1048
+ return singleResponse(result.items[0], options);
1049
+ }
1050
+ return archiveResponse(result, options);
1051
+ }
1052
+ async function singleResponse(item, options) {
1053
+ const media = await download(item, options);
1054
+ const headers = new Headers({
1055
+ "content-disposition": `attachment; filename="${item.filename}"`,
1056
+ "content-type": media.headers.get("content-type") ?? item.mime,
1057
+ "x-media-count": "1",
1058
+ "x-media-id": item.id,
1059
+ "x-media-kind": item.kind,
1060
+ "x-media-platform": item.platform
1061
+ });
1062
+ const length = media.headers.get("content-length");
1063
+ if (length) {
1064
+ headers.set("content-length", length);
1065
+ }
1066
+ return new Response(media.body, { headers });
1067
+ }
1068
+ async function archiveResponse(result, options) {
1069
+ const { bytes, filename: filename2 } = await archive(result, options);
1070
+ const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
1071
+ return new Response(body, {
1072
+ headers: {
1073
+ "content-disposition": `attachment; filename="${filename2}"`,
1074
+ "content-length": String(bytes.length),
1075
+ "content-type": "application/zip",
1076
+ "x-media-count": String(result.items.length),
1077
+ "x-media-id": result.id,
1078
+ "x-media-platform": result.platform
1079
+ }
1080
+ });
1081
+ }
1082
+ export {
1083
+ toResponse,
1084
+ postfetch,
1085
+ download,
1086
+ detect,
1087
+ archive,
1088
+ PostfetchError
1089
+ };