Files
surfnathanrip/backend/src/services/db.ts
T
CallMeVerity eb56ad5183 Initial commit
2026-06-03 00:44:48 +01:00

230 lines
6.5 KiB
TypeScript

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;
}