summaryrefslogtreecommitdiff
path: root/internal/service
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-10 09:10:01 +0300
committerPaul Buetow <paul@buetow.org>2026-05-10 09:10:01 +0300
commit93366c7a8603a64b410521dd19d967c0cd4d7e84 (patch)
treed9d2439921d02c07aa12e72a790ef3273c3384a8 /internal/service
parent297a2f4e2f0aba6f139736030a7845e0d91e5856 (diff)
internal/service: fix ListEpisodes global pagination by using cross-feed SQL query
Previously ListEpisodes requested per-feed episodes with the full limit from every feed, concatenated them, and then sliced the resulting slice in memory. This silently skipped episodes from deeper pages because the per-feed DB limit never returned them. Fix: Add a new repository method ListEpisodesByFeedIDsWithStatus that takes a slice of feed IDs and applies LIMIT/OFFSET globally in a single SQL query (IN (...)). The service builds the feed ID list and delegates pagination to the database instead of emulating it in memory. Files changed: - internal/repository/podcast.go: add ListEpisodesByFeedIDsWithStatus - internal/repository/podcast_repo.go: extend PodcastRepo interface - internal/repository/mock.go: add mock implementation - internal/service/podcast.go: replace per-feed loop with new method - internal/service/podcast_test.go: add tests for ListEpisodes - internal/repository/podcast_test.go: add integration tests for new repo method - internal/api/handlers_test.go: add mockPingStore method
Diffstat (limited to 'internal/service')
-rw-r--r--internal/service/podcast.go19
-rw-r--r--internal/service/podcast_test.go76
2 files changed, 80 insertions, 15 deletions
diff --git a/internal/service/podcast.go b/internal/service/podcast.go
index 9c9e90d..1d4f16b 100644
--- a/internal/service/podcast.go
+++ b/internal/service/podcast.go
@@ -420,22 +420,11 @@ func (s *podcastService) ListEpisodes(ctx context.Context, setID, userID int64,
return nil, ErrNotFound
}
- all := make([]model.PodcastEpisodeWithStatus, 0)
- for _, feed := range feeds {
- episodes, err := s.store.ListEpisodesWithStatus(ctx, userID, feed.ID, limit, 0)
- if err != nil {
- return nil, err
- }
- all = append(all, episodes...)
- }
- if offset >= len(all) {
- return []model.PodcastEpisodeWithStatus{}, nil
- }
- end := len(all)
- if limit > 0 && offset+limit < end {
- end = offset + limit
+ feedIDs := make([]int64, len(feeds))
+ for i, f := range feeds {
+ feedIDs[i] = f.ID
}
- return all[offset:end], nil
+ return s.store.ListEpisodesByFeedIDsWithStatus(ctx, userID, feedIDs, limit, offset)
}
func (s *podcastService) DownloadEpisode(ctx context.Context, episodeID, userID int64) (*model.Media, error) {
diff --git a/internal/service/podcast_test.go b/internal/service/podcast_test.go
index 3fb94f9..f630ee0 100644
--- a/internal/service/podcast_test.go
+++ b/internal/service/podcast_test.go
@@ -914,3 +914,79 @@ func TestPodcastService_CheckFeeds_FeedError_Continues(t *testing.T) {
}
mu.Unlock()
}
+
+func TestPodcastService_ListEpisodes_GlobalPagination(t *testing.T) {
+ ctx := context.Background()
+ svc, store := setupPodcastService(t)
+
+ store.SetPermissionRepo = repository.MockSetPermissionRepo{
+ GetPermissionFunc: func(ctx context.Context, setID, userID int64) (*model.SetPermission, error) {
+ return nil, nil
+ },
+ }
+ store.UserRepo = repository.MockUserRepo{
+ GetUserByIDFunc: func(ctx context.Context, id int64) (*model.User, error) {
+ return &model.User{ID: id, IsAdmin: true}, nil
+ },
+ }
+
+ store.PodcastRepo = repository.MockPodcastRepo{
+ ListFeedsBySetIDFunc: func(ctx context.Context, setID int64) ([]model.PodcastFeed, error) {
+ return []model.PodcastFeed{
+ {ID: 1, SetID: setID, Title: "Feed A"},
+ {ID: 2, SetID: setID, Title: "Feed B"},
+ }, nil
+ },
+ ListEpisodesByFeedIDsWithStatusFunc: func(ctx context.Context, userID int64, feedIDs []int64, limit, offset int) ([]model.PodcastEpisodeWithStatus, error) {
+ // Verify service passes both feed IDs and the original limit/offset.
+ if len(feedIDs) != 2 {
+ t.Errorf("expected 2 feedIDs, got %d", len(feedIDs))
+ }
+ if limit != 3 {
+ t.Errorf("expected limit 3, got %d", limit)
+ }
+ if offset != 5 {
+ t.Errorf("expected offset 5, got %d", offset)
+ }
+ return []model.PodcastEpisodeWithStatus{
+ {PodcastEpisode: model.PodcastEpisode{ID: 1, Title: "Ep 1"}},
+ {PodcastEpisode: model.PodcastEpisode{ID: 2, Title: "Ep 2"}},
+ {PodcastEpisode: model.PodcastEpisode{ID: 3, Title: "Ep 3"}},
+ }, nil
+ },
+ }
+
+ result, err := svc.ListEpisodes(ctx, 42, 1, 3, 5)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(result) != 3 {
+ t.Fatalf("expected 3 episodes, got %d", len(result))
+ }
+}
+
+func TestPodcastService_ListEpisodes_NoFeeds(t *testing.T) {
+ ctx := context.Background()
+ svc, store := setupPodcastService(t)
+
+ store.SetPermissionRepo = repository.MockSetPermissionRepo{
+ GetPermissionFunc: func(ctx context.Context, setID, userID int64) (*model.SetPermission, error) {
+ return nil, nil
+ },
+ }
+ store.UserRepo = repository.MockUserRepo{
+ GetUserByIDFunc: func(ctx context.Context, id int64) (*model.User, error) {
+ return &model.User{ID: id, IsAdmin: true}, nil
+ },
+ }
+ store.PodcastRepo = repository.MockPodcastRepo{
+ ListFeedsBySetIDFunc: func(ctx context.Context, setID int64) ([]model.PodcastFeed, error) {
+ return []model.PodcastFeed{}, nil
+ },
+ }
+
+ _, err := svc.ListEpisodes(ctx, 42, 1, 10, 0)
+ if !errors.Is(err, ErrNotFound) {
+ t.Fatalf("expected ErrNotFound, got %v", err)
+ }
+}