import fs from "node:fs/promises"; import path from "node:path"; import { spawn } from "node:child_process"; const CHANNEL_VIDEOS_URL = "https://www.youtube.com/@simonlol/videos"; const OUT_DIR = path.join(process.cwd(), "src", "content", "blogs"); function run(cmd, args) { return new Promise((resolve, reject) => { const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] }); let out = ""; let err = ""; child.stdout.on("data", (d) => (out += d.toString("utf8"))); child.stderr.on("data", (d) => (err += d.toString("utf8"))); child.on("error", reject); child.on("close", (code) => { if (code === 0) resolve({ out, err }); else reject(new Error(`${cmd} exited with ${code}\n${err}`)); }); }); } function escYaml(s) { return String(s).replaceAll('"', "\\\""); } function videoMd({ id, title, uploadDate, viewCount }) { const safeTitle = title?.trim() || id; // yt-dlp gives upload_date as YYYYMMDD. const pubDate = uploadDate && /^\d{8}$/.test(uploadDate) ? `${uploadDate.slice(0, 4)}-${uploadDate.slice(4, 6)}-${uploadDate.slice(6, 8)}` : new Date().toISOString().slice(0, 10); return `---\n` + `title: "${escYaml(safeTitle)}"\n` + `pubDate: ${pubDate}\n` + `views: ${Number.isFinite(Number(viewCount)) ? Number(viewCount) : 0}\n` + `description: "YouTube video"\n` + `---\n\n` + `
\n` + ` ", ">")}"\n` + ` src="https://www.youtube-nocookie.com/embed/${id}?rel=0"\n` + ` style="position:absolute; inset:0; width:100%; height:100%; border:0;"\n` + ` allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"\n` + ` allowfullscreen\n` + ` referrerpolicy="strict-origin-when-cross-origin"\n` + ` >\n` + `
\n`; } async function main() { // Ensure output dir exists await fs.mkdir(OUT_DIR, { recursive: true }); // Fetch list via yt-dlp. // Note: `--flat-playlist` does not include `upload_date` (it returns NA). // We fetch ids+titles flat, then fetch upload_date per video id. const { out } = await run("yt-dlp", [ "--flat-playlist", "--print", "%(id)s\t%(title)s", CHANNEL_VIDEOS_URL, ]); const lines = out .split("\n") .map((l) => l.trim()) .filter(Boolean); const videos = lines .map((l) => { const [id, ...rest] = l.split("\t"); const title = rest.join("\t"); return { id, title }; }) .filter((v) => v.id && v.id !== "NA"); for (const v of videos) { try { const { out: metaOut } = await run("yt-dlp", [ "--no-playlist", "--print", "%(upload_date)s\t%(view_count)s", `https://www.youtube.com/watch?v=${v.id}`, ]); const first = metaOut.trim().split("\n")[0] || ""; const [uploadDate = "", viewCount = ""] = first.split("\t"); v.uploadDate = uploadDate; v.viewCount = viewCount; } catch { v.uploadDate = ""; v.viewCount = ""; } } // Read existing generated files const existing = await fs.readdir(OUT_DIR); const existingGenerated = new Set(existing.filter((f) => f.startsWith("video-") && f.endsWith(".md"))); // Write/update current for (const v of videos) { const fileName = `video-${v.id}.md`; existingGenerated.delete(fileName); await fs.writeFile(path.join(OUT_DIR, fileName), videoMd(v), "utf8"); } // Remove stale generated files for (const stale of existingGenerated) { await fs.unlink(path.join(OUT_DIR, stale)); } } main().catch((e) => { console.error("[sync-youtube-videos]", e?.message || e); process.exit(1); });