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

@@ -236,6 +236,20 @@ nav p {
word-break: break-word;
}
#clientInfo .tdVal a {
color: #8fdcff;
text-decoration: none;
border-bottom: 1px dotted rgba(143, 220, 255, 0.6);
word-break: break-all;
}
#clientInfo .tdVal a:hover,
#clientInfo .tdVal a:focus {
color: #d7f4ff;
border-bottom-color: #d7f4ff;
text-decoration: none;
}
.dashboard-cards {
display: grid;
grid-template-columns: repeat(6, minmax(130px, 1fr));

View File

@@ -22,7 +22,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
// Kategorien für die Einstellungen
var settingsCategory = new Array();
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"));

View File

@@ -1052,6 +1052,30 @@ function PageReady() {
}, 10000);
return;
}
function isClientInfoHttpURL(value) {
return /^https?:\/\//i.test(value);
}
function setClientInfoValue(key, value) {
var element = document.getElementById(key);
if (element == null) {
return;
}
var textValue = "";
if (value != undefined && value != null) {
textValue = String(value);
}
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
element.innerHTML = "";
var anchor = document.createElement("A");
anchor.href = textValue;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.textContent = textValue;
element.appendChild(anchor);
return;
}
element.innerHTML = textValue;
}
function createLayout() {
var contentRegion = document.getElementById("content");
if (contentRegion != null) {
@@ -1061,9 +1085,7 @@ function createLayout() {
var obj = SERVER["clientInfo"];
var keys = getObjKeys(obj);
for (var i = 0; i < keys.length; i++) {
if (document.getElementById(keys[i])) {
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
}
setClientInfoValue(keys[i], obj[keys[i]]);
}
renderStatusCards();
if (!document.getElementById("main-menu")) {

View File

@@ -275,6 +275,18 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdRight);
break;
// Select
case "xepg.missing.epg.mode":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.xepgMissingEPGMode.title}}" + ":";
var tdRight = document.createElement("TD");
var text = ["{{.settings.xepgMissingEPGMode.info_strict}}", "{{.settings.xepgMissingEPGMode.info_relaxed}}"];
var values = ["strict", "relaxed"];
var select = content.createSelect(text, values, data, settingsKey);
select.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(select);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "tuner":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":";
@@ -440,6 +452,9 @@ var SettingsCategory = /** @class */ (function () {
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}";
break;
case "xepg.missing.epg.mode":
text = "{{.settings.xepgMissingEPGMode.description}}";
break;
case "udpxy":
text = "{{.settings.udpxy.description}}";
break;

View File

@@ -399,6 +399,13 @@
"title": "Replace missing program images",
"description": "If the poster in the XMLTV program is missing, the channel logo will be used."
},
"xepgMissingEPGMode":
{
"title": "Missing EPG Handling",
"description": "Strict: channels are deactivated when XMLTV mappings disappear.<br>Relaxed: channels stay active with last-known mappings and fall back to a dummy guide whenever guide data cannot be resolved.",
"info_strict": "Strict (deactivate channels)",
"info_relaxed": "Relaxed (keep active / dummy guide)"
},
"xteveAutoUpdate":
{
"title": "Automatic update of xTeVe",

View File

@@ -85,6 +85,16 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
case "xepg.replace.missing.images":
createXEPGFiles = true
case "xepg.missing.epg.mode":
if v, ok := value.(string); ok {
mode := strings.ToLower(strings.TrimSpace(v))
if mode != "relaxed" {
mode = "strict"
}
value = mode
}
reloadData = true
case "backup.path":
value = strings.TrimRight(value.(string), string(os.PathSeparator)) + string(os.PathSeparator)
err = checkFolder(value.(string))

View File

@@ -1,100 +1,101 @@
package src
import (
"fmt"
"strings"
"fmt"
"strings"
)
// ShowSystemInfo : Systeminformationen anzeigen
func ShowSystemInfo() {
fmt.Print("Creating the information takes a moment...")
err := buildDatabaseDVR()
if err != nil {
ShowError(err, 0)
return
}
fmt.Print("Creating the information takes a moment...")
err := buildDatabaseDVR()
if err != nil {
ShowError(err, 0)
return
}
buildXEPG(false)
buildXEPG(false)
fmt.Println("OK")
println()
fmt.Println("OK")
println()
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter)))
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount)))
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter)))
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount)))
println()
fmt.Println(fmt.Sprintf("IPv4 Addresses:"))
println()
fmt.Println(fmt.Sprintf("IPv4 Addresses:"))
for i, ipv4 := range System.IPAddressesV4 {
for i, ipv4 := range System.IPAddressesV4 {
switch count := i; {
switch count := i; {
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
}
}
}
}
println()
fmt.Println(fmt.Sprintf("IPv6 Addresses:"))
println()
fmt.Println(fmt.Sprintf("IPv6 Addresses:"))
for i, ipv4 := range System.IPAddressesV6 {
for i, ipv4 := range System.IPAddressesV6 {
switch count := i; {
switch count := i; {
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
}
}
}
}
println("---")
println("---")
fmt.Println("Settings [General]")
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
fmt.Println("Settings [General]")
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
println("---")
println("---")
fmt.Println("Settings [Files]")
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ",")))
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages))
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
fmt.Println("Settings [Files]")
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ",")))
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages))
fmt.Println(fmt.Sprintf("Missing EPG Mode: %s", Settings.XepgMissingEPGMode))
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
println("---")
println("---")
fmt.Println("Settings [Streaming]")
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer))
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy))
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))
fmt.Println("Settings [Streaming]")
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer))
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy))
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))
println("---")
println("---")
fmt.Println("Settings [Backup]")
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath))
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep))
fmt.Println("Settings [Backup]")
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath))
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep))
}

