feat(podcasts): API endpoints — add/list/detail, subscriptions, downloads, sequence, search (task 1.7)
The §8 surface, wired through main.go → router.Config → /api/podcasts (JWT). Scoping is the engine's (D20): lookups outside the caller's scope simply return no rows → 404; the only Go-level checks are explicit-argument authorization (AddFeed's library + allow_member_feeds gate) and the admin role on feed deletion. Endpoints: POST/GET /feeds (add: fetch+parse+upsert+subscribe, optional download_all; list: subscriptions + episode counts + facet values computed in Go — feed counts are tens), GET /feeds/:id (detail with download states + gap count), PUT/DELETE /subscription (the ONLY user-writable podcast state, D9; unsubscribe retains files per D6), POST /refresh, POST /episodes/:eid/download (+ episode_ids[] bulk and eid='all' = Download All, exactly the non-downloaded set), PUT /order (D5 per-user override in the reader_settings KV — podcast_order:<feed_id>; 'native' deletes the row so default and explicit choice are indistinguishable), GET /sequence (effective order — override applied, native fallback — DOWNLOADED-only with the gap list; this payload powers web chain mode and, per §15, the future media3 queue), GET /search (subscription-scoped feed + episode search covering un-downloaded and historical rows, D14), DELETE /feeds/:id (admin; best-effort file removal, cascade deletes rows). Gap definition (countGaps): an un-downloaded episode with a downloaded episode AFTER it in list order — the D8 'middle episodes missing' banner condition.
This commit is contained in:
1 parent
a6723e6ac8
commit
45b1152a15
5 files changed
+772
No files matched your search
@@ -0,0 +1,24 @@
|
||||
package router
|
||||
|
||||
// Podcast routes (plan §8). All under /api/podcasts with the JWT
|
||||
// middleware; scoping is RLS's job (D20) — handlers add no visibility
|
||||
// plumbing.
|
||||
|
||||
func registerPodcastRoutes(cfg *Config) {
|
||||
g := cfg.Echo.Group("/api/podcasts", createJWTMiddleware(cfg))
|
||||
|
||||
g.POST("/feeds", cfg.PodcastHandler.AddFeed)
|
||||
g.GET("/feeds", cfg.PodcastHandler.ListFeeds)
|
||||
g.GET("/search", cfg.PodcastHandler.Search)
|
||||
|
||||
// :id-scoped (RLS answers 404 for feeds outside the caller's scope).
|
||||
feeds := g.Group("/feeds/:id")
|
||||
feeds.GET("", cfg.PodcastHandler.GetFeed)
|
||||
feeds.GET("/sequence", cfg.PodcastHandler.GetFeedSequence)
|
||||
feeds.PUT("/order", cfg.PodcastHandler.SetFeedOrder)
|
||||
feeds.PUT("/subscription", cfg.PodcastHandler.UpdateSubscription)
|
||||
feeds.DELETE("/subscription", cfg.PodcastHandler.DeleteSubscription)
|
||||
feeds.POST("/refresh", cfg.PodcastHandler.RefreshFeed)
|
||||
feeds.POST("/episodes/:eid/download", cfg.PodcastHandler.DownloadEpisodes)
|
||||
feeds.DELETE("", cfg.PodcastHandler.DeleteFeed) // admin-only (handler checks role)
|
||||
}
|
||||
Reference in new issue
Block a user