Fix Drone YAML, add regex filters and per-playlist buffer mode (backend)
continuous-integration/drone/push Build is passing

- .drone.yml: two command strings contained ": " and were parsed as maps
  by Drone's YAML loader ("cannot unmarshal !!map into string"). Quoted.
- Filters: new type "regex-filter". The rule is a Go regular expression
  tried against the channel name, the group title and the raw attribute
  line; case-insensitive unless the filter is marked case sensitive.
  Patterns are validated when the filter is saved and when rules are
  rebuilt, and compiled once (cached). The two fixed {include}/!{exclude}
  patterns are compiled at package level instead of per stream.
- Per-playlist buffer: a playlist's "buffer" parameter ("-", "xteve",
  "ffmpeg", "vlc") overrides the global setting for that playlist in
  the stream handler, the buffer start, the tuner lookup and the external
  process buffer. Anything else falls back to the global setting.
  (UI controls for both follow with the Phase 5 frontend work.)
- Tests: TestRegexFilter, TestBufferModeFor.
This commit is contained in:
nathan committed 2026-09-26 13:24:11 +10:00
1 parent 3a3ba861dc
commit 0b7a54d7da
7 files changed
+190 -17

No files matched your search

+51 -6
View File
@@ -8,6 +8,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
m3u "xteve/src/internal/m3u-parser"
)
@@ -34,12 +35,47 @@ func parsePlaylist(filename, fileType string) (channels []any, err error) {
}
// Streams filtern
// Patterns for the {include} and !{exclude} parts of a filter rule.
var filterIncludeRe = regexp.MustCompile(`[{]+[^.]+[}]`)
var filterExcludeRe = regexp.MustCompile(`!+[{]+[^.]+[}]`)
// regexFilterCache : compiled "regex-filter" rules, keyed by rule and case mode.
var regexFilterCache sync.Map
// compileRegexFilter : compiles (and caches) a regex-filter rule. Case
// insensitive rules get the (?i) flag. Returns nil for an invalid pattern.
func compileRegexFilter(rule string, caseSensitive bool) *regexp.Regexp {
var key = fmt.Sprintf("%t:%s", caseSensitive, rule)
if v, ok := regexFilterCache.Load(key); ok {
return v.(*regexp.Regexp)
}
var pattern = rule
if !caseSensitive {
pattern = "(?i)" + rule
}
re, err := regexp.Compile(pattern)
if err != nil {
return nil
}
regexFilterCache.Store(key, re)
return re
}
// validateRegexFilter : error for an invalid regex-filter rule.
func validateRegexFilter(rule string) error {
_, err := regexp.Compile(rule)
return err
}
func filterThisStream(s any) (status bool) {
status = false
var stream = s.(map[string]string)
var regexpYES = `[{]+[^.]+[}]`
var regexpNO = `!+[{]+[^.]+[}]`
for _, filter := range Data.Filter {
@@ -53,6 +89,17 @@ func filterThisStream(s any) (status bool) {
var streamValues = strings.Replace(stream["_values"], "\r", "", -1)
// Regular expression, tried against the channel name, then the group
// title, then the raw attribute line (so anchors work on the name).
if filter.Type == "regex-filter" {
if re := compileRegexFilter(filter.Rule, filter.CaseSensitive); re != nil {
if re.MatchString(stream["name"]) || re.MatchString(stream["group-title"]) || re.MatchString(streamValues) {
return true
}
}
continue
}
if v, ok := stream["group-title"]; ok {
group = v
}
@@ -62,8 +109,7 @@ func filterThisStream(s any) (status bool) {
}
// Unerwünschte Streams !{DEU}
r := regexp.MustCompile(regexpNO)
val := r.FindStringSubmatch(filter.Rule)
val := filterExcludeRe.FindStringSubmatch(filter.Rule)
if len(val) == 1 {
@@ -74,8 +120,7 @@ func filterThisStream(s any) (status bool) {
}
// Muss zusätzlich erfüllt sein {DEU}
r = regexp.MustCompile(regexpYES)
val = r.FindStringSubmatch(filter.Rule)
val = filterIncludeRe.FindStringSubmatch(filter.Rule)
if len(val) == 1 {