Add round-robin posting mode

One story per interval_hours (default 4), cycling through enabled sources
in config order. Empty sources are skipped and the pointer advances to
whichever source actually posted. State persists across restarts.

Duplicate-flagged stories now get a _duplicate sentinel channel so they
stay out of the rotation pool alongside _discarded.
This commit is contained in:
prosolis
2026-05-22 19:58:46 -07:00
parent 69967b25c6
commit 3baec4c8bc
7 changed files with 451 additions and 2 deletions

26
main.go
View File

@@ -15,6 +15,7 @@ import (
"pete/internal/ingestion"
"pete/internal/matrix"
"pete/internal/poster"
"pete/internal/scheduler"
"pete/internal/storage"
)
@@ -94,7 +95,10 @@ func main() {
go queue.Start(ctx)
slog.Info("post queue started")
// Build the pipeline callback: classify → enqueue
roundRobinMode := cfg.Posting.RoundRobin.Enabled
// Build the pipeline callback: classify → enqueue (or just classify+store
// when round-robin mode is enabled; the scheduler does the enqueueing).
processItem := func(ctx context.Context, item *ingestion.FeedItem) {
result, err := cls.Classify(ctx, item)
if err != nil {
@@ -123,6 +127,9 @@ func main() {
storage.MarkClassified(item.GUID, result.Channel, platforms)
if result.DuplicateOf != nil {
// Overwrite with sentinel so the round-robin scheduler doesn't pick
// this up later. Immediate-mode posting already returned above.
storage.MarkClassified(item.GUID, "_duplicate", "[]")
slog.Info("story deduplicated",
"guid", item.GUID,
"duplicate_of", *result.DuplicateOf,
@@ -130,6 +137,14 @@ func main() {
return
}
if roundRobinMode {
// Story stays in DB classified+postable; the scheduler will pick
// it up on the next rotation tick.
slog.Info("story classified, awaiting round-robin tick",
"guid", item.GUID, "source", item.Source, "channel", result.Channel)
return
}
// Validate image
imageURL := ""
if item.ImageURL != "" && ingestion.ValidateImageURL(item.ImageURL) {
@@ -154,6 +169,15 @@ func main() {
poller.Start(ctx)
slog.Info("pollers started")
if roundRobinMode {
rr := scheduler.New(cfg.Sources, cfg.Posting.RoundRobin.IntervalHours, queue)
if rr == nil {
slog.Warn("round-robin enabled but no sources are enabled; nothing will post")
} else {
go rr.Start(ctx)
}
}
// Run maintenance on startup
storage.RunMaintenance(cfg.Storage.RecentWindowHours, cfg.Storage.ClassificationLogDays)