Files
xTeVe/src/data.go
Nathan Coad 125b0bb35f
All checks were successful
continuous-integration/drone/push Build is passing
Enhance XEPG channel mapping and settings management
2026-02-13 16:09:00 +11:00

1122 lines
21 KiB
Go

package src
import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"sort"
"strconv"
"strings"
"time"
"xteve/src/internal/authentication"
"xteve/src/internal/imgcache"
)
// Einstellungen ändern (WebUI)
func updateServerSettings(request RequestStruct) (settings SettingsStruct, err error) {
var oldSettings = jsonToMap(mapToJSON(Settings))
var newSettings = jsonToMap(mapToJSON(request.Settings))
var reloadData = false
var cacheImages = false
var createXEPGFiles = false
var triggerPlexGuideReload = false
var debug string
// -vvv [URL] --sout '#transcode{vcodec=mp4v, acodec=mpga} :standard{access=http, mux=ogg}'
for key, value := range newSettings {
if _, ok := oldSettings[key]; ok {
switch key {
case "tuner":
showWarning(2105)
case "use_plexAPI":
triggerPlexGuideReload = true
case "plex.url":
if v, ok := value.(string); ok {
value = strings.TrimRight(strings.TrimSpace(v), "/")
}
triggerPlexGuideReload = true
case "plex.token":
if v, ok := value.(string); ok {
value = strings.TrimSpace(v)
}
triggerPlexGuideReload = true
case "epgSource":
reloadData = true
case "update":
// Leerzeichen aus den Werten entfernen und Formatierung der Uhrzeit überprüfen (0000 - 2359)
var newUpdateTimes = make([]string, 0)
for _, v := range value.([]any) {
v = strings.Replace(v.(string), " ", "", -1)
_, err := time.Parse("1504", v.(string))
if err != nil {
ShowError(err, 1012)
return Settings, err
}
newUpdateTimes = append(newUpdateTimes, v.(string))
}
if len(newUpdateTimes) == 0 {
//newUpdateTimes = append(newUpdateTimes, "0000")
}
value = newUpdateTimes
case "cache.images":
cacheImages = true
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))
if err == nil {
err = checkFilePermission(value.(string))
if err != nil {
return
}
}
if err != nil {
return
}
case "temp.path":
value = strings.TrimRight(value.(string), string(os.PathSeparator)) + string(os.PathSeparator)
err = checkFolder(value.(string))
if err == nil {
err = checkFilePermission(value.(string))
if err != nil {
return
}
}
if err != nil {
return
}
case "ffmpeg.path", "vlc.path":
var path = value.(string)
if len(path) > 0 {
err = checkFile(path)
if err != nil {
return
}
}
case "scheme.m3u", "scheme.xml":
createXEPGFiles = true
}
oldSettings[key] = value
if key == "plex.token" {
debug = fmt.Sprintf("Save Setting:Key: %s | Value: ******** (%T)", key, value)
} else {
switch fmt.Sprintf("%T", value) {
case "bool":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value)
case "string":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value)
case "[]interface {}":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value)
case "float64":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
default:
debug = fmt.Sprintf("%T", value)
}
}
showDebug(debug, 1)
}
}
// Einstellungen aktualisieren
err = json.Unmarshal([]byte(mapToJSON(oldSettings)), &Settings)
if err != nil {
return
}
if Settings.AuthenticationWEB == false {
Settings.AuthenticationAPI = false
Settings.AuthenticationM3U = false
Settings.AuthenticationPMS = false
Settings.AuthenticationWEB = false
Settings.AuthenticationXML = false
}
// Buffer Einstellungen überprüfen
if len(Settings.FFmpegOptions) == 0 {
Settings.FFmpegOptions = System.FFmpeg.DefaultOptions
}
if len(Settings.VLCOptions) == 0 {
Settings.VLCOptions = System.VLC.DefaultOptions
}
switch Settings.Buffer {
case "ffmpeg":
if len(Settings.FFmpegPath) == 0 {
err = errors.New(getErrMsg(2020))
return
}
case "vlc":
if len(Settings.VLCPath) == 0 {
err = errors.New(getErrMsg(2021))
return
}
}
err = saveSettings(Settings)
if err == nil {
settings = Settings
if reloadData == true {
err = buildDatabaseDVR()
if err != nil {
return
}
buildXEPG(false)
}
if cacheImages == true {
if Settings.EpgSource == "XEPG" && System.ImageCachingInProgress == 0 {
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
if err != nil {
ShowError(err, 0)
}
switch Settings.CacheImages {
case false:
createXMLTVFile()
createM3UFile()
case true:
go func() {
createXMLTVFile()
createM3UFile()
System.ImageCachingInProgress = 1
showInfo("Image Caching:Images are cached")
Data.Cache.Images.Image.Caching()
showInfo("Image Caching:Done")
System.ImageCachingInProgress = 0
buildXEPG(false)
}()
}
}
}
if createXEPGFiles == true {
go func() {
createXMLTVFile()
createM3UFile()
}()
}
if triggerPlexGuideReload == true {
queuePlexGuideRefresh("settings change")
}
}
return
}
// Providerdaten speichern (WebUI)
func saveFiles(request RequestStruct, fileType string) (err error) {
var filesMap = make(map[string]any)
var newData = make(map[string]any)
var indicator string
var reloadData = false
switch fileType {
case "m3u":
filesMap = Settings.Files.M3U
newData = request.Files.M3U
indicator = "M"
case "hdhr":
filesMap = Settings.Files.HDHR
newData = request.Files.HDHR
indicator = "H"
case "xmltv":
filesMap = Settings.Files.XMLTV
newData = request.Files.XMLTV
indicator = "X"
}
if len(filesMap) == 0 {
filesMap = make(map[string]any)
}
for dataID, data := range newData {
if dataID == "-" {
// Neue Providerdatei
dataID = indicator + randomString(19)
data.(map[string]any)["new"] = true
filesMap[dataID] = data
} else {
// Bereits vorhandene Providerdatei
for key, value := range data.(map[string]any) {
var oldData = filesMap[dataID].(map[string]any)
oldData[key] = value
}
}
switch fileType {
case "m3u":
Settings.Files.M3U = filesMap
case "hdhr":
Settings.Files.HDHR = filesMap
case "xmltv":
Settings.Files.XMLTV = filesMap
}
// Neue Providerdatei
if _, ok := data.(map[string]any)["new"]; ok {
reloadData = true
err = getProviderData(fileType, dataID)
delete(data.(map[string]any), "new")
if err != nil {
delete(filesMap, dataID)
return
}
}
if _, ok := data.(map[string]any)["delete"]; ok {
deleteLocalProviderFiles(dataID, fileType)
reloadData = true
}
err = saveSettings(Settings)
if err != nil {
return
}
if reloadData == true {
err = buildDatabaseDVR()
if err != nil {
return err
}
buildXEPG(false)
}
Settings, _ = loadSettings()
}
return
}
// Providerdaten manuell aktualisieren (WebUI)
func updateFile(request RequestStruct, fileType string) (err error) {
var updateData = make(map[string]any)
switch fileType {
case "m3u":
updateData = request.Files.M3U
case "hdhr":
updateData = request.Files.HDHR
case "xmltv":
updateData = request.Files.XMLTV
}
for dataID := range updateData {
err = getProviderData(fileType, dataID)
if err == nil {
err = buildDatabaseDVR()
buildXEPG(false)
}
}
return
}
// Providerdaten löschen (WebUI)
func deleteLocalProviderFiles(dataID, fileType string) {
var removeData = make(map[string]any)
var fileExtension string
switch fileType {
case "m3u":
removeData = Settings.Files.M3U
fileExtension = ".m3u"
case "hdhr":
removeData = Settings.Files.HDHR
fileExtension = ".json"
case "xmltv":
removeData = Settings.Files.XMLTV
fileExtension = ".xml"
}
if _, ok := removeData[dataID]; ok {
delete(removeData, dataID)
os.RemoveAll(System.Folder.Data + dataID + fileExtension)
}
return
}
// Filtereinstellungen speichern (WebUI)
func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
var filterMap = make(map[int64]any)
var newData = make(map[int64]any)
var defaultFilter FilterStruct
var newFilter = false
defaultFilter.Active = true
defaultFilter.CaseSensitive = false
filterMap = Settings.Filter
newData = request.Filter
var createNewID = func() (id int64) {
newID:
if _, ok := filterMap[id]; ok {
id++
goto newID
}
return id
}
for dataID, data := range newData {
if dataID == -1 {
// Neuer Filter
newFilter = true
dataID = createNewID()
filterMap[dataID] = jsonToMap(mapToJSON(defaultFilter))
}
// Filter aktualisieren / löschen
for key, value := range data.(map[string]any) {
// Filter löschen
if _, ok := data.(map[string]any)["delete"]; ok {
delete(filterMap, dataID)
break
}
if filter, ok := data.(map[string]any)["filter"].(string); ok {
if len(filter) == 0 {
err = errors.New(getErrMsg(1014))
if newFilter == true {
delete(filterMap, dataID)
}
return
}
}
if oldData, ok := filterMap[dataID].(map[string]any); ok {
oldData[key] = value
}
}
}
err = saveSettings(Settings)
if err != nil {
return
}
settings = Settings
err = buildDatabaseDVR()
if err != nil {
return
}
buildXEPG(false)
return
}
// XEPG Mapping speichern
func saveXEpgMapping(request RequestStruct) (err error) {
var tmp = Data.XEPG
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
if err != nil {
ShowError(err, 0)
}
err = json.Unmarshal([]byte(mapToJSON(request.EpgMapping)), &tmp)
if err != nil {
return
}
err = saveMapToJSONFile(System.File.XEPG, request.EpgMapping)
if err != nil {
return err
}
Data.XEPG.Channels = request.EpgMapping
if System.ScanInProgress == 0 {
System.ScanInProgress = 1
cleanupXEPG()
System.ScanInProgress = 0
buildXEPG(true)
} else {
// Wenn während des erstellen der Datanbank das Mapping erneut gespeichert wird, wird die Datenbank erst später erneut aktualisiert.
go func() {
if System.BackgroundProcess == true {
return
}
System.BackgroundProcess = true
for {
time.Sleep(time.Duration(1) * time.Second)
if System.ScanInProgress == 0 {
break
}
}
System.ScanInProgress = 1
cleanupXEPG()
System.ScanInProgress = 0
buildXEPG(false)
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.BackgroundProcess = false
}()
}
return
}
// Benutzerdaten speichern (WebUI)
func saveUserData(request RequestStruct) (err error) {
var userData = request.UserData
var newCredentials = func(userID string, newUserData map[string]any) (err error) {
var newUsername, newPassword string
if username, ok := newUserData["username"].(string); ok {
newUsername = username
}
if password, ok := newUserData["password"].(string); ok {
newPassword = password
}
if len(newUsername) > 0 {
err = authentication.ChangeCredentials(userID, newUsername, newPassword)
}
return
}
for userID, newUserData := range userData {
err = newCredentials(userID, newUserData.(map[string]any))
if err != nil {
return
}
if request.DeleteUser == true {
err = authentication.RemoveUser(userID)
return
}
delete(newUserData.(map[string]any), "password")
delete(newUserData.(map[string]any), "confirm")
if _, ok := newUserData.(map[string]any)["delete"]; ok {
authentication.RemoveUser(userID)
} else {
err = authentication.WriteUserData(userID, newUserData.(map[string]any))
if err != nil {
return
}
}
}
return
}
// Neuen Benutzer anlegen (WebUI)
func saveNewUser(request RequestStruct) (err error) {
var data = request.UserData
var username = data["username"].(string)
var password = data["password"].(string)
delete(data, "password")
delete(data, "confirm")
userID, err := authentication.CreateNewUser(username, password)
if err != nil {
return
}
err = authentication.WriteUserData(userID, data)
return
}
// Wizard (WebUI)
func saveWizard(request RequestStruct) (nextStep int, err error) {
var wizard = jsonToMap(mapToJSON(request.Wizard))
for key, value := range wizard {
switch key {
case "tuner":
Settings.Tuner = int(value.(float64))
nextStep = 1
case "epgSource":
Settings.EpgSource = value.(string)
nextStep = 2
case "m3u", "xmltv":
var filesMap = make(map[string]any)
var data = make(map[string]any)
var indicator, dataID string
filesMap = make(map[string]any)
data["type"] = key
data["new"] = true
switch key {
case "m3u":
filesMap = Settings.Files.M3U
data["name"] = "M3U"
indicator = "M"
case "xmltv":
filesMap = Settings.Files.XMLTV
data["name"] = "XMLTV"
indicator = "X"
}
dataID = indicator + randomString(19)
data["file.source"] = value.(string)
filesMap[dataID] = data
switch key {
case "m3u":
Settings.Files.M3U = filesMap
nextStep = 3
err = getProviderData(key, dataID)
if err != nil {
ShowError(err, 000)
delete(filesMap, dataID)
return
}
err = buildDatabaseDVR()
if err != nil {
ShowError(err, 000)
delete(filesMap, dataID)
return
}
if Settings.EpgSource == "PMS" {
nextStep = 10
}
case "xmltv":
Settings.Files.XMLTV = filesMap
nextStep = 10
err = getProviderData(key, dataID)
if err != nil {
ShowError(err, 000)
delete(filesMap, dataID)
return
}
buildXEPG(false)
System.ScanInProgress = 0
}
}
}
if nextStep == 10 {
Settings.WizardCompleted = true
}
err = saveSettings(Settings)
if err != nil {
return
}
return
}
// Filterregeln erstellen
func createFilterRules() (err error) {
Data.Filter = nil
var dataFilter Filter
for _, f := range Settings.Filter {
var filter FilterStruct
var exclude, include string
err = json.Unmarshal([]byte(mapToJSON(f)), &filter)
if err != nil {
return
}
switch filter.Type {
case "custom-filter":
dataFilter.CaseSensitive = filter.CaseSensitive
dataFilter.Rule = filter.Filter
dataFilter.Type = filter.Type
Data.Filter = append(Data.Filter, dataFilter)
case "group-title":
if len(filter.Include) > 0 {
include = fmt.Sprintf(" {%s}", filter.Include)
}
if len(filter.Exclude) > 0 {
exclude = fmt.Sprintf(" !{%s}", filter.Exclude)
}
dataFilter.CaseSensitive = filter.CaseSensitive
dataFilter.Rule = fmt.Sprintf("%s%s%s", filter.Filter, include, exclude)
dataFilter.Type = filter.Type
Data.Filter = append(Data.Filter, dataFilter)
}
}
return
}
// Datenbank für das DVR System erstellen
func buildDatabaseDVR() (err error) {
System.ScanInProgress = 1
Data.Streams.All = make([]any, 0, System.UnfilteredChannelLimit)
Data.Streams.Active = make([]any, 0, System.UnfilteredChannelLimit)
Data.Streams.Inactive = make([]any, 0, System.UnfilteredChannelLimit)
Data.Playlist.M3U.Groups.Text = []string{}
Data.Playlist.M3U.Groups.Value = []string{}
Data.StreamPreviewUI.Active = []string{}
Data.StreamPreviewUI.Inactive = []string{}
var availableFileTypes = []string{"m3u", "hdhr"}
var tmpGroupsM3U = make(map[string]int64)
err = createFilterRules()
if err != nil {
return
}
for _, fileType := range availableFileTypes {
var playlistFile = getLocalProviderFiles(fileType)
for n, i := range playlistFile {
var channels []any
var groupTitle, tvgID, uuid int = 0, 0, 0
var keys = []string{"group-title", "tvg-id", "uuid"}
var compatibility = make(map[string]int)
var id = strings.TrimSuffix(getFilenameFromPath(i), path.Ext(getFilenameFromPath(i)))
var playlistName = getProviderParameter(id, fileType, "name")
switch fileType {
case "m3u":
channels, err = parsePlaylist(i, fileType)
case "hdhr":
channels, err = parsePlaylist(i, fileType)
}
if err != nil {
ShowError(err, 1005)
err = errors.New(playlistName + ": Local copy of the file no longer exists")
ShowError(err, 0)
playlistFile = append(playlistFile[:n], playlistFile[n+1:]...)
}
// Streams analysieren
for _, stream := range channels {
var s = stream.(map[string]string)
s["_file.m3u.path"] = i
s["_file.m3u.name"] = playlistName
s["_file.m3u.id"] = id
// Kompatibilität berechnen
for _, key := range keys {
switch key {
case "uuid":
if value, ok := s["_uuid.key"]; ok {
if len(value) > 0 {
uuid++
}
}
case "group-title":
if value, ok := s[key]; ok {
if len(value) > 0 {
if _, ok := tmpGroupsM3U[value]; ok {
tmpGroupsM3U[value]++
} else {
tmpGroupsM3U[value] = 1
}
groupTitle++
}
}
case "tvg-id":
if value, ok := s[key]; ok {
if len(value) > 0 {
tvgID++
}
}
}
}
Data.Streams.All = append(Data.Streams.All, stream)
// Neuer Filter ab Version 1.3.0
var preview string
var status = filterThisStream(stream)
if name, ok := s["name"]; ok {
var group string
if v, ok := s["group-title"]; ok {
group = v
}
preview = fmt.Sprintf("%s [%s]", name, group)
}
switch status {
case true:
Data.StreamPreviewUI.Active = append(Data.StreamPreviewUI.Active, preview)
Data.Streams.Active = append(Data.Streams.Active, stream)
case false:
Data.StreamPreviewUI.Inactive = append(Data.StreamPreviewUI.Inactive, preview)
Data.Streams.Inactive = append(Data.Streams.Inactive, stream)
}
}
if tvgID == 0 {
compatibility["tvg.id"] = 0
} else {
compatibility["tvg.id"] = int(tvgID * 100 / len(channels))
}
if groupTitle == 0 {
compatibility["group.title"] = 0
} else {
compatibility["group.title"] = int(groupTitle * 100 / len(channels))
}
if uuid == 0 {
compatibility["stream.id"] = 0
} else {
compatibility["stream.id"] = int(uuid * 100 / len(channels))
}
compatibility["streams"] = len(channels)
setProviderCompatibility(id, fileType, compatibility)
}
}
for group, count := range tmpGroupsM3U {
var text = fmt.Sprintf("%s (%d)", group, count)
var value = fmt.Sprintf("%s", group)
Data.Playlist.M3U.Groups.Text = append(Data.Playlist.M3U.Groups.Text, text)
Data.Playlist.M3U.Groups.Value = append(Data.Playlist.M3U.Groups.Value, value)
}
sort.Strings(Data.Playlist.M3U.Groups.Text)
sort.Strings(Data.Playlist.M3U.Groups.Value)
if len(Data.Streams.Active) == 0 && len(Data.Streams.All) <= System.UnfilteredChannelLimit && len(Settings.Filter) == 0 {
Data.Streams.Active = Data.Streams.All
Data.Streams.Inactive = make([]any, 0)
Data.StreamPreviewUI.Active = Data.StreamPreviewUI.Inactive
Data.StreamPreviewUI.Inactive = []string{}
}
if len(Data.Streams.Active) > System.PlexChannelLimit {
showWarning(2000)
}
if len(Settings.Filter) == 0 && len(Data.Streams.All) > System.UnfilteredChannelLimit {
showWarning(2001)
}
System.ScanInProgress = 0
showInfo(fmt.Sprintf("All streams:%d", len(Data.Streams.All)))
showInfo(fmt.Sprintf("Active streams:%d", len(Data.Streams.Active)))
showInfo(fmt.Sprintf("Filter:%d", len(Data.Filter)))
sort.Strings(Data.StreamPreviewUI.Active)
sort.Strings(Data.StreamPreviewUI.Inactive)
if Settings.EpgSource != "XEPG" {
queuePlexGuideRefresh("lineup update")
}
return
}
// Speicherort aller lokalen Providerdateien laden, immer für eine Dateityp (M3U, XMLTV usw.)
func getLocalProviderFiles(fileType string) (localFiles []string) {
var fileExtension string
var dataMap = make(map[string]any)
switch fileType {
case "m3u":
fileExtension = ".m3u"
dataMap = Settings.Files.M3U
case "hdhr":
fileExtension = ".json"
dataMap = Settings.Files.HDHR
case "xmltv":
fileExtension = ".xml"
dataMap = Settings.Files.XMLTV
}
for dataID := range dataMap {
localFiles = append(localFiles, System.Folder.Data+dataID+fileExtension)
}
return
}
// Providerparameter anhand von dem Key ausgeben
func getProviderParameter(id, fileType, key string) (s string) {
var dataMap = make(map[string]any)
switch fileType {
case "m3u":
dataMap = Settings.Files.M3U
case "hdhr":
dataMap = Settings.Files.HDHR
case "xmltv":
dataMap = Settings.Files.XMLTV
}
if data, ok := dataMap[id].(map[string]any); ok {
if v, ok := data[key].(string); ok {
s = v
}
if v, ok := data[key].(float64); ok {
s = strconv.Itoa(int(v))
}
}
return
}
// Provider Statistiken Kompatibilität aktualisieren
func setProviderCompatibility(id, fileType string, compatibility map[string]int) {
var dataMap = make(map[string]any)
switch fileType {
case "m3u":
dataMap = Settings.Files.M3U
case "hdhr":
dataMap = Settings.Files.HDHR
case "xmltv":
dataMap = Settings.Files.XMLTV
}
if data, ok := dataMap[id].(map[string]any); ok {
data["compatibility"] = compatibility
switch fileType {
case "m3u":
Settings.Files.M3U = dataMap
case "hdhr":
Settings.Files.HDHR = dataMap
case "xmltv":
Settings.Files.XMLTV = dataMap
}
saveSettings(Settings)
}
}