package poller import ( "fmt" "log" "strings" "time" "drmdecryption/apps/agent/internal/client" "drmdecryption/apps/agent/internal/queue" ) type Poller struct { API *client.Client Queue *queue.Store MaxAttempts int Interval time.Duration Log *log.Logger } func (p *Poller) Once() error { streams, err := p.API.ListStreams() if err != nil { return err } for _, st := range streams { if !st.Enabled { continue } reason := needsRefresh(st) if reason == "" { continue } j, added, err := p.Queue.EnqueueIfIdle(st.ID, st.Name, st.App, st.Channel, reason, p.MaxAttempts) if err != nil { return err } if added { p.logf("enqueued job=%d stream=%s reason=%s", j.ID, st.Name, reason) continue } if active, ok, err := p.Queue.ActiveJobForStream(st.ID); err == nil && ok { extra := "" if active.Status == queue.StatusQueued && !active.NotBefore.IsZero() && active.NotBefore.After(time.Now().UTC()) { extra = fmt.Sprintf(" backoff_until=%s", active.NotBefore.Format(time.RFC3339)) } p.logf("stream %s needs %s but job=%d already %s (attempts %d/%d)%s", st.Name, reason, active.ID, active.Status, active.Attempts, active.MaxAttempts, extra) } } return nil } func (p *Poller) Loop(stop <-chan struct{}) { t := time.NewTicker(p.Interval) defer t.Stop() _ = p.Once() for { select { case <-stop: return case <-t.C: if err := p.Once(); err != nil { p.logf("poll error: %v", err) } } } } func needsRefresh(st client.Stream) string { if strings.TrimSpace(st.MPD) == "" || strings.TrimSpace(st.Key) == "" { return "missing_credentials" } if !validKeyPair(st.Key) { return "invalid_credentials" } h := strings.ToLower(strings.TrimSpace(st.Health)) switch h { case "down", "error", "failed": return "down" } return "" } // validKeyPair accepts KID:KEY as 32-hex:32-hex (colons/dashes in KID ignored). func validKeyPair(key string) bool { parts := strings.SplitN(strings.TrimSpace(key), ":", 2) if len(parts) != 2 { return false } hexOnly := func(s string) string { var b strings.Builder for _, r := range s { switch { case r >= '0' && r <= '9', r >= 'a' && r <= 'f', r >= 'A' && r <= 'F': b.WriteRune(r) } } return b.String() } kid, k := hexOnly(parts[0]), hexOnly(parts[1]) return len(kid) == 32 && len(k) == 32 } func (p *Poller) logf(format string, args ...any) { if p.Log != nil { p.Log.Printf(format, args...) return } log.Printf(format, args...) }