import { desc, eq } from "drizzle-orm"; import { db } from "@/lib/db/client"; import { recordings, sessionDevices, devices, playSessions, type RecordingDeviceSlot } from "@/lib/db/schema"; import { getEventsForSession } from "./session-events"; /** * A recording is a thin pointer over an already-captured play_session, not a * duplicated event pipeline: every live command is always persisted to * session_events (stats need it regardless of whether the user saves a * recording), so "saving a recording" just snapshots the session's device * slots and points a new row at it. This keeps the replay-loader query and * the stats-timeline query reading the exact same rows, with zero risk of * live/recorded data drifting apart. */ export async function createRecording(input: { sourcePlaySessionId: number; name: string; description?: string; }) { const [session] = await db.select().from(playSessions).where(eq(playSessions.id, input.sourcePlaySessionId)); if (!session) throw new Error("source play session not found"); const sessionDeviceRows = await db .select({ sessionDeviceId: sessionDevices.id, slotLabel: sessionDevices.slotLabel, bleName: devices.bleName, deviceClass: devices.deviceClass, capabilities: devices.capabilities, }) .from(sessionDevices) .innerJoin(devices, eq(sessionDevices.deviceId, devices.id)) .where(eq(sessionDevices.playSessionId, input.sourcePlaySessionId)); const deviceSlots: RecordingDeviceSlot[] = sessionDeviceRows.map((row) => ({ slotLabel: row.slotLabel, recordedBleName: row.bleName, deviceClass: row.deviceClass, capabilities: row.capabilities, sourceSessionDeviceId: row.sessionDeviceId, })); const [recording] = await db .insert(recordings) .values({ sourcePlaySessionId: input.sourcePlaySessionId, name: input.name, description: input.description, createdAt: Date.now(), durationMs: session.durationMs ?? 0, deviceSlots, playCount: 0, }) .returning(); return recording; } export async function listRecordings() { return db.select().from(recordings).orderBy(desc(recordings.createdAt)); } export async function getRecording(id: number) { const [recording] = await db.select().from(recordings).where(eq(recordings.id, id)); if (!recording) return undefined; const events = await getEventsForSession(recording.sourcePlaySessionId); return { recording, events }; } export async function renameRecording(id: number, input: { name?: string; description?: string }) { const [updated] = await db.update(recordings).set(input).where(eq(recordings.id, id)).returning(); return updated; } export async function deleteRecording(id: number) { await db.delete(recordings).where(eq(recordings.id, id)); } export async function incrementPlayCount(id: number, playedAt: number): Promise { const [existing] = await db.select().from(recordings).where(eq(recordings.id, id)); if (!existing) return; await db .update(recordings) .set({ playCount: existing.playCount + 1, lastPlayedAt: playedAt }) .where(eq(recordings.id, id)); }