aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-15 14:09:39 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-15 14:09:39 -0800
commit1d3ad564df8cc09b16172901916bcef9abe31ab4 (patch)
tree342ebced54435226c71bc720e2275edc3894bc24
parentd176a6b6b79b76f2324a356abda4ac254ec4fc29 (diff)
downloadneko-1d3ad564df8cc09b16172901916bcef9abe31ab4.tar.gz
neko-1d3ad564df8cc09b16172901916bcef9abe31ab4.tar.bz2
neko-1d3ad564df8cc09b16172901916bcef9abe31ab4.zip
Backend: Support multi-feed filtering in stream API
-rw-r--r--api/api.go19
-rw-r--r--internal/crawler/integration_test.go2
-rw-r--r--models/item/item.go14
-rw-r--r--models/item/item_test.go30
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)
}