package api import ( "errors" "net/http" "time" "github.com/jackc/pgx/v5" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/auth" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/playlists" ) // foryouRefreshResp is the wire shape for POST // /api/playlists/system/for-you/refresh. track_ids is the playlist's // tracks in playlist position order — the frontend enqueues this // list directly to start playback without a second roundtrip. // // playlist_id is null + track_ids empty when the build succeeded but // the user's library yielded no eligible candidates (degenerate // empty-library case). type foryouRefreshResp struct { PlaylistID *string `json:"playlist_id"` TrackCount int32 `json:"track_count"` TrackIDs []string `json:"track_ids"` } // handleForYouRefresh re-runs BuildSystemPlaylists synchronously for // the calling user and returns their freshly-built For-You playlist. // Used by the home Playlists row's For-You tile play button: click // triggers refresh, the response carries the track IDs, the // frontend enqueues + auto-plays. // // Authenticated user only — each user refreshes only their own // For-You. func (h *handlers) handleForYouRefresh(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { writeErr(w, apierror.Unauthorized("auth_required", "")) return } if err := playlists.BuildSystemPlaylists(r.Context(), h.pool, h.logger, user.ID, time.Now(), h.dataDir); err != nil { h.logger.Error("for-you refresh: build failed", "user_id", uuidToString(user.ID), "err", err) writeErr(w, apierror.InternalMsg("build failed", err)) return } q := dbq.New(h.pool) sysVariant := "for_you" pl, err := q.GetSystemPlaylistByVariantForUser(r.Context(), dbq.GetSystemPlaylistByVariantForUserParams{ UserID: user.ID, SystemVariant: &sysVariant, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { // Build succeeded but no For-You row — empty library. writeJSON(w, http.StatusOK, foryouRefreshResp{ PlaylistID: nil, TrackCount: 0, TrackIDs: []string{}, }) return } h.logger.Error("for-you refresh: lookup failed", "user_id", uuidToString(user.ID), "err", err) writeErr(w, apierror.InternalMsg("lookup failed", err)) return } // Fetch tracks in playlist position order so the frontend can // enqueue them directly. rows, err := q.ListPlaylistTracks(r.Context(), pl.ID) if err != nil { h.logger.Error("for-you refresh: list tracks failed", "playlist_id", uuidToString(pl.ID), "err", err) writeErr(w, apierror.InternalMsg("list tracks failed", err)) return } trackIDs := make([]string, 0, len(rows)) for _, row := range rows { // Skip rows where the track was removed from the library // (LiveTrackID won't be Valid). The snapshot row still // exists in playlist_tracks, but there's nothing to enqueue. if !row.LiveTrackID.Valid { continue } trackIDs = append(trackIDs, uuidToString(row.LiveTrackID)) } idStr := uuidToString(pl.ID) writeJSON(w, http.StatusOK, foryouRefreshResp{ PlaylistID: &idStr, TrackCount: pl.TrackCount, TrackIDs: trackIDs, }) }