From c735cd53b59db7591b3c640c7afe09a60d846707 Mon Sep 17 00:00:00 2001 From: RXWatcher <14085001+RXWatcher@users.noreply.github.com> Date: Tue, 26 May 2026 22:25:55 +0200 Subject: [PATCH] feat(audiobooks): wire Phase 1 close-out stores + mount routes Wires RSSFeedStore into BuildABSHandler. Mounts the ten new authenticated routes (stats x3 + author x1 + series x1 + continue x2 + feeds-auth x3) under both /abs/api and /api inside bearerAuth. Mounts the three public RSS routes (.xml + slug + /file/{ino}) in the unauth public block. handleAuthorImage wired to CoverResolver. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/audiobooks/abs/handler.go | 16 ++++++++++++ internal/audiobooks/abs/libraries_handler.go | 27 +++++++++++++++----- internal/audiobooks/service.go | 6 +++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/internal/audiobooks/abs/handler.go b/internal/audiobooks/abs/handler.go index 02939113..97c2ee01 100644 --- a/internal/audiobooks/abs/handler.go +++ b/internal/audiobooks/abs/handler.go @@ -317,6 +317,11 @@ func (h *Handler) mountRoutes(r chi.Router) { r.Head(prefix+"/public/session/{sid}/track/{idx}", h.handlePublicTrack) } + // Public RSS feed routes — slug is the capability token, no auth. + r.Get("/feed/{slug}.xml", h.handlePublicFeed) + r.Get("/feed/{slug}", h.handlePublicFeed) + r.Get("/feed/{slug}/file/{ino}", h.handlePublicFeedFile) + // Stage 3: playback session + file routes, registered under both the // legacy /abs/api prefix and the canonical /api prefix that the official // ABS mobile client builds against (no /abs prefix at server root). @@ -380,6 +385,17 @@ func (h *Handler) mountRoutes(r chi.Router) { r.Get(prefix+"/me/smart-collections/{id}/items", h.handleSmartCollectionItems) r.Patch(prefix+"/me/smart-collections/{id}", h.handleUpdateSmartCollection) r.Delete(prefix+"/me/smart-collections/{id}", h.handleDeleteSmartCollection) + // Phase 1 close-out: listening stats / author+series / continue / RSS auth. + r.Get(prefix+"/me/listening-stats", h.handleListeningStats) + r.Get(prefix+"/me/listening-sessions", h.handleListeningSessions) + r.Get(prefix+"/me/listening-sessions/{sid}", h.handleListeningSessionDetail) + r.Get(prefix+"/authors/{id}", h.handleAuthorDetail) + r.Get(prefix+"/series/{id}", h.handleSeriesDetail) + r.Get(prefix+"/me/progress/{itemId}/remove-from-continue-listening", h.handleRemoveFromContinueListening) + r.Get(prefix+"/me/progress/{itemId}/readd-to-continue-listening", h.handleReaddToContinueListening) + r.Get(prefix+"/feeds", h.handleListRSSFeeds) + r.Post(prefix+"/feeds/item/{itemId}/open", h.handleOpenItemFeed) + r.Post(prefix+"/feeds/{id}/close", h.handleCloseFeed) } }) diff --git a/internal/audiobooks/abs/libraries_handler.go b/internal/audiobooks/abs/libraries_handler.go index 93a75d53..3e36497c 100644 --- a/internal/audiobooks/abs/libraries_handler.go +++ b/internal/audiobooks/abs/libraries_handler.go @@ -243,12 +243,27 @@ func (h *Handler) handleItemCover(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target, http.StatusFound) } -// handleAuthorImage — GET /abs/api/authors/{id}/image (unauthenticated) -// -// Author images are not yet stored in silo; return a clean 404 so the -// ABS client renders the placeholder. -func (h *Handler) handleAuthorImage(w http.ResponseWriter, _ *http.Request) { - http.NotFound(w, nil) +// handleAuthorImage — GET /authors/{id}/image. Public unauthenticated +// route (mounted outside bearerAuth in handler.go). Uses MediaStore +// to resolve the people row, then CoverResolver to mint a presigned +// URL and 302-redirect to it. +func (h *Handler) handleAuthorImage(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + author, err := h.deps.MediaStore.GetAuthorByID(r.Context(), id) + if err != nil || author.PosterPath == "" { + http.Error(w, "author image not found", http.StatusNotFound) + return + } + if h.deps.CoverResolver == nil { + http.Error(w, "image resolver not configured", http.StatusServiceUnavailable) + return + } + url := h.deps.CoverResolver(r.Context(), author.PosterPath, "") + if url == "" { + http.Error(w, "image resolution failed", http.StatusNotFound) + return + } + http.Redirect(w, r, url, http.StatusFound) } // handleLibraryAuthors — GET /abs/api/libraries/{id}/authors diff --git a/internal/audiobooks/service.go b/internal/audiobooks/service.go index 14e2b3d8..1245a564 100644 --- a/internal/audiobooks/service.go +++ b/internal/audiobooks/service.go @@ -112,6 +112,11 @@ func (s *Service) BuildABSHandler(deps ABSHandlerDeps) *abs.Handler { smartCollectionStore = &ABSSmartCollectionStore{Pool: deps.Pool} } + var rssFeedStore abs.RSSFeedStore + if deps.Pool != nil { + rssFeedStore = &ABSRSSFeedStore{Pool: deps.Pool} + } + var configProvider abs.ConfigProvider if deps.Settings != nil { configProvider = &ABSConfigProvider{Settings: deps.Settings} @@ -151,6 +156,7 @@ func (s *Service) BuildABSHandler(deps ABSHandlerDeps) *abs.Handler { CollectionStore: collectionStore, PlaylistStore: playlistStore, SmartCollectionStore: smartCollectionStore, + RSSFeedStore: rssFeedStore, SocketIO: socketServer, CoverResolver: func(ctx context.Context, path, variant string) string { if deps.Detail == nil {