Files
spsays-bff/internal/handler/audio_progress.go
T

99 lines
3.2 KiB
Go

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=<url-encoded 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)
}