diff options
| -rw-r--r-- | cmd/neko/main.go | 19 | ||||
| -rw-r--r-- | models/item/item.go | 30 | ||||
| -rw-r--r-- | models/item/item_test.go | 76 |
3 files changed, 117 insertions, 8 deletions
diff --git a/cmd/neko/main.go b/cmd/neko/main.go index 3fd03f6..cc724df 100644 --- a/cmd/neko/main.go +++ b/cmd/neko/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "os" "time" @@ -10,8 +11,7 @@ import ( "adammathes.com/neko/internal/exporter" "adammathes.com/neko/models" "adammathes.com/neko/models/feed" - - "flag" + "adammathes.com/neko/models/item" "adammathes.com/neko/internal/safehttp" "adammathes.com/neko/internal/vlog" @@ -31,6 +31,8 @@ func Run(args []string) error { var help, update, verbose, proxyImages, secureCookies bool var configFile, dbfile, newFeed, export, password string var port, minutes int + var purge int + var purgeUnread bool f := flag.NewFlagSet("neko", flag.ContinueOnError) @@ -51,6 +53,9 @@ func Run(args []string) error { f.StringVar(&export, "export", "", "export feed: text, opml, html, json") f.StringVar(&export, "x", "", "export feed (short)") + f.IntVar(&purge, "purge", 0, "purge read items older than N days") + f.BoolVar(&purgeUnread, "purge-unread", false, "when purging, also include unread items") + // options f.StringVar(&dbfile, "database", "", "sqlite database file") f.StringVar(&dbfile, "d", "", "sqlite database file (short)") @@ -126,6 +131,16 @@ func Run(args []string) error { models.InitDB() + if purge > 0 { + vlog.Printf("purging items older than %d days (include unread: %t)\n", purge, purgeUnread) + affected, err := item.Purge(purge, purgeUnread) + if err != nil { + return err + } + vlog.Printf("purged %d items\n", affected) + return nil + } + if update { vlog.Printf("starting crawl\n") crawler.Crawl() 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) + } +} |
