Change /video/ route to /run/, add embed support
This commit is contained in:
@@ -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, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
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
@@ -2,8 +2,7 @@ import { Elysia, status } from "elysia";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { videoRoutes } from "./routes/videos";
|
||||
import { initDb } from "./services/db";
|
||||
import { join } from "path";
|
||||
import { existsSync, statSync } from "fs";
|
||||
import { renderRunPage, getOembed } from "./embeds";
|
||||
|
||||
initDb();
|
||||
|
||||
@@ -16,28 +15,20 @@ const app = new Elysia()
|
||||
exposeHeaders: ["Content-Range", "Accept-Ranges", "Content-Length"],
|
||||
}),
|
||||
)
|
||||
.onError(({ error }) => {
|
||||
console.error("Unhandled error:", error);
|
||||
return status(500, { error: "Internal server error" });
|
||||
})
|
||||
.use(videoRoutes)
|
||||
.get("/api/health", () => ({ status: "ok" }))
|
||||
.get("*", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (pathname.startsWith("/api")) return status(404, "Not found");
|
||||
|
||||
const filePath = join(STATIC_DIR, pathname);
|
||||
if (existsSync(filePath) && statSync(filePath).isFile())
|
||||
return Bun.file(filePath);
|
||||
|
||||
const indexPath = join(STATIC_DIR, "index.html");
|
||||
if (existsSync(indexPath)) return Bun.file(indexPath);
|
||||
|
||||
return status(404, "Not found");
|
||||
.get("/api/oembed/:id", ({ params: { id } }) => {
|
||||
const oembed = getOembed(id);
|
||||
if (!oembed) return status(404, { error: "Not found" });
|
||||
return oembed;
|
||||
})
|
||||
.get("/run/:id", ({ params: { id } }) => {
|
||||
const result = renderRunPage(id, STATIC_DIR);
|
||||
if (!result) return status(404, "Not found");
|
||||
return new Response(result.html, {
|
||||
headers: { "Content-Type": result.contentType },
|
||||
});
|
||||
})
|
||||
.listen(PORT);
|
||||
|
||||
console.log(`surf.nathan.rip running on port ${PORT}`);
|
||||
console.log(`Serving static files from ${STATIC_DIR}`);
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function App() {
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="video/:id" element={<VideoDetail />} />
|
||||
<Route path="run/:id" element={<VideoDetail />} />
|
||||
<Route path="upload" element={<Upload />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function VideoCard({ video }: { video: VideoListItem }) {
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/video/${video.id}`}
|
||||
to={`/run/${video.id}`}
|
||||
className="group flex h-full flex-col shadow no-underline"
|
||||
>
|
||||
<div className="relative aspect-video overflow-hidden rounded-t">
|
||||
|
||||
@@ -66,8 +66,14 @@ export default function VideoDetail() {
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
fetchVideo(id)
|
||||
.then(setVideo)
|
||||
.then((v) => {
|
||||
setVideo(v);
|
||||
document.title = `${getMapDisplayName(v.mapName)} - ${v.playerName} · surf.nathan.rip`;
|
||||
})
|
||||
.catch((err) => setError(err.message));
|
||||
return () => {
|
||||
document.title = "surf.nathan.rip";
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (error) {
|
||||
|
||||
Reference in New Issue
Block a user