import path from "node:path"; import { readFile } from "node:fs/promises"; import type { Paths } from "../lib/paths"; import type { ChannelConfig } from "../lib/channelConfig"; import { defaultWebpageUrl, detectPlatform } from "../lib/platform"; import { extractVideoId } from "../ytdlp/runYtdlp"; async function readPlaylistUrls(playlistPath: string): Promise { let raw: string; try { raw = await readFile(playlistPath, "utf8"); } catch { return []; } return raw .split("\n") .map((s) => s.trim()) .filter(Boolean); } export async function findVideoSourceUrl( paths: Paths, slug: string, videoId: string, config?: ChannelConfig, ): Promise { const channelRoot = path.join(paths.channelsDir, slug); const metaPath = path.join(channelRoot, "data", videoId, "metadata.info.json"); try { const raw = await readFile(metaPath, "utf8"); const parsed = JSON.parse(raw); if (typeof parsed?.webpage_url === "string" && parsed.webpage_url) { return parsed.webpage_url; } } catch { // fall through to playlist scan } // Post-reconcile a video's dir name is its canonical id, so match the // requested videoId against each URL's canonical id directly. const urls = await readPlaylistUrls(path.join(channelRoot, "playlist")); for (const url of urls) { if (extractVideoId(url) === videoId) return url; } // Last resort: re-create the URL from the canonical id (the dir name) and the // channel's platform. Only fires when both metadata.info.json and the // playlist came up empty, so the exact original URL always wins when present. // Requires a known platform — we don't blindly guess one. const platform = config?.platform ?? detectPlatform(config?.url ?? null); if (platform) return defaultWebpageUrl(platform, videoId); return null; }