From 4b33c65f5c95671b3a096434c94d04b2a39da7f2 Mon Sep 17 00:00:00 2001 From: Thomas Rademaker Date: Wed, 18 Mar 2026 13:21:00 -0400 Subject: [PATCH] support additional podcast index passthrough apis --- appview/handlers/episodes.go | 23 ++++++++++++---- appview/handlers/podcast.go | 49 +++++++++++++++++++++++++++++++--- appview/handlers/search.go | 17 ++++++++++-- appview/podcastindex/cache.go | 42 +++++++++++++++++++++++++++++ appview/podcastindex/client.go | 29 ++++++++++++++++++++ appview/server.go | 2 ++ 6 files changed, 151 insertions(+), 11 deletions(-) diff --git a/appview/handlers/episodes.go b/appview/handlers/episodes.go index 72b8877..cc68b62 100644 --- a/appview/handlers/episodes.go +++ b/appview/handlers/episodes.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "net/http" "github.com/labstack/echo/v4" @@ -8,14 +9,23 @@ import ( func (h *Handlers) GetEpisodes(c echo.Context) error { feedID := parseInt(c.QueryParam("feedId"), 0) - if feedID <= 0 { - return writeError(c, http.StatusBadRequest, "InvalidRequest", "feedId is required") - } + feedURL := c.QueryParam("feedUrl") max := parseLimit(c.QueryParam("max"), 50, 200) - raw, err := h.pi.GetEpisodesByFeedID(feedID, max) + var raw json.RawMessage + var err error + + switch { + case feedID > 0: + raw, err = h.pi.GetEpisodesByFeedID(feedID, max) + case feedURL != "": + raw, err = h.pi.GetEpisodesByFeedURL(feedURL, max) + default: + return writeError(c, http.StatusBadRequest, "InvalidRequest", "feedId or feedUrl is required") + } + if err != nil { - h.logger.Warn("get episodes failed", "err", err, "feedId", feedID) + h.logger.Warn("get episodes failed", "err", err, "feedId", feedID, "feedUrl", feedURL) return writeError(c, http.StatusBadGateway, "UpstreamError", "podcast index request failed") } @@ -38,6 +48,9 @@ func (h *Handlers) GetEpisodes(c echo.Context) error { } episodeID := anyToInt(item["id"]) if episodeID > 0 { + if feedID <= 0 { + feedID = anyToInt(item["feedId"]) + } item["social"] = h.episodeSocialCounts(c.Request().Context(), feedID, episodeID) } items = append(items, item) diff --git a/appview/handlers/podcast.go b/appview/handlers/podcast.go index e62c0a1..2334eb5 100644 --- a/appview/handlers/podcast.go +++ b/appview/handlers/podcast.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "net/http" "github.com/labstack/echo/v4" @@ -8,13 +9,22 @@ import ( func (h *Handlers) GetPodcast(c echo.Context) error { feedID := parseInt(c.QueryParam("feedId"), 0) - if feedID <= 0 { - return writeError(c, http.StatusBadRequest, "InvalidRequest", "feedId is required") + itunesID := parseInt(c.QueryParam("itunesId"), 0) + + var raw json.RawMessage + var err error + + switch { + case feedID > 0: + raw, err = h.pi.GetPodcastByFeedID(feedID) + case itunesID > 0: + raw, err = h.pi.GetPodcastByItunesID(itunesID) + default: + return writeError(c, http.StatusBadRequest, "InvalidRequest", "feedId or itunesId is required") } - raw, err := h.pi.GetPodcastByFeedID(feedID) if err != nil { - h.logger.Warn("get podcast failed", "err", err, "feedId", feedID) + h.logger.Warn("get podcast failed", "err", err, "feedId", feedID, "itunesId", itunesID) return writeError(c, http.StatusBadGateway, "UpstreamError", "podcast index request failed") } @@ -22,6 +32,10 @@ func (h *Handlers) GetPodcast(c echo.Context) error { if err != nil { return writeJSONBlob(c, raw) } + + if feedID <= 0 { + feedID = feedIDFromPayload(payload) + } payload["social"] = h.podcastSocialCounts(c.Request().Context(), feedID) return c.JSON(http.StatusOK, payload) @@ -67,3 +81,30 @@ func (h *Handlers) GetCategories(c echo.Context) error { } return writeJSONBlob(c, payload) } + +func (h *Handlers) GetRecentEpisodes(c echo.Context) error { + max := parseLimit(c.QueryParam("max"), 20, 100) + + raw, err := h.pi.GetRecentEpisodes(max) + if err != nil { + h.logger.Warn("get recent episodes failed", "err", err) + return writeError(c, http.StatusBadGateway, "UpstreamError", "podcast index request failed") + } + return writeJSONBlob(c, raw) +} + +func (h *Handlers) GetStats(c echo.Context) error { + raw, err := h.pi.GetStats() + if err != nil { + h.logger.Warn("get stats failed", "err", err) + return writeError(c, http.StatusBadGateway, "UpstreamError", "podcast index request failed") + } + return writeJSONBlob(c, raw) +} + +func feedIDFromPayload(payload map[string]any) int { + if feed, ok := payload["feed"].(map[string]any); ok { + return anyToInt(feed["id"]) + } + return 0 +} diff --git a/appview/handlers/search.go b/appview/handlers/search.go index e7f4919..ea13dac 100644 --- a/appview/handlers/search.go +++ b/appview/handlers/search.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "net/http" "github.com/labstack/echo/v4" @@ -12,10 +13,22 @@ func (h *Handlers) SearchPodcasts(c echo.Context) error { return writeError(c, http.StatusBadRequest, "InvalidRequest", "q is required") } max := parseLimit(c.QueryParam("max"), 20, 100) + searchType := c.QueryParam("searchType") + + var raw json.RawMessage + var err error + + switch searchType { + case "title": + raw, err = h.pi.SearchByTitle(q, max) + case "person": + raw, err = h.pi.SearchByPerson(q, max) + default: + raw, err = h.pi.SearchByTerm(q, max) + } - raw, err := h.pi.SearchByTerm(q, max) if err != nil { - h.logger.Warn("podcast search failed", "err", err) + h.logger.Warn("podcast search failed", "err", err, "searchType", searchType) return writeError(c, http.StatusBadGateway, "UpstreamError", "podcast index request failed") } diff --git a/appview/podcastindex/cache.go b/appview/podcastindex/cache.go index dc2feb9..887f12f 100644 --- a/appview/podcastindex/cache.go +++ b/appview/podcastindex/cache.go @@ -103,3 +103,45 @@ func (cc *CachedClient) GetCategories() (json.RawMessage, error) { return cc.inner.GetCategories() }) } + +func (cc *CachedClient) GetRecentEpisodes(max int) (json.RawMessage, error) { + key := fmt.Sprintf("recent:episodes:%d", max) + return cc.getOrFetch(key, 15*time.Minute, func() (json.RawMessage, error) { + return cc.inner.GetRecentEpisodes(max) + }) +} + +func (cc *CachedClient) GetPodcastByItunesID(itunesID int) (json.RawMessage, error) { + key := fmt.Sprintf("podcast:itunes:%d", itunesID) + return cc.getOrFetch(key, 24*time.Hour, func() (json.RawMessage, error) { + return cc.inner.GetPodcastByItunesID(itunesID) + }) +} + +func (cc *CachedClient) GetEpisodesByFeedURL(feedURL string, max int) (json.RawMessage, error) { + key := fmt.Sprintf("episodes:url:%s:%d", feedURL, max) + return cc.getOrFetch(key, 15*time.Minute, func() (json.RawMessage, error) { + return cc.inner.GetEpisodesByFeedURL(feedURL, max) + }) +} + +func (cc *CachedClient) SearchByTitle(query string, max int) (json.RawMessage, error) { + key := fmt.Sprintf("search:title:%s:%d", query, max) + return cc.getOrFetch(key, time.Hour, func() (json.RawMessage, error) { + return cc.inner.SearchByTitle(query, max) + }) +} + +func (cc *CachedClient) SearchByPerson(query string, max int) (json.RawMessage, error) { + key := fmt.Sprintf("search:person:%s:%d", query, max) + return cc.getOrFetch(key, time.Hour, func() (json.RawMessage, error) { + return cc.inner.SearchByPerson(query, max) + }) +} + +func (cc *CachedClient) GetStats() (json.RawMessage, error) { + key := "stats:current" + return cc.getOrFetch(key, 24*time.Hour, func() (json.RawMessage, error) { + return cc.inner.GetStats() + }) +} diff --git a/appview/podcastindex/client.go b/appview/podcastindex/client.go index ba98d95..d6a0ba6 100644 --- a/appview/podcastindex/client.go +++ b/appview/podcastindex/client.go @@ -95,3 +95,32 @@ func (c *Client) GetTrending(max int, lang string, categories string) (json.RawM func (c *Client) GetCategories() (json.RawMessage, error) { return c.get("/categories/list", url.Values{}) } + +func (c *Client) GetRecentEpisodes(max int) (json.RawMessage, error) { + params := url.Values{"max": {strconv.Itoa(max)}} + return c.get("/recent/episodes", params) +} + +func (c *Client) GetPodcastByItunesID(itunesID int) (json.RawMessage, error) { + params := url.Values{"id": {strconv.Itoa(itunesID)}} + return c.get("/podcasts/byitunesid", params) +} + +func (c *Client) GetEpisodesByFeedURL(feedURL string, max int) (json.RawMessage, error) { + params := url.Values{"url": {feedURL}, "max": {strconv.Itoa(max)}} + return c.get("/episodes/byfeedurl", params) +} + +func (c *Client) SearchByTitle(query string, max int) (json.RawMessage, error) { + params := url.Values{"q": {query}, "max": {strconv.Itoa(max)}} + return c.get("/search/bytitle", params) +} + +func (c *Client) SearchByPerson(query string, max int) (json.RawMessage, error) { + params := url.Values{"q": {query}, "max": {strconv.Itoa(max)}} + return c.get("/search/byperson", params) +} + +func (c *Client) GetStats() (json.RawMessage, error) { + return c.get("/stats/current", url.Values{}) +} diff --git a/appview/server.go b/appview/server.go index 5b0510d..8391aa6 100644 --- a/appview/server.go +++ b/appview/server.go @@ -127,6 +127,8 @@ func (srv *Server) registerRoutes() { xrpc.GET("/xyz.effem.podcast.getEpisode", h.GetEpisode) xrpc.GET("/xyz.effem.podcast.getTrending", h.GetTrending) xrpc.GET("/xyz.effem.podcast.getCategories", h.GetCategories) + xrpc.GET("/xyz.effem.podcast.getRecentEpisodes", h.GetRecentEpisodes) + xrpc.GET("/xyz.effem.podcast.getStats", h.GetStats) } func (srv *Server) RunAPI(ctx context.Context) error { -- 2.51.2