import fs from "node:fs"; import path from "node:path"; import type { Metadata } from "next"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { parseMomentKey } from "yt-dlp-transcript-common/lib/citations/moments"; import { isReportId } from "yt-dlp-transcript-common/lib/report/schema"; import { REPORT_HISTORY_FORMAT, reportHistoryViewPath, type ReportHistoryView, } from "yt-dlp-transcript-common/lib/report/revisions"; import { MOMENT_INDEX_FORMAT, MOMENT_PAGE_FORMAT, MOMENTS_INDEX_PATH, REPORT_INDEX_FORMAT, REPORT_PAGE_FORMAT, REPORTS_INDEX_PATH, REPORT_FEED_MIME, momentViewPath, reportFullTitle, reportViewPath, type MomentIndexView, type MomentPageView, type ReportIndexView, type ReportPageView, } from "yt-dlp-transcript-common/lib/report/views"; // The report and moment views compose's reports stage writes into public/ // (common/lib/report/views.ts names every file), read at build time like // lib/archives.ts reads the archive manifest. Every reader is tolerant: a file // that is absent, unreadable or of another format reads as "none", so a site // without reports builds exactly as before and a cited site with none renders // its empty state. Read synchronously — a static export, evaluated once per // build. function readView(urlPath: string, format: T["format"]): T | null { try { const file = path.join(getPaths().exportPublicDir, ...urlPath.split("/").filter(Boolean)); const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as T; return parsed && parsed.format === format ? parsed : null; } catch { return null; } } export function readReportIndex(): ReportIndexView | null { const v = readView(REPORTS_INDEX_PATH, REPORT_INDEX_FORMAT); return v && Array.isArray(v.reports) ? v : null; } // Whether this build has a report to link to: drives the header's Reports link. export function hasReports(): boolean { return (readReportIndex()?.reports.length ?? 0) > 0; } // The report ids to build pages for, in index order (only well-formed ids: // each is a path segment). export function reportIds(): string[] { return (readReportIndex()?.reports ?? []).map((r) => r.id).filter(isReportId); } export function readReportView(reportId: string): ReportPageView | null { if (!isReportId(reportId)) return null; const v = readView(reportViewPath(reportId), REPORT_PAGE_FORMAT); return v && v.id === reportId && Array.isArray(v.sections) ? v : null; } // A report's revision history (/reports//history/history.json), or null // when compose published none for it. export function readReportHistory(reportId: string): ReportHistoryView | null { if (!isReportId(reportId)) return null; const v = readView(reportHistoryViewPath(reportId), REPORT_HISTORY_FORMAT); return v && v.reportId === reportId && Array.isArray(v.revisions) && v.revisions.length > 0 ? v : null; } // The build's one report, when it publishes exactly one: a cited site's home // page IS that report. export function onlyReportView(): ReportPageView | null { const ids = reportIds(); return ids.length === 1 ? readReportView(ids[0]) : null; } // A report's page metadata — its page's, and a one-report cited site's home's, // so the tab reads the same on both: the title as one line names it // (`: `, the layout's template adds the site), the subtitle as // the description, and its timeline's feeds when it publishes them (a // `<link rel="alternate">` each, by absolute URL: the layout sets no // metadataBase). export function reportMetadata(view: ReportPageView): Metadata { const title = reportFullTitle(view); return { title, ...(view.subtitle ? { description: view.subtitle } : {}), ...(view.feeds ? { alternates: { types: { [REPORT_FEED_MIME.rss]: [{ url: view.feeds.rss, title }], [REPORT_FEED_MIME.json]: [{ url: view.feeds.json, title }], }, }, } : {}), }; } // The moment keys to build pages for (only keys exactly as momentKey spells // them: each segment is a path segment of a page written to disk). export function momentKeys(): string[] { const v = readView<MomentIndexView>(MOMENTS_INDEX_PATH, MOMENT_INDEX_FORMAT); return (Array.isArray(v?.moments) ? v.moments : []).filter((k) => parseMomentKey(k) !== null); } export function readMomentView(key: string): MomentPageView | null { if (!parseMomentKey(key)) return null; const v = readView<MomentPageView>(momentViewPath(key), MOMENT_PAGE_FORMAT); return v && v.key === key ? v : null; }