Remove recordings feature, replay sessions directly, bump to 0.6.0
CI / Build and push image (push) Successful in 1m41s
CI / Static checks (push) Successful in 2m12s

Recordings were just a thin named pointer over an already-captured
session's events, so the whole separate feature (recordings table, API
routes, pages, UI) is gone: any completed session can now be named and
replayed directly. Replaying no longer creates a session or duplicates
events of its own - it just bumps the source session's playCount/lastPlayedAt.

Also renames play_sessions/playSession(s) to sessions/session(s) throughout
the schema, queries, API routes, and UI for consistency, and updates the
README to match the new flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 21:38:51 +02:00
co-authored by Claude Sonnet 5
parent 2a3c4ff1f2
commit 401b9b5033
46 changed files with 1710 additions and 1018 deletions
+11 -19
View File
@@ -1,9 +1,8 @@
import { getDevice } from "./client";
import { buildOutputCommand, findFeature, type ButtplugRuntime } from "./commands";
import { eventBuffer } from "./event-buffer";
import type { ActuatorInfo, CommandEvent } from "./types";
export interface RecordingEventRow {
export interface SessionEventRow {
tsMs: number;
commandType: CommandEvent["commandType"];
featureIndex: number;
@@ -13,13 +12,14 @@ export interface RecordingEventRow {
}
export interface PlayerOptions {
events: RecordingEventRow[];
/** The recording's actual duration (recordings.durationMs from the DB) - this is the source
* of truth for playback length, NOT the last event's timestamp: a recording can run for a
* while after its last command (e.g. the user stopped the toy but let the session continue),
* so deriving duration from events would end playback early and misreport it as "finished". */
events: SessionEventRow[];
/** The source session's actual duration (sessions.duration_ms from the DB) - this is
* the source of truth for playback length, NOT the last event's timestamp: a session can run
* for a while after its last command (e.g. the user stopped the toy but let the session
* continue), so deriving duration from events would end playback early and misreport it as
* "finished". */
durationMs: number;
/** Recording's session_device_id -> currently-connected device index, from the remap step. */
/** Source session's session_device_id -> currently-connected device index, from the remap step. */
sessionDeviceIdToDeviceIndex: Map<number, number>;
actuatorsByDeviceIndex: Map<number, ActuatorInfo[]>;
runtime: ButtplugRuntime;
@@ -30,11 +30,11 @@ export interface PlayerOptions {
}
/**
* Replays a recording's events against the currently-connected devices,
* Replays a session's events against the currently-connected devices,
* using performance.now()-relative scheduling (not wall-clock Date.now())
* so long sessions don't accumulate drift from setTimeout jitter.
*/
export class RecordingPlayer {
export class SessionPlayer {
private readonly durationMs: number;
private startedAtPerf = 0;
private pausedAtMs = 0;
@@ -110,7 +110,7 @@ export class RecordingPlayer {
}, 200);
}
private async dispatch(event: RecordingEventRow): Promise<void> {
private async dispatch(event: SessionEventRow): Promise<void> {
const deviceIndex = this.options.sessionDeviceIdToDeviceIndex.get(event.sessionDeviceId);
if (deviceIndex === undefined) return;
@@ -130,14 +130,6 @@ export class RecordingPlayer {
const cmd = buildOutputCommand(this.options.runtime, actuator, event.value, event.durationMs ?? undefined);
await feature.runOutput(cmd);
}
eventBuffer.record({
deviceIndex,
commandType: event.commandType,
featureIndex: event.featureIndex,
value: event.value,
durationMs: event.durationMs ?? undefined,
});
} catch (err) {
this.options.onError?.(err instanceof Error ? err.message : String(err));
}