Files
Strix/pkg/generate/config.go
T
eduard256 12780d7803 Add credential extraction registry for generate
Protocols like Xiaomi need credentials (tokens) in a separate top-level
YAML section, not in the stream URL itself. Introduce a registry pattern
mirroring streams.HandleFunc / tester.RegisterSource:

- pkg/generate/registry.go: ExtractFunc + RegisterExtract
- Extractors clean the URL (strip ?token=...) and return section/key/value
- writeCredentials emits sorted sections between go2rtc: and cameras:
- upsertCredentials in addToConfig merges into existing sections:
  * replaces value if key exists (token refresh)
  * inserts in sorted order if new
  * creates new top-level section before cameras: if missing

Xiaomi registers its extractor from internal/xiaomi/xiaomi.go. Adding
Tapo/Ring/Roborock later is one line + a small function in their
internal/*/ module -- zero changes in pkg/generate/.
2026-04-18 08:36:48 +00:00

209 lines
4.7 KiB
Go

package generate
import (
"fmt"
"net/url"
"regexp"
"strings"
)
var needMP4 = map[string]bool{"bubble": true}
var reIPv4 = regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
func Generate(req *Request) (*Response, error) {
if req.MainStream == "" {
return nil, fmt.Errorf("generate: mainStream required")
}
info := buildInfo(req)
if len(req.Objects) > 0 && (req.Detect == nil || !req.Detect.Enabled) {
if req.Detect == nil {
req.Detect = &DetectConfig{Enabled: true}
} else {
req.Detect.Enabled = true
}
}
existing := strings.TrimSpace(req.ExistingConfig)
// generate from scratch if no config or config has no go2rtc streams section
if existing == "" || !strings.Contains(existing, "go2rtc:") {
config := newConfig(info, req)
lines := strings.Count(config, "\n") + 1
added := make([]int, lines)
for i := range added {
added[i] = i + 1
}
return &Response{Config: config, Added: added}, nil
}
return addToConfig(req.ExistingConfig, info, req)
}
func buildInfo(req *Request) *cameraInfo {
mainScheme := urlScheme(req.MainStream)
ip := extractIP(req.MainStream)
sanitized := strings.NewReplacer(".", "_", ":", "_").Replace(ip)
base := "camera"
streamBase := "stream"
if ip != "" {
base = "camera_" + sanitized
streamBase = sanitized
}
mainSource, mainSection, mainKey, mainValue := runExtract(req.MainStream)
info := &cameraInfo{
CameraName: base,
MainStreamName: streamBase + "_main",
MainSource: mainSource,
}
if mainSection != "" {
info.addCredential(mainSection, mainKey, mainValue)
}
if req.Name != "" {
info.CameraName = req.Name
info.MainStreamName = req.Name + "_main"
}
if req.Go2RTC != nil {
if req.Go2RTC.MainStreamName != "" {
info.MainStreamName = req.Go2RTC.MainStreamName
}
if req.Go2RTC.MainStreamSource != "" {
src, section, key, value := runExtract(req.Go2RTC.MainStreamSource)
info.MainSource = src
if section != "" {
info.addCredential(section, key, value)
}
}
}
info.MainPath = "rtsp://127.0.0.1:8554/" + info.MainStreamName
if needMP4[mainScheme] {
info.MainPath += "?mp4"
}
info.MainInputArgs = "preset-rtsp-restream"
if req.Frigate != nil {
if req.Frigate.MainStreamPath != "" {
info.MainPath = req.Frigate.MainStreamPath
}
if req.Frigate.MainStreamInputArgs != "" {
info.MainInputArgs = req.Frigate.MainStreamInputArgs
}
}
if req.SubStream != "" {
subScheme := urlScheme(req.SubStream)
subName := streamBase + "_sub"
if req.Name != "" {
subName = req.Name + "_sub"
}
subSource, subSection, subKey, subValue := runExtract(req.SubStream)
if subSection != "" {
info.addCredential(subSection, subKey, subValue)
}
subPath := "rtsp://127.0.0.1:8554/" + subName
if needMP4[subScheme] {
subPath += "?mp4"
}
subInputArgs := "preset-rtsp-restream"
if req.Go2RTC != nil {
if req.Go2RTC.SubStreamName != "" {
subName = req.Go2RTC.SubStreamName
}
if req.Go2RTC.SubStreamSource != "" {
src, section, key, value := runExtract(req.Go2RTC.SubStreamSource)
subSource = src
if section != "" {
info.addCredential(section, key, value)
}
}
}
if req.Frigate != nil {
if req.Frigate.SubStreamPath != "" {
subPath = req.Frigate.SubStreamPath
}
if req.Frigate.SubStreamInputArgs != "" {
subInputArgs = req.Frigate.SubStreamInputArgs
}
}
info.SubStreamName = subName
info.SubSource = subSource
info.SubPath = subPath
info.SubInputArgs = subInputArgs
}
return info
}
func newConfig(info *cameraInfo, req *Request) string {
var b strings.Builder
b.WriteString("mqtt:\n enabled: false\n\n")
b.WriteString("record:\n enabled: true\n\n")
b.WriteString("go2rtc:\n streams:\n")
writeStreamLines(&b, info)
writeCredentials(&b, info.Credentials)
b.WriteString("cameras:\n")
writeCameraBlock(&b, info, req)
b.WriteString("version: 0.17-0\n")
return b.String()
}
// internals
type cameraInfo struct {
CameraName string
MainStreamName string
MainSource string
MainPath string
MainInputArgs string
SubStreamName string
SubSource string
SubPath string
SubInputArgs string
Credentials map[string]map[string]string // section -> key -> value
}
func (c *cameraInfo) addCredential(section, key, value string) {
if c.Credentials == nil {
c.Credentials = map[string]map[string]string{}
}
if c.Credentials[section] == nil {
c.Credentials[section] = map[string]string{}
}
c.Credentials[section][key] = value
}
func urlScheme(rawURL string) string {
if i := strings.IndexByte(rawURL, ':'); i > 0 {
return rawURL[:i]
}
return ""
}
func extractIP(rawURL string) string {
if u, err := url.Parse(rawURL); err == nil && u.Hostname() != "" {
return u.Hostname()
}
if m := reIPv4.FindString(rawURL); m != "" {
return m
}
return ""
}