feat(audiobooks): list endpoint at GET /api/v1/audiobooks

Paginated list of media_items with type='audiobook' scoped to the
caller's accessible libraries via the existing access filter.
Mirrors silo's existing list-style handlers for movies and series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
RXWatcher
2026-05-24 14:33:21 +02:00
co-authored by Claude Opus 4.7
parent bf86b031b3
commit cc12ab5d5d
2 changed files with 76 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/Silo-Server/silo-server/internal/catalog"
"github.com/Silo-Server/silo-server/internal/models"
)
// AudiobookHandler serves silo-native audiobook endpoints under
// /api/v1/audiobooks/*. Sub-plan 3 implements list/detail/progress;
// sub-plan 4's ABS-compat layer is separate.
type AudiobookHandler struct {
Items *catalog.ItemRepository
}
// HandleListAudiobooks serves GET /api/v1/audiobooks?limit=&offset=.
// Paginated catalog of media_items with type='audiobook' scoped to the
// caller's accessible libraries.
func (h *AudiobookHandler) HandleListAudiobooks(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 || limit > 200 {
limit = 50
}
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if offset < 0 {
offset = 0
}
filter := requestAccessFilter(r)
items, total, err := h.Items.Search(r.Context(), "", []string{"audiobook"}, limit, offset, filter)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal_error", "list audiobooks failed")
return
}
resp := struct {
Items []audiobookListItem `json:"items"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}{
Items: audiobookListItems(items),
Total: total,
Limit: limit,
Offset: offset,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}
type audiobookListItem struct {
ContentID string `json:"content_id"`
Title string `json:"title"`
Year int `json:"year"`
PosterURL string `json:"poster_url,omitempty"`
}
func audiobookListItems(items []*models.MediaItem) []audiobookListItem {
out := make([]audiobookListItem, 0, len(items))
for _, it := range items {
out = append(out, audiobookListItem{
ContentID: it.ContentID,
Title: it.Title,
Year: it.Year,
PosterURL: it.PosterPath,
})
}
return out
}
+6
View File
@@ -1554,6 +1554,12 @@ func NewRouter(deps Dependencies) chi.Router {
r.Get("/sections/recipes", recipeHandler.HandleList)
r.Get("/sections/recipes/{type}/candidates", recipeHandler.HandleCandidates)
// Audiobook endpoints (no profile required — catalog-level list).
if itemRepo != nil {
audiobookHandler := &handlers.AudiobookHandler{Items: itemRepo}
r.Get("/audiobooks", audiobookHandler.HandleListAudiobooks)
}
// Section endpoints (profile-scoped).
if sectionHandler != nil {
r.Group(func(r chi.Router) {