@postfetch/core 0.12.1 → 0.12.2

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
@@ -365,8 +365,9 @@ function embedHeaders() {
365
365
  async function resolveInstagram(input) {
366
366
  const code = shortcode(input.url);
367
367
  const videoRequired = /\/(?:reel|reels|tv)\//.test(asUrl(input.url).pathname);
368
+ const page = await pageMedia(input.net, code, input.preferredWidth);
368
369
  const resolvers = [
369
- () => pageMedia(input.net, code, input.preferredWidth),
370
+ async () => page.media,
370
371
  () => mobileMedia(input.net, code, input.preferredWidth),
371
372
  () => embedMedia(input.net, code),
372
373
  () => graphqlMedia(input.net, code)
@@ -380,7 +381,7 @@ async function resolveInstagram(input) {
380
381
  }
381
382
  }
382
383
  if (!media) {
383
- throw await instagramUnavailable(input.net, code);
384
+ throw await instagramUnavailable(input.net, code, page.reason);
384
385
  }
385
386
  const items = mediaItems(media, code, input.preferredWidth);
386
387
  if (items.length === 0) {
@@ -388,12 +389,12 @@ async function resolveInstagram(input) {
388
389
  }
389
390
  return { archiveFilename: filename(`instagram_${code}.zip`), comments: [], id: code, items, metadata: instagramMetadata(media), platform: "instagram" };
390
391
  }
391
- async function instagramUnavailable(net, code) {
392
+ async function instagramUnavailable(net, code, pageReason) {
392
393
  const url = new URL("https://i.instagram.com/api/v1/oembed/");
393
394
  url.searchParams.set("url", `https://www.instagram.com/p/${code}/`);
394
395
  const response = await net(url.href, { headers: mobileHeaders() }, 1).catch(() => null);
395
396
  const body = response ? await response.text().catch(() => "") : "";
396
- const reason = instagramUnavailableReason(response?.status ?? 0, body);
397
+ const reason = pageReason ?? instagramUnavailableReason(response?.status ?? 0, body);
397
398
  const probe = response ? `oembed ${response.status}` : "oembed unreachable";
398
399
  return new PostfetchError(reasonStatus(reason), `Instagram post unavailable: ${reason} (${probe})`, reason);
399
400
  }
@@ -407,7 +408,7 @@ function instagramUnavailableReason(status, body) {
407
408
  if (status !== 0 && status < 400) {
408
409
  return "unavailable";
409
410
  }
410
- if (/MIN_AGE_ACCOUNT|under 18/i.test(body)) {
411
+ if (/MIN_AGE_ACCOUNT|under 18|age[- ]restricted/i.test(body)) {
411
412
  return "ageRestricted";
412
413
  }
413
414
  if (/private account|"is_private":\s*true|account is private/i.test(body)) {
@@ -480,11 +481,15 @@ function shortcode(input) {
480
481
  async function pageMedia(net, code, preferredWidth) {
481
482
  const response = await net(`https://www.instagram.com/p/${code}/`, { headers: navigationHeaders() });
482
483
  if (!response.ok) {
483
- return null;
484
+ return { media: null };
484
485
  }
485
486
  const html = await response.text();
486
487
  const media = inlineMedia(html, code);
487
- return media && mediaItems(media, code, preferredWidth).length > 0 ? media : null;
488
+ const reason = media && object(media.gating_ruling) ? instagramUnavailableReason(400, JSON.stringify(media.gating_ruling)) : "unavailable";
489
+ return {
490
+ media: media && mediaItems(media, code, preferredWidth).length > 0 ? media : null,
491
+ reason: reason === "unavailable" ? undefined : reason
492
+ };
488
493
  }
489
494
  function inlineMedia(html, code) {
490
495
  for (const match of html.matchAll(/<script type="application\/json"[^>]*>(.*?)<\/script>/gs)) {
@@ -515,7 +520,7 @@ function searchMedia(node, code) {
515
520
  return null;
516
521
  }
517
522
  const hasMedia = Array.isArray(node.video_versions) || Array.isArray(node.carousel_media) || object(node.image_versions2) && Array.isArray(node.image_versions2.candidates);
518
- if (hasMedia && node.code === code) {
523
+ if ((hasMedia || object(node.gating_ruling)) && node.code === code) {
519
524
  return node;
520
525
  }
521
526
  for (const key in node) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postfetch/core",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Zero-dependency typed core that turns Facebook, Instagram, LinkedIn, Pinterest, Reddit, SoundCloud, TikTok, X and YouTube post URLs into media files.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/instagram.ts CHANGED
@@ -59,8 +59,9 @@ function embedHeaders(): Record<string, string> {
59
59
  export async function resolveInstagram(input: ResolveContext): Promise<PostfetchResult> {
60
60
  const code = shortcode(input.url);
61
61
  const videoRequired = /\/(?:reel|reels|tv)\//.test(asUrl(input.url).pathname);
62
+ const page = await pageMedia(input.net, code, input.preferredWidth);
62
63
  const resolvers: Array<() => Promise<Json | null>> = [
63
- () => pageMedia(input.net, code, input.preferredWidth),
64
+ async () => page.media,
64
65
  () => mobileMedia(input.net, code, input.preferredWidth),
65
66
  () => embedMedia(input.net, code),
66
67
  () => graphqlMedia(input.net, code),
@@ -74,7 +75,7 @@ export async function resolveInstagram(input: ResolveContext): Promise<Postfetch
74
75
  }
75
76
  }
76
77
  if (!media) {
77
- throw await instagramUnavailable(input.net, code);
78
+ throw await instagramUnavailable(input.net, code, page.reason);
78
79
  }
79
80
  const items = mediaItems(media, code, input.preferredWidth);
80
81
  if (items.length === 0) {
@@ -83,12 +84,14 @@ export async function resolveInstagram(input: ResolveContext): Promise<Postfetch
83
84
  return { archiveFilename: filename(`instagram_${code}.zip`), comments: [], id: code, items, metadata: instagramMetadata(media), platform: "instagram" };
84
85
  }
85
86
 
86
- async function instagramUnavailable(net: Net, code: string): Promise<PostfetchError> {
87
+ async function instagramUnavailable(net: Net, code: string, pageReason?: PostfetchReason): Promise<PostfetchError> {
87
88
  const url = new URL("https://i.instagram.com/api/v1/oembed/");
88
89
  url.searchParams.set("url", `https://www.instagram.com/p/${code}/`);
89
90
  const response = await net(url.href, { headers: mobileHeaders() }, 1).catch(() => null);
90
91
  const body = response ? await response.text().catch(() => "") : "";
91
- const reason = instagramUnavailableReason(response?.status ?? 0, body);
92
+ // The page can identify an age gate even when oembed only reports a generic
93
+ // geoblock_required error with no MIN_AGE_ACCOUNT marker.
94
+ const reason = pageReason ?? instagramUnavailableReason(response?.status ?? 0, body);
92
95
  // Carry the oembed status so an unclassified failure still leaves a raw clue
93
96
  // (200 = post is public but no media could be parsed; 429 = throttled; …).
94
97
  const probe = response ? `oembed ${response.status}` : "oembed unreachable";
@@ -109,7 +112,7 @@ export function instagramUnavailableReason(status: number, body: string): Postfe
109
112
  if (status !== 0 && status < 400) {
110
113
  return "unavailable";
111
114
  }
112
- if (/MIN_AGE_ACCOUNT|under 18/i.test(body)) {
115
+ if (/MIN_AGE_ACCOUNT|under 18|age[- ]restricted/i.test(body)) {
113
116
  return "ageRestricted";
114
117
  }
115
118
  if (/private account|"is_private":\s*true|account is private/i.test(body)) {
@@ -190,14 +193,20 @@ function shortcode(input: string): string {
190
193
  return code;
191
194
  }
192
195
 
193
- async function pageMedia(net: Net, code: string, preferredWidth: number): Promise<Json | null> {
196
+ async function pageMedia(net: Net, code: string, preferredWidth: number): Promise<{ media: Json | null; reason?: PostfetchReason }> {
194
197
  const response = await net(`https://www.instagram.com/p/${code}/`, { headers: navigationHeaders() });
195
198
  if (!response.ok) {
196
- return null;
199
+ return { media: null };
197
200
  }
198
201
  const html = await response.text();
199
202
  const media = inlineMedia(html, code);
200
- return media && mediaItems(media, code, preferredWidth).length > 0 ? media : null;
203
+ const reason = media && object(media.gating_ruling)
204
+ ? instagramUnavailableReason(400, JSON.stringify(media.gating_ruling))
205
+ : "unavailable";
206
+ return {
207
+ media: media && mediaItems(media, code, preferredWidth).length > 0 ? media : null,
208
+ reason: reason === "unavailable" ? undefined : reason,
209
+ };
201
210
  }
202
211
 
203
212
  function inlineMedia(html: string, code: string): Json | null {
@@ -233,7 +242,7 @@ function searchMedia(node: unknown, code: string): Json | null {
233
242
  Array.isArray(node.video_versions) ||
234
243
  Array.isArray(node.carousel_media) ||
235
244
  (object(node.image_versions2) && Array.isArray(node.image_versions2.candidates));
236
- if (hasMedia && node.code === code) {
245
+ if ((hasMedia || object(node.gating_ruling)) && node.code === code) {
237
246
  return node;
238
247
  }
239
248
  for (const key in node) {