Capture, decrypt, and restream toolkit with compiled-in app modules (RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www. BBC module.yaml is published (clear streams); other module values stay local.
111 lines
2.5 KiB
Go
111 lines
2.5 KiB
Go
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...)
|
|
}
|