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