| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- 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` +
- `<div style="position:relative; width:100%; aspect-ratio:16/9; background:#000;">\n` +
- ` <iframe\n` +
- ` title="${safeTitle.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")}"\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` +
- ` ></iframe>\n` +
- `</div>\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);
- });
|