Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b485fe73f | |||
| 31e110af5f | |||
| b9190a9b44 | |||
| 6ce14dda9f | |||
| 97da0d435a | |||
| e50aee1e62 | |||
| 8138f4e36a | |||
| e99aaea193 | |||
| 4fd31ba07d | |||
| 2f62e68688 | |||
| 1693b3849b | |||
| d6cd848257 | |||
| eb56ad5183 |
+3
-3
@@ -29,7 +29,7 @@
|
||||
|
||||
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||
|
||||
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN
|
||||
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.1058.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-IRgNfn8U3zfsZ0JkpmwjS59R/XyHMHxpuwW6HVuJhik+FsbClhNkujEO0w1WqJvXrF4FX+7qIAwUrvlwNvaZ7Q=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA=="],
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g=="],
|
||||
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.6", "", { "dependencies": { "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ=="],
|
||||
|
||||
|
||||
+13
-1
@@ -24,7 +24,7 @@ function mapDisplayName(mapName: string): string {
|
||||
}
|
||||
|
||||
function pageTitle(video: VideoEntry): string {
|
||||
return `${mapDisplayName(video.mapName)} - ${video.playerName} · surf.nathan.rip`;
|
||||
return `${mapDisplayName(video.mapName)} — ${video.playerName} · surf.nathan.rip`;
|
||||
}
|
||||
|
||||
function ogTitle(video: VideoEntry): string {
|
||||
@@ -49,6 +49,7 @@ function buildOgsHtml(video: VideoEntry): string {
|
||||
const videoStreamUrl = `${BASE_URL}${getStreamUrl(video.id)}`;
|
||||
|
||||
let tags =
|
||||
// oEmbed discovery — lets Discord (and others) find the JSON endpoint
|
||||
` <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` +
|
||||
@@ -63,12 +64,14 @@ function buildOgsHtml(video: VideoEntry): string {
|
||||
` <meta property="og:image:height" content="720" />`;
|
||||
}
|
||||
|
||||
// Video embed — Discord & Facebook
|
||||
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" />`;
|
||||
|
||||
// Twitter / X
|
||||
tags +=
|
||||
`\n <meta name="twitter:card" content="summary_large_image" />\n` +
|
||||
` <meta name="twitter:title" content="${escapeHtml(title)}" />\n` +
|
||||
@@ -78,6 +81,8 @@ function buildOgsHtml(video: VideoEntry): string {
|
||||
tags += `\n <meta name="twitter:image" content="${thumbnailUrl}" />`;
|
||||
}
|
||||
|
||||
// Steam uses the same og: tags, nothing extra needed
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
@@ -113,6 +118,12 @@ export function getOembed(id: string): OembedResponse | null {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given run ID, return the SPA's index.html with OG meta tags
|
||||
* injected into <head> and the <title> updated. If the run doesn't exist,
|
||||
* returns the plain SPA html (React Router will show a 404). If the
|
||||
* static index.html isn't available, returns null.
|
||||
*/
|
||||
export function renderRunPage(
|
||||
id: string,
|
||||
staticDir: string,
|
||||
@@ -128,6 +139,7 @@ export function renderRunPage(
|
||||
|
||||
const video = getVideoById(id);
|
||||
if (!video) {
|
||||
// Serve the plain SPA — React Router handles the 404
|
||||
return { html: indexHtml, contentType: "text/html" };
|
||||
}
|
||||
|
||||
|
||||
@@ -391,6 +391,7 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
|
||||
":",
|
||||
msg,
|
||||
);
|
||||
// S3 errors are server errors, not "not found"
|
||||
const statusCode = msg.includes("NoSuchKey") ? 404 : 502;
|
||||
return status(statusCode, {
|
||||
error:
|
||||
|
||||
@@ -168,12 +168,14 @@ export async function getVideoStream(
|
||||
): Promise<VideoStreamResult> {
|
||||
const s3 = getRustFsClient();
|
||||
|
||||
// Build the GetObject command, optionally with a Range
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: RUSTFS_BUCKET,
|
||||
Key: key,
|
||||
...(rangeHeader ? { Range: rangeHeader } : {}),
|
||||
});
|
||||
|
||||
// Retry once on transient failures (cold S3 connections can fail the first request)
|
||||
let result;
|
||||
try {
|
||||
result = await s3.send(command);
|
||||
@@ -198,11 +200,14 @@ export async function getVideoStream(
|
||||
|
||||
const contentType = result.ContentType || "video/webm";
|
||||
|
||||
// Convert SDK stream to web ReadableStream
|
||||
const sdkStream = result.Body as any;
|
||||
const webStream: ReadableStream<Uint8Array> = sdkStream.transformToWebStream
|
||||
? sdkStream.transformToWebStream()
|
||||
: sdkStream;
|
||||
|
||||
// If we sent a range request, parse the Content-Range header from S3
|
||||
// Format: "bytes start-end/total"
|
||||
if (rangeHeader && result.ContentRange) {
|
||||
const match = result.ContentRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
|
||||
if (match) {
|
||||
@@ -218,6 +223,7 @@ export async function getVideoStream(
|
||||
}
|
||||
}
|
||||
|
||||
// No range request (or no Content-Range in response) — full object
|
||||
return {
|
||||
stream: webStream,
|
||||
contentType,
|
||||
|
||||
+3
-3
@@ -57,7 +57,7 @@
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan
|
||||
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||
|
||||
@@ -83,9 +83,9 @@
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function VideoDetail() {
|
||||
fetchVideo(id)
|
||||
.then((v) => {
|
||||
setVideo(v);
|
||||
document.title = `${getMapDisplayName(v.mapName)} - ${v.playerName} · surf.nathan.rip`;
|
||||
document.title = `${getMapDisplayName(v.mapName)} — ${v.playerName} · surf.nathan.rip`;
|
||||
})
|
||||
.catch((err) => setError(err.message));
|
||||
return () => {
|
||||
|
||||
Reference in New Issue
Block a user