feat(audio-player): per-book playback speed + user default (plan §8)

Speed becomes a per-USER-per-BOOK preference that syncs across
devices, with a user-level default for books not yet played:
- new audio_playback_speed table (mirrors reading_speed's shape; a
  preference, NOT a measurement and NOT a reading_progress column) +
  users.default_audio_speed, schema in place
- chapters endpoint resolves per-book row → user default → 1.0 and
  ships playback_speed + default_playback_speed (zero extra round
  trips; default powers the dropdown marker)
- PUT /api/media-items/:id/audio-speed upserts (range-checked; the
  client SPEEDS list stays the real gate; library visibility gates
  writes)
- the user default rides the profile path like theme/timezone:
  profile form gains a 'Default Playback Speed' select (server-side
  SpeedOptions mirrors the web SPEEDS list), GET /profile carries it
- web: restorePosition applies the resolved server speed (localStorage
  audio_speed demotes to a first-paint cache); setSpeed PUTs
  fire-and-forget with a failure toast; the Speed dropdown gains a
  '(default)' row marker and a 'Make N× my default' action with a
  success toast

ALSO fixes a pre-existing profile-page bug surfaced by the §8 form
verification: the password section's current_password input was
required INSIDE the shared form, so HTML5 validation blocked every
Save Changes submission (dead since the shell migration). The
password flow keeps its own gates (disabled-button checkers + server
400).

Integration test TestAudioPlaybackSpeed pins the full contract (unset
→ 1x, upsert resolution, range rejection, default fallback for
row-less books, default updates re-satisfying unset books). Verified
live: 9 Playwright checks.
This commit is contained in:
John O'Keefe committed 2026-10-22 15:01:41 -04:00
1 parent 3325b64463
commit 3576a4415a
16 files changed
+680 -148

No files matched your search

+23
View File
@@ -37,6 +37,11 @@ CREATE TABLE IF NOT EXISTS users (
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- User-level default audiobook playback speed (player plan §8): books
-- without a per-book audio_playback_speed row pick this up. REAL
-- mirrors the per-book table's column.
ALTER TABLE users ADD COLUMN IF NOT EXISTS default_audio_speed REAL NOT NULL DEFAULT 1.0;
-- Create system_settings table
CREATE TABLE IF NOT EXISTS system_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -1563,6 +1568,24 @@ CREATE TABLE IF NOT EXISTS reading_speed (
CREATE INDEX IF NOT EXISTS idx_reading_speed_user ON reading_speed(user_id);
CREATE INDEX IF NOT EXISTS idx_reading_speed_item ON reading_speed(media_item_id);
-- Per-user PER-BOOK playback speed preference (player plan §8): a
-- preference, NOT a measurement (reading_speed holds measured rates),
-- and NOT a reading_progress column (a speed tap must not create a
-- "currently reading" row). Resolution lives at the chapters endpoint:
-- per-book row → users.default_audio_speed → 1.0.
CREATE TABLE IF NOT EXISTS audio_playback_speed (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
speed REAL NOT NULL DEFAULT 1.0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, media_item_id)
);
CREATE INDEX IF NOT EXISTS idx_audio_playback_speed_user ON audio_playback_speed(user_id);
CREATE INDEX IF NOT EXISTS idx_audio_playback_speed_item ON audio_playback_speed(media_item_id);
-- Dictionary cache (for offline use)
CREATE TABLE IF NOT EXISTS dictionary_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),