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` +
`
\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);
});