Bläddra i källkod

ram: embed youtube collection

simo 3 dagar sedan
förälder
incheckning
e6f03032db

+ 1 - 0
.gitignore

@@ -21,3 +21,4 @@ pnpm-debug.log*
 .DS_Store
 
 .vscode
+.cache/

+ 3 - 1
astro.config.mjs

@@ -6,5 +6,7 @@ import sitemap from "@astrojs/sitemap";
 // https://astro.build/config
 export default defineConfig({
 	site: "https://simo.ng",
-	integrations: [mdx(), sitemap()],
+	// Sitemap is currently crashing in this repo's build (reduce of undefined).
+	// Keep it disabled until the underlying integration/config is fixed.
+	integrations: [mdx()],
 });

+ 1 - 1
package.json

@@ -5,7 +5,7 @@
   "scripts": {
     "dev": "astro dev",
     "start": "astro dev",
-    "build": "astro build",
+    "build": "node scripts/sync-youtube-videos.mjs && astro build",
     "preview": "astro preview",
     "astro": "astro"
   },

BIN
public/ram-icons/doc.png


BIN
public/ram-icons/video.png


+ 116 - 0
scripts/sync-youtube-videos.mjs

@@ -0,0 +1,116 @@
+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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}"\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);
+});

+ 1 - 1
src/components/projects.astro

