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.
290 lines
6.1 KiB
Go
290 lines
6.1 KiB
Go
package src
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
m3u "xteve/src/internal/m3u-parser"
|
|
)
|
|
|
|
// Playlisten parsen
|
|
func parsePlaylist(filename, fileType string) (channels []any, err error) {
|
|
|
|
content, err := readByteFromFile(filename)
|
|
var id = strings.TrimSuffix(getFilenameFromPath(filename), path.Ext(getFilenameFromPath(filename)))
|
|
var playlistName = getProviderParameter(id, fileType, "name")
|
|
|
|
if err == nil {
|
|
|
|
switch fileType {
|
|
case "m3u":
|
|
channels, err = m3u.MakeInterfaceFromM3U(content)
|
|
case "hdhr":
|
|
channels, err = makeInteraceFromHDHR(content, playlistName, id)
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// 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)
|
|
|
|
for _, filter := range Data.Filter {
|
|
|
|
if filter.Rule == "" {
|
|
continue
|
|
}
|
|
|
|
var group, name, search string
|
|
var exclude, include string
|
|
var match = false
|
|
|
|
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
|
|
}
|
|
|
|
if v, ok := stream["name"]; ok {
|
|
name = v
|
|
}
|
|
|
|
// Unerwünschte Streams !{DEU}
|
|
val := filterExcludeRe.FindStringSubmatch(filter.Rule)
|
|
|
|
if len(val) == 1 {
|
|
|
|
exclude = val[0][2 : len(val[0])-1]
|
|
filter.Rule = strings.Replace(filter.Rule, " "+val[0], "", -1)
|
|
filter.Rule = strings.Replace(filter.Rule, val[0], "", -1)
|
|
|
|
}
|
|
|
|
// Muss zusätzlich erfüllt sein {DEU}
|
|
val = filterIncludeRe.FindStringSubmatch(filter.Rule)
|
|
|
|
if len(val) == 1 {
|
|
|
|
include = val[0][1 : len(val[0])-1]
|
|
filter.Rule = strings.Replace(filter.Rule, " "+val[0], "", -1)
|
|
filter.Rule = strings.Replace(filter.Rule, val[0], "", -1)
|
|
|
|
}
|
|
|
|
switch filter.CaseSensitive {
|
|
|
|
case false:
|
|
|
|
streamValues = strings.ToLower(streamValues)
|
|
filter.Rule = strings.ToLower(filter.Rule)
|
|
exclude = strings.ToLower(exclude)
|
|
include = strings.ToLower(include)
|
|
group = strings.ToLower(group)
|
|
name = strings.ToLower(name)
|
|
|
|
}
|
|
|
|
switch filter.Type {
|
|
|
|
case "group-title":
|
|
search = name
|
|
|
|
if group == filter.Rule {
|
|
match = true
|
|
}
|
|
|
|
case "custom-filter":
|
|
search = streamValues
|
|
if strings.Contains(search, filter.Rule) {
|
|
match = true
|
|
}
|
|
}
|
|
|
|
if match {
|
|
|
|
if len(exclude) > 0 {
|
|
var status = checkConditions(search, exclude, "exclude")
|
|
if !status {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if len(include) > 0 {
|
|
var status = checkConditions(search, include, "include")
|
|
if !status {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Bedingungen für den Filter
|
|
func checkConditions(streamValues, conditions, coType string) (status bool) {
|
|
|
|
switch coType {
|
|
|
|
case "exclude":
|
|
status = true
|
|
|
|
case "include":
|
|
status = false
|
|
|
|
}
|
|
|
|
conditions = strings.Replace(conditions, ", ", ",", -1)
|
|
conditions = strings.Replace(conditions, " ,", ",", -1)
|
|
|
|
var keys = strings.Split(conditions, ",")
|
|
|
|
for _, key := range keys {
|
|
|
|
if strings.Contains(streamValues, key) {
|
|
|
|
switch coType {
|
|
|
|
case "exclude":
|
|
return false
|
|
|
|
case "include":
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// xTeVe M3U Datei erstellen
|
|
func buildM3U(groups []string) (m3u string, err error) {
|
|
|
|
var imgc = Data.Cache.Images
|
|
var m3uChannels = make(map[float64]XEPGChannelStruct)
|
|
var channelNumbers []float64
|
|
|
|
for _, dxc := range Data.XEPG.Channels {
|
|
|
|
var xepgChannel XEPGChannelStruct
|
|
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
|
if err == nil {
|
|
|
|
if xepgChannel.XActive {
|
|
|
|
if len(groups) > 0 {
|
|
|
|
if indexOfString(xepgChannel.XGroupTitle, groups) == -1 {
|
|
goto Done
|
|
}
|
|
|
|
}
|
|
|
|
var channelNumber, err = strconv.ParseFloat(strings.TrimSpace(xepgChannel.XChannelID), 64)
|
|
|
|
if err == nil {
|
|
m3uChannels[channelNumber] = xepgChannel
|
|
channelNumbers = append(channelNumbers, channelNumber)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Done:
|
|
}
|
|
|
|
// M3U Inhalt erstellen
|
|
sort.Float64s(channelNumbers)
|
|
|
|
var xmltvURL = fmt.Sprintf("%s://%s/xmltv/xteve.xml", System.ServerProtocol.XML, System.Domain)
|
|
m3u = fmt.Sprintf(`#EXTM3U url-tvg="%s" x-tvg-url="%s"`+"\n", xmltvURL, xmltvURL)
|
|
|
|
for _, channelNumber := range channelNumbers {
|
|
|
|
var channel = m3uChannels[channelNumber]
|
|
|
|
var parameter = fmt.Sprintf(`#EXTINF:0 channelID="%s" tvg-chno="%s" tvg-name="%s" tvg-id="%s" tvg-logo="%s" group-title="%s",%s`+"\n", channel.XEPG, channel.XChannelID, channel.XName, channel.XChannelID, imgc.Image.GetURL(channel.TvgLogo), channel.XGroupTitle, channel.XName)
|
|
var stream, err = createStreamingURL("M3U", channel.FileM3UID, channel.XChannelID, channel.XName, channel.URL)
|
|
if err == nil {
|
|
m3u = m3u + parameter + stream + "\n"
|
|
}
|
|
|
|
}
|
|
|
|
if len(groups) == 0 {
|
|
|
|
var filename = System.Folder.Data + "xteve.m3u"
|
|
err = writeByteToFile(filename, []byte(m3u))
|
|
|
|
}
|
|
|
|
return
|
|
}
|