View File

@@ -300,6 +300,7 @@ type SettingsStruct struct {
UUID string `json:"uuid"`
UDPxy string `json:"udpxy"`
Version string `json:"version"`
XepgMissingEPGMode string `json:"xepg.missing.epg.mode"`
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
WizardCompleted bool `json:"wizard.completed"`

View File

@@ -43,6 +43,7 @@ type RequestStruct struct {
UDPxy *string `json:"udpxy,omitempty"`
Update *[]string `json:"update,omitempty"`
UserAgent *string `json:"user.agent,omitempty"`
XepgMissingEPGMode *string `json:"xepg.missing.epg.mode,omitempty"`
XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"`
XteveAutoUpdate *bool `json:"xteveAutoUpdate,omitempty"`
SchemeM3U *string `json:"scheme.m3u,omitempty"`

View File

@@ -141,6 +141,7 @@ func loadSettings() (settings SettingsStruct, err error) {
defaults["language"] = "en"
defaults["log.entries.ram"] = 500
defaults["mapping.first.channel"] = 1000
defaults["xepg.missing.epg.mode"] = "strict"
defaults["xepg.replace.missing.images"] = true
defaults["m3u8.adaptive.bandwidth.mbps"] = 10
defaults["port"] = "34400"
@@ -230,6 +231,11 @@ func saveSettings(settings SettingsStruct) (err error) {
settings.UserAgent = defaultUserAgent
}
settings.XepgMissingEPGMode = strings.ToLower(strings.TrimSpace(settings.XepgMissingEPGMode))
if settings.XepgMissingEPGMode != "relaxed" {
settings.XepgMissingEPGMode = "strict"
}
settings.TempPath = strings.TrimRight(settings.TempPath, string(os.PathSeparator)) + string(os.PathSeparator)
System.Folder.Temp = settings.TempPath + settings.UUID + string(os.PathSeparator)

File diff suppressed because one or more lines are too long

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])

View File

@@ -23,7 +23,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
// Kategorien für die Einstellungen
var settingsCategory = new Array()
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"))

View File

@@ -1055,6 +1055,35 @@ function PageReady() {
return
}
function isClientInfoHttpURL(value:string):boolean {
return /^https?:\/\//i.test(value)
}
function setClientInfoValue(key:string, value:any) {
var element = document.getElementById(key)
if (element == null) {
return
}
var textValue = ""
if (value != undefined && value != null) {
textValue = String(value)
}
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
element.innerHTML = ""
var anchor = document.createElement("A")
anchor.href = textValue
anchor.target = "_blank"
anchor.rel = "noopener noreferrer"
anchor.textContent = textValue
element.appendChild(anchor)
return
}
element.innerHTML = textValue
}
function createLayout() {
// Client Info
@@ -1062,9 +1091,7 @@ function createLayout() {
var keys = getObjKeys(obj);
for (var i = 0; i < keys.length; i++) {
if (document.getElementById(keys[i])) {
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
}
setClientInfoValue(keys[i], obj[keys[i]])
}
renderStatusCards()

View File

@@ -315,9 +315,9 @@ class SettingsCategory {
setting.appendChild(tdRight)
break
case "use_plexAPI":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.usePlexAPI.title}}" + ":"
case "use_plexAPI":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.usePlexAPI.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
@@ -325,14 +325,30 @@ class SettingsCategory {
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
// Select
case "tuner":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
// Select
case "xepg.missing.epg.mode":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.xepgMissingEPGMode.title}}" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["{{.settings.xepgMissingEPGMode.info_strict}}", "{{.settings.xepgMissingEPGMode.info_relaxed}}"]
var values:any[] = ["strict", "relaxed"]
var select = content.createSelect(text, values, data, settingsKey)
select.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(select)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "tuner":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
var tdRight = document.createElement("TD")
var text = new Array()
@@ -549,12 +565,16 @@ class SettingsCategory {
text = "{{.settings.cacheImages.description}}"
break
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"
break
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"
break
case "udpxy":
text = "{{.settings.udpxy.description}}"
case "xepg.missing.epg.mode":
text = "{{.settings.xepgMissingEPGMode.description}}"
break
case "udpxy":
text = "{{.settings.udpxy.description}}"
break
default: