Enhance XEPG channel mapping and settings management
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-13 16:09:00 +11:00
parent 32c3d779c0
commit 125b0bb35f
15 changed files with 465 additions and 133 deletions

View File

@@ -9,6 +9,7 @@ import (
"path"
"runtime"
"sort"
"unicode"
"crypto/md5"
"encoding/hex"
@@ -526,10 +527,132 @@ func createXEPGDatabase() (err error) {
return
}
func normalizeXEPGMatchValue(value string) string {
value = strings.TrimSpace(value)
if len(value) == 0 {
return ""
}
return strings.Map(func(r rune) rune {
switch {
case unicode.IsLetter(r):
return unicode.ToLower(r)
case unicode.IsDigit(r):
return r
default:
return -1
}
}, value)
}
func appendXEPGIssueSample(samples map[string]struct{}, value string) {
value = strings.TrimSpace(value)
if len(value) == 0 {
return
}
samples[value] = struct{}{}
}
func xepgIssueSampleText(samples map[string]struct{}, max int) string {
if len(samples) == 0 {
return ""
}
list := make([]string, 0, len(samples))
for name := range samples {
list = append(list, name)
}
sort.Strings(list)
if len(list) > max {
return strings.Join(list[:max], ", ") + ", ..."
}
return strings.Join(list, ", ")
}
func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGChannelStruct) (channelID string, channel map[string]any, ok bool) {
var candidateValues = map[string]struct{}{}
addCandidate := func(value string) {
normalized := normalizeXEPGMatchValue(value)
if len(normalized) == 0 {
return
}
candidateValues[normalized] = struct{}{}
}
addCandidate(xepgChannel.XName)
addCandidate(xepgChannel.Name)
addCandidate(xepgChannel.TvgName)
addCandidate(xepgChannel.TvgID)
if len(candidateValues) == 0 {
return "", nil, false
}
if _, exists := candidateValues[normalizeXEPGMatchValue(xepgChannel.TvgID)]; exists {
if direct, found := xmltvChannels[xepgChannel.TvgID].(map[string]any); found {
return xepgChannel.TvgID, direct, true
}
}
var matches []struct {
id string
channel map[string]any
}
for id, data := range xmltvChannels {
xmltvChannel, castOK := data.(map[string]any)
if castOK == false {
continue
}
if _, match := candidateValues[normalizeXEPGMatchValue(id)]; match {
matches = append(matches, struct {
id string
channel map[string]any
}{id: id, channel: xmltvChannel})
continue
}
displayName, hasDisplayName := xmltvChannel["display-name"].(string)
if hasDisplayName == false {
continue
}
if _, match := candidateValues[normalizeXEPGMatchValue(displayName)]; match {
matches = append(matches, struct {
id string
channel map[string]any
}{id: id, channel: xmltvChannel})
}
}
if len(matches) != 1 {
return "", nil, false
}
return matches[0].id, matches[0].channel, true
}
// Kanäle automatisch zuordnen und das Mapping überprüfen
func mapping() (err error) {
showInfo("XEPG:" + "Map channels")
strictMissingEPGMode := Settings.XepgMissingEPGMode != "relaxed"
missingEPGCount := 0
missingXMLTVCount := 0
autoRemapCount := 0
relaxedKeepCount := 0
relaxedDummyCount := 0
missingEPGSamples := map[string]struct{}{}
missingXMLTVSamples := map[string]struct{}{}
autoRemapSamples := map[string]struct{}{}
relaxedKeepSamples := map[string]struct{}{}
relaxedDummySamples := map[string]struct{}{}
for xepg, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
@@ -604,20 +727,68 @@ func mapping() (err error) {
}
} else {
if channelID, replacementChannel, remapOK := findXEPGReplacementChannel(value, xepgChannel); remapOK {
xepgChannel.XMapping = channelID
ShowError(fmt.Errorf("Missing EPG data: %s", xepgChannel.Name), 0)
showWarning(2302)
xepgChannel.XActive = false
if logo, ok := replacementChannel["icon"].(string); ok {
if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 {
xepgChannel.TvgLogo = logo
}
}
autoRemapCount++
name := strings.TrimSpace(xepgChannel.Name)
if len(name) == 0 {
name = strings.TrimSpace(xepgChannel.XName)
}
if len(name) == 0 {
name = xepg
}
appendXEPGIssueSample(autoRemapSamples, name)
} else {
name := strings.TrimSpace(xepgChannel.Name)
if len(name) == 0 {
name = strings.TrimSpace(xepgChannel.XName)
}
if len(name) == 0 {
name = xepg
}
if strictMissingEPGMode == true {
missingEPGCount++
appendXEPGIssueSample(missingEPGSamples, name)
xepgChannel.XActive = false
} else {
relaxedKeepCount++
appendXEPGIssueSample(relaxedKeepSamples, name)
}
}
}
} else {
var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file)))
providerName := getProviderParameter(fileID, "xmltv", "name")
if len(strings.TrimSpace(providerName)) == 0 {
providerName = file
}
ShowError(fmt.Errorf("Missing XMLTV file: %s", getProviderParameter(fileID, "xmltv", "name")), 0)
showWarning(2301)
xepgChannel.XActive = false
if strictMissingEPGMode == true {
missingXMLTVCount++
appendXEPGIssueSample(missingXMLTVSamples, providerName)
xepgChannel.XActive = false
} else {
relaxedDummyCount++
name := strings.TrimSpace(xepgChannel.Name)
if len(name) == 0 {
name = strings.TrimSpace(xepgChannel.XName)
}
if len(name) == 0 {
name = xepg
}
appendXEPGIssueSample(relaxedDummySamples, name)
}
}
@@ -639,6 +810,28 @@ func mapping() (err error) {
}
if autoRemapCount > 0 {
showInfo(fmt.Sprintf("XEPG:%d channel mappings were auto-remapped (examples: %s)", autoRemapCount, xepgIssueSampleText(autoRemapSamples, 8)))
}
if missingEPGCount > 0 {
showWarning(2302)
showInfo(fmt.Sprintf("XEPG:%d channels have missing EPG mappings and were deactivated (examples: %s)", missingEPGCount, xepgIssueSampleText(missingEPGSamples, 8)))
}
if missingXMLTVCount > 0 {
showWarning(2301)
showInfo(fmt.Sprintf("XEPG:%d channels reference missing XMLTV files and were deactivated (sources: %s)", missingXMLTVCount, xepgIssueSampleText(missingXMLTVSamples, 5)))
}
if relaxedKeepCount > 0 {
showInfo(fmt.Sprintf("XEPG:%d channels kept active in relaxed mode despite missing EPG mappings (examples: %s)", relaxedKeepCount, xepgIssueSampleText(relaxedKeepSamples, 8)))
}
if relaxedDummyCount > 0 {
showInfo(fmt.Sprintf("XEPG:%d channels will use xTeVe Dummy guide in relaxed mode because XMLTV sources were unavailable (examples: %s)", relaxedDummyCount, xepgIssueSampleText(relaxedDummySamples, 8)))
}
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
if err != nil {
return
@@ -740,6 +933,13 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
var xmltvFile = System.Folder.Data + xepgChannel.XmltvFile
var channelID = xepgChannel.XMapping
relaxedMissingEPGMode := Settings.XepgMissingEPGMode == "relaxed"
fallbackToDummy := func() {
dummyChannel := xepgChannel
dummyChannel.XmltvFile = "xTeVe Dummy"
dummyChannel.XMapping = "240_Minutes"
xepgXML = createDummyProgram(dummyChannel)
}
var xmltv XMLTV
@@ -749,6 +949,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
err = getLocalXMLTV(xmltvFile, &xmltv)
if err != nil {
if relaxedMissingEPGMode == true {
fallbackToDummy()
err = nil
}
return
}
@@ -822,6 +1026,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
}
if len(xepgXML.Program) == 0 && relaxedMissingEPGMode == true && xmltvFile != System.Folder.Data+"xTeVe Dummy" {
fallbackToDummy()
}
return
}
@@ -835,7 +1043,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
var currentDay = currentTime.Format("20060102")
var startTime, _ = time.Parse("20060102150405", currentDay+"000000")
showInfo("Create Dummy Guide:" + "Time offset" + offset + " - " + xepgChannel.XName)
showDebug("Create Dummy Guide:"+"Time offset"+offset+" - "+xepgChannel.XName, 2)
var dl = strings.Split(xepgChannel.XMapping, "_")
dummyLength, err := strconv.Atoi(dl[0])