Change /video/ route to /run/, add embed support

This commit is contained in:
CallMeVerity
2026-06-04 22:42:39 +01:00
parent f0f50a6f12
commit 32e13fcc85
5 changed files with 165 additions and 24 deletions
+144
View File
@@ -0,0 +1,144 @@
import { getVideoById } from "./services/db";
import { getStreamUrl, getObjectUrl } from "./services/rustfs";
import type { VideoEntry } from "./types/video";
import { readFileSync } from "fs";
const BASE_URL = process.env.BASE_URL || "https://surf.nathan.rip";
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function formatRunTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toFixed(3).padStart(6, "0")}`;
}
function mapDisplayName(mapName: string): string {
return mapName.replace(/_/g, " ").toUpperCase();
}
function pageTitle(video: VideoEntry): string {
return `${mapDisplayName(video.mapName)} - ${video.playerName} · surf.nathan.rip`;
}
function ogTitle(video: VideoEntry): string {
return mapDisplayName(video.mapName);
}
function ogDescription(video: VideoEntry): string {
const parts: string[] = [formatRunTime(video.runTime)];
if (video.tier != null) parts.push(`Tier ${video.tier}`);
return `${video.playerName} on ${mapDisplayName(video.mapName)} · ${parts.join(" · ")}`;
}
function buildOgsHtml(video: VideoEntry): string {
const runUrl = `${BASE_URL}/run/${video.id}`;
const title = ogTitle(video);
const description = ogDescription(video);
const oembedUrl = `${BASE_URL}/api/oembed/${video.id}`;
const thumbnailUrl = video.thumbnailKey
? getObjectUrl(video.thumbnailKey)
: undefined;
const videoStreamUrl = `${BASE_URL}${getStreamUrl(video.id)}`;
let tags =
` <link rel="alternate" type="application/json+oembed" href="${escapeHtml(oembedUrl)}" title="${escapeHtml(title)}" />\n` +
` <meta property="og:title" content="${escapeHtml(title)}" />\n` +
` <meta property="og:description" content="${escapeHtml(description)}" />\n` +
` <meta property="og:url" content="${runUrl}" />\n` +
` <meta property="og:type" content="video.other" />\n` +
` <meta property="og:site_name" content="surf.nathan.rip" />`;
if (thumbnailUrl) {
tags +=
`\n <meta property="og:image" content="${thumbnailUrl}" />\n` +
` <meta property="og:image:width" content="1280" />\n` +
` <meta property="og:image:height" content="720" />`;
}
tags +=
`\n <meta property="og:video" content="${videoStreamUrl}" />\n` +
` <meta property="og:video:type" content="video/mp4" />\n` +
` <meta property="og:video:width" content="1280" />\n` +
` <meta property="og:video:height" content="720" />`;
tags +=
`\n <meta name="twitter:card" content="summary_large_image" />\n` +
` <meta name="twitter:title" content="${escapeHtml(title)}" />\n` +
` <meta name="twitter:description" content="${escapeHtml(description)}" />`;
if (thumbnailUrl) {
tags += `\n <meta name="twitter:image" content="${thumbnailUrl}" />`;
}
return tags;
}
export interface OembedResponse {
type: string;
version: string;
title: string;
author_name: string;
author_url: string;
provider_name: string;
provider_url: string;
thumbnail_url?: string;
html?: string;
}
export function getOembed(id: string): OembedResponse | null {
const video = getVideoById(id);
if (!video) return null;
const thumbnailUrl = video.thumbnailKey
? getObjectUrl(video.thumbnailKey)
: undefined;
return {
type: "video",
version: "1.0",
title: ogTitle(video),
author_name: video.playerName,
author_url: `https://dashboard.momentum-mod.org/maps/${video.mapName}`,
provider_name: "surf.nathan.rip",
provider_url: BASE_URL,
thumbnail_url: thumbnailUrl,
};
}
export function renderRunPage(
id: string,
staticDir: string,
): { html: string; contentType: string } | null {
const indexPath = `${staticDir}/index.html`;
let indexHtml: string;
try {
indexHtml = readFileSync(indexPath, "utf-8");
} catch {
return null;
}
const video = getVideoById(id);
if (!video) {
return { html: indexHtml, contentType: "text/html" };
}
const ogTags = buildOgsHtml(video);
const title = pageTitle(video);
const enhancedHtml = indexHtml
.replace("</head>", `${ogTags}\n </head>`)
.replace(
/<title>[^<]*<\/title>/,
`<title>${escapeHtml(title)}</title>`,
);
return { html: enhancedHtml, contentType: "text/html" };
}
+12 -21
View File
@@ -2,8 +2,7 @@ import { Elysia, status } from "elysia";
import { cors } from "@elysiajs/cors"; import { cors } from "@elysiajs/cors";
import { videoRoutes } from "./routes/videos"; import { videoRoutes } from "./routes/videos";
import { initDb } from "./services/db"; import { initDb } from "./services/db";
import { join } from "path"; import { renderRunPage, getOembed } from "./embeds";
import { existsSync, statSync } from "fs";
initDb(); initDb();
@@ -16,28 +15,20 @@ const app = new Elysia()
exposeHeaders: ["Content-Range", "Accept-Ranges", "Content-Length"], exposeHeaders: ["Content-Range", "Accept-Ranges", "Content-Length"],
}), }),
) )
.onError(({ error }) => {
console.error("Unhandled error:", error);
return status(500, { error: "Internal server error" });
})
.use(videoRoutes) .use(videoRoutes)
.get("/api/health", () => ({ status: "ok" })) .get("/api/health", () => ({ status: "ok" }))
.get("*", ({ request }) => { .get("/api/oembed/:id", ({ params: { id } }) => {
const url = new URL(request.url); const oembed = getOembed(id);
const pathname = url.pathname; if (!oembed) return status(404, { error: "Not found" });
return oembed;
if (pathname.startsWith("/api")) return status(404, "Not found"); })
.get("/run/:id", ({ params: { id } }) => {
const filePath = join(STATIC_DIR, pathname); const result = renderRunPage(id, STATIC_DIR);
if (existsSync(filePath) && statSync(filePath).isFile()) if (!result) return status(404, "Not found");
return Bun.file(filePath); return new Response(result.html, {
headers: { "Content-Type": result.contentType },
const indexPath = join(STATIC_DIR, "index.html"); });
if (existsSync(indexPath)) return Bun.file(indexPath);
return status(404, "Not found");
}) })
.listen(PORT); .listen(PORT);
console.log(`surf.nathan.rip running on port ${PORT}`); console.log(`surf.nathan.rip running on port ${PORT}`);
console.log(`Serving static files from ${STATIC_DIR}`);
+1 -1
View File
@@ -10,7 +10,7 @@ export default function App() {
<Routes> <Routes>
<Route element={<Layout />}> <Route element={<Layout />}>
<Route index element={<Home />} /> <Route index element={<Home />} />
<Route path="video/:id" element={<VideoDetail />} /> <Route path="run/:id" element={<VideoDetail />} />
<Route path="upload" element={<Upload />} /> <Route path="upload" element={<Upload />} />
</Route> </Route>
</Routes> </Routes>
+1 -1
View File
@@ -52,7 +52,7 @@ export default function VideoCard({ video }: { video: VideoListItem }) {
return ( return (
<Link <Link
to={`/video/${video.id}`} to={`/run/${video.id}`}
className="group flex h-full flex-col shadow no-underline" className="group flex h-full flex-col shadow no-underline"
> >
<div className="relative aspect-video overflow-hidden rounded-t"> <div className="relative aspect-video overflow-hidden rounded-t">
+7 -1
View File
@@ -66,8 +66,14 @@ export default function VideoDetail() {
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
fetchVideo(id) fetchVideo(id)
.then(setVideo) .then((v) => {
setVideo(v);
document.title = `${getMapDisplayName(v.mapName)} - ${v.playerName} · surf.nathan.rip`;
})
.catch((err) => setError(err.message)); .catch((err) => setError(err.message));
return () => {
document.title = "surf.nathan.rip";
};
}, [id]); }, [id]);
if (error) { if (error) {