package handler import ( "encoding/json" "net/http" "time" "github.com/prabhupadasays/spsays-bff/internal/middleware" "github.com/prabhupadasays/spsays-bff/internal/model" "github.com/prabhupadasays/spsays-bff/internal/service" "github.com/prabhupadasays/spsays-bff/pkg/response" ) // maxAudioProgressItems bounds a single upsert payload. A normal save // sends one item; the on-login sync sends the user's whole local set, // which is realistically small. The cap just guards against abuse. const maxAudioProgressItems = 2000 // AudioProgressHandler serves the audio player's per-file resume points. // All routes are auth-required; the user comes from the request context // populated by middleware.RequireAuth. type AudioProgressHandler struct { svc *service.AudioProgressService } func NewAudioProgressHandler(svc *service.AudioProgressService) *AudioProgressHandler { return &AudioProgressHandler{svc: svc} } type audioProgressItems struct { Items []model.AudioProgress `json:"items"` } // Get — GET /users/me/audio-progress. Returns every saved resume point so // the client can hydrate its in-memory map on sign-in. func (h *AudioProgressHandler) Get(w http.ResponseWriter, r *http.Request) { uid := middleware.UserID(r.Context()) items, err := h.svc.List(r.Context(), uid) if err != nil { response.Error(w, http.StatusInternalServerError, "could not load audio progress") return } if items == nil { items = []model.AudioProgress{} } response.JSON(w, http.StatusOK, audioProgressItems{Items: items}) } // Put — PUT /users/me/audio-progress. Upserts a batch of resume points // (one item for a normal save; N for the on-login sync). Newest-wins is // enforced in SQL, so order/conflicts are safe. func (h *AudioProgressHandler) Put(w http.ResponseWriter, r *http.Request) { uid := middleware.UserID(r.Context()) var body audioProgressItems if err := json.NewDecoder(r.Body).Decode(&body); err != nil { response.Error(w, http.StatusBadRequest, "invalid JSON body (expect { items: [...] })") return } if len(body.Items) > maxAudioProgressItems { response.Error(w, http.StatusBadRequest, "too many items") return } clean := make([]model.AudioProgress, 0, len(body.Items)) for _, it := range body.Items { if it.Key == "" || it.Position < 0 { continue // drop malformed entries rather than fail the batch } if it.UpdatedAt.IsZero() { it.UpdatedAt = time.Now().UTC() } clean = append(clean, it) } if err := h.svc.Upsert(r.Context(), uid, clean); err != nil { response.Error(w, http.StatusInternalServerError, "could not save audio progress") return } response.NoContent(w) } // Delete — DELETE /users/me/audio-progress?key=. Removes a // single resume point (used when a track plays to completion). The key is // a URL, so it travels as a query param to avoid path-encoding issues. func (h *AudioProgressHandler) Delete(w http.ResponseWriter, r *http.Request) { uid := middleware.UserID(r.Context()) key := r.URL.Query().Get("key") if key == "" { response.Error(w, http.StatusBadRequest, "missing key") return } if err := h.svc.Delete(r.Context(), uid, key); err != nil { response.Error(w, http.StatusInternalServerError, "could not delete audio progress") return } response.NoContent(w) }