Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87e408f1a7 | |||
| 32e13fcc85 | |||
| f0f50a6f12 | |||
| c0b93a2a6e | |||
| ff2c3997a0 | |||
| 8121369a58 | |||
| cc3904705c | |||
| 17ea6ce7c4 | |||
| 1704b0b736 | |||
| 6da2539c03 | |||
| dc021e4856 | |||
| ba9752d33c | |||
| 3369f22f69 |
+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/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//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
|
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN
|
||||||
|
|
||||||
"@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=="],
|
"@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/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//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
"@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/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=="],
|
"@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/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//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
|
|||||||
+1
-13
@@ -24,7 +24,7 @@ function mapDisplayName(mapName: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pageTitle(video: VideoEntry): 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 {
|
function ogTitle(video: VideoEntry): string {
|
||||||
@@ -49,7 +49,6 @@ function buildOgsHtml(video: VideoEntry): string {
|
|||||||
const videoStreamUrl = `${BASE_URL}${getStreamUrl(video.id)}`;
|
const videoStreamUrl = `${BASE_URL}${getStreamUrl(video.id)}`;
|
||||||
|
|
||||||
let tags =
|
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` +
|
` <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:title" content="${escapeHtml(title)}" />\n` +
|
||||||
` <meta property="og:description" content="${escapeHtml(description)}" />\n` +
|
` <meta property="og:description" content="${escapeHtml(description)}" />\n` +
|
||||||
@@ -64,14 +63,12 @@ function buildOgsHtml(video: VideoEntry): string {
|
|||||||
` <meta property="og:image:height" content="720" />`;
|
` <meta property="og:image:height" content="720" />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video embed — Discord & Facebook
|
|
||||||
tags +=
|
tags +=
|
||||||
`\n <meta property="og:video" content="${videoStreamUrl}" />\n` +
|
`\n <meta property="og:video" content="${videoStreamUrl}" />\n` +
|
||||||
` <meta property="og:video:type" content="video/mp4" />\n` +
|
` <meta property="og:video:type" content="video/mp4" />\n` +
|
||||||
` <meta property="og:video:width" content="1280" />\n` +
|
` <meta property="og:video:width" content="1280" />\n` +
|
||||||
` <meta property="og:video:height" content="720" />`;
|
` <meta property="og:video:height" content="720" />`;
|
||||||
|
|
||||||
// Twitter / X
|
|
||||||
tags +=
|
tags +=
|
||||||
`\n <meta name="twitter:card" content="summary_large_image" />\n` +
|
`\n <meta name="twitter:card" content="summary_large_image" />\n` +
|
||||||
` <meta name="twitter:title" content="${escapeHtml(title)}" />\n` +
|
` <meta name="twitter:title" content="${escapeHtml(title)}" />\n` +
|
||||||
@@ -81,8 +78,6 @@ function buildOgsHtml(video: VideoEntry): string {
|
|||||||
tags += `\n <meta name="twitter:image" content="${thumbnailUrl}" />`;
|
tags += `\n <meta name="twitter:image" content="${thumbnailUrl}" />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Steam uses the same og: tags, nothing extra needed
|
|
||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +113,6 @@ 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(
|
export function renderRunPage(
|
||||||
id: string,
|
id: string,
|
||||||
staticDir: string,
|
staticDir: string,
|
||||||
@@ -139,7 +128,6 @@ export function renderRunPage(
|
|||||||
|
|
||||||
const video = getVideoById(id);
|
const video = getVideoById(id);
|
||||||
if (!video) {
|
if (!video) {
|
||||||
// Serve the plain SPA — React Router handles the 404
|
|
||||||
return { html: indexHtml, contentType: "text/html" };
|
return { html: indexHtml, contentType: "text/html" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -391,7 +391,6 @@ export const videoRoutes = new Elysia({ prefix: "/api/videos" })
|
|||||||
":",
|
":",
|
||||||
msg,
|
msg,
|
||||||
);
|
);
|
||||||
// S3 errors are server errors, not "not found"
|
|
||||||
const statusCode = msg.includes("NoSuchKey") ? 404 : 502;
|
const statusCode = msg.includes("NoSuchKey") ? 404 : 502;
|
||||||
return status(statusCode, {
|
return status(statusCode, {
|
||||||
error:
|
error:
|
||||||
|
|||||||
@@ -168,14 +168,12 @@ export async function getVideoStream(
|
|||||||
): Promise<VideoStreamResult> {
|
): Promise<VideoStreamResult> {
|
||||||
const s3 = getRustFsClient();
|
const s3 = getRustFsClient();
|
||||||
|
|
||||||
// Build the GetObject command, optionally with a Range
|
|
||||||
const command = new GetObjectCommand({
|
const command = new GetObjectCommand({
|
||||||
Bucket: RUSTFS_BUCKET,
|
Bucket: RUSTFS_BUCKET,
|
||||||
Key: key,
|
Key: key,
|
||||||
...(rangeHeader ? { Range: rangeHeader } : {}),
|
...(rangeHeader ? { Range: rangeHeader } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Retry once on transient failures (cold S3 connections can fail the first request)
|
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await s3.send(command);
|
result = await s3.send(command);
|
||||||
@@ -200,14 +198,11 @@ export async function getVideoStream(
|
|||||||
|
|
||||||
const contentType = result.ContentType || "video/webm";
|
const contentType = result.ContentType || "video/webm";
|
||||||
|
|
||||||
// Convert SDK stream to web ReadableStream
|
|
||||||
const sdkStream = result.Body as any;
|
const sdkStream = result.Body as any;
|
||||||
const webStream: ReadableStream<Uint8Array> = sdkStream.transformToWebStream
|
const webStream: ReadableStream<Uint8Array> = sdkStream.transformToWebStream
|
||||||
? sdkStream.transformToWebStream()
|
? sdkStream.transformToWebStream()
|
||||||
: sdkStream;
|
: sdkStream;
|
||||||
|
|
||||||
// If we sent a range request, parse the Content-Range header from S3
|
|
||||||
// Format: "bytes start-end/total"
|
|
||||||
if (rangeHeader && result.ContentRange) {
|
if (rangeHeader && result.ContentRange) {
|
||||||
const match = result.ContentRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
|
const match = result.ContentRange.match(/bytes (\d+)-(\d+)\/(\d+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -223,7 +218,6 @@ export async function getVideoStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No range request (or no Content-Range in response) — full object
|
|
||||||
return {
|
return {
|
||||||
stream: webStream,
|
stream: webStream,
|
||||||
contentType,
|
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/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//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
"@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/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=="],
|
"@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-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///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
"@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)
|
fetchVideo(id)
|
||||||
.then((v) => {
|
.then((v) => {
|
||||||
setVideo(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));
|
.catch((err) => setError(err.message));
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user