Initial commit

This commit is contained in:
CallMeVerity
2026-06-03 00:44:48 +01:00
commit 3369f22f69
36 changed files with 3419 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
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 } from "fs";
initDb();
const PORT = parseInt(process.env.PORT || "3001");
const STATIC_DIR = process.env.STATIC_DIR || "./public";
const app = new Elysia()
.use(cors())
.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)) return Bun.file(filePath);
const indexPath = join(STATIC_DIR, "index.html");
if (existsSync(indexPath)) return Bun.file(indexPath);
return status(404, "Not found");
})
.listen(PORT);
console.log(`surf.nathan.rip running on port ${PORT}`);
console.log(`Serving static files from ${STATIC_DIR}`);
+367
View File
@@ -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 };
});
+229
View File
@@ -0,0 +1,229 @@
import { Database } from "bun:sqlite";
import { mkdirSync } from "fs";
import { dirname } from "path";
import type { VideoEntry, VideoListItem, PreviousPB } from "../types/video";
const DB_PATH = process.env.DB_PATH || "./data/surf.db";
let db: Database | null = null;
export function initDb(): void {
if (db) return;
mkdirSync(dirname(DB_PATH), { recursive: true });
db = new Database(DB_PATH, { create: true });
db.exec("PRAGMA journal_mode = WAL");
db.exec("PRAGMA strict_mode = ON");
db.exec(`
CREATE TABLE IF NOT EXISTS videos (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
map_name TEXT NOT NULL,
player_name TEXT NOT NULL,
steam_id TEXT NOT NULL,
run_time REAL NOT NULL,
total_ticks INTEGER NOT NULL,
tick_interval REAL NOT NULL,
video_key TEXT NOT NULL,
mtv_key TEXT NOT NULL,
thumbnail_key TEXT,
tier INTEGER,
json_stats TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_videos_map ON videos(map_name);
CREATE INDEX IF NOT EXISTS idx_videos_created ON videos(created_at DESC);
`);
const columns = db.prepare("PRAGMA table_info(videos)").all() as {
name: string;
}[];
if (!columns.some((col) => col.name === "tier")) {
db.exec("ALTER TABLE videos ADD COLUMN tier INTEGER");
}
if (!columns.some((col) => col.name === "map_id")) {
db.exec("ALTER TABLE videos ADD COLUMN map_id INTEGER");
}
if (!columns.some((col) => col.name === "rank")) {
db.exec("ALTER TABLE videos ADD COLUMN rank INTEGER");
}
if (!columns.some((col) => col.name === "rank_updated_at")) {
db.exec("ALTER TABLE videos ADD COLUMN rank_updated_at TEXT");
}
if (!columns.some((col) => col.name === "previous_pbs")) {
db.exec("ALTER TABLE videos ADD COLUMN previous_pbs TEXT");
}
}
function getDb(): Database {
if (!db) initDb();
return db!;
}
export function insertVideo(entry: VideoEntry): void {
const d = getDb();
d.prepare(
`INSERT INTO videos (id, title, description, map_name, player_name, steam_id, run_time, total_ticks, tick_interval, video_key, mtv_key, thumbnail_key, tier, map_id, json_stats, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
entry.id,
entry.title,
entry.description,
entry.mapName,
entry.playerName,
entry.steamId,
entry.runTime,
entry.totalTicks,
entry.tickInterval,
entry.videoKey,
entry.mtvKey,
entry.thumbnailKey ?? null,
entry.tier ?? null,
entry.mapId ?? null,
entry.jsonStats ?? null,
entry.createdAt,
);
}
export function getAllVideos(): VideoListItem[] {
const d = getDb();
return d
.prepare(
`SELECT id, title, map_name, player_name, run_time, tier, thumbnail_key, created_at
FROM videos ORDER BY created_at DESC`,
)
.all()
.map(rowToListItem);
}
export function getVideoById(id: string): VideoEntry | null {
const d = getDb();
const row = d.prepare(`SELECT * FROM videos WHERE id = ?`).get(id);
return row ? rowToEntry(row as any) : null;
}
export function deleteVideoById(id: string): boolean {
const d = getDb();
const result = d.prepare(`DELETE FROM videos WHERE id = ?`).run(id);
return result.changes > 0;
}
function rowToListItem(row: any): VideoListItem {
return {
id: row.id,
title: row.title,
mapName: row.map_name,
playerName: row.player_name,
runTime: row.run_time,
tier: row.tier ?? undefined,
thumbnailUrl: row.thumbnail_key || undefined,
createdAt: row.created_at,
previousPbs: row.previous_pbs
? JSON.parse(row.previous_pbs)
: undefined,
};
}
function rowToEntry(row: any): VideoEntry {
return {
id: row.id,
title: row.title,
description: row.description,
mapName: row.map_name,
playerName: row.player_name,
steamId: row.steam_id,
runTime: row.run_time,
totalTicks: row.total_ticks,
tickInterval: row.tick_interval,
videoKey: row.video_key,
mtvKey: row.mtv_key,
thumbnailKey: row.thumbnail_key ?? undefined,
tier: row.tier ?? undefined,
mapId: row.map_id ?? undefined,
jsonStats: row.json_stats ?? undefined,
createdAt: row.created_at,
previousPbs: row.previous_pbs
? JSON.parse(row.previous_pbs)
: undefined,
};
}
export function updateThumbnailKey(
id: string,
thumbnailKey: string | null,
): boolean {
const d = getDb();
const result = d
.prepare("UPDATE videos SET thumbnail_key = ? WHERE id = ?")
.run(thumbnailKey, id);
return result.changes > 0;
}
export function getVideoByMapAndPlayer(
mapName: string,
steamId: string,
): VideoEntry | null {
const d = getDb();
const row = d
.prepare(`SELECT * FROM videos WHERE map_name = ? AND steam_id = ?`)
.get(mapName, steamId);
return row ? rowToEntry(row as any) : null;
}
export function updateVideoPB(
id: string,
entry: VideoEntry,
previousPbs: PreviousPB[],
): void {
const d = getDb();
d.prepare(
`UPDATE videos SET
title = ?,
description = ?,
map_name = ?,
player_name = ?,
steam_id = ?,
run_time = ?,
total_ticks = ?,
tick_interval = ?,
video_key = ?,
mtv_key = ?,
thumbnail_key = ?,
tier = ?,
map_id = ?,
json_stats = ?,
created_at = ?,
previous_pbs = ?
WHERE id = ?`,
).run(
entry.title,
entry.description,
entry.mapName,
entry.playerName,
entry.steamId,
entry.runTime,
entry.totalTicks,
entry.tickInterval,
entry.videoKey,
entry.mtvKey,
entry.thumbnailKey ?? null,
entry.tier ?? null,
entry.mapId ?? null,
entry.jsonStats ?? null,
entry.createdAt,
JSON.stringify(previousPbs),
id,
);
}
export function updateTier(id: string, tier: number | null): boolean {
const d = getDb();
const result = d
.prepare("UPDATE videos SET tier = ? WHERE id = ?")
.run(tier, id);
return result.changes > 0;
}
+114
View File
@@ -0,0 +1,114 @@
const MOMENTUM_API = "https://api.momentum-mod.org/v1";
interface MomentumMapThumbnail {
id: string;
small: string;
medium: string;
large: string;
xl: string;
}
interface MomentumMapLeaderboard {
gamemode: number;
trackType: number;
trackNum: number;
style: number;
tier: number | null;
tags: number[];
type: number;
linear: boolean | null;
}
interface MomentumMapInfo {
description: string;
youtubeID: string;
creationDate: string;
approvedDate: string;
requiredGames: number[];
}
interface MomentumMap {
id: number;
name: string;
thumbnail: MomentumMapThumbnail | null;
images: MomentumMapThumbnail[];
info: MomentumMapInfo | null;
leaderboards: MomentumMapLeaderboard[];
}
interface MomentumMapsResponse {
totalCount: number;
returnCount: number;
data: MomentumMap[];
}
export interface MapInfo {
mapId: number | null;
thumbnailUrl: string | null;
tier: number | null;
mapDate: string | null;
}
const mapInfoCache = new Map<string, MapInfo>();
function extractSurfTier(
leaderboards: MomentumMapLeaderboard[],
): number | null {
const mainLeaderboard = leaderboards.find(
(lb) => lb.gamemode === 1 && lb.trackType === 0 && lb.style === 0,
);
return mainLeaderboard?.tier ?? null;
}
export async function getMapInfo(mapName: string): Promise<MapInfo> {
const cached = mapInfoCache.get(mapName);
if (cached) return cached;
try {
const url = `${MOMENTUM_API}/maps?search=${encodeURIComponent(mapName)}&take=1&expand=info,leaderboards`;
const res = await fetch(url);
if (!res.ok) {
const info: MapInfo = {
mapId: null,
thumbnailUrl: null,
tier: null,
mapDate: null,
};
mapInfoCache.set(mapName, info);
return info;
}
const json: MomentumMapsResponse = await res.json();
const map = json.data?.[0];
if (!map || map.name !== mapName) {
const info: MapInfo = {
mapId: null,
thumbnailUrl: null,
tier: null,
mapDate: null,
};
mapInfoCache.set(mapName, info);
return info;
}
const thumbnailUrl =
map.thumbnail?.small ?? map.images?.[0]?.small ?? null;
const tier = extractSurfTier(map.leaderboards ?? []);
const mapDate = map.info?.creationDate ?? null;
const info: MapInfo = { mapId: map.id, thumbnailUrl, tier, mapDate };
mapInfoCache.set(mapName, info);
return info;
} catch {
const info: MapInfo = {
mapId: null,
thumbnailUrl: null,
tier: null,
mapDate: null,
};
mapInfoCache.set(mapName, info);
return info;
}
}
export async function getMapThumbnail(mapName: string): Promise<string | null> {
const info = await getMapInfo(mapName);
return info.thumbnailUrl;
}
+97
View File
@@ -0,0 +1,97 @@
import type { MtvHeader, MtvMetadata } from "../types/video";
const MTV_MAGIC = 0x56544d4d;
const HEADER_SIZE = 0xc3;
function readNullTerminatedString(buffer: ArrayBuffer, offset: number, maxLen: number): string {
const view = new Uint8Array(buffer, offset, maxLen);
let end = 0;
while (end < maxLen && view[end] !== 0) end++;
return new TextDecoder().decode(view.slice(0, end));
}
export function parseMtvHeader(buffer: ArrayBuffer): MtvHeader | null {
if (buffer.byteLength < HEADER_SIZE) return null;
const view = new DataView(buffer);
const magic = view.getUint32(0, true);
if (magic !== MTV_MAGIC) return null;
const version = view.getUint32(4, true);
const unknown08 = view.getFloat32(8, true);
const unknown0c = view.getUint32(0x0c, true);
const mapName = readNullTerminatedString(buffer, 0x10, 64);
const mapHash = readNullTerminatedString(buffer, 0x50, 41);
const unknown79 = view.getUint8(0x79);
const compression = view.getUint8(0x7a);
const tickInterval = view.getFloat32(0x7b, true);
const steamId = view.getBigUint64(0x7f, true);
const playerName = readNullTerminatedString(buffer, 0x87, 32);
const unknownA7 = view.getUint8(0xa7);
const unknownA8 = view.getUint8(0xa8);
const runTime = view.getFloat64(0xa9, true);
const totalTicks = view.getUint32(0xb1, true);
const seekEntryCount = view.getUint32(0xb5, true);
const baselineCount = view.getUint32(0xb9, true);
const entityTypeCount = view.getUint8(0xbd);
const tempEntityCount = view.getUint8(0xbe);
const stringTableCount = view.getUint8(0xbf);
return {
magic,
version,
unknown08,
unknown0c,
mapName,
mapHash,
unknown79,
compression,
tickInterval,
steamId,
playerName,
unknownA7,
unknownA8,
runTime,
totalTicks,
seekEntryCount,
baselineCount,
entityTypeCount,
tempEntityCount,
stringTableCount,
};
}
export function parseMtvFile(buffer: ArrayBuffer): MtvMetadata | null {
const header = parseMtvHeader(buffer);
if (!header) return null;
let jsonStats: string | undefined;
if (buffer.byteLength > HEADER_SIZE + 4) {
const view = new DataView(buffer);
const jsonLen = view.getUint32(HEADER_SIZE, true);
if (jsonLen > 0 && buffer.byteLength >= HEADER_SIZE + 4 + jsonLen) {
const jsonBytes = new Uint8Array(buffer, HEADER_SIZE + 4, jsonLen - 1);
jsonStats = new TextDecoder().decode(jsonBytes);
}
}
return {
mapName: header.mapName,
mapHash: header.mapHash,
playerName: header.playerName,
steamId: header.steamId.toString(),
runTime: header.runTime,
totalTicks: header.totalTicks,
tickInterval: header.tickInterval,
version: header.version,
jsonStats,
};
}
export function formatRunTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toFixed(3).padStart(6, "0")}`;
}
+82
View File
@@ -0,0 +1,82 @@
import {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const RUSTFS_ENDPOINT = process.env.RUSTFS_ENDPOINT || "http://localhost:9000";
const RUSTFS_ACCESS_KEY = process.env.RUSTFS_ACCESS_KEY || "minioadmin";
const RUSTFS_SECRET_KEY = process.env.RUSTFS_SECRET_KEY || "minioadmin";
const RUSTFS_BUCKET = process.env.RUSTFS_BUCKET || "surf";
const RUSTFS_PUBLIC = (process.env.RUSTFS_PUBLIC || "true") === "true";
let client: S3Client | null = null;
function getRustFsClient(): S3Client {
if (!client) {
client = new S3Client({
endpoint: RUSTFS_ENDPOINT,
region: "us-east-1",
credentials: {
accessKeyId: RUSTFS_ACCESS_KEY,
secretAccessKey: RUSTFS_SECRET_KEY,
},
forcePathStyle: true,
});
}
return client;
}
function getPublicUrl(key: string): string {
if (RUSTFS_PUBLIC) {
return `${RUSTFS_ENDPOINT}/${RUSTFS_BUCKET}/${key}`;
}
return "";
}
export function getObjectUrl(key: string): string {
const publicUrl = getPublicUrl(key);
if (publicUrl) return publicUrl;
return `/api/videos/file/${encodeURIComponent(key)}`;
}
export async function uploadObject(
key: string,
body: Buffer,
contentType: string,
): Promise<void> {
const s3 = getRustFsClient();
await s3.send(
new PutObjectCommand({
Bucket: RUSTFS_BUCKET,
Key: key,
Body: body,
ContentType: contentType,
}),
);
}
export async function deleteObject(key: string): Promise<void> {
const s3 = getRustFsClient();
await s3.send(
new DeleteObjectCommand({
Bucket: RUSTFS_BUCKET,
Key: key,
}),
);
}
export async function getPresignedUploadUrl(
key: string,
contentType: string,
expiresInSeconds = 3600,
): Promise<string> {
const s3 = getRustFsClient();
const command = new PutObjectCommand({
Bucket: RUSTFS_BUCKET,
Key: key,
ContentType: contentType,
});
return getSignedUrl(s3, command, { expiresIn: expiresInSeconds });
}
+71
View File
@@ -0,0 +1,71 @@
export interface MtvHeader {
magic: number;
version: number;
unknown08: number;
unknown0c: number;
mapName: string;
mapHash: string;
unknown79: number;
compression: number;
tickInterval: number;
steamId: bigint;
playerName: string;
unknownA7: number;
unknownA8: number;
runTime: number;
totalTicks: number;
seekEntryCount: number;
baselineCount: number;
entityTypeCount: number;
tempEntityCount: number;
stringTableCount: number;
}
export interface MtvMetadata {
mapName: string;
mapHash: string;
playerName: string;
steamId: string;
runTime: number;
totalTicks: number;
tickInterval: number;
version: number;
jsonStats?: string;
}
export interface PreviousPB {
runTime: number;
createdAt: string;
}
export interface VideoEntry {
id: string;
title: string;
description: string;
mapName: string;
playerName: string;
steamId: string;
runTime: number;
totalTicks: number;
tickInterval: number;
videoKey: string;
mtvKey: string;
thumbnailKey?: string;
tier?: number | null;
mapId?: number | null;
jsonStats?: string;
createdAt: string;
previousPbs?: PreviousPB[];
}
export interface VideoListItem {
id: string;
title: string;
mapName: string;
playerName: string;
runTime: number;
tier?: number | null;
thumbnailUrl?: string;
createdAt: string;
previousPbs?: PreviousPB[];
}