230 lines
6.5 KiB
TypeScript
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;
|
|
}
|