// Package watch implements waiting/polling for new free game offers. package watch import ( "context" "log" "time" "gogs.netdome.biz/paul/cheapo/internal/game" ) // Notifier is called for every game that newly became free. type Notifier interface { Notify(games []game.Game) } // NotifierFunc adapts a function to Notifier. type NotifierFunc func(games []game.Game) func (f NotifierFunc) Notify(games []game.Game) { f(games) } // Watcher polls providers and reports games that were not seen before. type Watcher struct { Providers []game.Provider Interval time.Duration seen map[string]bool } // NewWatcher creates a watcher. Interval defaults to 15m. func NewWatcher(interval time.Duration, providers ...game.Provider) *Watcher { if interval <= 0 { interval = 15 * time.Minute } return &Watcher{Providers: providers, Interval: interval, seen: map[string]bool{}} } // Run polls all providers until ctx is done. On the first pass it reports all // current free games as "new" unless baselineOnly is true (useful to seed the // seen-set without notifying). func (w *Watcher) Run(ctx context.Context, baselineOnly bool, n Notifier) error { t := time.NewTicker(w.Interval) defer t.Stop() first := true for { var total int for _, p := range w.Providers { games, err := p.GetFreeGames(ctx) if err != nil { // One flaky provider shall not kill the watcher. log.Printf("watcher: %s: %v", p.Name(), err) continue } total += len(games) var fresh []game.Game for _, g := range games { if !w.seen[g.ID] { fresh = append(fresh, g) } w.seen[g.ID] = true } if len(fresh) > 0 && !(first && baselineOnly) { n.Notify(fresh) } } if first { log.Printf("watcher: baseline set with %d current free games across %d providers, polling every %s", total, len(w.Providers), w.Interval) first = false } select { case <-ctx.Done(): return ctx.Err() case <-t.C: } } }