feat: audio track history
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user