Initial commit
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
import { Elysia, status } from "elysia";
|
||||
import {
|
||||
getAllVideos,
|
||||
getVideoById,
|
||||
deleteVideoById,
|
||||
insertVideo,
|
||||
updateThumbnailKey,
|
||||
updateTier,
|
||||
getVideoByMapAndPlayer,
|
||||
updateVideoPB,
|
||||
} from "../services/db";
|
||||
import {
|
||||
uploadObject,
|
||||
deleteObject,
|
||||
getObjectUrl,
|
||||
getPresignedUploadUrl,
|
||||
} from "../services/rustfs";
|
||||
import { parseMtvFile } from "../services/mtv-parser";
|
||||
import { getMapInfo } from "../services/momentum-api";
|
||||
import type { VideoEntry, PreviousPB } from "../types/video";
|
||||
|
||||
const ADMIN_TOKEN = process.env.ADMIN_TOKEN;
|
||||
|
||||
if (!ADMIN_TOKEN) {
|
||||
console.error(
|
||||
"ADMIN_TOKEN environment variable is required. Set it to secure upload/delete endpoints.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function sanitizeFileName(name: string): string {
|
||||
return name
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^a-zA-Z0-9_.\-]/g, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export const videoRoutes = new Elysia({ prefix: "/api/videos" })
|
||||
.guard({
|
||||
beforeHandle: ({ request }) => {
|
||||
const isWrite =
|
||||
request.method !== "GET" &&
|
||||
request.method !== "HEAD" &&
|
||||
request.method !== "OPTIONS";
|
||||
if (isWrite) {
|
||||
const auth = request.headers.get("authorization");
|
||||
const token = auth?.startsWith("Bearer ")
|
||||
? auth.slice(7)
|
||||
: null;
|
||||
if (!token || token !== ADMIN_TOKEN)
|
||||
return status(401, "Unauthorized");
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
.get("/", () => {
|
||||
const videos = getAllVideos();
|
||||
return videos.map((v) => ({
|
||||
...v,
|
||||
thumbnailUrl: v.thumbnailUrl
|
||||
? getObjectUrl(v.thumbnailUrl)
|
||||
: undefined,
|
||||
}));
|
||||
})
|
||||
|
||||
.post("/upload-url", async ({ request }) => {
|
||||
let formData: FormData;
|
||||
try {
|
||||
formData = await request.formData();
|
||||
} catch {
|
||||
return status(400, { error: "Failed to parse form data" });
|
||||
}
|
||||
|
||||
const mtv = formData.get("mtv") as File | null;
|
||||
const videoFileName = formData.get("videoFileName") as string | null;
|
||||
const videoContentType =
|
||||
(formData.get("videoContentType") as string | null) || "video/webm";
|
||||
const runDate = formData.get("runDate") as string | null;
|
||||
|
||||
if (!mtv || !videoFileName) {
|
||||
return status(400, {
|
||||
error: "mtv file and videoFileName are required",
|
||||
});
|
||||
}
|
||||
|
||||
const mtvBuffer = await mtv.arrayBuffer();
|
||||
const metadata = parseMtvFile(mtvBuffer);
|
||||
if (!metadata) {
|
||||
return status(400, {
|
||||
error: "Invalid .mtv file: could not parse header",
|
||||
});
|
||||
}
|
||||
|
||||
const existing = getVideoByMapAndPlayer(
|
||||
metadata.mapName,
|
||||
metadata.steamId,
|
||||
);
|
||||
if (existing) {
|
||||
if (existing.runTime <= metadata.runTime) {
|
||||
return status(409, {
|
||||
error: `PB not improved. Your current PB on ${metadata.mapName} is ${existing.runTime}, this run is ${metadata.runTime}`,
|
||||
});
|
||||
}
|
||||
|
||||
const deletes = [
|
||||
deleteObject(existing.videoKey),
|
||||
deleteObject(existing.mtvKey),
|
||||
];
|
||||
if (existing.thumbnailKey)
|
||||
deletes.push(deleteObject(existing.thumbnailKey));
|
||||
await Promise.all(deletes);
|
||||
|
||||
const previousPbs: PreviousPB[] = [
|
||||
{ runTime: existing.runTime, createdAt: existing.createdAt },
|
||||
...(existing.previousPbs ?? []),
|
||||
];
|
||||
|
||||
const id = existing.id;
|
||||
const createdAt =
|
||||
runDate && !isNaN(Date.parse(runDate))
|
||||
? new Date(runDate).toISOString()
|
||||
: new Date().toISOString();
|
||||
|
||||
const sanitizedVideoName = sanitizeFileName(videoFileName);
|
||||
const videoKey = `videos/${id}/${sanitizedVideoName}`;
|
||||
const mtvKey = `videos/${id}/${sanitizeFileName(mtv.name)}`;
|
||||
|
||||
await uploadObject(
|
||||
mtvKey,
|
||||
Buffer.from(mtvBuffer),
|
||||
"application/octet-stream",
|
||||
);
|
||||
|
||||
let tier: number | null | undefined;
|
||||
let mapId: number | null | undefined;
|
||||
let thumbnailKey: string | undefined;
|
||||
const mapInfo = await getMapInfo(metadata.mapName);
|
||||
mapId = mapInfo.mapId ?? undefined;
|
||||
tier = mapInfo.tier;
|
||||
|
||||
if (mapInfo.thumbnailUrl) {
|
||||
thumbnailKey = `videos/${id}/thumbnail.jpg`;
|
||||
try {
|
||||
const imgRes = await fetch(mapInfo.thumbnailUrl);
|
||||
if (imgRes.ok) {
|
||||
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
|
||||
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
|
||||
} else {
|
||||
thumbnailKey = undefined;
|
||||
}
|
||||
} catch {
|
||||
thumbnailKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const title = `${metadata.mapName} - ${metadata.playerName}`;
|
||||
const entry: VideoEntry = {
|
||||
id,
|
||||
title,
|
||||
description: "",
|
||||
mapName: metadata.mapName,
|
||||
playerName: metadata.playerName,
|
||||
steamId: metadata.steamId,
|
||||
runTime: metadata.runTime,
|
||||
totalTicks: metadata.totalTicks,
|
||||
tickInterval: metadata.tickInterval,
|
||||
videoKey,
|
||||
mtvKey,
|
||||
thumbnailKey,
|
||||
tier,
|
||||
mapId,
|
||||
jsonStats: metadata.jsonStats,
|
||||
createdAt,
|
||||
previousPbs,
|
||||
};
|
||||
|
||||
updateVideoPB(id, entry, previousPbs);
|
||||
|
||||
const presignedUrls = {
|
||||
video: await getPresignedUploadUrl(videoKey, videoContentType),
|
||||
};
|
||||
|
||||
return {
|
||||
...entry,
|
||||
presignedUrls,
|
||||
videoUrl: getObjectUrl(videoKey),
|
||||
mtvUrl: getObjectUrl(mtvKey),
|
||||
thumbnailUrl: thumbnailKey
|
||||
? getObjectUrl(thumbnailKey)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const createdAt =
|
||||
runDate && !isNaN(Date.parse(runDate))
|
||||
? new Date(runDate).toISOString()
|
||||
: new Date().toISOString();
|
||||
|
||||
const sanitizedVideoName = sanitizeFileName(videoFileName);
|
||||
const videoKey = `videos/${id}/${sanitizedVideoName}`;
|
||||
const mtvKey = `videos/${id}/${sanitizeFileName(mtv.name)}`;
|
||||
|
||||
await uploadObject(
|
||||
mtvKey,
|
||||
Buffer.from(mtvBuffer),
|
||||
"application/octet-stream",
|
||||
);
|
||||
|
||||
let tier: number | null | undefined;
|
||||
let mapId: number | null | undefined;
|
||||
let thumbnailKey: string | undefined;
|
||||
const mapInfo = await getMapInfo(metadata.mapName);
|
||||
mapId = mapInfo.mapId ?? undefined;
|
||||
tier = mapInfo.tier;
|
||||
|
||||
if (mapInfo.thumbnailUrl) {
|
||||
thumbnailKey = `videos/${id}/thumbnail.jpg`;
|
||||
try {
|
||||
const imgRes = await fetch(mapInfo.thumbnailUrl);
|
||||
if (imgRes.ok) {
|
||||
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
|
||||
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
|
||||
} else {
|
||||
thumbnailKey = undefined;
|
||||
}
|
||||
} catch {
|
||||
thumbnailKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const title = `${metadata.mapName} - ${metadata.playerName}`;
|
||||
const entry: VideoEntry = {
|
||||
id,
|
||||
title,
|
||||
description: "",
|
||||
mapName: metadata.mapName,
|
||||
playerName: metadata.playerName,
|
||||
steamId: metadata.steamId,
|
||||
runTime: metadata.runTime,
|
||||
totalTicks: metadata.totalTicks,
|
||||
tickInterval: metadata.tickInterval,
|
||||
videoKey,
|
||||
mtvKey,
|
||||
thumbnailKey,
|
||||
tier,
|
||||
mapId,
|
||||
jsonStats: metadata.jsonStats,
|
||||
createdAt,
|
||||
};
|
||||
|
||||
insertVideo(entry);
|
||||
|
||||
const presignedUrls = {
|
||||
video: await getPresignedUploadUrl(videoKey, videoContentType),
|
||||
};
|
||||
|
||||
return {
|
||||
...entry,
|
||||
presignedUrls,
|
||||
videoUrl: getObjectUrl(videoKey),
|
||||
mtvUrl: getObjectUrl(mtvKey),
|
||||
thumbnailUrl: thumbnailKey ? getObjectUrl(thumbnailKey) : undefined,
|
||||
};
|
||||
})
|
||||
|
||||
.post("/:id/complete", async ({ params: { id } }) => {
|
||||
const video = getVideoById(id);
|
||||
if (!video) return status(404, { error: "Not found" });
|
||||
|
||||
return {
|
||||
...video,
|
||||
videoUrl: getObjectUrl(video.videoKey),
|
||||
mtvUrl: getObjectUrl(video.mtvKey),
|
||||
thumbnailUrl: video.thumbnailKey
|
||||
? getObjectUrl(video.thumbnailKey)
|
||||
: undefined,
|
||||
};
|
||||
})
|
||||
|
||||
.get("/:id", ({ params: { id } }) => {
|
||||
const video = getVideoById(id);
|
||||
if (!video) return status(404, { error: "Not found" });
|
||||
return {
|
||||
...video,
|
||||
videoUrl: getObjectUrl(video.videoKey),
|
||||
mtvUrl: getObjectUrl(video.mtvKey),
|
||||
thumbnailUrl: video.thumbnailKey
|
||||
? getObjectUrl(video.thumbnailKey)
|
||||
: undefined,
|
||||
};
|
||||
})
|
||||
|
||||
.post("/:id/refresh-info", async ({ params: { id } }) => {
|
||||
const video = getVideoById(id);
|
||||
if (!video) return status(404, { error: "Not found" });
|
||||
|
||||
const mapInfo = await getMapInfo(video.mapName);
|
||||
|
||||
let thumbnailKey = video.thumbnailKey;
|
||||
if (!thumbnailKey && mapInfo.thumbnailUrl) {
|
||||
thumbnailKey = `videos/${video.id}/thumbnail.jpg`;
|
||||
try {
|
||||
const imgRes = await fetch(mapInfo.thumbnailUrl);
|
||||
if (imgRes.ok) {
|
||||
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
|
||||
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
|
||||
updateThumbnailKey(id, thumbnailKey);
|
||||
} else {
|
||||
thumbnailKey = undefined;
|
||||
}
|
||||
} catch {
|
||||
thumbnailKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
updateTier(id, mapInfo.tier);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tier: mapInfo.tier,
|
||||
thumbnailUrl: thumbnailKey ? getObjectUrl(thumbnailKey) : undefined,
|
||||
};
|
||||
})
|
||||
|
||||
.post("/:id/refresh-thumbnail", async ({ params: { id } }) => {
|
||||
const video = getVideoById(id);
|
||||
if (!video) return status(404, { error: "Not found" });
|
||||
|
||||
const mapInfo = await getMapInfo(video.mapName);
|
||||
if (!mapInfo.thumbnailUrl) {
|
||||
return status(404, {
|
||||
error: `No thumbnail found on Momentum Mod for ${video.mapName}`,
|
||||
});
|
||||
}
|
||||
|
||||
const thumbnailKey = `videos/${video.id}/thumbnail.jpg`;
|
||||
try {
|
||||
const imgRes = await fetch(mapInfo.thumbnailUrl);
|
||||
if (!imgRes.ok)
|
||||
return status(502, { error: "Failed to download thumbnail" });
|
||||
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
|
||||
await uploadObject(thumbnailKey, imgBuf, "image/jpeg");
|
||||
} catch {
|
||||
return status(502, { error: "Failed to download thumbnail" });
|
||||
}
|
||||
|
||||
updateThumbnailKey(id, thumbnailKey);
|
||||
updateTier(id, mapInfo.tier);
|
||||
|
||||
return { success: true, thumbnailUrl: getObjectUrl(thumbnailKey) };
|
||||
})
|
||||
|
||||
.delete("/:id", async ({ params: { id } }) => {
|
||||
const video = getVideoById(id);
|
||||
if (!video) return status(404, { error: "Not found" });
|
||||
|
||||
const deletes = [
|
||||
deleteObject(video.videoKey),
|
||||
deleteObject(video.mtvKey),
|
||||
];
|
||||
if (video.thumbnailKey) deletes.push(deleteObject(video.thumbnailKey));
|
||||
await Promise.all(deletes);
|
||||
|
||||
deleteVideoById(id);
|
||||
return { success: true };
|
||||
});
|
||||
Reference in New Issue
Block a user