Fall through to the post page when a reel embeds only its cover frame
CI / Typecheck, test, build (pull_request) Successful in 29s
CI / Typecheck, test, build (pull_request) Successful in 29s
The captioned embed stopped shipping a reel's video_url and never draws a logged-out <video>, so both the payload and the DOM hand back the cover frame alone. Because that still counts as media, the post-page fallback never fired and a reel resolved to a still image. Trigger the fallback when the URL is a reel (or the payload declared a video) but no video was found, so it reaches the post page, whose payload still carries the progressive file in video_versions. The embed's caption, handle and avatar are carried forward, since the post-page payload does not include the owner. Co-authored-by: anthropic/claude-opus-4-8
This commit is contained in:
co-authored by
anthropic/claude-opus-4-8
parent
c08b628cce
commit
cc4f094fb8
@@ -254,15 +254,31 @@ async function resolve(ctx: ResolveContext): Promise<Post> {
|
|||||||
if (scraped && !scraped.text && dom.text) scraped.text = dom.text;
|
if (scraped && !scraped.text && dom.text) scraped.text = dom.text;
|
||||||
if (scraped && !scraped.handle && dom.handle) scraped.handle = dom.handle;
|
if (scraped && !scraped.handle && dom.handle) scraped.handle = dom.handle;
|
||||||
|
|
||||||
if (!scraped?.media.length) {
|
// The embed no longer ships a reel's `video_url`, and its player never
|
||||||
|
// draws a `<video>` logged out — so both payload and DOM hand back only
|
||||||
|
// the cover frame. An image where a video belongs is a miss, not a result:
|
||||||
|
// fall through to the post page, whose payload still carries
|
||||||
|
// `video_versions`. Keep the embed's caption/handle/avatar either way.
|
||||||
|
const hasVideo = scraped?.media.some((item) => item.kind === 'video') === true;
|
||||||
|
if (!scraped?.media.length || (expectsVideo && !hasVideo)) {
|
||||||
// The embed refuses some posts outright ("the link may be broken").
|
// The embed refuses some posts outright ("the link may be broken").
|
||||||
// Try the post itself: its payload first, then its link preview.
|
// Try the post itself: its payload first, then its link preview.
|
||||||
|
const carry = scraped;
|
||||||
await page.goto(originalUrl, { waitUntil: 'domcontentloaded' }).catch(() => undefined);
|
await page.goto(originalUrl, { waitUntil: 'domcontentloaded' }).catch(() => undefined);
|
||||||
scraped = fromPayloads(await scriptTexts(ctx)) ?? scraped;
|
const fromPost = fromPayloads(await scriptTexts(ctx));
|
||||||
|
if (fromPost?.media.length) {
|
||||||
|
scraped = {
|
||||||
|
...fromPost,
|
||||||
|
...(fromPost.text ? {} : carry?.text ? { text: carry.text } : {}),
|
||||||
|
...(fromPost.handle ? {} : carry?.handle ? { handle: carry.handle } : {}),
|
||||||
|
...(fromPost.avatar ? {} : carry?.avatar ? { avatar: carry.avatar } : {}),
|
||||||
|
...(fromPost.displayName ? {} : carry?.displayName ? { displayName: carry.displayName } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!scraped?.media.length) {
|
if (!scraped?.media.length) {
|
||||||
const og = await fromOpenGraph(ctx);
|
const og = await fromOpenGraph(ctx);
|
||||||
if (og.media.length) scraped = { ...og, ...(scraped?.handle ? { handle: scraped.handle } : {}) };
|
if (og.media.length) scraped = { ...og, ...(carry?.handle ? { handle: carry.handle } : {}) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"__typename": "XIGPolarisVideoMedia",
|
||||||
|
"pk": "3983940149641276516",
|
||||||
|
"code": "DdJzHVRyOxk",
|
||||||
|
"media_type": 2,
|
||||||
|
"product_type": "clips",
|
||||||
|
"caption": {
|
||||||
|
"text": "I just quit my job at the BPAF and things are not looking good you guys"
|
||||||
|
},
|
||||||
|
"accessibility_caption": "Video by Vinny Thomas on September 11, 2026.",
|
||||||
|
"original_height": 1280,
|
||||||
|
"original_width": 720,
|
||||||
|
"video_duration": 60.486,
|
||||||
|
"image_versions2": {
|
||||||
|
"candidates": [
|
||||||
|
{
|
||||||
|
"url": "https://scontent-lga3-1.cdninstagram.com/v/t51.82787-15/806299489_18624728518024548_8282126995775909321_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=109&ig_cache_key=abc.3-ccb7-5&efg=cover_frame&width=720",
|
||||||
|
"height": 1280,
|
||||||
|
"width": 720
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://scontent-lga3-1.cdninstagram.com/v/t51.82787-15/806299489_18624728518024548_8282126995775909321_n.jpg?stp=dst-jpg_e15_s640x640_tt6&_nc_cat=109&ig_cache_key=abc.3-ccb7-5&efg=cover_frame&width=640",
|
||||||
|
"height": 640,
|
||||||
|
"width": 640
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://scontent-lga3-1.cdninstagram.com/v/t51.82787-15/806299489_18624728518024548_8282126995775909321_n.jpg?stp=dst-jpg_e15_s320x320_tt6&_nc_cat=109&ig_cache_key=abc.3-ccb7-5&efg=cover_frame&width=320",
|
||||||
|
"height": 320,
|
||||||
|
"width": 320
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"has_audio": true,
|
||||||
|
"video_versions": [
|
||||||
|
{
|
||||||
|
"type": 101,
|
||||||
|
"url": "https://scontent-lga3-3.cdninstagram.com/o1/v/t2/f2/m86/AQOsyEB80vwVJZ1X8Ang509fDOvIQYYT5Sw8ILSanSG2u_RQcsqL4IXxAs-sjup.mp4?_nc_cat=102&_nc_sid=5e9851&efg=xpv_progressive_720&width=720"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 102,
|
||||||
|
"url": "https://scontent-lga3-3.cdninstagram.com/o1/v/t2/f2/m86/AQOsyEB80vwVJZ1X8Ang509fDOvIQYYT5Sw8ILSanSG2u_RQcsqL4IXxAs-sjup.mp4?_nc_cat=102&_nc_sid=5e9851&efg=xpv_progressive_480&width=480"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { fromGraph } from '../src/platforms/instagram.ts';
|
import { fromGraph } from '../src/platforms/instagram.ts';
|
||||||
|
import { findMetaMedia, mediaFromMetaNode } from '../src/platforms/meta-media.ts';
|
||||||
import { fixture } from './helpers.ts';
|
import { fixture } from './helpers.ts';
|
||||||
|
|
||||||
test('a carousel becomes one media entry per child, in order', () => {
|
test('a carousel becomes one media entry per child, in order', () => {
|
||||||
@@ -26,6 +27,24 @@ test('a reel becomes a progressive video with its cover frame as the poster', ()
|
|||||||
assert.ok(item?.width && item.height, 'expected dimensions for the aspect ratio');
|
assert.ok(item?.width && item.height, 'expected dimensions for the aspect ratio');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a reel whose embed only ships a cover frame still finds its video on the post page', () => {
|
||||||
|
// The captioned embed stopped shipping a reel's `video_url` and never draws
|
||||||
|
// a logged-out `<video>`, so both payload and DOM hand back the poster only.
|
||||||
|
// The post page still carries the progressive file in `video_versions`
|
||||||
|
// (the meta-media schema), which is where the adapter now falls through to.
|
||||||
|
const node = findMetaMedia(fixture('instagram/reel-video-versions.json'));
|
||||||
|
assert.ok(node, 'expected to find the media node in the post payload');
|
||||||
|
const media = mediaFromMetaNode(node!, 'https://www.instagram.com/');
|
||||||
|
assert.equal(media.length, 1);
|
||||||
|
const [item] = media;
|
||||||
|
assert.equal(item?.kind, 'video', 'a reel is a video, not its cover frame');
|
||||||
|
assert.match(item?.url ?? '', /^https:\/\/.*\.mp4/);
|
||||||
|
assert.ok(!item?.url.includes('.m3u8'), 'the native player needs a real file, not HLS');
|
||||||
|
// The cover frame becomes the poster, not the media itself.
|
||||||
|
assert.ok(item?.kind === 'video' && item.poster?.url.includes('t51.82787-15'), 'expected the cover frame as poster');
|
||||||
|
assert.ok(item?.width === 720 && item.height === 1280, 'expected dimensions for the aspect ratio');
|
||||||
|
});
|
||||||
|
|
||||||
test('captions keep their line breaks', () => {
|
test('captions keep their line breaks', () => {
|
||||||
const scraped = fromGraph(fixture('instagram/carousel.json'));
|
const scraped = fromGraph(fixture('instagram/carousel.json'));
|
||||||
assert.ok(scraped.text?.includes('\n'), 'expected the caption to keep paragraph breaks');
|
assert.ok(scraped.text?.includes('\n'), 'expected the caption to keep paragraph breaks');
|
||||||
|
|||||||
Reference in New Issue
Block a user