@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/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 navigationHeaders() {
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": fingerprint.acceptLanguage,
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": fingerprint.userAgent
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 Error("Facebook video not found");
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") ?? source(html, "sd_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 url = source(html, "playable_url_quality_hd") ?? source(html, "browser_native_hd_url") ?? source(html, "playable_url") ?? source(html, "browser_native_sd_url");
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 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);
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
- doc_id: "8845758582119845",
516
+ av: "0",
517
+ doc_id: "26130443479876713",
485
518
  fb_api_caller_class: "RelayModern",
486
- fb_api_req_friendly_name: "PolarisPostActionLoadPostQueryQuery",
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": "PolarisPostActionLoadPostQueryQuery",
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
- csrf && `csrftoken=${csrf}`,
528
- polaris?.device_id && `ig_did=${polaris.device_id}`,
529
- polaris?.machine_id && `mid=${polaris.machine_id}`,
530
- "wd=1280x720",
531
- "dpr=2",
532
- "ig_nrcb=1"
533
- ].filter((value) => typeof value === "string" && value.length > 0).join("; ");
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 userAgent = browserUserAgent();
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 fetchVideoPage(input.net, id, userAgent).catch((error) => {
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, userAgent) {
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 imageUrl = object(image.imageURL) ? image.imageURL : null;
1542
- const list = imageUrl && Array.isArray(imageUrl.urlList) ? imageUrl.urlList : [];
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 media = Array.isArray(tweet.mediaDetails) ? tweet.mediaDetails.filter(object) : [];
1600
- const items = media.flatMap((entry, index) => twitterItem(entry, id, index + 1));
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: { lang: string(tweet.lang) ?? undefined }
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
- switch (detect(trimmed)) {
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 itemBytes(net, item);
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 itemBytes(net, item), name: item.filename })));
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 itemBytes(net, item);
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 itemBytes(net, item) {
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,