import { chromium, type BrowserContextOptions } from 'playwright' import { isNumericMediaId, mediaIdFromThreadObject, postShortcode, shortcodeFromPermalink, } from './threads-media-id' const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' function walkJson(data: unknown, visit: (obj: Record) => void): void { if (!data || typeof data !== 'object') return if (Array.isArray(data)) { for (const item of data) walkJson(item, visit) return } const obj = data as Record visit(obj) for (const value of Object.values(obj)) { if (value && typeof value === 'object') walkJson(value, visit) } } function findMediaIdForShortcode(data: unknown, shortcode: string): string | undefined { const want = shortcode.trim().toLowerCase() if (!want) return undefined let found: string | undefined walkJson(data, (obj) => { if (found) return const code = postShortcode(obj) if (!code || code.toLowerCase() !== want) return const mediaId = mediaIdFromThreadObject(obj) if (mediaId) found = mediaId }) return found } /** Open a Threads permalink and extract the numeric media id (pk) for API replies. */ export async function resolveMediaIdFromPermalink( permalink: string, storageState?: string, ): Promise { const link = permalink.trim() const shortcode = shortcodeFromPermalink(link) if (!link || !shortcode) return undefined const browser = await chromium.launch({ headless: process.env.PLAYWRIGHT_HEADLESS !== 'false', args: ['--disable-blink-features=AutomationControlled', '--no-first-run', '--no-default-browser-check'], }) try { const contextOptions: BrowserContextOptions = { userAgent: USER_AGENT, viewport: { width: 1280, height: 900 }, locale: 'zh-TW', timezoneId: 'Asia/Taipei', } const trimmedState = storageState?.trim() ?? '' if (trimmedState) { contextOptions.storageState = JSON.parse(trimmedState) as BrowserContextOptions['storageState'] } const context = await browser.newContext(contextOptions) const page = await context.newPage() const payloads: unknown[] = [] page.on('response', async (response) => { const url = response.url() if (!url.includes('graphql') && !url.includes('threads') && !url.includes('instagram')) return try { const contentType = response.headers()['content-type'] ?? '' if (!contentType.includes('json')) return payloads.push(await response.json()) } catch { // ignore } }) await page.goto(link, { waitUntil: 'domcontentloaded', timeout: 45_000 }) await page.waitForTimeout(1500) for (const payload of payloads) { const mediaId = findMediaIdForShortcode(payload, shortcode) if (isNumericMediaId(mediaId)) return mediaId } const scripts = await page.locator('script[type="application/json"][data-sjs]').all() for (const script of scripts) { try { const raw = await script.textContent() if (!raw) continue const json = JSON.parse(raw) const mediaId = findMediaIdForShortcode(json, shortcode) if (isNumericMediaId(mediaId)) return mediaId } catch { // skip malformed script } } return undefined } finally { await browser.close() } }