package src import ( "encoding/json" "encoding/xml" "errors" "fmt" "io/ioutil" "path" "runtime" "sort" "unicode" "crypto/md5" "encoding/hex" "strconv" "strings" "time" "xteve/src/internal/imgcache" ) // Provider XMLTV Datei überprüfen func checkXMLCompatibility(id string, body []byte) (err error) { var xmltv XMLTV var compatibility = make(map[string]int) err = xml.Unmarshal(body, &xmltv) if err != nil { return } compatibility["xmltv.channels"] = len(xmltv.Channel) compatibility["xmltv.programs"] = len(xmltv.Program) setProviderCompatibility(id, "xmltv", compatibility) return } // XEPG Daten erstellen func buildXEPG(background bool) { if System.ScanInProgress == 1 { return } System.ScanInProgress = 1 var err error 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) } if Settings.EpgSource == "XEPG" { switch background { case true: go func() { createXEPGMapping() createXEPGDatabase() mapping() cleanupXEPG() createXMLTVFile() createM3UFile() queuePlexGuideRefresh("xepg rebuild") showInfo("XEPG:" + fmt.Sprintf("Ready to use")) if Settings.CacheImages == true && System.ImageCachingInProgress == 0 { go func() { System.ImageCachingInProgress = 1 showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue))) Data.Cache.Images.Image.Caching() Data.Cache.Images.Image.Remove() showInfo("Image Caching:Done") createXMLTVFile() createM3UFile() queuePlexGuideRefresh("xepg image cache refresh") System.ImageCachingInProgress = 0 }() } System.ScanInProgress = 0 // Cache löschen /* Data.Cache.XMLTV = make(map[string]XMLTV) Data.Cache.XMLTV = nil */ runtime.GC() }() case false: createXEPGMapping() createXEPGDatabase() mapping() cleanupXEPG() go func() { createXMLTVFile() createM3UFile() queuePlexGuideRefresh("xepg rebuild") if Settings.CacheImages == true && System.ImageCachingInProgress == 0 { go func() { System.ImageCachingInProgress = 1 showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue))) Data.Cache.Images.Image.Caching() Data.Cache.Images.Image.Remove() showInfo("Image Caching:Done") createXMLTVFile() createM3UFile() queuePlexGuideRefresh("xepg image cache refresh") System.ImageCachingInProgress = 0 }() } showInfo("XEPG:" + fmt.Sprintf("Ready to use")) System.ScanInProgress = 0 // Cache löschen //Data.Cache.XMLTV = make(map[string]XMLTV) //Data.Cache.XMLTV = nil runtime.GC() }() } } else { getLineup() System.ScanInProgress = 0 } } // XEPG Daten aktualisieren func updateXEPG(background bool) { if System.ScanInProgress == 1 { return } System.ScanInProgress = 1 if Settings.EpgSource == "XEPG" { switch background { case false: createXEPGDatabase() mapping() cleanupXEPG() go func() { createXMLTVFile() createM3UFile() queuePlexGuideRefresh("xepg update") showInfo("XEPG:" + fmt.Sprintf("Ready to use")) System.ScanInProgress = 0 }() case true: System.ScanInProgress = 0 } } else { System.ScanInProgress = 0 } // Cache löschen //Data.Cache.XMLTV = nil //make(map[string]XMLTV) //Data.Cache.XMLTV = make(map[string]XMLTV) return } // Mapping Menü für die XMLTV Dateien erstellen func createXEPGMapping() { Data.XMLTV.Files = getLocalProviderFiles("xmltv") Data.XMLTV.Mapping = make(map[string]any) var tmpMap = make(map[string]any) var friendlyDisplayName = func(channel Channel) (displayName string) { var dn = channel.DisplayName displayName = dn[0].Value switch len(dn) { case 1: displayName = dn[0].Value default: displayName = fmt.Sprintf("%s (%s)", dn[1].Value, dn[0].Value) } return } if len(Data.XMLTV.Files) > 0 { for i := len(Data.XMLTV.Files) - 1; i >= 0; i-- { var file = Data.XMLTV.Files[i] var err error var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file))) showInfo("XEPG:" + "Parse XMLTV file: " + getProviderParameter(fileID, "xmltv", "name")) //xmltv, err = getLocalXMLTV(file) var xmltv XMLTV err = getLocalXMLTV(file, &xmltv) if err != nil { Data.XMLTV.Files = append(Data.XMLTV.Files, Data.XMLTV.Files[i+1:]...) var errMsg = err.Error() err = errors.New(getProviderParameter(fileID, "xmltv", "name") + ": " + errMsg) ShowError(err, 000) } // XML Parsen (Provider Datei) if err == nil { // Daten aus der XML Datei in eine temporäre Map schreiben var xmltvMap = make(map[string]any) for _, c := range xmltv.Channel { var channel = make(map[string]any) channel["id"] = c.ID channel["display-name"] = friendlyDisplayName(*c) channel["icon"] = c.Icon.Src xmltvMap[c.ID] = channel } tmpMap[getFilenameFromPath(file)] = xmltvMap Data.XMLTV.Mapping[getFilenameFromPath(file)] = xmltvMap } } Data.XMLTV.Mapping = tmpMap tmpMap = make(map[string]any) } else { if System.ConfigurationWizard == false { showWarning(1007) } } // Auswahl für den Dummy erstellen var dummy = make(map[string]any) var times = []string{"30", "60", "90", "120", "180", "240", "360"} for _, i := range times { var dummyChannel = make(map[string]string) dummyChannel["display-name"] = i + " Minutes" dummyChannel["id"] = i + "_Minutes" dummyChannel["icon"] = "" dummy[dummyChannel["id"]] = dummyChannel } Data.XMLTV.Mapping["xTeVe Dummy"] = dummy return } // XEPG Datenbank erstellen / aktualisieren func createXEPGDatabase() (err error) { var allChannelNumbers = make([]float64, 0, System.UnfilteredChannelLimit) Data.Cache.Streams.Active = make([]string, 0, System.UnfilteredChannelLimit) Data.XEPG.Channels = make(map[string]any, System.UnfilteredChannelLimit) Data.XEPG.Channels, err = loadJSONFileToMap(System.File.XEPG) if err != nil { ShowError(err, 1004) return err } var createNewID = func() (xepg string) { var firstID = 0 //len(Data.XEPG.Channels) newXEPGID: if _, ok := Data.XEPG.Channels["x-ID."+strconv.FormatInt(int64(firstID), 10)]; ok { firstID++ goto newXEPGID } xepg = "x-ID." + strconv.FormatInt(int64(firstID), 10) return } var getFreeChannelNumber = func() (xChannelID string) { sort.Float64s(allChannelNumbers) var firstFreeNumber float64 = Settings.MappingFirstChannel for { if indexOfFloat64(firstFreeNumber, allChannelNumbers) == -1 { xChannelID = fmt.Sprintf("%g", firstFreeNumber) allChannelNumbers = append(allChannelNumbers, firstFreeNumber) return } firstFreeNumber++ } return } var generateHashForChannel = func(m3uID string, groupTitle string, tvgID string, tvgName string, uuidKey string, uuidValue string) string { hash := md5.Sum([]byte(m3uID + groupTitle + tvgID + tvgName + uuidKey + uuidValue)) return hex.EncodeToString(hash[:]) } showInfo("XEPG:" + "Update database") // Kanal mit fehlenden Kanalnummern löschen. Delete channel with missing channel numbers for id, dxc := range Data.XEPG.Channels { var xepgChannel XEPGChannelStruct err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) if err != nil { return } if len(xepgChannel.XChannelID) == 0 { delete(Data.XEPG.Channels, id) } if xChannelID, err := strconv.ParseFloat(xepgChannel.XChannelID, 64); err == nil { allChannelNumbers = append(allChannelNumbers, xChannelID) } } // Make a map of the db channels based on their previously downloaded attributes -- filename, group, title, etc var xepgChannelsValuesMap = make(map[string]XEPGChannelStruct, System.UnfilteredChannelLimit) for _, v := range Data.XEPG.Channels { var channel XEPGChannelStruct err = json.Unmarshal([]byte(mapToJSON(v)), &channel) if err != nil { return } channelHash := generateHashForChannel(channel.FileM3UID, channel.GroupTitle, channel.TvgID, channel.TvgName, channel.UUIDKey, channel.UUIDValue) xepgChannelsValuesMap[channelHash] = channel } for _, dsa := range Data.Streams.Active { var channelExists = false // Entscheidet ob ein Kanal neu zu Datenbank hinzugefügt werden soll. Decides whether a channel should be added to the database var channelHasUUID = false // Überprüft, ob der Kanal (Stream) eindeutige ID's besitzt. Checks whether the channel (stream) has unique IDs var currentXEPGID string // Aktuelle Datenbank ID (XEPG). Wird verwendet, um den Kanal in der Datenbank mit dem Stream der M3u zu aktualisieren. Current database ID (XEPG) Used to update the channel in the database with the stream of the M3u var m3uChannel M3UChannelStructXEPG err = json.Unmarshal([]byte(mapToJSON(dsa)), &m3uChannel) if err != nil { return } Data.Cache.Streams.Active = append(Data.Cache.Streams.Active, m3uChannel.Name+m3uChannel.FileM3UID) // Try to find the channel based on matching all known values. If that fails, then move to full channel scan m3uChannelHash := generateHashForChannel(m3uChannel.FileM3UID, m3uChannel.GroupTitle, m3uChannel.TvgID, m3uChannel.TvgName, m3uChannel.UUIDKey, m3uChannel.UUIDValue) if val, ok := xepgChannelsValuesMap[m3uChannelHash]; ok { channelExists = true currentXEPGID = val.XEPG if len(m3uChannel.UUIDValue) > 0 { channelHasUUID = true } } else { // XEPG Datenbank durchlaufen um nach dem Kanal zu suchen. Run through the XEPG database to search for the channel (full scan) for _, dxc := range xepgChannelsValuesMap { if m3uChannel.FileM3UID == dxc.FileM3UID { dxc.FileM3UID = m3uChannel.FileM3UID dxc.FileM3UName = m3uChannel.FileM3UName // Vergleichen des Streams anhand einer UUID in der M3U mit dem Kanal in der Databank. Compare the stream using a UUID in the M3U with the channel in the database if len(dxc.UUIDValue) > 0 && len(m3uChannel.UUIDValue) > 0 { if dxc.UUIDValue == m3uChannel.UUIDValue && dxc.UUIDKey == m3uChannel.UUIDKey { channelExists = true channelHasUUID = true currentXEPGID = dxc.XEPG break } } else { // Vergleichen des Streams mit dem Kanal in der Databank anhand des Kanalnamens. Compare the stream to the channel in the database using the channel name if dxc.Name == m3uChannel.Name { channelExists = true currentXEPGID = dxc.XEPG break } } } } } switch channelExists { case true: // Bereits vorhandener Kanal var xepgChannel XEPGChannelStruct err = json.Unmarshal([]byte(mapToJSON(Data.XEPG.Channels[currentXEPGID])), &xepgChannel) if err != nil { return } // Streaming URL aktualisieren xepgChannel.URL = m3uChannel.URL // Name aktualisieren, anhand des Names wird überprüft ob der Kanal noch in einer Playlist verhanden. Funktion: cleanupXEPG xepgChannel.Name = m3uChannel.Name // Kanalname aktualisieren, nur mit Kanal ID's möglich if channelHasUUID == true { if xepgChannel.XUpdateChannelName == true { xepgChannel.XName = m3uChannel.Name } } // Kanallogo aktualisieren. Wird bei vorhandenem Logo in der XMLTV Datei wieder überschrieben if xepgChannel.XUpdateChannelIcon == true { xepgChannel.TvgLogo = m3uChannel.TvgLogo } Data.XEPG.Channels[currentXEPGID] = xepgChannel case false: // Neuer Kanal var xepg = createNewID() var xChannelID = getFreeChannelNumber() var newChannel XEPGChannelStruct newChannel.FileM3UID = m3uChannel.FileM3UID newChannel.FileM3UName = m3uChannel.FileM3UName newChannel.FileM3UPath = m3uChannel.FileM3UPath newChannel.Values = m3uChannel.Values newChannel.GroupTitle = m3uChannel.GroupTitle newChannel.Name = m3uChannel.Name newChannel.TvgID = m3uChannel.TvgID newChannel.TvgLogo = m3uChannel.TvgLogo newChannel.TvgName = m3uChannel.TvgName newChannel.URL = m3uChannel.URL newChannel.XmltvFile = "" newChannel.XMapping = "" if len(m3uChannel.UUIDKey) > 0 { newChannel.UUIDKey = m3uChannel.UUIDKey newChannel.UUIDValue = m3uChannel.UUIDValue } newChannel.XName = m3uChannel.Name newChannel.XGroupTitle = m3uChannel.GroupTitle newChannel.XEPG = xepg newChannel.XChannelID = xChannelID Data.XEPG.Channels[xepg] = newChannel } } showInfo("XEPG:" + "Save DB file") err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels) if err != nil { return } 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 err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) if err != nil { return } // Automatische Mapping für neue Kanäle. Wird nur ausgeführt, wenn der Kanal deaktiviert ist und keine XMLTV Datei und kein XMLTV Kanal zugeordnet ist. if xepgChannel.XActive == false { // Werte kann "-" sein, deswegen len < 1 if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XMapping) < 1 { var tvgID = xepgChannel.TvgID // Default für neuen Kanal setzen xepgChannel.XmltvFile = "-" xepgChannel.XMapping = "-" Data.XEPG.Channels[xepg] = xepgChannel for file, xmltvChannels := range Data.XMLTV.Mapping { if channel, ok := xmltvChannels.(map[string]any)[tvgID]; ok { if channelID, ok := channel.(map[string]any)["id"].(string); ok { xepgChannel.XmltvFile = file xepgChannel.XMapping = channelID xepgChannel.XActive = true // Falls in der XMLTV Datei ein Logo existiert, wird dieses verwendet. Falls nicht, dann das Logo aus der M3U Datei if icon, ok := channel.(map[string]any)["icon"].(string); ok { if len(icon) > 0 { xepgChannel.TvgLogo = icon } } Data.XEPG.Channels[xepg] = xepgChannel break } } } } } // Überprüfen, ob die zugeordneten XMLTV Dateien und Kanäle noch existieren. if xepgChannel.XActive == true { var mapping = xepgChannel.XMapping var file = xepgChannel.XmltvFile if file != "xTeVe Dummy" { if value, ok := Data.XMLTV.Mapping[file].(map[string]any); ok { if channel, ok := value[mapping].(map[string]any); ok { // Kanallogo aktualisieren if logo, ok := channel["icon"].(string); ok { if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 { xepgChannel.TvgLogo = logo } } } else { if channelID, replacementChannel, remapOK := findXEPGReplacementChannel(value, xepgChannel); remapOK { xepgChannel.XMapping = channelID 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 } 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) } } } if len(xepgChannel.XmltvFile) == 0 { xepgChannel.XmltvFile = "-" xepgChannel.XActive = false } if len(xepgChannel.XMapping) == 0 { xepgChannel.XMapping = "-" xepgChannel.XActive = false } Data.XEPG.Channels[xepg] = xepgChannel } } 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 } return } // XMLTV Datei erstellen func createXMLTVFile() (err error) { // Image Cache // 4edd81ab7c368208cc6448b615051b37.jpg var imgc = Data.Cache.Images Data.Cache.ImagesFiles = []string{} Data.Cache.ImagesURLS = []string{} Data.Cache.ImagesCache = []string{} files, err := ioutil.ReadDir(System.Folder.ImagesCache) if err == nil { for _, file := range files { if indexOfString(file.Name(), Data.Cache.ImagesCache) == -1 { Data.Cache.ImagesCache = append(Data.Cache.ImagesCache, file.Name()) } } } if len(Data.XMLTV.Files) == 0 && len(Data.Streams.Active) == 0 { Data.XEPG.Channels = make(map[string]any) return } showInfo("XEPG:" + fmt.Sprintf("Create XMLTV file (%s)", System.File.XML)) var xepgXML XMLTV xepgXML.Generator = System.Name if System.Branch == "master" { xepgXML.Source = fmt.Sprintf("%s - %s", System.Name, System.Version) } else { xepgXML.Source = fmt.Sprintf("%s - %s.%s", System.Name, System.Version, System.Build) } var tmpProgram = &XMLTV{} for _, dxc := range Data.XEPG.Channels { var xepgChannel XEPGChannelStruct err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) if err == nil { if xepgChannel.XActive == true { // Kanäle var channel Channel channel.ID = xepgChannel.XChannelID channel.Icon = Icon{Src: imgc.Image.GetURL(xepgChannel.TvgLogo)} channel.DisplayName = append(channel.DisplayName, DisplayName{Value: xepgChannel.XName}) xepgXML.Channel = append(xepgXML.Channel, &channel) // Programme *tmpProgram, err = getProgramData(xepgChannel) if err == nil { for _, program := range tmpProgram.Program { xepgXML.Program = append(xepgXML.Program, program) } } } } } var content, _ = xml.MarshalIndent(xepgXML, " ", " ") var xmlOutput = []byte(xml.Header + string(content)) writeByteToFile(System.File.XML, xmlOutput) showInfo("XEPG:" + fmt.Sprintf("Compress XMLTV file (%s)", System.Compressed.GZxml)) err = compressGZIP(&xmlOutput, System.Compressed.GZxml) xepgXML = XMLTV{} return } // Programmdaten erstellen (createXMLTVFile) 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 if xmltvFile == System.Folder.Data+"xTeVe Dummy" { xmltv = createDummyProgram(xepgChannel) } else { err = getLocalXMLTV(xmltvFile, &xmltv) if err != nil { if relaxedMissingEPGMode == true { fallbackToDummy() err = nil } return } } for _, xmltvProgram := range xmltv.Program { if xmltvProgram.Channel == channelID { //fmt.Println(&channelID) var program = &Program{} // Channel ID program.Channel = xepgChannel.XChannelID program.Start = xmltvProgram.Start program.Stop = xmltvProgram.Stop // Title program.Title = xmltvProgram.Title // Sub title (Untertitel) program.SubTitle = xmltvProgram.SubTitle // Description (Beschreibung) program.Desc = xmltvProgram.Desc // Category (Kategorie) getCategory(program, xmltvProgram, xepgChannel) // Credits : (Credits) program.Credits = xmltvProgram.Credits // Rating (Bewertung) program.Rating = xmltvProgram.Rating // StarRating (Bewertung / Kritiken) program.StarRating = xmltvProgram.StarRating // Country (Länder) program.Country = xmltvProgram.Country // Program icon (Poster / Cover) getPoster(program, xmltvProgram, xepgChannel) // Language (Sprache) program.Language = xmltvProgram.Language // Episodes numbers (Episodennummern) getEpisodeNum(program, xmltvProgram, xepgChannel) // Video (Videoparameter) getVideo(program, xmltvProgram, xepgChannel) // Date (Datum) program.Date = xmltvProgram.Date // Previously shown (Wiederholung) program.PreviouslyShown = xmltvProgram.PreviouslyShown // New (Neu) program.New = xmltvProgram.New // Live program.Live = xmltvProgram.Live // Premiere program.Premiere = xmltvProgram.Premiere xepgXML.Program = append(xepgXML.Program, program) } } if len(xepgXML.Program) == 0 && relaxedMissingEPGMode == true && xmltvFile != System.Folder.Data+"xTeVe Dummy" { fallbackToDummy() } return } // Dummy Daten erstellen (createXMLTVFile) func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) { var imgc = Data.Cache.Images var currentTime = time.Now() var dateArray = strings.Fields(currentTime.String()) var offset = " " + dateArray[2] var currentDay = currentTime.Format("20060102") var startTime, _ = time.Parse("20060102150405", currentDay+"000000") showDebug("Create Dummy Guide:"+"Time offset"+offset+" - "+xepgChannel.XName, 2) var dl = strings.Split(xepgChannel.XMapping, "_") dummyLength, err := strconv.Atoi(dl[0]) if err != nil { ShowError(err, 000) return } for d := range 4 { var epgStartTime = startTime.Add(time.Hour * time.Duration(d*24)) for t := dummyLength; t <= 1440; t = t + dummyLength { var epgStopTime = epgStartTime.Add(time.Minute * time.Duration(dummyLength)) var epg Program poster := Poster{} epg.Channel = xepgChannel.XMapping epg.Start = epgStartTime.Format("20060102150405") + offset epg.Stop = epgStopTime.Format("20060102150405") + offset epg.Title = append(epg.Title, &Title{Value: xepgChannel.XName + " (" + epgStartTime.Weekday().String()[0:2] + ". " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04") + ")", Lang: "en"}) if len(xepgChannel.XDescription) == 0 { epg.Desc = append(epg.Desc, &Desc{Value: "xTeVe: (" + strconv.Itoa(dummyLength) + " Minutes) " + epgStartTime.Weekday().String() + " " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04"), Lang: "en"}) } else { epg.Desc = append(epg.Desc, &Desc{Value: xepgChannel.XDescription, Lang: "en"}) } if Settings.XepgReplaceMissingImages == true { poster.Src = imgc.Image.GetURL(xepgChannel.TvgLogo) epg.Poster = append(epg.Poster, poster) } if xepgChannel.XCategory != "Movie" { epg.EpisodeNum = append(epg.EpisodeNum, &EpisodeNum{Value: epgStartTime.Format("2006-01-02 15:04:05"), System: "original-air-date"}) } epg.New = &New{Value: ""} dummyXMLTV.Program = append(dummyXMLTV.Program, &epg) epgStartTime = epgStopTime } } return } // Kategorien erweitern (createXMLTVFile) func getCategory(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) { for _, i := range xmltvProgram.Category { category := &Category{} category.Value = i.Value category.Lang = i.Lang program.Category = append(program.Category, category) } if len(xepgChannel.XCategory) > 0 { category := &Category{} category.Value = xepgChannel.XCategory category.Lang = "en" program.Category = append(program.Category, category) } return } // Programm Poster Cover aus der XMLTV Datei laden func getPoster(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) { var imgc = Data.Cache.Images for _, poster := range xmltvProgram.Poster { poster.Src = imgc.Image.GetURL(poster.Src) program.Poster = append(program.Poster, poster) } if Settings.XepgReplaceMissingImages == true { if len(xmltvProgram.Poster) == 0 { var poster Poster poster.Src = imgc.Image.GetURL(poster.Src) program.Poster = append(program.Poster, poster) } } } // Episodensystem übernehmen, falls keins vorhanden ist und eine Kategorie im Mapping eingestellt wurden, wird eine Episode erstellt func getEpisodeNum(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) { program.EpisodeNum = xmltvProgram.EpisodeNum if len(xepgChannel.XCategory) > 0 && xepgChannel.XCategory != "Movie" { if len(xmltvProgram.EpisodeNum) == 0 { var timeLayout = "20060102150405" t, err := time.Parse(timeLayout, strings.Split(xmltvProgram.Start, " ")[0]) if err == nil { program.EpisodeNum = append(program.EpisodeNum, &EpisodeNum{Value: t.Format("2006-01-02 15:04:05"), System: "original-air-date"}) } else { ShowError(err, 0) } } } return } // Videoparameter erstellen (createXMLTVFile) func getVideo(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) { var video Video video.Present = xmltvProgram.Video.Present video.Colour = xmltvProgram.Video.Colour video.Aspect = xmltvProgram.Video.Aspect video.Quality = xmltvProgram.Video.Quality if len(xmltvProgram.Video.Quality) == 0 { if strings.Contains(strings.ToUpper(xepgChannel.XName), " HD") || strings.Contains(strings.ToUpper(xepgChannel.XName), " FHD") { video.Quality = "HDTV" } if strings.Contains(strings.ToUpper(xepgChannel.XName), " UHD") || strings.Contains(strings.ToUpper(xepgChannel.XName), " 4K") { video.Quality = "UHDTV" } } program.Video = video return } // Lokale Provider XMLTV Datei laden func getLocalXMLTV(file string, xmltv *XMLTV) (err error) { if _, ok := Data.Cache.XMLTV[file]; !ok { // Cache initialisieren if len(Data.Cache.XMLTV) == 0 { Data.Cache.XMLTV = make(map[string]XMLTV) } // XML Daten lesen content, err := readByteFromFile(file) // Lokale XML Datei existiert nicht im Ordner: data if err != nil { ShowError(err, 1004) err = errors.New("Local copy of the file no longer exists") return err } // XML Datei parsen err = xml.Unmarshal(content, &xmltv) if err != nil { return err } Data.Cache.XMLTV[file] = *xmltv } else { *xmltv = Data.Cache.XMLTV[file] } return } // M3U Datei erstellen func createM3UFile() { showInfo("XEPG:" + fmt.Sprintf("Create M3U file (%s)", System.File.M3U)) _, err := buildM3U([]string{}) if err != nil { ShowError(err, 000) } saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS) return } // XEPG Datenbank bereinigen func cleanupXEPG() { //fmt.Println(Settings.Files.M3U) var sourceIDs []string for source := range Settings.Files.M3U { sourceIDs = append(sourceIDs, source) } for source := range Settings.Files.HDHR { sourceIDs = append(sourceIDs, source) } showInfo("XEPG:" + fmt.Sprintf("Cleanup database")) Data.XEPG.XEPGCount = 0 for id, dxc := range Data.XEPG.Channels { var xepgChannel XEPGChannelStruct err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) if err == nil { if indexOfString(xepgChannel.Name+xepgChannel.FileM3UID, Data.Cache.Streams.Active) == -1 { delete(Data.XEPG.Channels, id) } else { if xepgChannel.XActive == true { Data.XEPG.XEPGCount++ } } if indexOfString(xepgChannel.FileM3UID, sourceIDs) == -1 { delete(Data.XEPG.Channels, id) } } } err := saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels) if err != nil { ShowError(err, 000) return } showInfo("XEPG Channels:" + fmt.Sprintf("%d", Data.XEPG.XEPGCount)) if len(Data.Streams.Active) > 0 && Data.XEPG.XEPGCount == 0 { showWarning(2005) } return } // Streaming URL für die Channels App generieren func getStreamByChannelID(channelID string) (playlistID, streamURL string, err error) { err = errors.New("Channel not found") for _, dxc := range Data.XEPG.Channels { var xepgChannel XEPGChannelStruct err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel) fmt.Println(xepgChannel.XChannelID) if err == nil { if channelID == xepgChannel.XChannelID { playlistID = xepgChannel.FileM3UID streamURL = xepgChannel.URL return playlistID, streamURL, nil } } } return }