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

24
apps/capture/README.md Normal file
View file

@ -0,0 +1,24 @@
# capture
One-shot phone MITM capture, hosted by the single binary as `drm capture`.
```text
apps/capture/
capturecmd/ the command body, imported by apps/cli
```
```bash
./bin/drm capture # passive: play anything
./bin/drm capture --app rte # launch, navigate by hand
./bin/drm capture --app rte --channel rteone --auto-play \
--wvd data/device.wvd # fully automated
```
Writes `outputs/<app>/<stamp>/` plus `latest/`. App modules are compiled in, so
there is no modules directory to point at; `drm modules` lists what is available.
**Full guide: [docs/capture.md](../../docs/capture.md)** — device setup, flags,
discovering a new app, and troubleshooting.
The catalog lookup that used to be a separate `tg4info` binary is now the
provider-agnostic `drm catalog` (`apps/cli/catalogcmd`).

View file

@ -0,0 +1,99 @@
// Package capturecmd is the one-shot phone MITM capture command, exposed as a
// library so the single drm binary can host it as a subcommand.
package capturecmd
import (
"flag"
"fmt"
"strings"
"time"
"drmdecryption/adb"
"drmdecryption/app"
"drmdecryption/modcfg"
"drmdecryption/phonecap"
)
// Run performs one capture. App modules are compiled in, so there is no modules
// directory to scan: --app names a registered provider.
func Run(args []string) error {
fs := flag.NewFlagSet("capture", flag.ExitOnError)
appName := fs.String("app", "", "app module. Empty = passive MITM, no launch/auto-play")
channel := fs.String("channel", "", "channel id for --auto-play")
autoPlay := fs.Bool("auto-play", false, "auto-navigate to --channel (requires --app and --channel)")
waitSec := fs.Int("wait", 180, "seconds to wait for license/mpd capture")
python := fs.String("python", "", "python for wvkey.py (default: .venv)")
wvd := fs.String("wvd", "", "path to .wvd device file (required for key fetch)")
userAgent := fs.String("user-agent", "", "optional User-Agent for license requests")
serial := fs.String("serial", "", "adb device serial (default: sole/USB selected device)")
closeApp := fs.Bool("close", true, "force-stop --app when capture finishes (ignored in passive mode)")
keyMode := fs.String("key-mode", "", "override the app's key mode: modulardrm | raw | none")
// Each module contributes --<app>.<field> overrides for its local values.
app.BindFlags(fs)
fs.Usage = func() {
fmt.Fprintf(fs.Output(), "capture — one-shot phone MITM capture\n\nUsage:\n capture --app <%s> [--channel ID] [--auto-play] [--wvd PATH]\n capture (passive: play anything on the phone)\n\nFlags:\n", strings.Join(app.Names(), "|"))
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
return err
}
c := adb.New()
if *serial != "" {
c = c.WithSerial(*serial)
}
// No --app: passive MITM — the operator plays whatever they want and we dump
// whatever DRM traffic appears.
if strings.TrimSpace(*appName) == "" {
_, err := phonecap.RunPassive(phonecap.Options{
Client: c,
Python: *python,
WVD: *wvd,
UserAgent: *userAgent,
Wait: time.Duration(*waitSec) * time.Second,
KeyMode: *keyMode,
ClearProxy: true,
})
return err
}
a, err := app.Get(*appName)
if err != nil {
return err
}
if *autoPlay && strings.TrimSpace(*channel) == "" {
return fmt.Errorf("--auto-play requires --channel (%s)", knownChannels(a))
}
if *channel != "" && !a.HasChannel(*channel) {
return fmt.Errorf("unknown channel %q for %s (%s)", *channel, a.Name(), knownChannels(a))
}
_, err = phonecap.Run(phonecap.Options{
App: a,
Channel: *channel,
Client: c,
Python: *python,
WVD: *wvd,
UserAgent: *userAgent,
Wait: time.Duration(*waitSec) * time.Second,
NoAutoPlay: !*autoPlay,
CloseApp: *closeApp,
KeyMode: *keyMode,
ClearProxy: true,
})
return err
}
// knownChannels describes what the operator can pick, or where to declare it when
// the module has no local values file yet.
func knownChannels(a app.App) string {
ids := []string{}
for _, ch := range a.Channels() {
ids = append(ids, ch.ID)
}
if len(ids) == 0 {
return "no channels configured — add " + modcfg.Path(a.Name())
}
return "known: " + strings.Join(ids, ", ")
}

13
apps/capture/go.mod Normal file
View file

@ -0,0 +1,13 @@
module drmdecryption/apps/capture
go 1.25.0
require drmdecryption v0.0.0
require (
go.starlark.net v0.0.0-20260930220527-d7438c5a85ac // indirect
golang.org/x/sys v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace drmdecryption => ../pkg

12
apps/capture/go.sum Normal file
View file

@ -0,0 +1,12 @@
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
go.starlark.net v0.0.0-20260930220527-d7438c5a85ac h1:pNA9PRXGPFURORAsI3IqfQJ4RLsRXqghCFdvypd/4GY=
go.starlark.net v0.0.0-20260930220527-d7438c5a85ac/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=