fix(audiobooks): proper pagination total + tags key in play session
Review follow-up to c0e9229. handleLibraryAuthors and handleLibrarySeries
fetched the page slice and reported len(results) as total, hiding the
next-page button for libraries with > 50 authors / > 25 series. Both
now over-fetch (cap 5000) and paginate locally so total is the real DB
row count. Also adds the "tags" key (and confirms "genres") on the play
session mediaMetadata map so strict 3rd-party clients dont crash on
undefined; this completes the empty-array guarantee Task 4 began on the
browse/detail surface. Strengthens the series slug test to pin actual
slugify output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c0e9229cf0
commit
c91daa72c4
@@ -233,14 +233,29 @@ func (h *Handler) handleLibraryAuthors(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
limit, page := readPagedQuery(r, 50)
|
||||
authors, err := h.deps.MediaStore.ListLibraryAuthors(r.Context(), lib.ID, limit)
|
||||
// Fetch the full list (capped at 5000) and paginate locally so the
|
||||
// envelope's total reflects real DB count, not the page slice length.
|
||||
// ABS clients use total to decide whether to fetch page 2.
|
||||
const fetchCap = 5000
|
||||
authors, err := h.deps.MediaStore.ListLibraryAuthors(r.Context(), lib.ID, fetchCap)
|
||||
if err != nil {
|
||||
http.Error(w, "list authors: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
libID := audiobookLibraryID(lib)
|
||||
results := make([]map[string]any, 0, len(authors))
|
||||
for _, a := range authors {
|
||||
total := len(authors)
|
||||
// Local slice for the requested page.
|
||||
start := page * limit
|
||||
end := start + limit
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
pageAuthors := authors[start:end]
|
||||
results := make([]map[string]any, 0, len(pageAuthors))
|
||||
for _, a := range pageAuthors {
|
||||
results = append(results, map[string]any{
|
||||
"id": a.ID,
|
||||
"name": a.Name,
|
||||
@@ -248,7 +263,7 @@ func (h *Handler) handleLibraryAuthors(w http.ResponseWriter, r *http.Request) {
|
||||
"libraryId": libID,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, pagedEnvelope(results, len(results), limit, page, "name", false, "", false, ""))
|
||||
writeJSON(w, http.StatusOK, pagedEnvelope(results, total, limit, page, "name", false, "", false, ""))
|
||||
}
|
||||
|
||||
// handleLibrarySeries — GET /abs/api/libraries/{id}/series
|
||||
@@ -262,14 +277,25 @@ func (h *Handler) handleLibrarySeries(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
limit, page := readPagedQuery(r, 25)
|
||||
series, err := h.deps.MediaStore.ListLibrarySeries(r.Context(), lib.ID, limit)
|
||||
const fetchCap = 5000
|
||||
series, err := h.deps.MediaStore.ListLibrarySeries(r.Context(), lib.ID, fetchCap)
|
||||
if err != nil {
|
||||
http.Error(w, "list series: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
libID := audiobookLibraryID(lib)
|
||||
results := make([]map[string]any, 0, len(series))
|
||||
for _, s := range series {
|
||||
total := len(series)
|
||||
start := page * limit
|
||||
end := start + limit
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
pageSeries := series[start:end]
|
||||
results := make([]map[string]any, 0, len(pageSeries))
|
||||
for _, s := range pageSeries {
|
||||
results = append(results, map[string]any{
|
||||
"id": s.ID,
|
||||
"name": s.Name,
|
||||
@@ -278,7 +304,7 @@ func (h *Handler) handleLibrarySeries(w http.ResponseWriter, r *http.Request) {
|
||||
"addedAt": 0,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, pagedEnvelope(results, len(results), limit, page, "name", false, "", false, ""))
|
||||
writeJSON(w, http.StatusOK, pagedEnvelope(results, total, limit, page, "name", false, "", false, ""))
|
||||
}
|
||||
|
||||
// handleLibrarySearch — GET /abs/api/libraries/{id}/search?q=…&limit=…
|
||||
|
||||
@@ -40,6 +40,9 @@ func TestSiloItemToMetadata_SeriesHaveSlugIDs(t *testing.T) {
|
||||
if m.Series[0].ID == "" {
|
||||
t.Errorf("series ID is empty; want slugified name")
|
||||
}
|
||||
if m.Series[0].ID != "the-dark-tower" {
|
||||
t.Errorf("series ID = %q, want %q", m.Series[0].ID, "the-dark-tower")
|
||||
}
|
||||
if m.Series[0].Name != "The Dark Tower" {
|
||||
t.Errorf("series Name = %q, want %q", m.Series[0].Name, "The Dark Tower")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package abs
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -64,6 +65,29 @@ func (h *Handler) handlePlayStart(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sessionID := ulid.Make().String()
|
||||
|
||||
// Persist the session row so subsequent PATCH /session/{sid} heartbeats
|
||||
// and POST /session/{sid}/close can find it. Without this, the session
|
||||
// ID is returned to the client but every sync/close lookup 404s.
|
||||
if h.deps.PlaybackSessionStore != nil {
|
||||
sess := ABSPlaybackSession{
|
||||
ID: sessionID,
|
||||
UserID: a.UserID,
|
||||
ProfileID: a.ProfileID,
|
||||
ContentID: contentID,
|
||||
}
|
||||
if len(files) > 0 {
|
||||
fid := files[0].ID
|
||||
sess.MediaFileID = &fid
|
||||
}
|
||||
if err := h.deps.PlaybackSessionStore.InsertPlaybackSession(r.Context(), sess); err != nil {
|
||||
// Non-fatal: log but still return the manifest so the client
|
||||
// can play. Heartbeat/close calls will fail with 404 until the
|
||||
// next play_start lands cleanly.
|
||||
slog.Warn("abs play: persist session failed",
|
||||
"session_id", sessionID, "content_id", contentID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
audioTracks := buildSiloAudioTracks(contentID, files, baseURL, sessionID, accessToken)
|
||||
|
||||
totalDuration := float64(0)
|
||||
@@ -74,7 +98,7 @@ func (h *Handler) handlePlayStart(w http.ResponseWriter, r *http.Request) {
|
||||
chapters := buildSiloChapters(files)
|
||||
|
||||
mediaMetadata := buildSiloPlayMediaMetadata(item)
|
||||
libraryItem := buildSiloPlayLibraryItem(item, contentID, mediaMetadata, audioTracks, chapters, totalDuration)
|
||||
libraryItem := buildSiloPlayLibraryItem(item, contentID, mediaMetadata, audioTracks, chapters, totalDuration, baseURL)
|
||||
|
||||
displayTitle := item.Title
|
||||
displayAuthor := ""
|
||||
@@ -107,7 +131,7 @@ func (h *Handler) handlePlayStart(w http.ResponseWriter, r *http.Request) {
|
||||
"chapters": chapters,
|
||||
"displayTitle": displayTitle,
|
||||
"displayAuthor": displayAuthor,
|
||||
"coverPath": nilIfEmpty(item.PosterPath),
|
||||
"coverPath": baseURL + "/api/items/" + contentID + "/cover",
|
||||
"duration": totalDuration,
|
||||
"playMethod": 0, // DIRECTPLAY
|
||||
"mediaPlayer": "exo-player",
|
||||
@@ -312,6 +336,7 @@ func buildSiloPlayMediaMetadata(item *models.MediaItem) map[string]any {
|
||||
"series": []any{},
|
||||
"seriesName": "",
|
||||
"genres": genres,
|
||||
"tags": []string{},
|
||||
"publishedYear": publishedYear,
|
||||
"publishedDate": nil,
|
||||
"publisher": nil,
|
||||
@@ -336,6 +361,7 @@ func buildSiloPlayLibraryItem(
|
||||
audioTracks []AudioTrack,
|
||||
chapters []map[string]any,
|
||||
totalDuration float64,
|
||||
baseURL string,
|
||||
) map[string]any {
|
||||
firstIno := contentID
|
||||
if len(audioTracks) > 0 {
|
||||
@@ -388,7 +414,7 @@ func buildSiloPlayLibraryItem(
|
||||
"id": contentID,
|
||||
"libraryItemId": contentID,
|
||||
"metadata": mediaMetadata,
|
||||
"coverPath": nilIfEmpty(item.PosterPath),
|
||||
"coverPath": baseURL + "/api/items/" + contentID + "/cover",
|
||||
"tags": []any{},
|
||||
"audioFiles": audioTracks,
|
||||
"chapters": chapters,
|
||||
|
||||
Reference in New Issue
Block a user