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:
RXWatcher
2026-05-26 07:19:10 +02:00
co-authored by Claude Opus 4.7
parent c0e9229cf0
commit c91daa72c4
3 changed files with 66 additions and 11 deletions
+34 -8
View File
@@ -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")
}
+29 -3
View File
@@ -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,