diff --git a/client/src/api.ts b/client/src/api.ts index 4ccba8f..97e4cfe 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -25,6 +25,7 @@ function normalizeDetail(payload: any): EntryDetail { id: String(payload?.id ?? ''), title: payload?.title || payload?.meta?.title_en || String(payload?.id ?? 'Untitled'), meta: payload?.meta || {}, + ig_meta: payload?.ig_meta, items, quiz: Array.isArray(payload?.quiz) ? payload.quiz : [], ui_hints: payload?.ui_hints || {}, diff --git a/client/src/types.ts b/client/src/types.ts index 0c19448..ca706a9 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -64,6 +64,15 @@ export interface EntryDetail { type?: string; title_en?: string; }; + ig_meta?: { + username?: string; + full_name?: string; + profile_pic_url?: string; + post_url?: string; + profile_url?: string; + post_date?: string; + description?: string; + }; items: EntryItems; quiz: QuizQuestion[]; ui_hints?: { diff --git a/server/src/index.ts b/server/src/index.ts index b866934..7226e0b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -140,6 +140,32 @@ function computeCounts(items: EntryData['items'], quiz: EntryData['quiz']) { }; } +function extractIgMeta(raw: any): EntryRecord['igMeta'] { + if (!raw || typeof raw !== 'object') return undefined; + const username = raw.username || raw.owner?.username; + const full_name = raw.fullname || raw.full_name || raw.owner?.full_name; + const post_url = raw.post_url || raw.postUrl || raw.permalink; + const profile_pic_url = + raw.profile_pic_url || + raw.owner?.hd_profile_pic_url_info?.url || + raw.owner?.profile_pic_url || + raw.owner?.profile_pic_url_info?.url; + const post_date = raw.post_date || raw.date || raw.taken_at_timestamp || raw.timestamp; + const description = raw.description || raw.caption; + const profile_url = username ? `https://www.instagram.com/${username}/` : undefined; + + if (!username && !profile_pic_url && !post_date && !description) return undefined; + return { + username, + full_name, + profile_pic_url, + post_url, + profile_url, + post_date: post_date ? String(post_date) : undefined, + description, + }; +} + async function loadEntries() { entryIndex.clear(); @@ -159,6 +185,7 @@ async function loadEntries() { const dir = path.dirname(resolvedMp4); const baseName = path.basename(resolvedMp4, '.mp4'); const jsonPath = path.join(dir, `${baseName}.json`); + const mp4MetaPath = path.join(dir, `${baseName}.mp4.json`); if (!(await fileExists(jsonPath))) { continue; @@ -169,6 +196,20 @@ async function loadEntries() { continue; } + let igMeta: EntryRecord['igMeta']; + const resolvedMp4Meta = path.resolve(mp4MetaPath); + if (await fileExists(resolvedMp4Meta)) { + if (ensureWithinDataRoot(resolvedMp4Meta)) { + try { + const raw = await fs.readFile(resolvedMp4Meta, 'utf-8'); + const json = JSON.parse(raw); + igMeta = extractIgMeta(json); + } catch (err) { + console.warn(`Failed to parse mp4 metadata ${resolvedMp4Meta}:`, err); + } + } + } + let parsed: EntryData | null = null; try { const raw = await fs.readFile(resolvedJson, 'utf-8'); @@ -196,6 +237,7 @@ async function loadEntries() { items: parsed.items || { grammar: [], vocab: [], conversation: [], key_phrases: [] }, quiz: parsed.quiz || [], ui_hints: parsed.ui_hints || { recommended_order: [] }, + igMeta, videoPath: resolvedMp4, jsonPath: resolvedJson, video_url, @@ -214,6 +256,7 @@ function sanitizeEntryResponse(entry: EntryRecord) { items: entry.items || { grammar: [], vocab: [], conversation: [], key_phrases: [] }, quiz: entry.quiz || [], ui_hints: entry.ui_hints || { recommended_order: [] }, + ig_meta: entry.igMeta, video_url: entry.video_url, counts: entry.counts, };