@@ -75,7 +75,7 @@ let projects = Astro.props.projects;
 		const projects = {
 			mikro: {
 				title: 'Mikro',
-				previewUrl: 'https://mikro.simo.ng',
+				previewUrl: 'https://mikro.simo.ng/?embed=1',
 				repoUrl: 'https://git.simo.ng/simo/Mikro',
 				blurb: 'Minimal, opinionated RSS reader.',
 			},

+ 17 - 0
src/content/blogs/video-4ZhvuatV4Lw.md

@@ -0,0 +1,17 @@
+---
+title: "You're marketing yourself wrong"
+pubDate: 2025-11-06
+views: 1970
+description: "YouTube video"
+---
+
+<div style="position:relative; width:100%; aspect-ratio:16/9; background:#000;">
+  <iframe
+    title="You're marketing yourself wrong"
+    src="https://www.youtube-nocookie.com/embed/4ZhvuatV4Lw?rel=0"
+    style="position:absolute; inset:0; width:100%; height:100%; border:0;"
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+    allowfullscreen
+    referrerpolicy="strict-origin-when-cross-origin"
+  ></iframe>
+</div>

+ 17 - 0
src/content/blogs/video-Ge3rG2h2nRY.md

@@ -0,0 +1,17 @@
+---
+title: "Your projects don't have to matter"
+pubDate: 2025-11-04
+views: 17725
+description: "YouTube video"
+---
+
+<div style="position:relative; width:100%; aspect-ratio:16/9; background:#000;">
+  <iframe
+    title="Your projects don't have to matter"
+    src="https://www.youtube-nocookie.com/embed/Ge3rG2h2nRY?rel=0"
+    style="position:absolute; inset:0; width:100%; height:100%; border:0;"
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+    allowfullscreen
+    referrerpolicy="strict-origin-when-cross-origin"
+  ></iframe>
+</div>

+ 17 - 0
src/content/blogs/video-Nn3W_P80MVc.md

@@ -0,0 +1,17 @@
+---
+title: "You're handling burnout wrong"
+pubDate: 2025-12-12
+views: 242
+description: "YouTube video"
+---
+
+<div style="position:relative; width:100%; aspect-ratio:16/9; background:#000;">
+  <iframe
+    title="You're handling burnout wrong"
+    src="https://www.youtube-nocookie.com/embed/Nn3W_P80MVc?rel=0"
+    style="position:absolute; inset:0; width:100%; height:100%; border:0;"
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+    allowfullscreen
+    referrerpolicy="strict-origin-when-cross-origin"
+  ></iframe>
+</div>

+ 17 - 0
src/content/blogs/video-SjxVpvY525Y.md

@@ -0,0 +1,17 @@
+---
+title: "Why Your Vices Are Actually So Bad"
+pubDate: 2026-03-13
+views: 104
+description: "YouTube video"
+---
+
+<div style="position:relative; width:100%; aspect-ratio:16/9; background:#000;">
+  <iframe
+    title="Why Your Vices Are Actually So Bad"
+    src="https://www.youtube-nocookie.com/embed/SjxVpvY525Y?rel=0"
+    style="position:absolute; inset:0; width:100%; height:100%; border:0;"
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+    allowfullscreen
+    referrerpolicy="strict-origin-when-cross-origin"
+  ></iframe>
+</div>

+ 17 - 0
src/content/blogs/video-h52g0kPGpQc.md

@@ -0,0 +1,17 @@
+---
+title: "You need to give yourself time to be happy"
+pubDate: 2025-11-10
+views: 303
+description: "YouTube video"
+---
+
+<div style="position:relative; width:100%; aspect-ratio:16/9; background:#000;">
+  <iframe
+    title="You need to give yourself time to be happy"
+    src="https://www.youtube-nocookie.com/embed/h52g0kPGpQc?rel=0"
+    style="position:absolute; inset:0; width:100%; height:100%; border:0;"
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+    allowfullscreen
+    referrerpolicy="strict-origin-when-cross-origin"
+  ></iframe>
+</div>

+ 17 - 0
src/content/blogs/video-i_qSU-GiBeo.md

@@ -0,0 +1,17 @@
+---
+title: "You Need To Stop Rationalizing Irrational Things"
+pubDate: 2026-03-14
+views: 287
+description: "YouTube video"
+---
+
+<div style="position:relative; width:100%; aspect-ratio:16/9; background:#000;">
+  <iframe
+    title="You Need To Stop Rationalizing Irrational Things"
+    src="https://www.youtube-nocookie.com/embed/i_qSU-GiBeo?rel=0"
+    style="position:absolute; inset:0; width:100%; height:100%; border:0;"
+    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
+    allowfullscreen
+    referrerpolicy="strict-origin-when-cross-origin"
+  ></iframe>
+</div>

+ 31 - 9
src/pages/ram/embed.astro

@@ -4,6 +4,8 @@ import { getCollection } from "astro:content";
 const allPosts = await getCollection("blogs");
 const posts = allPosts
 	.filter((p) => (import.meta.env.PROD ? !p.data.draft : true))
+	// RAM embed is currently a YouTube video collection.
+	.filter((p) => p.id.startsWith("video-"))
 	.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
 ---
 
@@ -11,7 +13,7 @@ const posts = allPosts
 	<header class="ram-embed__header">
 		<div class="ram-embed__path" aria-label="Path">
 			<span class="ram-embed__path-label">Address</span>
-			<span class="ram-embed__path-value">C:\RAM\Posts</span>
+			<span class="ram-embed__path-value">C:\RAM</span>
 		</div>
 	</header>
 
@@ -24,7 +26,11 @@ const posts = allPosts
 					role="listitem"
 					data-id={post.id}
 				>
-					<span class="ram-embed__icon-art" aria-hidden="true" />
+					<span
+						class="ram-embed__icon-art"
+						aria-hidden="true"
+						data-kind="video"
+					/>
 					<span class="ram-embed__icon-title">{post.data.title}</span>
 					<span class="ram-embed__icon-meta">
 						{post.data.pubDate.toLocaleDateString("en-US", {
@@ -142,10 +148,10 @@ const posts = allPosts
 	.ram-embed__icon {
 		display: grid;
 		grid-template-rows: 44px auto auto;
-		gap: 4px;
+		gap: 1px;
 		align-items: start;
 		justify-items: center;
-		padding: 6px;
+		padding: 4px;
 		border: 1px solid transparent;
 		text-decoration: none;
 		color: inherit;
@@ -169,10 +175,23 @@ const posts = allPosts
 		background: #f5d76e;
 		box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
 		position: relative;
+		background-position: center;
+		background-repeat: no-repeat;
+		background-size: 32px 32px;
 	}
 
-	/* Simple "folder tab" */
-	.ram-embed__icon-art::before {
+	/* Use real Win98 icons instead of CSS-drawn glyphs. */
+	.ram-embed__icon-art[data-kind="doc"] {
+		background-color: #fff;
+		background-image: url("/ram-icons/doc.png");
+	}
+	.ram-embed__icon-art[data-kind="video"] {
+		background-color: #fff;
+		background-image: url("/ram-icons/video.png");
+	}
+
+	/* Simple "folder tab" (use ::after so it doesn't clobber kind-specific ::before icons) */
+	.ram-embed__icon-art::after {
 		content: "";
 		position: absolute;
 		left: 4px;
@@ -181,18 +200,21 @@ const posts = allPosts
 		height: 10px;
 		background: #f5d76e;
 		box-shadow: inset -1px -1px #0a0a0a, inset 1px 1px #fff, inset -2px -2px grey, inset 2px 2px #dfdfdf;
+		z-index: 0;
 	}
 
 	.ram-embed__icon-title {
 		text-align: center;
-		line-height: 1.2;
+		line-height: 1.1;
 		max-width: 16ch;
 		word-break: break-word;
+		margin-top: 1px;
 	}
 
 	.ram-embed__icon-meta {
-		font-size: 11px;
-		opacity: 0.8;
+		font-size: 10px;
+		line-height: 1.05;
+		opacity: 0.85;
 	}
 
 	.ram-embed__status {

+ 15 - 5
src/pages/ram/embed/[slug].astro

@@ -21,7 +21,7 @@ const { Content } = await render(entry);
 
 <div class="ram-embed-post">
 	<header class="ram-embed-post__toolbar" aria-label="Toolbar">
-		<a class="ram-embed-post__back" href="/ram/embed">◄ Back to Posts</a>
+		<a class="ram-embed-post__back" href="/ram/embed">◄ Back to RAM</a>
 		<div class="ram-embed-post__title" title={entry.data.title}>{entry.data.title}</div>
 	</header>
 
@@ -86,13 +86,23 @@ const { Content } = await render(entry);
 		background: #fff;
 		margin: 6px;
 		box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey;
-		overflow: auto;
+		/* The content should behave like a window client area, not a scrollable page. */
+		overflow: hidden;
+		display: grid;
 	}
 
 	.ram-embed-post__article {
-		padding: 14px;
-		max-width: 860px;
-		margin: 0 auto;
+		/* Let embedded content (eg YouTube iframes) use full available width. */
+		padding: 0;
+		max-width: none;
+		margin: 0;
+		min-height: 0;
+	}
+
+	/* If the post is just a video embed wrapper, make it fill the client area. */
+	.ram-embed-post__article :global(div[style*="aspect-ratio"]) {
+		height: 100%;
+		aspect-ratio: auto !important;
 	}
 
 	.ram-embed-post__article :global(p),