diff --git a/internal/audiobooks/abs/libraries_handler.go b/internal/audiobooks/abs/libraries_handler.go index f4ee58b9..8841c32f 100644 --- a/internal/audiobooks/abs/libraries_handler.go +++ b/internal/audiobooks/abs/libraries_handler.go @@ -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=… diff --git a/internal/audiobooks/abs/libraries_metadata_test.go b/internal/audiobooks/abs/libraries_metadata_test.go index 9e0671cf..6a43161e 100644 --- a/internal/audiobooks/abs/libraries_metadata_test.go +++ b/internal/audiobooks/abs/libraries_metadata_test.go @@ -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") } diff --git a/internal/audiobooks/abs/play_response.go b/internal/audiobooks/abs/play_response.go index 1c2df604..34712f25 100644 --- a/internal/audiobooks/abs/play_response.go +++ b/internal/audiobooks/abs/play_response.go @@ -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,