UPSERTs user_watch_progress for the caller's (user_id, profile_id, content_id). Body carries position_seconds; clients are expected to post every 5-10s during playback plus on pause/seek (matching silo's existing video progress cadence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.3 KiB
Go
69 lines
2.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
apimw "github.com/Silo-Server/silo-server/internal/api/middleware"
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
)
|
|
|
|
type audiobookProgressRequest struct {
|
|
PositionSeconds float64 `json:"position_seconds"`
|
|
MediaFileID int `json:"media_file_id,omitempty"`
|
|
}
|
|
|
|
// HandleReportAudiobookProgress serves POST /api/v1/audiobooks/{id}/progress.
|
|
// UPSERTs into user_watch_progress for the caller's (user_id, profile_id, content_id).
|
|
// Duration is unknown at the audiobook level (it lives on the media_file), so we
|
|
// pass 0 and let the store compute completion via thresholds against position only.
|
|
func (h *AudiobookHandler) HandleReportAudiobookProgress(w http.ResponseWriter, r *http.Request) {
|
|
contentID := chi.URLParam(r, "id")
|
|
if contentID == "" {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "id is required")
|
|
return
|
|
}
|
|
|
|
var req audiobookProgressRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "invalid request body")
|
|
return
|
|
}
|
|
if req.PositionSeconds < 0 {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "position_seconds must be >= 0")
|
|
return
|
|
}
|
|
|
|
userID := apimw.GetUserID(r.Context())
|
|
profileID := apimw.GetProfileID(r.Context())
|
|
if profileID == "" {
|
|
profileID = r.Header.Get("X-Profile-Id")
|
|
}
|
|
if userID == 0 || profileID == "" {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", "authentication required")
|
|
return
|
|
}
|
|
|
|
if h.StoreProvider == nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "store unavailable")
|
|
return
|
|
}
|
|
store, err := h.StoreProvider.ForUser(r.Context(), userID)
|
|
if err != nil || store == nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "store unavailable")
|
|
return
|
|
}
|
|
|
|
// Duration is not carried in the progress body; pass 0 so the store
|
|
// uses position-only heuristics. ProgressThresholds zero value means
|
|
// "use defaults" (90% watched, 5% min-resume).
|
|
if err := store.SetProgress(r.Context(), profileID, contentID, req.PositionSeconds, 0, userstore.ProgressThresholds{}); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "save progress failed")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|