@postfetch/core 0.5.2 → 0.7.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/README.md +8 -4
- package/dist/download.d.ts +20 -0
- package/dist/fingerprint.d.ts +5 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +500 -59
- package/dist/internal.d.ts +13 -2
- package/dist/linkedin.d.ts +3 -0
- package/dist/postfetch.d.ts +11 -3
- package/package.json +3 -2
- package/src/download.ts +32 -0
- package/src/facebook.ts +27 -13
- package/src/fingerprint.ts +21 -7
- package/src/index.ts +12 -4
- package/src/instagram.ts +54 -22
- package/src/internal.ts +12 -1
- package/src/linkedin.ts +243 -0
- package/src/postfetch.ts +88 -7
- package/src/tiktok.ts +121 -16
- package/src/twitter.ts +33 -3
package/dist/index.js
CHANGED
|
@@ -124,20 +124,25 @@ function browserFingerprint() {
|
|
|
124
124
|
userAgent: `Mozilla/5.0 (${platform.uaToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version}.0.0.0 Safari/537.36`
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
-
function
|
|
128
|
-
const fingerprint = browserFingerprint();
|
|
127
|
+
function baseNavigationHeaders(userAgent, acceptLanguage) {
|
|
129
128
|
return {
|
|
130
129
|
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
131
|
-
"accept-language":
|
|
132
|
-
"sec-ch-ua": fingerprint.secChUa,
|
|
133
|
-
"sec-ch-ua-mobile": "?0",
|
|
134
|
-
"sec-ch-ua-platform": fingerprint.secChUaPlatform,
|
|
130
|
+
"accept-language": acceptLanguage,
|
|
135
131
|
"sec-fetch-dest": "document",
|
|
136
132
|
"sec-fetch-mode": "navigate",
|
|
137
133
|
"sec-fetch-site": "none",
|
|
138
134
|
"sec-fetch-user": "?1",
|
|
139
135
|
"upgrade-insecure-requests": "1",
|
|
140
|
-
"user-agent":
|
|
136
|
+
"user-agent": userAgent
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function navigationHeaders() {
|
|
140
|
+
const fingerprint = browserFingerprint();
|
|
141
|
+
return {
|
|
142
|
+
...baseNavigationHeaders(fingerprint.userAgent, fingerprint.acceptLanguage),
|
|
143
|
+
"sec-ch-ua": fingerprint.secChUa,
|
|
144
|
+
"sec-ch-ua-mobile": "?0",
|
|
145
|
+
"sec-ch-ua-platform": fingerprint.secChUaPlatform
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
function browserUserAgent() {
|
|
@@ -148,19 +153,22 @@ function firefoxUserAgent() {
|
|
|
148
153
|
const version = pick(firefoxVersions);
|
|
149
154
|
return `Mozilla/5.0 (${platform.uaToken}; rv:${version}.0) Gecko/20100101 Firefox/${version}.0`;
|
|
150
155
|
}
|
|
156
|
+
function firefoxNavigationHeaders(userAgent = firefoxUserAgent()) {
|
|
157
|
+
return baseNavigationHeaders(userAgent, pick(acceptLanguages));
|
|
158
|
+
}
|
|
151
159
|
function instagramAppUserAgent() {
|
|
152
160
|
return pick(instagramAppUserAgents);
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
// src/facebook.ts
|
|
156
164
|
async function resolveFacebook(input) {
|
|
157
|
-
const canonical = await canonicalUrl(input.net, input.url);
|
|
165
|
+
const canonical = await canonicalUrl(input.net, normalizedInputUrl(input.url));
|
|
158
166
|
const id = facebookId(canonical) ?? facebookId(input.url) ?? "video";
|
|
159
|
-
const embed = await embedVideo(input.net, canonical);
|
|
160
|
-
const watch = !embed && /^\d+$/.test(id) ? await watchVideo(input.net, id) : null;
|
|
167
|
+
const embed = await embedVideo(input.net, canonical, input.preferredWidth);
|
|
168
|
+
const watch = !embed && /^\d+$/.test(id) ? await watchVideo(input.net, id, input.preferredWidth) : null;
|
|
161
169
|
const url = embed ?? watch?.url ?? null;
|
|
162
170
|
if (!url) {
|
|
163
|
-
throw new
|
|
171
|
+
throw new PostfetchError(404, "Facebook video not found", "notFound");
|
|
164
172
|
}
|
|
165
173
|
const item = {
|
|
166
174
|
filename: filename(`facebook_${id}.mp4`),
|
|
@@ -173,6 +181,11 @@ async function resolveFacebook(input) {
|
|
|
173
181
|
};
|
|
174
182
|
return { archiveFilename: filename(`facebook_${id}.zip`), id, items: [item], metadata: watch?.metadata, platform: "facebook" };
|
|
175
183
|
}
|
|
184
|
+
function normalizedInputUrl(input) {
|
|
185
|
+
const url = asUrl(input);
|
|
186
|
+
const share = url.pathname.match(/^\/share\/([a-z])\/([A-Za-z0-9]+)/i);
|
|
187
|
+
return share ? `${url.origin}/share/${share[1]}/${share[2]}/` : input;
|
|
188
|
+
}
|
|
176
189
|
function navigationHeaders2() {
|
|
177
190
|
return {
|
|
178
191
|
"accept-language": "en-US,en;q=0.9",
|
|
@@ -185,25 +198,30 @@ async function canonicalUrl(net, input) {
|
|
|
185
198
|
const resolved = asUrl(response.url);
|
|
186
199
|
return `${resolved.origin}${resolved.pathname}`;
|
|
187
200
|
}
|
|
188
|
-
async function embedVideo(net, canonical) {
|
|
201
|
+
async function embedVideo(net, canonical, preferredWidth) {
|
|
189
202
|
const embed = `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(canonical)}`;
|
|
190
203
|
const response = await net(embed, { headers: navigationHeaders2() });
|
|
191
204
|
if (!response.ok) {
|
|
192
205
|
return null;
|
|
193
206
|
}
|
|
194
207
|
const html = await response.text();
|
|
195
|
-
return source(html, "hd_src")
|
|
208
|
+
return preferredSource(preferredWidth, source(html, "hd_src"), source(html, "sd_src"));
|
|
196
209
|
}
|
|
197
|
-
async function watchVideo(net, id) {
|
|
210
|
+
async function watchVideo(net, id, preferredWidth) {
|
|
198
211
|
const headers = { ...navigationHeaders2(), accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" };
|
|
199
212
|
const response = await net(`https://www.facebook.com/watch/?v=${id}&_rdr`, { headers });
|
|
200
213
|
if (!response.ok) {
|
|
201
214
|
return null;
|
|
202
215
|
}
|
|
203
216
|
const html = await response.text();
|
|
204
|
-
const
|
|
217
|
+
const hd = source(html, "playable_url_quality_hd") ?? source(html, "browser_native_hd_url");
|
|
218
|
+
const sd = source(html, "playable_url") ?? source(html, "browser_native_sd_url");
|
|
219
|
+
const url = preferredSource(preferredWidth, hd, sd);
|
|
205
220
|
return url ? { url, metadata: watchMetadata(html) } : null;
|
|
206
221
|
}
|
|
222
|
+
function preferredSource(preferredWidth, hd, sd) {
|
|
223
|
+
return preferredWidth >= 540 ? hd ?? sd : sd ?? hd;
|
|
224
|
+
}
|
|
207
225
|
function watchMetadata(html) {
|
|
208
226
|
const owner = html.match(/"name":("(?:\\.|[^"\\])*"),"__isVideoOwner"/);
|
|
209
227
|
const name = owner ? JSON.parse(owner[1]) : undefined;
|
|
@@ -271,7 +289,21 @@ function embedHeaders() {
|
|
|
271
289
|
}
|
|
272
290
|
async function resolveInstagram(input) {
|
|
273
291
|
const code = shortcode(input.url);
|
|
274
|
-
const
|
|
292
|
+
const videoRequired = /\/(?:reel|reels|tv)\//.test(asUrl(input.url).pathname);
|
|
293
|
+
const resolvers = [
|
|
294
|
+
() => pageMedia(input.net, code, input.preferredWidth),
|
|
295
|
+
() => mobileMedia(input.net, code, input.preferredWidth),
|
|
296
|
+
() => embedMedia(input.net, code),
|
|
297
|
+
() => graphqlMedia(input.net, code)
|
|
298
|
+
];
|
|
299
|
+
let media = null;
|
|
300
|
+
for (const resolve of resolvers) {
|
|
301
|
+
const candidate = await resolve();
|
|
302
|
+
if (candidate && (!videoRequired || mediaItems(candidate, code, input.preferredWidth).some((item) => item.kind === "video"))) {
|
|
303
|
+
media = candidate;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
275
307
|
if (!media) {
|
|
276
308
|
throw await instagramUnavailable(input.net, code);
|
|
277
309
|
}
|
|
@@ -481,23 +513,20 @@ async function graphqlMedia(net, code) {
|
|
|
481
513
|
}
|
|
482
514
|
const body = new URLSearchParams({
|
|
483
515
|
...params.body,
|
|
484
|
-
|
|
516
|
+
av: "0",
|
|
517
|
+
doc_id: "26130443479876713",
|
|
485
518
|
fb_api_caller_class: "RelayModern",
|
|
486
|
-
fb_api_req_friendly_name: "
|
|
519
|
+
fb_api_req_friendly_name: "PolarisPostRootQuery",
|
|
487
520
|
server_timestamps: "true",
|
|
488
|
-
variables: JSON.stringify({
|
|
489
|
-
fetch_tagged_user_count: null,
|
|
490
|
-
hoisted_comment_id: null,
|
|
491
|
-
hoisted_reply_id: null,
|
|
492
|
-
shortcode: code
|
|
493
|
-
})
|
|
521
|
+
variables: JSON.stringify({ shortcode: code })
|
|
494
522
|
});
|
|
495
523
|
const response = await net("https://www.instagram.com/graphql/query", {
|
|
496
524
|
body,
|
|
497
525
|
headers: {
|
|
498
526
|
...embedHeaders(),
|
|
499
527
|
...params.headers,
|
|
500
|
-
"X-FB-Friendly-Name": "
|
|
528
|
+
"X-FB-Friendly-Name": "PolarisPostRootQuery",
|
|
529
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
501
530
|
"content-type": "application/x-www-form-urlencoded"
|
|
502
531
|
},
|
|
503
532
|
method: "POST"
|
|
@@ -523,14 +552,14 @@ async function graphqlParams(net, code) {
|
|
|
523
552
|
const push = entryObject("InstagramWebPushInfo", html);
|
|
524
553
|
const lsd = entryObject("LSD", html)?.token ?? randomToken();
|
|
525
554
|
const csrf = entryObject("InstagramSecurityConfig", html)?.csrf_token;
|
|
526
|
-
const cookie =
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
polaris?.
|
|
530
|
-
"
|
|
531
|
-
|
|
532
|
-
"
|
|
533
|
-
|
|
555
|
+
const cookie = instagramCookies(response.headers, {
|
|
556
|
+
csrftoken: string(csrf),
|
|
557
|
+
dpr: "2",
|
|
558
|
+
ig_did: string(polaris?.device_id),
|
|
559
|
+
ig_nrcb: "1",
|
|
560
|
+
mid: string(polaris?.machine_id),
|
|
561
|
+
wd: "1280x720"
|
|
562
|
+
});
|
|
534
563
|
return {
|
|
535
564
|
headers: {
|
|
536
565
|
"X-CSRFToken": string(csrf) ?? "",
|
|
@@ -564,6 +593,12 @@ async function graphqlParams(net, code) {
|
|
|
564
593
|
};
|
|
565
594
|
}
|
|
566
595
|
function gqlShortcodeMedia(data) {
|
|
596
|
+
const webInfo = object(data.xdt_api__v1__media__shortcode__web_info) ? data.xdt_api__v1__media__shortcode__web_info : null;
|
|
597
|
+
const webItems = webInfo && Array.isArray(webInfo.items) ? webInfo.items : [];
|
|
598
|
+
const webMedia = webItems[0];
|
|
599
|
+
if (object(webMedia)) {
|
|
600
|
+
return webMedia;
|
|
601
|
+
}
|
|
567
602
|
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;
|
|
568
603
|
return object(media) ? media : null;
|
|
569
604
|
}
|
|
@@ -583,6 +618,24 @@ function mediaItems(media, code, preferredWidth) {
|
|
|
583
618
|
}
|
|
584
619
|
return instagramItem(media, code, null, preferredWidth);
|
|
585
620
|
}
|
|
621
|
+
function instagramCookies(headers, fallback) {
|
|
622
|
+
const getter = headers.getSetCookie;
|
|
623
|
+
const raw = typeof getter === "function" ? getter.call(headers) : headers.get("set-cookie")?.split(/,(?=[^;]+?=)/) ?? [];
|
|
624
|
+
const cookies = new Map;
|
|
625
|
+
for (const value of raw) {
|
|
626
|
+
const pair = value.split(";", 1)[0]?.trim();
|
|
627
|
+
const separator = pair?.indexOf("=") ?? -1;
|
|
628
|
+
if (pair && separator > 0) {
|
|
629
|
+
cookies.set(pair.slice(0, separator), pair.slice(separator + 1));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
for (const [name, value] of Object.entries(fallback)) {
|
|
633
|
+
if (value && !cookies.has(name)) {
|
|
634
|
+
cookies.set(name, value);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return [...cookies].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
638
|
+
}
|
|
586
639
|
function instagramItem(media, code, index, preferredWidth) {
|
|
587
640
|
const video = selectVersion(media.video_versions, preferredWidth) ?? string(media.video_url);
|
|
588
641
|
const suffix = index === null ? "" : `_${index}`;
|
|
@@ -646,6 +699,208 @@ function randomToken(length = 8) {
|
|
|
646
699
|
return crypto.getRandomValues(new Uint8Array(length)).reduce((value, byte) => value + (byte % 36).toString(36), "");
|
|
647
700
|
}
|
|
648
701
|
|
|
702
|
+
// src/linkedin.ts
|
|
703
|
+
async function resolveLinkedin(input) {
|
|
704
|
+
const reference = postReference(input.url);
|
|
705
|
+
const pageUrl = `https://www.linkedin.com/feed/update/urn:li:${reference.kind}:${reference.id}`;
|
|
706
|
+
const response = await input.net(pageUrl, { headers: navigationHeaders() });
|
|
707
|
+
if (response.status === 404) {
|
|
708
|
+
throw new PostfetchError(404, "LinkedIn post not found", "notFound");
|
|
709
|
+
}
|
|
710
|
+
if (response.status === 429 || response.status === 999) {
|
|
711
|
+
throw new PostfetchError(429, "LinkedIn rate limit reached", "rateLimited");
|
|
712
|
+
}
|
|
713
|
+
if (!response.ok) {
|
|
714
|
+
throw new PostfetchError(response.status, "LinkedIn post unavailable", "unavailable");
|
|
715
|
+
}
|
|
716
|
+
if (response.url.includes("/signup/") || response.url.includes("/login")) {
|
|
717
|
+
throw new PostfetchError(401, "LinkedIn login required", "loginRequired");
|
|
718
|
+
}
|
|
719
|
+
const html = await response.text();
|
|
720
|
+
const post = linkedinPost(html);
|
|
721
|
+
if (!post) {
|
|
722
|
+
throw new PostfetchError(404, "LinkedIn post unavailable", "unavailable");
|
|
723
|
+
}
|
|
724
|
+
const canonical = string(post["@id"]);
|
|
725
|
+
const canonicalReference = canonical ? postReferenceOrNull(canonical) : null;
|
|
726
|
+
const id = canonicalReference?.id ?? reference.id;
|
|
727
|
+
const items = linkedinItems(post, html, id, pageUrl);
|
|
728
|
+
if (items.length === 0) {
|
|
729
|
+
throw new Error("LinkedIn media url not found");
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
archiveFilename: filename(`linkedin_${id}.zip`),
|
|
733
|
+
id,
|
|
734
|
+
items,
|
|
735
|
+
metadata: linkedinMetadata(post),
|
|
736
|
+
platform: "linkedin"
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function linkedinMetadata(post) {
|
|
740
|
+
const author = linkedinAuthor(post);
|
|
741
|
+
return {
|
|
742
|
+
title: string(post.text) ?? string(post.name) ?? string(post.headline) ?? undefined,
|
|
743
|
+
text: string(post.description) ?? string(post.articleBody) ?? string(post.text) ?? undefined,
|
|
744
|
+
author: author ? {
|
|
745
|
+
handle: linkedinHandle(string(author.url)) ?? undefined,
|
|
746
|
+
name: string(author.name) ?? undefined
|
|
747
|
+
} : undefined,
|
|
748
|
+
createdAt: isoFromDateString(post.datePublished ?? post.uploadDate),
|
|
749
|
+
likeCount: interactionCount(post.interactionStatistic, "LikeAction"),
|
|
750
|
+
commentCount: count(post.commentCount) ?? interactionCount(post.interactionStatistic, "CommentAction")
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function postReference(input) {
|
|
754
|
+
const reference = postReferenceOrNull(input);
|
|
755
|
+
if (!reference) {
|
|
756
|
+
throw new PostfetchError(400, "LinkedIn post id not found");
|
|
757
|
+
}
|
|
758
|
+
return reference;
|
|
759
|
+
}
|
|
760
|
+
function postReferenceOrNull(input) {
|
|
761
|
+
let pathname;
|
|
762
|
+
try {
|
|
763
|
+
pathname = new URL(input).pathname.replace(/%3A/gi, ":");
|
|
764
|
+
} catch {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const urn = pathname.match(/\/(?:embed\/)?feed\/update\/urn:li:(activity|share|ugcpost):(\d+)\/?$/i);
|
|
768
|
+
const post = pathname.match(/\/posts\/[^/]*-(activity|share|ugcpost)-(\d+)(?:-[^/]*)?\/?$/i);
|
|
769
|
+
const match = urn ?? post;
|
|
770
|
+
if (!match) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
switch (match[1].toLowerCase()) {
|
|
774
|
+
case "activity":
|
|
775
|
+
return { id: match[2], kind: "activity" };
|
|
776
|
+
case "share":
|
|
777
|
+
return { id: match[2], kind: "share" };
|
|
778
|
+
case "ugcpost":
|
|
779
|
+
return { id: match[2], kind: "ugcPost" };
|
|
780
|
+
default:
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function linkedinAuthor(post) {
|
|
785
|
+
if (object(post.creator)) {
|
|
786
|
+
return post.creator;
|
|
787
|
+
}
|
|
788
|
+
if (object(post.author)) {
|
|
789
|
+
return post.author;
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
function linkedinPost(html) {
|
|
794
|
+
for (const match of html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)) {
|
|
795
|
+
if (!/\btype=["']application\/ld\+json["']/i.test(match[1])) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const parsed = parseJson(match[2]);
|
|
799
|
+
for (const entry of jsonLdEntries(parsed)) {
|
|
800
|
+
if (hasSchemaType(entry, "VideoObject") || hasSchemaType(entry, "SocialMediaPosting")) {
|
|
801
|
+
return entry;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
function parseJson(value) {
|
|
808
|
+
try {
|
|
809
|
+
return JSON.parse(value);
|
|
810
|
+
} catch {
|
|
811
|
+
try {
|
|
812
|
+
return JSON.parse(decodeHtml(value));
|
|
813
|
+
} catch {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function jsonLdEntries(value) {
|
|
819
|
+
if (Array.isArray(value)) {
|
|
820
|
+
return value.filter(object);
|
|
821
|
+
}
|
|
822
|
+
if (!object(value)) {
|
|
823
|
+
return [];
|
|
824
|
+
}
|
|
825
|
+
return Array.isArray(value["@graph"]) ? value["@graph"].filter(object) : [value];
|
|
826
|
+
}
|
|
827
|
+
function hasSchemaType(value, expected) {
|
|
828
|
+
const type = value["@type"];
|
|
829
|
+
return type === expected || Array.isArray(type) && type.includes(expected);
|
|
830
|
+
}
|
|
831
|
+
function linkedinItems(post, html, id, referer) {
|
|
832
|
+
const headers = { referer, "user-agent": browserUserAgent() };
|
|
833
|
+
if (hasSchemaType(post, "VideoObject")) {
|
|
834
|
+
const url2 = bestVideoUrl(html) ?? string(post.contentUrl);
|
|
835
|
+
return url2 ? [{ filename: filename(`linkedin_${id}.mp4`), headers, id, kind: "video", mime: "video/mp4", platform: "linkedin", url: url2 }] : [];
|
|
836
|
+
}
|
|
837
|
+
const url = imageUrl(post.image);
|
|
838
|
+
return url ? [{ filename: filename(`linkedin_${id}.jpg`), headers, id, kind: "image", mime: "image/jpeg", platform: "linkedin", url }] : [];
|
|
839
|
+
}
|
|
840
|
+
function bestVideoUrl(html) {
|
|
841
|
+
const encoded = html.match(/<video\b[^>]*\bdata-sources=(["'])(.*?)\1[^>]*>/is)?.[2];
|
|
842
|
+
if (!encoded) {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
let parsed;
|
|
846
|
+
try {
|
|
847
|
+
parsed = JSON.parse(decodeHtml(encoded));
|
|
848
|
+
} catch {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const sources = Array.isArray(parsed) ? parsed.filter(object) : [];
|
|
852
|
+
const videos = sources.filter((source2) => string(source2.type) === "video/mp4" && string(source2.src));
|
|
853
|
+
const selected = videos.reduce((best, candidate) => {
|
|
854
|
+
const bitrate = count(candidate["data-bitrate"]) ?? 0;
|
|
855
|
+
const bestBitrate = best ? count(best["data-bitrate"]) ?? 0 : -1;
|
|
856
|
+
return bitrate > bestBitrate ? candidate : best;
|
|
857
|
+
}, null);
|
|
858
|
+
return selected ? string(selected.src) : null;
|
|
859
|
+
}
|
|
860
|
+
function imageUrl(value) {
|
|
861
|
+
if (typeof value === "string") {
|
|
862
|
+
return string(value);
|
|
863
|
+
}
|
|
864
|
+
if (Array.isArray(value)) {
|
|
865
|
+
return value.map(imageUrl).find((url) => Boolean(url)) ?? null;
|
|
866
|
+
}
|
|
867
|
+
return object(value) ? string(value.url) : null;
|
|
868
|
+
}
|
|
869
|
+
function interactionCount(value, action) {
|
|
870
|
+
const statistics = Array.isArray(value) ? value.filter(object) : object(value) ? [value] : [];
|
|
871
|
+
const statistic = statistics.find((entry) => string(entry.interactionType)?.endsWith(`/${action}`));
|
|
872
|
+
return statistic ? count(statistic.userInteractionCount) : undefined;
|
|
873
|
+
}
|
|
874
|
+
function linkedinHandle(value) {
|
|
875
|
+
if (!value) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
return new URL(value).pathname.match(/^\/(?:company|in|showcase)\/([^/]+)/)?.[1] ?? null;
|
|
880
|
+
} catch {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function decodeHtml(value) {
|
|
885
|
+
return value.replace(/&(quot|amp|lt|gt|#39|#x27|#(\d+)|#x([0-9a-f]+));/gi, (_entity, name, decimal, hex) => {
|
|
886
|
+
switch (name.toLowerCase()) {
|
|
887
|
+
case "quot":
|
|
888
|
+
return '"';
|
|
889
|
+
case "amp":
|
|
890
|
+
return "&";
|
|
891
|
+
case "lt":
|
|
892
|
+
return "<";
|
|
893
|
+
case "gt":
|
|
894
|
+
return ">";
|
|
895
|
+
case "#39":
|
|
896
|
+
case "#x27":
|
|
897
|
+
return "'";
|
|
898
|
+
default:
|
|
899
|
+
return String.fromCodePoint(decimal ? Number.parseInt(decimal, 10) : Number.parseInt(hex, 16));
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
649
904
|
// src/isobmff.ts
|
|
650
905
|
function parseBoxes(data, start = 0, end = data.length) {
|
|
651
906
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
@@ -1407,23 +1662,18 @@ function trackId(track) {
|
|
|
1407
1662
|
|
|
1408
1663
|
// src/tiktok.ts
|
|
1409
1664
|
var marker = '<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">';
|
|
1665
|
+
var pageAttemptCount = 12;
|
|
1410
1666
|
async function resolveTiktok(input) {
|
|
1411
|
-
const
|
|
1412
|
-
const pageUrl = await followShortlink(input.net, userAgent, input.url);
|
|
1667
|
+
const pageUrl = await followShortlink(input.net, browserUserAgent(), input.url);
|
|
1413
1668
|
const id = videoId(pageUrl);
|
|
1414
1669
|
if (!id) {
|
|
1415
1670
|
throw new Error("TikTok video id not found");
|
|
1416
1671
|
}
|
|
1417
|
-
const page = await
|
|
1418
|
-
if (!recoverablePageError(error)) {
|
|
1419
|
-
throw error;
|
|
1420
|
-
}
|
|
1421
|
-
return fetchVideoPage(input.net, id, firefoxUserAgent());
|
|
1422
|
-
});
|
|
1672
|
+
const page = await videoPage(input.net, id, asUrl(pageUrl).pathname.includes("/photo/"));
|
|
1423
1673
|
const user = author(page.item) ?? username(pageUrl) ?? "i";
|
|
1424
1674
|
const headers = {
|
|
1425
1675
|
referer: `https://www.tiktok.com/@${encodeURIComponent(user)}/video/${encodeURIComponent(id)}`,
|
|
1426
|
-
"user-agent": userAgent
|
|
1676
|
+
"user-agent": page.userAgent
|
|
1427
1677
|
};
|
|
1428
1678
|
if (page.cookie) {
|
|
1429
1679
|
headers.cookie = page.cookie;
|
|
@@ -1434,6 +1684,35 @@ async function resolveTiktok(input) {
|
|
|
1434
1684
|
}
|
|
1435
1685
|
return { archiveFilename: filename(`tiktok_${user}_${id}.zip`), id, items, metadata: tiktokMetadata(page.item), platform: "tiktok" };
|
|
1436
1686
|
}
|
|
1687
|
+
async function videoPage(net, id, photo) {
|
|
1688
|
+
for (let attempt = 0;attempt < pageAttemptCount; attempt += 1) {
|
|
1689
|
+
const baseHeaders2 = attempt % 2 === 0 ? navigationHeaders() : firefoxNavigationHeaders();
|
|
1690
|
+
const userAgent2 = baseHeaders2["user-agent"];
|
|
1691
|
+
const headers2 = {
|
|
1692
|
+
...baseHeaders2,
|
|
1693
|
+
referer: "https://www.tiktok.com/",
|
|
1694
|
+
"sec-fetch-site": "same-origin"
|
|
1695
|
+
};
|
|
1696
|
+
try {
|
|
1697
|
+
return { ...await fetchVideoPage(net, id, headers2), userAgent: userAgent2 };
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
if (!recoverablePageError(error)) {
|
|
1700
|
+
throw error;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
if (!photo) {
|
|
1705
|
+
throw new Error("TikTok hydration not found");
|
|
1706
|
+
}
|
|
1707
|
+
const baseHeaders = navigationHeaders();
|
|
1708
|
+
const userAgent = baseHeaders["user-agent"];
|
|
1709
|
+
const headers = {
|
|
1710
|
+
...baseHeaders,
|
|
1711
|
+
referer: "https://www.tiktok.com/",
|
|
1712
|
+
"sec-fetch-site": "same-origin"
|
|
1713
|
+
};
|
|
1714
|
+
return { ...await fetchEmbedPage(net, id, headers), userAgent };
|
|
1715
|
+
}
|
|
1437
1716
|
function tiktokMetadata(item) {
|
|
1438
1717
|
const user = object(item.author) ? item.author : null;
|
|
1439
1718
|
const stats = object(item.stats) ? item.stats : null;
|
|
@@ -1460,16 +1739,18 @@ function tiktokMetadata(item) {
|
|
|
1460
1739
|
}
|
|
1461
1740
|
};
|
|
1462
1741
|
}
|
|
1463
|
-
async function fetchVideoPage(net, id,
|
|
1742
|
+
async function fetchVideoPage(net, id, headers) {
|
|
1464
1743
|
const page = await net(`https://www.tiktok.com/@i/video/${id}`, {
|
|
1465
|
-
headers
|
|
1466
|
-
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
1467
|
-
referer: "https://www.tiktok.com/",
|
|
1468
|
-
"user-agent": userAgent
|
|
1469
|
-
}
|
|
1744
|
+
headers
|
|
1470
1745
|
});
|
|
1471
1746
|
return { cookie: cookieHeader(page.headers), item: itemStruct(await page.text()) };
|
|
1472
1747
|
}
|
|
1748
|
+
async function fetchEmbedPage(net, id, headers) {
|
|
1749
|
+
const page = await net(`https://www.tiktok.com/embed/v2/${id}`, {
|
|
1750
|
+
headers
|
|
1751
|
+
});
|
|
1752
|
+
return { cookie: cookieHeader(page.headers), item: embedItemStruct(await page.text(), id) };
|
|
1753
|
+
}
|
|
1473
1754
|
function isShortlinkHost(hostname) {
|
|
1474
1755
|
return hostname === "vm.tiktok.com" || hostname === "vt.tiktok.com";
|
|
1475
1756
|
}
|
|
@@ -1509,10 +1790,68 @@ function itemStruct(html) {
|
|
|
1509
1790
|
}
|
|
1510
1791
|
return item;
|
|
1511
1792
|
}
|
|
1793
|
+
function embedItemStruct(html, id) {
|
|
1794
|
+
const raw = html.match(/<script\b[^>]*\bid=["']__FRONTITY_CONNECT_STATE__["'][^>]*>([\s\S]*?)<\/script>/i)?.[1];
|
|
1795
|
+
if (!raw) {
|
|
1796
|
+
throw new Error("TikTok embed state not found");
|
|
1797
|
+
}
|
|
1798
|
+
const parsed = JSON.parse(raw);
|
|
1799
|
+
const source2 = object(parsed) && object(parsed.source) ? parsed.source : null;
|
|
1800
|
+
const data = source2 && object(source2.data) ? source2.data : null;
|
|
1801
|
+
const rawRoute = data ? data[`/embed/v2/${id}`] : null;
|
|
1802
|
+
const route = object(rawRoute) ? rawRoute : null;
|
|
1803
|
+
const videoData = route && object(route.videoData) ? route.videoData : null;
|
|
1804
|
+
const item = videoData && object(videoData.itemInfos) ? videoData.itemInfos : null;
|
|
1805
|
+
if (!videoData || !item) {
|
|
1806
|
+
throw new Error("TikTok embed item not found");
|
|
1807
|
+
}
|
|
1808
|
+
const authorInfo = object(videoData.authorInfos) ? videoData.authorInfos : {};
|
|
1809
|
+
const musicInfo = object(videoData.musicInfos) ? videoData.musicInfos : {};
|
|
1810
|
+
const video = object(item.video) ? item.video : {};
|
|
1811
|
+
const videoMeta = object(video.videoMeta) ? video.videoMeta : {};
|
|
1812
|
+
const imagePost = object(videoData.imagePostInfo) ? videoData.imagePostInfo : null;
|
|
1813
|
+
const displayImages = imagePost && Array.isArray(imagePost.displayImages) ? imagePost.displayImages.filter(object) : [];
|
|
1814
|
+
if (displayImages.length === 0) {
|
|
1815
|
+
throw new Error("TikTok video hydration not found");
|
|
1816
|
+
}
|
|
1817
|
+
return {
|
|
1818
|
+
...item,
|
|
1819
|
+
author: {
|
|
1820
|
+
nickname: string(authorInfo.nickName),
|
|
1821
|
+
uniqueId: string(authorInfo.uniqueId),
|
|
1822
|
+
verified: bool(authorInfo.verified)
|
|
1823
|
+
},
|
|
1824
|
+
desc: string(item.text),
|
|
1825
|
+
imagePost: {
|
|
1826
|
+
images: displayImages.map((image) => ({
|
|
1827
|
+
imageURL: {
|
|
1828
|
+
urlList: Array.isArray(image.urlList) ? image.urlList.map(string).filter((url) => Boolean(url)) : []
|
|
1829
|
+
}
|
|
1830
|
+
}))
|
|
1831
|
+
},
|
|
1832
|
+
music: {
|
|
1833
|
+
authorName: string(musicInfo.authorName),
|
|
1834
|
+
playUrl: firstString(musicInfo.playUrl),
|
|
1835
|
+
title: string(musicInfo.musicName)
|
|
1836
|
+
},
|
|
1837
|
+
stats: {
|
|
1838
|
+
commentCount: item.commentCount,
|
|
1839
|
+
diggCount: item.diggCount,
|
|
1840
|
+
playCount: item.playCount,
|
|
1841
|
+
shareCount: item.shareCount
|
|
1842
|
+
},
|
|
1843
|
+
video: {
|
|
1844
|
+
duration: videoMeta.duration
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1512
1848
|
function recoverablePageError(error) {
|
|
1513
1849
|
const message = error instanceof Error ? error.message : "";
|
|
1514
1850
|
return message === "TikTok WAF challenge" || message === "TikTok hydration not found";
|
|
1515
1851
|
}
|
|
1852
|
+
function firstString(value) {
|
|
1853
|
+
return Array.isArray(value) ? value.map(string).find((candidate) => candidate !== null) ?? null : string(value);
|
|
1854
|
+
}
|
|
1516
1855
|
function downloadUrl(item) {
|
|
1517
1856
|
const video = object(item.video) ? item.video : null;
|
|
1518
1857
|
return video ? string(video.playAddr) ?? string(video.downloadAddr) : null;
|
|
@@ -1538,8 +1877,8 @@ function imageItems3(item, user, id, headers) {
|
|
|
1538
1877
|
const imagePost = object(item.imagePost) ? item.imagePost : null;
|
|
1539
1878
|
const images = imagePost && Array.isArray(imagePost.images) ? imagePost.images.filter(object) : [];
|
|
1540
1879
|
return images.flatMap((image, index) => {
|
|
1541
|
-
const
|
|
1542
|
-
const list =
|
|
1880
|
+
const imageUrl2 = object(image.imageURL) ? image.imageURL : null;
|
|
1881
|
+
const list = imageUrl2 && Array.isArray(imageUrl2.urlList) ? imageUrl2.urlList : [];
|
|
1543
1882
|
const url = list.map(string).find((candidate) => Boolean(candidate)) ?? null;
|
|
1544
1883
|
return url ? [{
|
|
1545
1884
|
filename: filename(`tiktok_${user}_${id}_${index + 1}.jpg`),
|
|
@@ -1596,8 +1935,10 @@ async function resolveTwitter(input) {
|
|
|
1596
1935
|
if (string(tweet.__typename) === "TweetTombstone") {
|
|
1597
1936
|
throw new Error("Tweet is unavailable or age-restricted");
|
|
1598
1937
|
}
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1938
|
+
const items = twitterTweets(tweet, id).flatMap(({ id: postId2, tweet: post }) => {
|
|
1939
|
+
const media = Array.isArray(post.mediaDetails) ? post.mediaDetails.filter(object) : [];
|
|
1940
|
+
return media.flatMap((entry, index) => twitterItem(entry, postId2, index + 1));
|
|
1941
|
+
});
|
|
1601
1942
|
if (items.length === 0) {
|
|
1602
1943
|
throw new Error("Twitter media not found");
|
|
1603
1944
|
}
|
|
@@ -1605,6 +1946,16 @@ async function resolveTwitter(input) {
|
|
|
1605
1946
|
}
|
|
1606
1947
|
function twitterMetadata(tweet) {
|
|
1607
1948
|
const user = object(tweet.user) ? tweet.user : null;
|
|
1949
|
+
const extra = {};
|
|
1950
|
+
const lang = string(tweet.lang);
|
|
1951
|
+
if (lang) {
|
|
1952
|
+
extra.lang = lang;
|
|
1953
|
+
}
|
|
1954
|
+
const quotedTweet = object(tweet.quoted_tweet) ? tweet.quoted_tweet : null;
|
|
1955
|
+
const quotedId = quotedTweet ? string(quotedTweet.id_str) : null;
|
|
1956
|
+
if (quotedTweet && quotedId) {
|
|
1957
|
+
extra.quotedTweet = { id: quotedId, metadata: twitterMetadata(quotedTweet) };
|
|
1958
|
+
}
|
|
1608
1959
|
return {
|
|
1609
1960
|
text: string(tweet.text) ?? string(tweet.full_text) ?? undefined,
|
|
1610
1961
|
author: user ? {
|
|
@@ -1618,9 +1969,26 @@ function twitterMetadata(tweet) {
|
|
|
1618
1969
|
shareCount: count(tweet.retweet_count),
|
|
1619
1970
|
viewCount: count(tweet.view_count),
|
|
1620
1971
|
nsfw: bool(tweet.possibly_sensitive),
|
|
1621
|
-
extra
|
|
1972
|
+
extra
|
|
1622
1973
|
};
|
|
1623
1974
|
}
|
|
1975
|
+
function twitterTweets(tweet, rootId) {
|
|
1976
|
+
const tweets = [];
|
|
1977
|
+
const seen = new Set;
|
|
1978
|
+
let current = tweet;
|
|
1979
|
+
let fallbackId = rootId;
|
|
1980
|
+
while (current) {
|
|
1981
|
+
const id = string(current.id_str) ?? fallbackId;
|
|
1982
|
+
if (!id || seen.has(id)) {
|
|
1983
|
+
break;
|
|
1984
|
+
}
|
|
1985
|
+
tweets.push({ id, tweet: current });
|
|
1986
|
+
seen.add(id);
|
|
1987
|
+
current = object(current.quoted_tweet) ? current.quoted_tweet : null;
|
|
1988
|
+
fallbackId = null;
|
|
1989
|
+
}
|
|
1990
|
+
return tweets;
|
|
1991
|
+
}
|
|
1624
1992
|
async function syndication(net, id) {
|
|
1625
1993
|
const url = `https://cdn.syndication.twimg.com/tweet-result?id=${id}&lang=en&token=${syndicationToken(id)}`;
|
|
1626
1994
|
const response = await net(url, { headers: { accept: "application/json", "user-agent": browserUserAgent() } });
|
|
@@ -1886,16 +2254,25 @@ async function postfetch(url, options = {}) {
|
|
|
1886
2254
|
if (trimmed.length === 0) {
|
|
1887
2255
|
throw new PostfetchError(400, "url is required");
|
|
1888
2256
|
}
|
|
2257
|
+
if (options.tryMaxBytes !== undefined && (!Number.isSafeInteger(options.tryMaxBytes) || options.tryMaxBytes <= 0)) {
|
|
2258
|
+
throw new PostfetchError(400, "tryMaxBytes must be a positive integer");
|
|
2259
|
+
}
|
|
1889
2260
|
const context = {
|
|
1890
2261
|
net: createNet(options.fetch ?? globalThis.fetch),
|
|
1891
2262
|
preferredWidth: options.preferredWidth ?? 720,
|
|
1892
2263
|
url: trimmed
|
|
1893
2264
|
};
|
|
1894
|
-
|
|
2265
|
+
const result = await resolve(context);
|
|
2266
|
+
return options.tryMaxBytes === undefined ? result : trySmaller(context, result, options.tryMaxBytes);
|
|
2267
|
+
}
|
|
2268
|
+
async function resolve(context) {
|
|
2269
|
+
switch (detect(context.url)) {
|
|
1895
2270
|
case "facebook":
|
|
1896
2271
|
return resolveFacebook(context);
|
|
1897
2272
|
case "instagram":
|
|
1898
2273
|
return resolveInstagram(context);
|
|
2274
|
+
case "linkedin":
|
|
2275
|
+
return resolveLinkedin(context);
|
|
1899
2276
|
case "pinterest":
|
|
1900
2277
|
return resolvePinterest(context);
|
|
1901
2278
|
case "reddit":
|
|
@@ -1910,6 +2287,58 @@ async function postfetch(url, options = {}) {
|
|
|
1910
2287
|
return resolveYoutube(context);
|
|
1911
2288
|
}
|
|
1912
2289
|
}
|
|
2290
|
+
async function trySmaller(context, result, maxBytes) {
|
|
2291
|
+
const originalBytes = await resultBytes(context, result);
|
|
2292
|
+
if (originalBytes === null || originalBytes <= maxBytes) {
|
|
2293
|
+
return result;
|
|
2294
|
+
}
|
|
2295
|
+
try {
|
|
2296
|
+
const smaller = await resolve({ ...context, preferredWidth: 1 });
|
|
2297
|
+
if (sameMedia(result, smaller)) {
|
|
2298
|
+
return result;
|
|
2299
|
+
}
|
|
2300
|
+
const smallerBytes = await resultBytes(context, smaller);
|
|
2301
|
+
return smallerBytes !== null && smallerBytes < originalBytes ? smaller : result;
|
|
2302
|
+
} catch {
|
|
2303
|
+
return result;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
async function resultBytes(context, result) {
|
|
2307
|
+
const sizes = await Promise.all(result.items.map((item) => itemBytes(context, item)));
|
|
2308
|
+
if (sizes.some((size) => size === null)) {
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
2311
|
+
return sizes.reduce((total, size) => total + (size ?? 0), 0);
|
|
2312
|
+
}
|
|
2313
|
+
async function itemBytes(context, item) {
|
|
2314
|
+
if (item.hls) {
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
const [video, audio] = await Promise.all([
|
|
2318
|
+
contentLength(context, item.url, item.headers),
|
|
2319
|
+
item.audio ? contentLength(context, item.audio.url, item.audio.headers) : Promise.resolve(0)
|
|
2320
|
+
]);
|
|
2321
|
+
return video === null || audio === null ? null : video + audio;
|
|
2322
|
+
}
|
|
2323
|
+
async function contentLength(context, url, headers) {
|
|
2324
|
+
try {
|
|
2325
|
+
const response = await context.net(url, { headers, method: "HEAD" }, 1);
|
|
2326
|
+
const raw = response.ok ? response.headers.get("content-length") : null;
|
|
2327
|
+
if (!raw || !/^\d+$/.test(raw)) {
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
const bytes = Number(raw);
|
|
2331
|
+
return Number.isSafeInteger(bytes) ? bytes : null;
|
|
2332
|
+
} catch {
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
function sameMedia(left, right) {
|
|
2337
|
+
return left.items.length === right.items.length && left.items.every((item, index) => {
|
|
2338
|
+
const other = right.items[index];
|
|
2339
|
+
return item.url === other?.url && item.audio?.url === other.audio?.url;
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
1913
2342
|
function detect(url) {
|
|
1914
2343
|
const host = asUrl(url).hostname;
|
|
1915
2344
|
if (host.includes("tiktok.com")) {
|
|
@@ -1918,6 +2347,9 @@ function detect(url) {
|
|
|
1918
2347
|
if (host.includes("instagram.com")) {
|
|
1919
2348
|
return "instagram";
|
|
1920
2349
|
}
|
|
2350
|
+
if (host === "linkedin.com" || host.endsWith(".linkedin.com")) {
|
|
2351
|
+
return "linkedin";
|
|
2352
|
+
}
|
|
1921
2353
|
if (host.includes("pinterest.") || host === "pin.it") {
|
|
1922
2354
|
return "pinterest";
|
|
1923
2355
|
}
|
|
@@ -1936,7 +2368,7 @@ function detect(url) {
|
|
|
1936
2368
|
if (host === "x.com" || host.endsWith(".x.com") || host.includes("twitter.com")) {
|
|
1937
2369
|
return "twitter";
|
|
1938
2370
|
}
|
|
1939
|
-
throw new PostfetchError(400, "only Facebook, Instagram, Pinterest, Reddit, SoundCloud, TikTok, X and YouTube URLs are supported");
|
|
2371
|
+
throw new PostfetchError(400, "only Facebook, Instagram, LinkedIn, Pinterest, Reddit, SoundCloud, TikTok, X and YouTube URLs are supported");
|
|
1940
2372
|
}
|
|
1941
2373
|
// src/remux.ts
|
|
1942
2374
|
function mergeAudioVideo(video, audio) {
|
|
@@ -2164,7 +2596,7 @@ function buffered(item) {
|
|
|
2164
2596
|
async function download(item, options = {}) {
|
|
2165
2597
|
const net = createNet(options.fetch ?? globalThis.fetch);
|
|
2166
2598
|
if (buffered(item)) {
|
|
2167
|
-
const bytes = await
|
|
2599
|
+
const bytes = await itemBytes2(net, item);
|
|
2168
2600
|
return new Response(toArrayBuffer(bytes), { headers: { "content-length": String(bytes.length), "content-type": item.mime } });
|
|
2169
2601
|
}
|
|
2170
2602
|
const response = await net(item.url, { headers: item.headers });
|
|
@@ -2173,9 +2605,17 @@ async function download(item, options = {}) {
|
|
|
2173
2605
|
}
|
|
2174
2606
|
return response;
|
|
2175
2607
|
}
|
|
2608
|
+
async function downloadBlob(url, headers = {}, options = {}) {
|
|
2609
|
+
const net = createNet(options.fetch ?? globalThis.fetch);
|
|
2610
|
+
const response = await net(url, { headers });
|
|
2611
|
+
if (!response.ok || !response.body) {
|
|
2612
|
+
throw new PostfetchError(502, `download failed: ${response.status}`);
|
|
2613
|
+
}
|
|
2614
|
+
return response.blob();
|
|
2615
|
+
}
|
|
2176
2616
|
async function archive(result, options = {}) {
|
|
2177
2617
|
const net = createNet(options.fetch ?? globalThis.fetch);
|
|
2178
|
-
const files = await Promise.all(result.items.map(async (item) => ({ data: await
|
|
2618
|
+
const files = await Promise.all(result.items.map(async (item) => ({ data: await itemBytes2(net, item), name: item.filename })));
|
|
2179
2619
|
return { bytes: zip(files), filename: result.archiveFilename, mime: "application/zip" };
|
|
2180
2620
|
}
|
|
2181
2621
|
async function toResponse(result, options = {}) {
|
|
@@ -2187,7 +2627,7 @@ async function toResponse(result, options = {}) {
|
|
|
2187
2627
|
async function singleResponse(item, options) {
|
|
2188
2628
|
if (buffered(item)) {
|
|
2189
2629
|
const net = createNet(options.fetch ?? globalThis.fetch);
|
|
2190
|
-
const bytes = await
|
|
2630
|
+
const bytes = await itemBytes2(net, item);
|
|
2191
2631
|
return new Response(toArrayBuffer(bytes), {
|
|
2192
2632
|
headers: {
|
|
2193
2633
|
"content-disposition": `attachment; filename="${item.filename}"`,
|
|
@@ -2228,7 +2668,7 @@ async function archiveResponse(result, options) {
|
|
|
2228
2668
|
}
|
|
2229
2669
|
});
|
|
2230
2670
|
}
|
|
2231
|
-
async function
|
|
2671
|
+
async function itemBytes2(net, item) {
|
|
2232
2672
|
if (item.hls) {
|
|
2233
2673
|
const video = await assembleHls(net, item.url, item.headers);
|
|
2234
2674
|
if (item.audio) {
|
|
@@ -2261,6 +2701,7 @@ function toArrayBuffer(bytes) {
|
|
|
2261
2701
|
export {
|
|
2262
2702
|
toResponse,
|
|
2263
2703
|
postfetch,
|
|
2704
|
+
downloadBlob,
|
|
2264
2705
|
download,
|
|
2265
2706
|
detect,
|
|
2266
2707
|
archive,
|