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:
1 parent
3325b64463
commit
3576a4415a
16 files changed
+680
-148
No files matched your search
+11
-10
@@ -56,16 +56,17 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
|
||||
}
|
||||
|
||||
return templates.User{
|
||||
ID: userID,
|
||||
Email: userEmail,
|
||||
Username: userUsername,
|
||||
Role: userRole,
|
||||
Theme: userTheme,
|
||||
Token: token,
|
||||
Timezone: userTimezone,
|
||||
FirstName: utils.PgTextString(userDB.FirstName),
|
||||
LastName: utils.PgTextString(userDB.LastName),
|
||||
CreatedAt: userDB.CreatedAt.Time,
|
||||
ID: userID,
|
||||
Email: userEmail,
|
||||
Username: userUsername,
|
||||
Role: userRole,
|
||||
Theme: userTheme,
|
||||
Token: token,
|
||||
Timezone: userTimezone,
|
||||
FirstName: utils.PgTextString(userDB.FirstName),
|
||||
LastName: utils.PgTextString(userDB.LastName),
|
||||
CreatedAt: userDB.CreatedAt.Time,
|
||||
DefaultAudioSpeed: float64(userDB.DefaultAudioSpeed),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ func registerMediaRoutes(cfg *Config) {
|
||||
// books stream through the existing /uploads Range-serving path;
|
||||
// multi-file books additionally expose per-track streaming.
|
||||
protected.GET("/media-items/:id/chapters", cfg.MediaHandler.GetAudioChapters)
|
||||
protected.PUT("/media-items/:id/audio-speed", cfg.MediaHandler.SetAudioSpeed)
|
||||
protected.GET("/media-items/:id/audio/:trackIndex", cfg.MediaHandler.GetAudioTrack)
|
||||
|
||||
// Media rating routes (all authenticated users)
|
||||
|
||||
Reference in new issue
Block a user