From d84bf1c0e93a66b231c35ef47fe7ec3c1f8566d1 Mon Sep 17 00:00:00 2001 From: Thans Date: Thu, 24 Sep 2026 09:50:05 +0800 Subject: [PATCH] fix: extract Instagram profiles from page preload --- .../0028_instagram_profile_preload.sql | 406 ++++++++++++++++++ .../db/drizzle/PostgreSQL/meta/_journal.json | 9 +- .../SQLite/0023_instagram_profile_preload.sql | 404 +++++++++++++++++ packages/db/drizzle/SQLite/meta/_journal.json | 9 +- .../src/__tests__/instagram-handler.test.ts | 43 ++ templates/instagram-scraper/requestHandler.js | 61 ++- 6 files changed, 928 insertions(+), 4 deletions(-) create mode 100644 packages/db/drizzle/PostgreSQL/0028_instagram_profile_preload.sql create mode 100644 packages/db/drizzle/SQLite/0023_instagram_profile_preload.sql create mode 100644 packages/template-client/src/__tests__/instagram-handler.test.ts diff --git a/packages/db/drizzle/PostgreSQL/0028_instagram_profile_preload.sql b/packages/db/drizzle/PostgreSQL/0028_instagram_profile_preload.sql new file mode 100644 index 0000000..40c8904 --- /dev/null +++ b/packages/db/drizzle/PostgreSQL/0028_instagram_profile_preload.sql @@ -0,0 +1,406 @@ +-- Update the published Instagram handler without replacing unrelated template fields. +UPDATE "templates" +SET "custom_handlers" = jsonb_set("custom_handlers", '{requestHandler,code,source}', to_jsonb($ig_handler$ +const extractKeyedPayloadFromHtml = (html, targetKey = 'xdt_api__v1__profile_timeline') => { + const results = []; + if (typeof html !== 'string' || html.length === 0) return results; + + const matches = []; + // Only target `; + const wait = jest.fn(); + + const result = await run({ + request: { url: 'https://www.instagram.com/masonthames/' }, + html, + preNav: { has: async () => false, wait }, + }); + + expect(wait).not.toHaveBeenCalled(); + expect(result.jsonResult).toEqual([expect.objectContaining({ + type: 'profile', username: 'masonthames', followers_count: 3306694, + })]); + expect(result.markdown).toContain('Mason Thames'); + }); + + it('fails when a profile page has no public profile data', async () => { + await expect(run({ + request: { url: 'https://www.instagram.com/masonthames/' }, + html: '', + })).rejects.toThrow('No public Instagram data was extracted'); + }); +}); diff --git a/templates/instagram-scraper/requestHandler.js b/templates/instagram-scraper/requestHandler.js index 262bbcc..20d0a7d 100644 --- a/templates/instagram-scraper/requestHandler.js +++ b/templates/instagram-scraper/requestHandler.js @@ -72,6 +72,35 @@ const extractKeyedPayloadFromHtml = (html, targetKey = 'xdt_api__v1__profile_tim return timelineItems; }; +// Logged-out profile pages now preload public profile data under +// xig_user_by_username instead of requesting web_profile_info. +const extractProfileFromHtml = (html, username) => { + if (typeof html !== 'string' || !html || !username) return null; + const scripts = /]*type=["']application\/json["'])(?=[^>]*\bdata-sjs\b)[^>]*>([\s\S]*?)<\/script>/gi; + let match; + while ((match = scripts.exec(html)) !== null) { + const raw = match[1] || ''; + if (!raw.includes('xig_user_by_username') || raw.length > 1_000_000) continue; + try { + const pending = [JSON.parse(raw)]; + let visited = 0; + while (pending.length && visited++ < 30_000) { + const value = pending.pop(); + if (!value || typeof value !== 'object') continue; + const user = value.xig_user_by_username; + if (user && typeof user === 'object' && + String(user.username || '').toLowerCase() === username.toLowerCase()) { + return user; + } + for (const child of Object.values(value)) { + if (child && typeof child === 'object') pending.push(child); + } + } + } catch (_) { /* Ignore unrelated preloaded scripts. */ } + } + return null; +}; + const scrapeInstagram = async (context) => { const page = context.page; const preNav = context.preNav; @@ -98,6 +127,8 @@ const scrapeInstagram = async (context) => { log('URL type:', isInstagramProfileUrl ? 'profile' : isInstagramReelUrl ? 'reel' : 'unknown'); if (isInstagramProfileUrl) { + const username = url.match(/instagram\.com\/([\w.\-]+)/i)?.[1] || ''; + const preloadedUser = extractProfileFromHtml(context.html, username); try { log('Fetching instagramProfileInfo via preNav...'); let profileInfo; @@ -106,7 +137,7 @@ const scrapeInstagram = async (context) => { log('preNav has instagramProfileInfo:', exists); if (exists) { profileInfo = await (preNav.get?.('instagramProfileInfo')); - } else { + } else if (!preloadedUser) { log('Waiting for instagramProfileInfo (timeout: 30s)...'); profileInfo = await (preNav.wait?.('instagramProfileInfo', { timeoutMs: 30000 })); } @@ -197,6 +228,26 @@ const scrapeInstagram = async (context) => { log('Profile extraction error:', err?.message || err); console.error('Instagram profile extraction failed:', err?.message || err); } + + if (jsonResult.length === 0) { + const user = preloadedUser; + if (user) { + jsonResult.push({ + type: 'profile', + username: user.username, + full_name: user.full_name || '', + biography: user.biography || '', + profile_pic_url: user.profile_pic_url || '', + followers_count: user.follower_count ?? null, + following_count: user.following_count ?? null, + posts_count: user.all_media_count ?? null, + is_verified: !!user.is_verified, + is_private: !!user.is_private, + url, + }); + log('Profile extracted from page preload:', user.username); + } + } } if (isInstagramReelUrl) { @@ -326,13 +377,19 @@ const scrapeInstagram = async (context) => { if (Array.isArray(jsonResult) && jsonResult.length) { if (jsonResult.length === 1) { const it = jsonResult[0]; - markdown = `# Instagram Reel ${it.reel_id || ''}\n\n${buildReelMarkdown(it)}`; + markdown = it.type === 'profile' + ? `# ${it.full_name || it.username} (@${it.username})\n\n${it.biography || ''}\n\n- Followers: ${it.followers_count ?? 'unknown'}\n- Following: ${it.following_count ?? 'unknown'}\n- Posts: ${it.posts_count ?? 'unknown'}\n- Verified: ${it.is_verified ? 'yes' : 'no'}\n- URL: ${url}` + : `# Instagram Reel ${it.reel_id || ''}\n\n${buildReelMarkdown(it)}`; } else { const itemsMd = jsonResult.map(buildReelMarkdown).join('\n\n'); markdown = `## Reels\n\nLoaded ${jsonResult.length} reel(s) from ${url}\n\n${itemsMd}`; } } + if (jsonResult.length === 0) { + throw new Error(`No public Instagram data was extracted from ${url}`); + } + log('Final result: markdown length:', markdown.length, '| jsonResult count:', jsonResult.length); return { markdown, jsonResult }; };