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:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

152
apps/modules/tg4/steps.go Normal file
View file

@ -0,0 +1,152 @@
package tg4
import (
"fmt"
"strings"
"time"
"drmdecryption/adb"
"drmdecryption/uiflow"
)
// openLivePage navigates to the Live page: if a menu entry is visible tap it,
// otherwise open the side drawer first. Ported from the Starlark hook this module
// used to carry, with every selector and coordinate now coming from config.
func (a *App) openLivePage(c *adb.Client) error {
c.DismissShadeIfFocused()
nodes, err := c.DumpUI(a.CacheDir())
if err != nil {
return err
}
if a.onLivePage(nodes) {
fmt.Println("[*] already on Live")
return nil
}
if live := a.findMenuLive(nodes); live != nil {
fmt.Printf("[*] tap Live @ %d,%d\n", live.CX(), live.CY())
_ = c.Tap(live.CX(), live.CY())
time.Sleep(time.Duration(a.cfg.Timeouts.AfterLiveTap))
return nil
}
if !a.openDrawer(c) {
fmt.Printf("[*] drawer fallback tap %d,%d\n", a.cfg.UI.DrawerFallbackX, a.cfg.UI.DrawerFallbackY)
_ = c.Tap(a.cfg.UI.DrawerFallbackX, a.cfg.UI.DrawerFallbackY)
time.Sleep(time.Duration(a.cfg.Timeouts.AfterDrawer))
}
deadline := time.Now().Add(time.Duration(a.cfg.Timeouts.LiveNav))
for time.Now().Before(deadline) {
nodes, err := c.DumpUI(a.CacheDir())
if err == nil {
if live := a.findMenuLive(nodes); live != nil {
fmt.Printf("[*] tap Live @ %d,%d\n", live.CX(), live.CY())
_ = c.Tap(live.CX(), live.CY())
time.Sleep(time.Duration(a.cfg.Timeouts.AfterLiveTap))
break
}
a.openDrawer(c)
}
time.Sleep(800 * time.Millisecond)
}
deadline = time.Now().Add(time.Duration(a.cfg.Timeouts.LiveNav))
for time.Now().Before(deadline) {
if nodes, err := c.DumpUI(a.CacheDir()); err == nil && a.onLivePage(nodes) {
fmt.Printf("[*] Live page ready\n")
return nil
}
time.Sleep(800 * time.Millisecond)
}
return fmt.Errorf("Live page did not open")
}
// onLivePage is true when the Live title is showing and at least one content card
// is on screen.
func (a *App) onLivePage(nodes []adb.Node) bool {
hasTitle := false
for _, n := range nodes {
if !strings.EqualFold(strings.TrimSpace(n.Text), a.cfg.UI.LiveLabel) {
continue
}
if strings.Contains(n.ResourceID, a.cfg.UI.LiveTitleResource) ||
(n.Y1 < a.cfg.UI.TitleMaxY && !n.Clickable) {
hasTitle = true
break
}
}
if !hasTitle {
return false
}
return len(uiflow.Cards(nodes, a.cfg.Cards)) > 0
}
// openDrawer opens the side menu. Returns false when the drawer button could not
// be identified, so the caller can fall back to a fixed tap.
func (a *App) openDrawer(c *adb.Client) bool {
nodes, err := c.DumpUI(a.CacheDir())
if err != nil {
return false
}
// Already open?
for _, n := range nodes {
desc := strings.TrimSpace(n.Desc)
for _, d := range a.cfg.UI.DrawerDesc {
if desc == d && d == "Closed" {
return true
}
}
if strings.EqualFold(strings.TrimSpace(n.Text), a.cfg.UI.LiveLabel) &&
n.Clickable && n.X2 < a.cfg.UI.MenuMaxX {
return true
}
}
ham := a.findDrawerButton(nodes)
if ham == nil {
return false
}
fmt.Printf("[*] tap menu %q @ %d,%d\n", ham.Desc, ham.CX(), ham.CY())
_ = c.Tap(ham.CX(), ham.CY())
time.Sleep(time.Duration(a.cfg.Timeouts.AfterDrawer))
return true
}
func (a *App) findDrawerButton(nodes []adb.Node) *adb.Node {
for i := range nodes {
n := &nodes[i]
if !n.Clickable {
continue
}
desc := strings.TrimSpace(n.Desc)
for _, d := range a.cfg.UI.DrawerDesc {
if desc == d {
return n
}
}
}
// Geometric fallback: a small clickable node in the top-left corner.
for i := range nodes {
n := &nodes[i]
if n.Clickable && n.X1 < 50 && n.Y1 < 200 && n.X2 < 200 && n.Y2 < 300 {
return n
}
}
return nil
}
// findMenuLive locates the drawer's Live entry.
func (a *App) findMenuLive(nodes []adb.Node) *adb.Node {
for i := range nodes {
n := &nodes[i]
if !n.Clickable {
continue
}
if !strings.EqualFold(strings.TrimSpace(n.Text), a.cfg.UI.LiveLabel) {
continue
}
if n.X2 <= a.cfg.UI.MenuMaxX && n.Y1 > a.cfg.UI.MenuMinY && n.Y1 < a.cfg.UI.MenuMaxY {
return n
}
}
return nil
}