Initial commit: Null DRM Official
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.
This commit is contained in:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
111
apps/agent/internal/poller/poller.go
Normal file
111
apps/agent/internal/poller/poller.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
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...)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue