diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-15 10:51:26 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-15 10:51:26 -0800 |
| commit | 23258137e08a2e129ca25a8f7b0c9b286518eebd (patch) | |
| tree | 1782f8e55a1b8dbe02a09612dc9d1c381e7c68bf /models | |
| parent | 82ae577a037beb38582157dd686998836aa99d93 (diff) | |
| download | neko-23258137e08a2e129ca25a8f7b0c9b286518eebd.tar.gz neko-23258137e08a2e129ca25a8f7b0c9b286518eebd.tar.bz2 neko-23258137e08a2e129ca25a8f7b0c9b286518eebd.zip | |
Implement --purge and --purge-unread flags to delete old items
Diffstat (limited to 'models')
| -rw-r--r-- | models/item/item.go | 30 | ||||
| -rw-r--r-- | models/item/item_test.go | 76 |
2 files changed, 100 insertions, 6 deletions
diff --git a/models/item/item.go b/models/item/item.go index 3722e90..f960c65 100644 --- a/models/item/item.go +++ b/models/item/item.go @@ -57,8 +57,8 @@ func (i *Item) Print() { func (i *Item) Create() error { res, err := models.DB.Exec(`INSERT INTO - item(title, url, description, publish_date, feed_id) - VALUES(?, ?, ?, ?, ?)`, i.Title, i.Url, i.Description, i.PublishDate, i.FeedId) + item(title, url, description, publish_date, feed_id, read_state, starred) + VALUES(?, ?, ?, ?, ?, ?, ?)`, i.Title, i.Url, i.Description, i.PublishDate, i.FeedId, i.ReadState, i.Starred) if err != nil { vlog.Printf("Error on item.Create\n%v\n%v\n", i.Url, err) return err @@ -230,6 +230,32 @@ func Filter(max_id int64, feed_id int64, category string, unread_only bool, star return items, nil } +// Purge deletes items older than the specified number of days. +// By default it only deletes read items. +// If allItems is true, it also deletes unread items. +// Starred items are NEVER deleted. +func Purge(days int, allItems bool) (int64, error) { + query := `DELETE FROM item WHERE datetime(publish_date) < datetime('now', ?) AND starred == 0` + if !allItems { + query = query + ` AND read_state == 1` + } + vlog.Printf("Purge query: %s with param %s\n", query, fmt.Sprintf("-%d days", days)) + res, err := models.DB.Exec(query, fmt.Sprintf("-%d days", days)) + if err != nil { + return 0, err + } + affected, _ := res.RowsAffected() + vlog.Printf("Purge affected rows: %d\n", affected) + + // Cleanup FTS table - SQLite FTS4 doesn't automatically cleanup content table rows + // if we are using "content=item" (which we are). + // Actually we have triggers, so it should be fine. + // But VACUUM is good to reclaim space. + // _, _ = models.DB.Exec("VACUUM") + + return affected, nil +} + func (i *Item) CleanHeaderImage() { // TODO: blacklist of bad imgs if i.HeaderImage == "https://s0.wp.com/i/blank.jpg" { diff --git a/models/item/item_test.go b/models/item/item_test.go index 805c588..2f31ac7 100644 --- a/models/item/item_test.go +++ b/models/item/item_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" "testing" + "time" goose "github.com/advancedlogic/GoOse" @@ -638,16 +639,83 @@ func TestRewriteImagesSrcset(t *testing.T) { } func TestItemSaveError(t *testing.T) { - setupTestDB(t) + config.Config.DBFile = filepath.Join(t.TempDir(), "test_bad.db") + models.InitDB() + i := &Item{Id: 1} - // Close DB to force error models.DB.Close() - i.Save() // Should not panic, just log error + i.Save() } func TestItemFullSaveError(t *testing.T) { - setupTestDB(t) + config.Config.DBFile = filepath.Join(t.TempDir(), "test_bad_2.db") + models.InitDB() i := &Item{Id: 1} models.DB.Close() i.FullSave() // Should not panic } +func TestPurge(t *testing.T) { + setupTestDB(t) + feedId := createTestFeed(t) + + // Create items with different dates and states + now := time.Now() + // Use explicit dates, ensure SQLite parses them correctly + // Old date: 60 days ago + oldDate := now.AddDate(0, 0, -60).Format("2006-01-02 15:04:05") + // Very old date: 100 days ago + veryOldDate := now.AddDate(0, 0, -100).Format("2006-01-02 15:04:05") + + items := []*Item{ + {Title: "Old Read", Url: "http://example.com/1", PublishDate: oldDate, FeedId: feedId, ReadState: true}, + {Title: "Old Unread", Url: "http://example.com/2", PublishDate: oldDate, FeedId: feedId, ReadState: false}, + {Title: "Old Starred", Url: "http://example.com/3", PublishDate: oldDate, FeedId: feedId, ReadState: true, Starred: true}, + {Title: "Very Old Read", Url: "http://example.com/4", PublishDate: veryOldDate, FeedId: feedId, ReadState: true}, + } + + for _, i := range items { + err := i.Create() + if err != nil { + t.Fatal(err) + } + } + + // Purge read items older than 30 days + affected, err := Purge(30, false) + if err != nil { + t.Fatal(err) + } + + // Should have purged "Old Read" and "Very Old Read" + 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, "") + 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, "") + if len(remaining) != 2 { + t.Errorf("Expected 2 items remaining, got %d", len(remaining)) + } + + // Purge all items older than 30 days (including unread) + affected, err = Purge(30, true) + if err != nil { + t.Fatal(err) + } + + // Should have purged "Old Unread" + if affected != 1 { + t.Errorf("Expected 1 item purged, got %d", affected) + } + + //Verify "Old Starred" is still there + remaining, _ = Filter(0, 0, "", false, false, 0, "") + if len(remaining) != 1 || remaining[0].Title != "Old Starred" { + t.Errorf("Expected only 'Old Starred' to remain, got %v", remaining) + } +} |
