Nest credential sections under go2rtc in frigate config

Frigate rejects unknown top-level keys (extra="forbid" on root config),
but its RestreamConfig (go2rtc: block) allows extra keys. Move credential
sections under go2rtc: with 2/4 space indentation.

- writeCredentials emits "  xiaomi:" + "    \"<key>\": <value>"
- upsertSection matches 2-space section header + 4-space key regex
- insertNewSection places new nested sections after streams: block
- findStreamInsertPoint stops at sibling headers (2-space) inside go2rtc:
- Add xiaomi_test.go with 16 scenarios covering new config, addToConfig
  merging, token refresh, dedup, sort order, malformed URLs, special chars,
  go2rtc override, mixed protocols, and section order
This commit is contained in:
eduard256
2026-04-18 08:49:04 +00:00
parent 12780d7803
commit 3a48e23100
4 changed files with 476 additions and 40 deletions
+66 -30
View File
@@ -15,6 +15,7 @@ var (
reStreamName = regexp.MustCompile(`^\s{4}'?(\w[\w-]*)'?:`)
reStreamContent = regexp.MustCompile(`^\s{4,}`)
reNextSection = regexp.MustCompile(`^[a-z#]`)
reSibling = regexp.MustCompile(`^ \w`) // sibling under go2rtc: (ex. ` xiaomi:`)
reCameraBody = regexp.MustCompile(`^\s{2,}\S`)
reVersion = regexp.MustCompile(`^version:`)
)
@@ -138,6 +139,13 @@ func findStreamInsertPoint(lines []string) int {
continue
}
if in {
// stop at sibling under go2rtc: (ex. ` xiaomi:`)
if reSibling.MatchString(line) && !reStreamsHeader.MatchString(line) {
if last >= 0 {
return last + 1
}
return headerIdx + 1
}
if reStreamContent.MatchString(line) {
last = i
} else if reNextSection.MatchString(line) {
@@ -160,10 +168,10 @@ func findStreamInsertPoint(lines []string) int {
return -1
}
// upsertCredentials merges creds into existing top-level sections. For each
// section: if a matching line ` "<key>":` exists -- replace its value; else
// insert in sorted order. If the section itself doesn't exist -- create a new
// top-level block just before `cameras:`.
// upsertCredentials merges creds into credential sections nested under go2rtc:.
// For each section: if a matching line ` "<key>":` exists -- replace its
// value; else insert in sorted order. If the section itself doesn't exist --
// create a new nested block inside go2rtc: (after streams: block).
func upsertCredentials(lines []string, creds map[string]map[string]string, added map[int]bool) ([]string, map[int]bool) {
if len(creds) == 0 {
return lines, added
@@ -182,11 +190,12 @@ func upsertCredentials(lines []string, creds map[string]map[string]string, added
return lines, added
}
// upsertSection updates or appends a single top-level section.
var reCredKey = regexp.MustCompile(`^\s{2}"([^"]+)":`)
// ex. ` "4161148305": V1:xxx` -- 4-space indent under nested section
var reCredKey = regexp.MustCompile(`^\s{4}"([^"]+)":`)
func upsertSection(lines []string, section string, kv map[string]string, added map[int]bool) ([]string, map[int]bool) {
reHeader := regexp.MustCompile(`^` + regexp.QuoteMeta(section) + `:\s*$`)
// section header is nested under go2rtc:, ex. ` xiaomi:`
reHeader := regexp.MustCompile(`^ ` + regexp.QuoteMeta(section) + `:\s*$`)
headerIdx := -1
for i, line := range lines {
@@ -200,10 +209,16 @@ func upsertSection(lines []string, section string, kv map[string]string, added m
return insertNewSection(lines, section, kv, added)
}
// section exists -- find last content line of the section (skip trailing blanks)
// section exists -- find end (blank line, top-level header, or sibling 2-space key)
end := len(lines)
for i := headerIdx + 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == "" || reTopLevel.MatchString(lines[i]) {
line := lines[i]
if strings.TrimSpace(line) == "" || reTopLevel.MatchString(line) {
end = i
break
}
// sibling under go2rtc: has 2-space indent, not 4
if len(line) >= 2 && line[0] == ' ' && line[1] == ' ' && (len(line) == 2 || line[2] != ' ') {
end = i
break
}
@@ -216,9 +231,9 @@ func upsertSection(lines []string, section string, kv map[string]string, added m
sort.Strings(keys)
for _, k := range keys {
newLine := fmt.Sprintf(" %q: %s", k, kv[k])
newLine := fmt.Sprintf(" %q: %s", k, kv[k])
// try replace -- no length change, just mark modified line as added
// try replace -- no length change, just mark modified line
replaced := false
for i := headerIdx + 1; i < end; i++ {
if m := reCredKey.FindStringSubmatch(lines[i]); m != nil && m[1] == k {
@@ -257,35 +272,24 @@ func upsertSection(lines []string, section string, kv map[string]string, added m
return lines, added
}
// insertNewSection adds a new nested section under go2rtc:, after the streams:
// block but before any sibling go2rtc key or top-level header.
func insertNewSection(lines []string, section string, kv map[string]string, added map[int]bool) ([]string, map[int]bool) {
camIdx := -1
for i, line := range lines {
if reCamerasHeader.MatchString(line) {
camIdx = i
break
}
}
if camIdx == -1 {
camIdx = len(lines)
// find end of streams: block inside go2rtc:
insertAt := findGo2RTCInsertPoint(lines)
if insertAt < 0 {
return lines, added
}
// insert point: right before the blank line that precedes cameras:
// keep one blank line between blocks
insertAt := camIdx
for insertAt > 0 && strings.TrimSpace(lines[insertAt-1]) == "" {
insertAt--
}
block := []string{section + ":"}
block := []string{" " + section + ":"}
keys := make([]string, 0, len(kv))
for k := range kv {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
block = append(block, fmt.Sprintf(" %q: %s", k, kv[k]))
block = append(block, fmt.Sprintf(" %q: %s", k, kv[k]))
}
block = append(block, "")
lines = append(lines[:insertAt], append(block, lines[insertAt:]...)...)
added = shiftAdded(added, insertAt)
@@ -296,6 +300,38 @@ func insertNewSection(lines []string, section string, kv map[string]string, adde
return lines, added
}
// findGo2RTCInsertPoint returns the line index where a new nested section
// under go2rtc: should be inserted -- after the last non-blank content line
// of the go2rtc: block.
func findGo2RTCInsertPoint(lines []string) int {
reGo2RTCHeader := regexp.MustCompile(`^go2rtc:\s*$`)
headerIdx := -1
for i, line := range lines {
if reGo2RTCHeader.MatchString(line) {
headerIdx = i
break
}
}
if headerIdx == -1 {
return -1
}
last := headerIdx
for i := headerIdx + 1; i < len(lines); i++ {
line := lines[i]
if strings.TrimSpace(line) == "" {
continue
}
if reTopLevel.MatchString(line) {
break
}
last = i
}
return last + 1
}
// shiftAdded moves all marks at index >= from by +1. Also used with from=len(lines)
// as a no-op shift (just return same map).
func shiftAdded(added map[int]bool, from int) map[int]bool {