// Package catalogcmd resolves a channel's stream credentials from its provider's // public catalog, with no phone involved. It is provider-agnostic: any app module // implementing app.Catalog works here. package catalogcmd import ( "encoding/json" "flag" "fmt" "os" "sort" "strings" "drmdecryption/app" "drmdecryption/repo" "drmdecryption/session" ) // Run performs a catalog lookup for one channel. func Run(args []string) error { fs := flag.NewFlagSet("catalog", flag.ExitOnError) appName := fs.String("app", "", "app module to query") channel := fs.String("channel", "", "channel id") asJSON := fs.Bool("json", false, "print streamd-ready JSON only") list := fs.Bool("list", false, "list the app's channels and their catalog ids") writeOut := fs.Bool("out", false, "write outputs///session.json") withKeys := fs.Bool("keys", false, "also fetch PSSH and resolve KID:KEY via the local CDM") python := fs.String("python", "", "python for wvkey.py (default: .venv)") wvd := fs.String("wvd", "", "path to .wvd device file (required with --keys)") // Each module contributes --. overrides for its local values. app.BindFlags(fs) fs.Usage = func() { fmt.Fprintf(fs.Output(), "catalog — resolve a channel from its public catalog (no phone)\n\nUsage:\n catalog --app <%s> --channel ID [--keys] [--json]\n\nFlags:\n", strings.Join(catalogApps(), "|")) fs.PrintDefaults() } if err := fs.Parse(args); err != nil { return err } available := catalogApps() if strings.TrimSpace(*appName) == "" { if len(available) != 1 { return fmt.Errorf("--app required (apps with a catalog: %s)", strings.Join(available, ", ")) } *appName = available[0] } a, err := app.Get(*appName) if err != nil { return err } cat, ok := a.(app.Catalog) if !ok { return fmt.Errorf("app %q has no catalog — capture it from the phone instead: drm capture --app %s", a.Name(), a.Name()) } if *list { return listChannels(a, cat) } if strings.TrimSpace(*channel) == "" { return fmt.Errorf("--channel required (see: drm catalog --app %s --list)", a.Name()) } info, err := cat.Resolve(*channel) if err != nil { return err } if *withKeys { fmt.Fprintln(os.Stderr, "[*] fetching PSSH + Widevine keys…") if err := cat.EnrichWithKeys(&info, *python, *wvd); err != nil { return err } fmt.Fprintf(os.Stderr, "[+] key %s (%d total)\n", info.Key, len(info.Keys)) } if *writeOut || *withKeys { dir, err := session.WriteStream(repo.Root(), a.Name(), info) if err != nil { return err } fmt.Fprintf(os.Stderr, "[+] wrote %s\n", dir) } if *asJSON { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") return enc.Encode(info) } printInfo(a.Name(), info) return nil } // catalogApps lists compiled-in modules that can answer catalog lookups. func catalogApps() []string { out := []string{} for _, a := range app.All() { if _, ok := a.(app.Catalog); ok { out = append(out, a.Name()) } } sort.Strings(out) return out } func listChannels(a app.App, cat app.Catalog) error { if l, ok := cat.(app.CatalogLister); ok { rows, flags, err := l.ListChannels() if err != nil { return err } fmt.Printf("%-10s %-16s %s\n", "CHANNEL", "CATALOG ID", "LABEL") sort.Slice(rows, func(i, j int) bool { return rows[i].ID < rows[j].ID }) for _, r := range rows { id := r.CatalogID if id == "" { id = "-" } fmt.Printf("%-10s %-16s %s\n", r.ID, id, r.Label) } if len(flags) > 0 { keys := make([]string, 0, len(flags)) for k := range flags { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { parts = append(parts, fmt.Sprintf("%s=%v", k, flags[k])) } fmt.Printf("\nprovider flags: %s\n", strings.Join(parts, " ")) } return nil } // Fall back to the plain channel list every app can produce. fmt.Printf("%-10s %s\n", "CHANNEL", "LABEL") for _, ch := range a.Channels() { fmt.Printf("%-10s %s\n", ch.ID, ch.Label) } return nil } func printInfo(appName string, info session.Stream) { fmt.Printf("name: %s\n", info.Name) fmt.Printf("title: %s\n", info.Title) fmt.Printf("app/channel: %s / %s\n", info.App, info.Channel) if info.VideoID != "" { fmt.Printf("catalog id: %s\n", info.VideoID) } fmt.Printf("manifest: %s\n", info.MPD) fmt.Printf("manifest type: %s\n", info.ManifestType) fmt.Printf("license_url: %s\n", truncate(info.LicenseURL, 100)) if info.PSSH != "" { fmt.Printf("pssh: %s\n", truncate(info.PSSH, 64)) } if info.Key != "" { fmt.Printf("key: %s\n", info.Key) } for i, k := range info.Keys { if k == info.Key { continue } fmt.Printf("key[%d]: %s\n", i, k) } body, _ := json.MarshalIndent(map[string]any{ "name": info.Name, "title": info.Title, "app": info.App, "channel": info.Channel, "mpd": info.MPD, "key": info.Key, "headers_json": info.HeadersJSON, "rewriter": info.Rewriter, }, "", " ") fmt.Printf("\nstreamd create body:\n%s\n", body) fmt.Printf("\nTip: drm catalog --app %s --channel %s --keys --wvd path/to/device.wvd --json\n", appName, info.Channel) } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] + "..." }