summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/handlers_config.go17
-rw-r--r--internal/api/handlers_media.go12
-rw-r--r--internal/api/handlers_more_test.go22
-rw-r--r--internal/api/handlers_podcast.go14
-rw-r--r--internal/api/handlers_podcast_test.go60
-rw-r--r--internal/api/handlers_test.go3
-rw-r--r--internal/api/server.go7
7 files changed, 113 insertions, 22 deletions
diff --git a/internal/api/handlers_config.go b/internal/api/handlers_config.go
new file mode 100644
index 0000000..0fdefe7
--- /dev/null
+++ b/internal/api/handlers_config.go
@@ -0,0 +1,17 @@
+package api
+
+import (
+ "net/http"
+
+ "codeberg.org/snonux/player/internal"
+)
+
+func (s *Server) handleConfig(w http.ResponseWriter, _ *http.Request) {
+ pageSize := internal.DefaultMediaPageSize
+ if s.cfg != nil && s.cfg.MediaPageSize > 0 {
+ pageSize = s.cfg.MediaPageSize
+ }
+ writeJSON(w, http.StatusOK, map[string]int{
+ "media_page_size": pageSize,
+ })
+}
diff --git a/internal/api/handlers_media.go b/internal/api/handlers_media.go
index 8eed3cc..ef3e149 100644
--- a/internal/api/handlers_media.go
+++ b/internal/api/handlers_media.go
@@ -287,6 +287,18 @@ func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]bool{"favorite": fav})
}
+func (s *Server) handleListTags(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.tagSvc) {
+ return
+ }
+ tags, err := s.tagSvc.ListTags(r.Context(), userIDFromContext(r))
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, tags)
+}
+
func (s *Server) handleAddTag(w http.ResponseWriter, r *http.Request) {
if !requireService(w, s.tagSvc) {
return
diff --git a/internal/api/handlers_more_test.go b/internal/api/handlers_more_test.go
index 5d210f2..bc0edf3 100644
--- a/internal/api/handlers_more_test.go
+++ b/internal/api/handlers_more_test.go
@@ -87,6 +87,28 @@ func TestNewGracefulServer(t *testing.T) {
}
}
+func TestServer_Config(t *testing.T) {
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+ cfg := &internal.Config{MediaPageSize: 37}
+ srv := newTestServer(t, store, nil, sm, cfg, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
+
+ req := httptest.NewRequest(http.MethodGet, "/api/config", nil)
+ req.AddCookie(addSessionCookie(t, store, sm, 1))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+ if rr.Code != http.StatusOK {
+ t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code)
+ }
+ var body map[string]int
+ if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
+ t.Fatalf("decode response: %v", err)
+ }
+ if body["media_page_size"] != 37 {
+ t.Fatalf("expected media_page_size 37, got %d", body["media_page_size"])
+ }
+}
+
func TestPingStore_nonPinger(t *testing.T) {
store := &repository.MockStore{}
srv := newTestServer(t, store, nil, nil, &internal.Config{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
diff --git a/internal/api/handlers_podcast.go b/internal/api/handlers_podcast.go
index f93b15c..721e83e 100644
--- a/internal/api/handlers_podcast.go
+++ b/internal/api/handlers_podcast.go
@@ -13,25 +13,17 @@ import (
// ------------------------------------------------------------------
func (s *Server) handleListPodcasts(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.browseSvc) {
+ if !requireService(w, s.podcastSvc) {
return
}
- userID := userIDFromContext(r)
- sets, err := s.browseSvc.ListSets(r.Context(), userID)
+ feeds, err := s.podcastSvc.ListFeeds(r.Context(), userIDFromContext(r))
if err != nil {
s.logger.Error("list podcasts", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list podcasts"})
return
}
- // Filter to podcast sets only.
- var podcasts []interface{}
- for _, set := range sets {
- if set.IsPodcast {
- podcasts = append(podcasts, set)
- }
- }
- writeJSON(w, http.StatusOK, podcasts)
+ writeJSON(w, http.StatusOK, feeds)
}
func (s *Server) handleSubscribePodcast(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/api/handlers_podcast_test.go b/internal/api/handlers_podcast_test.go
index 963c2a0..12c6198 100644
--- a/internal/api/handlers_podcast_test.go
+++ b/internal/api/handlers_podcast_test.go
@@ -154,6 +154,25 @@ func TestPodcastE2E_FullFlow(t *testing.T) {
}))
defer rssServer.Close()
+ secondRSSBody := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0">
+ <channel>
+ <title>Second Podcast</title>
+ <description>Another test podcast</description>
+ <item>
+ <title>Second Episode</title>
+ <guid>second-ep-1</guid>
+ <enclosure url="%s/second.mp3" length="4321" type="audio/mpeg"/>
+ </item>
+ </channel>
+</rss>`, audioServer.URL)
+ secondRSSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/rss+xml")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(secondRSSBody))
+ }))
+ defer secondRSSServer.Close()
+
badRSSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
@@ -189,6 +208,27 @@ func TestPodcastE2E_FullFlow(t *testing.T) {
podcastSetID = feed.SetID
})
+ t.Run("subscribe second podcast uses same set", func(t *testing.T) {
+ body := fmt.Sprintf(`{"feed_url":"%s/rss.xml","set_name":"second-podcast"}`, secondRSSServer.URL)
+ req := httptest.NewRequest(http.MethodPost, "/api/podcasts", strings.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ req.AddCookie(cookie)
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Fatalf("expected %d, got %d: %s", http.StatusOK, rr.Code, rr.Body.String())
+ }
+
+ var feed model.PodcastFeed
+ if err := json.Unmarshal(rr.Body.Bytes(), &feed); err != nil {
+ t.Fatalf("unmarshal feed: %v", err)
+ }
+ if feed.SetID != podcastSetID {
+ t.Fatalf("expected set_id %d, got %d", podcastSetID, feed.SetID)
+ }
+ })
+
t.Run("list podcasts", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/podcasts", nil)
req.AddCookie(cookie)
@@ -199,20 +239,18 @@ func TestPodcastE2E_FullFlow(t *testing.T) {
t.Fatalf("expected %d, got %d: %s", http.StatusOK, rr.Code, rr.Body.String())
}
- var sets []model.Set
- if err := json.Unmarshal(rr.Body.Bytes(), &sets); err != nil {
- t.Fatalf("unmarshal sets: %v", err)
+ var feeds []model.PodcastFeed
+ if err := json.Unmarshal(rr.Body.Bytes(), &feeds); err != nil {
+ t.Fatalf("unmarshal feeds: %v", err)
}
- found := false
- for _, s := range sets {
- if s.ID == podcastSetID && s.IsPodcast {
- found = true
- break
- }
+ if len(feeds) != 2 {
+ t.Fatalf("expected 2 podcast feeds, got %d", len(feeds))
}
- if !found {
- t.Fatalf("expected podcast set %d in list", podcastSetID)
+ for _, feed := range feeds {
+ if feed.SetID != podcastSetID {
+ t.Fatalf("expected all feeds in set %d, got feed %+v", podcastSetID, feed)
+ }
}
})
diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go
index 6f9f75e..435a924 100644
--- a/internal/api/handlers_test.go
+++ b/internal/api/handlers_test.go
@@ -1515,6 +1515,9 @@ func (m *mockPingStore) GetFeedByID(ctx context.Context, id int64) (*model.Podca
func (m *mockPingStore) GetFeedBySetID(ctx context.Context, setID int64) (*model.PodcastFeed, error) {
return m.store.GetFeedBySetID(ctx, setID)
}
+func (m *mockPingStore) ListFeedsBySetID(ctx context.Context, setID int64) ([]model.PodcastFeed, error) {
+ return m.store.ListFeedsBySetID(ctx, setID)
+}
func (m *mockPingStore) ListFeeds(ctx context.Context) ([]model.PodcastFeed, error) {
return m.store.ListFeeds(ctx)
}
diff --git a/internal/api/server.go b/internal/api/server.go
index e545e94..1469626 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -165,6 +165,11 @@ func (s *Server) routesAuth() {
s.mux.Handle("POST /api/logout", s.requireSession(s.handleLogout))
}
+// routesConfig wires authenticated client configuration.
+func (s *Server) routesConfig() {
+ s.mux.Handle("GET /api/config", s.requireSession(s.handleConfig))
+}
+
// routesSets wires the set-related API routes.
func (s *Server) routesSets() {
s.mux.Handle("GET /api/sets", s.requireSession(s.handleListSets))
@@ -183,6 +188,7 @@ func (s *Server) routesMedia() {
s.mux.Handle("GET /api/media/{id}/thumbnail", s.requireSession(s.handleThumbnail))
s.mux.Handle("POST /api/media/{id}/thumbnail", s.requireSession(s.handleRegenThumbnail))
s.mux.Handle("POST /api/media/{id}/favorite", s.requireSession(s.handleFavorite))
+ s.mux.Handle("GET /api/tags", s.requireSession(s.handleListTags))
s.mux.Handle("POST /api/media/{id}/tags", s.requireSession(s.handleAddTag))
s.mux.Handle("DELETE /api/media/{id}/tags/{tag}", s.requireSession(s.handleRemoveTag))
s.mux.Handle("DELETE /api/media/{id}", s.requireSession(s.handleSoftDelete))
@@ -228,6 +234,7 @@ func (s *Server) routes() {
s.routesStatic()
s.routesHTML()
s.routesAuth()
+ s.routesConfig()
s.routesSets()
s.routesMedia()
s.routesNotes()