From 1d3ad564df8cc09b16172901916bcef9abe31ab4 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sun, 15 Feb 2026 14:09:39 -0800 Subject: Backend: Support multi-feed filtering in stream API --- api/api.go | 19 ++++++++++++++----- internal/crawler/integration_test.go | 2 +- models/item/item.go | 14 +++++++++----- models/item/item_test.go | 30 +++++++++++++++--------------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/api/api.go b/api/api.go index 6833ce4..16e8dd6 100644 --- a/api/api.go +++ b/api/api.go @@ -60,13 +60,22 @@ func (s *Server) HandleStream(w http.ResponseWriter, r *http.Request) { } maxID, _ := strconv.ParseInt(r.FormValue("max_id"), 10, 64) - feedID, _ := strconv.ParseInt(r.FormValue("feed_id"), 10, 64) - // Backward compatibility with feed_url if feed_id is not provided - if feedID == 0 && r.FormValue("feed_url") != "" { + var feedIDs []int64 + if idsStr := r.FormValue("feed_ids"); idsStr != "" { + for _, idStr := range strings.Split(idsStr, ",") { + if id, err := strconv.ParseInt(idStr, 10, 64); err == nil { + feedIDs = append(feedIDs, id) + } + } + } else if feedID, _ := strconv.ParseInt(r.FormValue("feed_id"), 10, 64); feedID != 0 { + feedIDs = []int64{feedID} + } else if r.FormValue("feed_url") != "" { var f feed.Feed _ = f.ByUrl(r.FormValue("feed_url")) - feedID = f.Id + if f.Id != 0 { + feedIDs = []int64{f.Id} + } } category := r.FormValue("tag") @@ -78,7 +87,7 @@ func (s *Server) HandleStream(w http.ResponseWriter, r *http.Request) { unreadOnly = false } - items, err := item.Filter(maxID, feedID, category, unreadOnly, starredOnly, 0, searchQuery) + items, err := item.Filter(maxID, feedIDs, category, unreadOnly, starredOnly, 0, searchQuery) if err != nil { log.Println(err) jsonError(w, "failed to filter items", http.StatusInternalServerError) diff --git a/internal/crawler/integration_test.go b/internal/crawler/integration_test.go index 633b60f..8223265 100644 --- a/internal/crawler/integration_test.go +++ b/internal/crawler/integration_test.go @@ -52,7 +52,7 @@ func TestCrawlIntegration(t *testing.T) { } // Verify items were stored - items, err := item.Filter(0, f.Id, "", false, false, 0, "") + items, err := item.Filter(0, []int64{f.Id}, "", false, false, 0, "") if err != nil { t.Fatalf("Failed to filter items: %v", err) } diff --git a/models/item/item.go b/models/item/item.go index dc277a3..189cb4a 100644 --- a/models/item/item.go +++ b/models/item/item.go @@ -97,7 +97,7 @@ func filterPolicy() *bluemonday.Policy { } func ItemById(id int64) *Item { - items, err := Filter(0, 0, "", false, false, id, "") + items, err := Filter(0, nil, "", false, false, id, "") if err != nil || len(items) == 0 { return nil } @@ -134,7 +134,7 @@ func (i *Item) GetFullContent() { } } -func Filter(max_id int64, feed_id int64, category string, unread_only bool, starred_only bool, item_id int64, search_query string) ([]*Item, error) { +func Filter(max_id int64, feed_ids []int64, category string, unread_only bool, starred_only bool, item_id int64, search_query string) ([]*Item, error) { var args []interface{} tables := " feed,item" @@ -155,9 +155,13 @@ func Filter(max_id int64, feed_id int64, category string, unread_only bool, star args = append(args, max_id) } - if feed_id != 0 { - query = query + " AND feed.id=? " - args = append(args, feed_id) + if len(feed_ids) > 0 { + placeholders := make([]string, len(feed_ids)) + for i := range feed_ids { + placeholders[i] = "?" + args = append(args, feed_ids[i]) + } + query = query + " AND feed.id IN (" + strings.Join(placeholders, ",") + ") " } if category != "" { diff --git a/models/item/item_test.go b/models/item/item_test.go index 2f31ac7..241a650 100644 --- a/models/item/item_test.go +++ b/models/item/item_test.go @@ -235,7 +235,7 @@ func TestFilterBasic(t *testing.T) { i.Create() // Filter with no constraints (except unread_only=false to not filter by read) - items, err := Filter(0, 0, "", false, false, 0, "") + items, err := Filter(0, nil, "", false, false, 0, "") if err != nil { t.Fatalf("Filter() should not error: %v", err) } @@ -261,7 +261,7 @@ func TestFilterByFeedId(t *testing.T) { i.Create() // Filter by a non-matching feed id - items, err := Filter(0, 999, "", false, false, 0, "") + items, err := Filter(0, []int64{999}, "", false, false, 0, "") if err != nil { t.Fatal(err) } @@ -270,7 +270,7 @@ func TestFilterByFeedId(t *testing.T) { } // Filter by matching feed id - items, err = Filter(0, feedId, "", false, false, 0, "") + items, err = Filter(0, []int64{feedId}, "", false, false, 0, "") if err != nil { t.Fatal(err) } @@ -293,7 +293,7 @@ func TestFilterUnreadOnly(t *testing.T) { i.Create() // All items start unread (read_state=0) - items, err := Filter(0, 0, "", true, false, 0, "") + items, err := Filter(0, nil, "", true, false, 0, "") if err != nil { t.Fatal(err) } @@ -305,7 +305,7 @@ func TestFilterUnreadOnly(t *testing.T) { i.ReadState = true i.Save() - items, err = Filter(0, 0, "", true, false, 0, "") + items, err = Filter(0, nil, "", true, false, 0, "") if err != nil { t.Fatal(err) } @@ -328,7 +328,7 @@ func TestFilterStarredOnly(t *testing.T) { i.Create() // Not starred yet - items, err := Filter(0, 0, "", false, true, 0, "") + items, err := Filter(0, nil, "", false, true, 0, "") if err != nil { t.Fatal(err) } @@ -340,7 +340,7 @@ func TestFilterStarredOnly(t *testing.T) { i.Starred = true i.Save() - items, err = Filter(0, 0, "", false, true, 0, "") + items, err = Filter(0, nil, "", false, true, 0, "") if err != nil { t.Fatal(err) } @@ -362,7 +362,7 @@ func TestFilterByItemId(t *testing.T) { } i.Create() - items, err := Filter(0, 0, "", false, false, i.Id, "") + items, err := Filter(0, nil, "", false, false, i.Id, "") if err != nil { t.Fatal(err) } @@ -385,7 +385,7 @@ func TestFilterByMaxId(t *testing.T) { i2.Create() // max_id = i2.Id should only return items with id < i2.Id - items, err := Filter(i2.Id, 0, "", false, false, 0, "") + items, err := Filter(i2.Id, nil, "", false, false, 0, "") if err != nil { t.Fatal(err) } @@ -461,7 +461,7 @@ func TestFilterByCategory(t *testing.T) { i2.Create() // Filter by category "technology" - items, err := Filter(0, 0, "technology", false, false, 0, "") + items, err := Filter(0, nil, "technology", false, false, 0, "") if err != nil { t.Fatalf("Filter by category should not error: %v", err) } @@ -483,7 +483,7 @@ func TestFilterBySearch(t *testing.T) { i2.Create() // Search for "Golang" - items, err := Filter(0, 0, "", false, false, 0, "Golang") + items, err := Filter(0, nil, "", false, false, 0, "Golang") if err != nil { t.Fatalf("Filter by search should not error: %v", err) } @@ -511,7 +511,7 @@ func TestFilterCombined(t *testing.T) { i1.Save() // Filter: unread only + starred only — should get only starred unread - items, err := Filter(0, 0, "", true, true, 0, "") + items, err := Filter(0, nil, "", true, true, 0, "") if err != nil { t.Fatal(err) } @@ -690,14 +690,14 @@ func TestPurge(t *testing.T) { if affected != 2 { t.Errorf("Expected 2 items purged, got %d", affected) // Debug logging to see what's left - remaining, _ := Filter(0, 0, "", false, false, 0, "") + remaining, _ := Filter(0, nil, "", false, false, 0, "") for _, r := range remaining { t.Logf("Remaining: %s (%s) Read: %t Starred: %t", r.Title, r.PublishDate, r.ReadState, r.Starred) } } // Verify remaining items count - remaining, _ := Filter(0, 0, "", false, false, 0, "") + remaining, _ := Filter(0, nil, "", false, false, 0, "") if len(remaining) != 2 { t.Errorf("Expected 2 items remaining, got %d", len(remaining)) } @@ -714,7 +714,7 @@ func TestPurge(t *testing.T) { } //Verify "Old Starred" is still there - remaining, _ = Filter(0, 0, "", false, false, 0, "") + remaining, _ = Filter(0, nil, "", false, false, 0, "") if len(remaining) != 1 || remaining[0].Title != "Old Starred" { t.Errorf("Expected only 'Old Starred' to remain, got %v", remaining) } -- cgit v1.2.3