99 lines
3.2 KiB
Go
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)
|
|
}
|