using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Remoting.Channels; using System.Text; using System.Xml; using System.Xml.Schema; using ChanSort.Api; namespace ChanSort.Loader.PhilipsXml { /* This loader supports 2 different kinds of XML files from Philips. Example from a ChannelMap_100\ChannelList\channellib\DVBC.xml: Example from a ChannelMap_105\ChannelList\s2channellib\DVBS.xml: Example from a ChannelMap_110\ChannelList\channellib\DVBC.xml: The other file was "CM_TPM1013E_LA_CK.xml" with entries like: DVB-T and DVB-C share the same number range, so they are treated as a unified logical list */ class Serializer : SerializerBase { private readonly ChannelList analogChannels = new ChannelList(SignalSource.DvbCT, "Analog C/T"); private readonly ChannelList dvbctChannels = new ChannelList(SignalSource.DvbCT, "DVB-C/T"); private readonly ChannelList satChannels = new ChannelList(SignalSource.DvbS, "DVB-S"); private readonly ChannelList allSatChannels = new ChannelList(SignalSource.DvbS, "DVB-S all"); private readonly ChannelList favChannels = new ChannelList(SignalSource.All, "Favorites"); private readonly List fileDataList = new List(); //private XmlDocument doc; //private byte[] content; //private string textContent; //private string newline; //private int formatVersion; #region ctor() public Serializer(string inputFile) : base(inputFile) { this.Features.ChannelNameEdit = ChannelNameEditMode.All; this.Features.CanSkipChannels = false; this.Features.CanLockChannels = true; this.Features.CanHideChannels = true; this.Features.DeleteMode = DeleteMode.Physically; this.Features.CanSaveAs = false; this.Features.AllowGapsInFavNumbers = false; this.Features.CanEditFavListNames = true; this.DataRoot.AddChannelList(this.analogChannels); this.DataRoot.AddChannelList(this.dvbctChannels); this.DataRoot.AddChannelList(this.satChannels); this.DataRoot.AddChannelList(this.allSatChannels); this.DataRoot.AddChannelList(this.favChannels); this.dvbctChannels.VisibleColumnFieldNames.Add("Source"); foreach (var list in this.DataRoot.ChannelLists) { list.VisibleColumnFieldNames.Remove("PcrPid"); list.VisibleColumnFieldNames.Remove("VideoPid"); list.VisibleColumnFieldNames.Remove("AudioPid"); list.VisibleColumnFieldNames.Remove("Skip"); list.VisibleColumnFieldNames.Remove("ShortName"); list.VisibleColumnFieldNames.Remove("Provider"); } this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.OriginalNetworkId)); this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.TransportStreamId)); this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.ServiceId)); this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.SymbolRate)); this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.ChannelOrTransponder)); this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.NetworkName)); this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.NetworkOperator)); this.favChannels.IsMixedSourceFavoritesList = true; } #endregion #region Load() public override void Load() { // read all files from a directory structure that looks like // ./CM_TPM1013E_LA_CK.xml // - or - // ChannelMap_100/ChannelList/channellib/DVBC.xml // ChannelMap_100/ChannelList/channellib/DVBT.xml // ChannelMap_100/ChannelList/s2channellib/DVBS.xml // ChannelMap_100/ChannelList/s2channellib/DVBSall.xml // ChannelMap_100/ChannelList/chanLst.bin // + optionally // ChannelMap_100/ChannelList/channelFile.bin // ChannelMap_100/ChannelList/Favorite.xml // ChannelMap_100/ChannelList/satInfo.bin var dataFiles = new[] { @"channellib\DVBC.xml", @"channellib\DVBT.xml", @"s2channellib\DVBS.xml", @"s2channellib\DVBSall.xml", @"Favorite.xml" }; // support for files in a ChannelMap_xxx directory structure bool isChannelMapFolderStructure = false; var dir = Path.GetDirectoryName(this.FileName); var dirName = Path.GetFileName(dir).ToLower(); if (dirName == "channellib" || dirName == "s2channellib") { dir = Path.GetDirectoryName(dir); isChannelMapFolderStructure = true; } var binFile = Path.Combine(dir, "chanLst.bin"); // the .bin file is used as a proxy for the whole directory structure if (File.Exists(binFile)) { this.FileName = binFile; isChannelMapFolderStructure = true; } else if (Path.GetExtension(this.FileName).ToLower() == ".bin") { // older Philips models export a visible file like Repair\CM_T911_LA_CK.BIN and an invisible (hidden+system) .xml file with the same name var xmlPath = Path.Combine(dir, Path.GetFileNameWithoutExtension(this.FileName) + ".xml"); if (File.Exists(xmlPath)) { try { File.SetAttributes(xmlPath, FileAttributes.Archive);} catch { /**/ } this.FileName = xmlPath; } } if (isChannelMapFolderStructure) { foreach (var file in dataFiles) { var fullPath = Path.GetFullPath(Path.Combine(dir, file)); this.LoadFile(fullPath); } } else { // otherwise load the single file that was originally selected by the user LoadFile(this.FileName); } } #endregion #region LoadFile() private void LoadFile(string fileName) { if (!File.Exists(fileName)) return; bool fail = false; var fileData = new FileData(); try { fileData.path = fileName; fileData.doc = new XmlDocument(); fileData.content = File.ReadAllBytes(fileName); fileData.textContent = Encoding.UTF8.GetString(fileData.content); fileData.newline = fileData.textContent.Contains("\r\n") ? "\r\n" : "\n"; var settings = new XmlReaderSettings { CheckCharacters = false, IgnoreProcessingInstructions = true, ValidationFlags = XmlSchemaValidationFlags.None, DtdProcessing = DtdProcessing.Ignore }; using (var reader = XmlReader.Create(new StringReader(fileData.textContent), settings)) { fileData.doc.Load(reader); } } catch { fail = true; } var root = fileData.doc.FirstChild; if (root is XmlDeclaration) root = root.NextSibling; if (fail || root == null || (root.LocalName != "ChannelMap" && root.LocalName != "FavoriteListMAP")) throw new FileLoadException("\"" + fileName + "\" is not a supported Philips XML file"); int rowId = 0; ChannelList curList = null; foreach (XmlNode child in root.ChildNodes) { switch (child.LocalName) { case "Channel": if (rowId == 0) curList = this.DetectFormatAndFeatures(fileData, child); if (curList != null) this.ReadChannel(fileData, curList, child, rowId++); break; case "FavoriteList": this.ReadFavList(child); break; } } this.fileDataList.Add(fileData); } #endregion #region DetectFormatAndFeatures() private ChannelList DetectFormatAndFeatures(FileData file, XmlNode node) { var setupNode = node["Setup"] ?? throw new FileLoadException("Missing Setup XML element"); var bcastNode = node["Broadcast"] ?? throw new FileLoadException("Missing Broadcast XML element"); var fname = Path.GetFileNameWithoutExtension(file.path).ToLower(); var medium = bcastNode.GetAttribute("medium"); if (medium == "" && fname.Length >= 4 && fname.StartsWith("dvb")) medium = fname; bool hasEncrypt = false; if (setupNode.HasAttribute("ChannelName")) { file.formatVersion = 1; this.Features.SupportedFavorites = Favorites.A; this.Features.SortedFavorites = true; var dtype = bcastNode.GetAttribute("DecoderType"); if (dtype == "1") medium = "dvbt"; else if (dtype == "2") medium = "dvbc"; else if (dtype == "3") medium = "dvbs"; hasEncrypt = setupNode.HasAttribute("Scrambled"); } else if (setupNode.HasAttribute("name")) { file.formatVersion = 2; this.Features.SupportedFavorites = 0; this.Features.SortedFavorites = false; foreach (var list in this.DataRoot.ChannelLists) { list.VisibleColumnFieldNames.Remove("Favorites"); list.VisibleColumnFieldNames.Remove("Lock"); list.VisibleColumnFieldNames.Remove("Hidden"); list.VisibleColumnFieldNames.Remove("ServiceType"); list.VisibleColumnFieldNames.Remove("ServiceTypeName"); list.VisibleColumnFieldNames.Remove("Encrypted"); } } else throw new FileLoadException("Unknown data format"); ChannelList chList = null; switch (medium) { case "analog": chList = this.analogChannels; break; case "dvbc": case "dvbt": chList = this.dvbctChannels; break; case "dvbs": chList = this.satChannels; break; case "dvbsall": chList = this.allSatChannels; break; } if (!hasEncrypt) chList?.VisibleColumnFieldNames.Remove("Encrypted"); return chList; } #endregion #region ReadChannel() private void ReadChannel(FileData file, ChannelList curList, XmlNode node, int rowId) { var setupNode = node["Setup"] ?? throw new FileLoadException("Missing Setup XML element"); var bcastNode = node["Broadcast"] ?? throw new FileLoadException("Missing Broadcast XML element"); var data = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var n in new[] {setupNode, bcastNode}) { foreach(XmlAttribute attr in n.Attributes) data.Add(attr.LocalName, attr.Value); } if (!data.ContainsKey("UniqueID") || !int.TryParse(data["UniqueID"], out var uniqueId)) // UniqueId only exists in ChannelMap_105 and later uniqueId = rowId; var chan = new Channel(curList.SignalSource & SignalSource.MaskAdInput, rowId, uniqueId, setupNode); chan.OldProgramNr = -1; chan.IsDeleted = false; if (file.formatVersion == 1) this.ParseChannelFormat1(data, chan); else if (file.formatVersion == 2) this.ParseChannelFormat2(data, chan); if ((chan.SignalSource & SignalSource.MaskAdInput) == SignalSource.DvbT) chan.ChannelOrTransponder = LookupData.Instance.GetDvbtTransponder(chan.FreqInMhz).ToString(); else if ((chan.SignalSource & SignalSource.MaskAdInput) == SignalSource.DvbC) chan.ChannelOrTransponder = LookupData.Instance.GetDvbcChannelName(chan.FreqInMhz); DataRoot.AddChannel(curList, chan); } #endregion #region ParseChannelFormat1 private void ParseChannelFormat1(Dictionary data, Channel chan) { chan.Format = 1; chan.RawSatellite = data.TryGet("SatelliteName"); chan.Satellite = DecodeName(chan.RawSatellite); chan.OldProgramNr = ParseInt(data.TryGet("ChannelNumber")); chan.RawName = data.TryGet("ChannelName"); chan.Name = DecodeName(chan.RawName); chan.Lock = data.TryGet("ChannelLock") == "1"; chan.Hidden = data.TryGet("UserHidden") == "1"; var fav = ParseInt(data.TryGet("FavoriteNumber")); chan.OldFavIndex[0] = fav == 0 ? -1 : fav; chan.OriginalNetworkId = ParseInt(data.TryGet("Onid")); chan.TransportStreamId = ParseInt(data.TryGet("Tsid")); chan.ServiceId = ParseInt(data.TryGet("Sid")); chan.FreqInMhz = ParseInt(data.TryGet("Frequency")); ; if (chan.FreqInMhz > 2000) chan.FreqInMhz /= 1000; if (chan.FreqInMhz > 2000) chan.FreqInMhz /= 1000; chan.ServiceType = ParseInt(data.TryGet("ServiceType")); var decoderType = data.TryGet("DecoderType"); if (decoderType == "1") chan.Source = "DVB-T"; else if (decoderType == "2") chan.Source = "DVB-C"; chan.SignalSource |= LookupData.Instance.IsRadioTvOrData(chan.ServiceType); chan.SymbolRate = ParseInt(data.TryGet("SymbolRate")); if (data.TryGetValue("Polarization", out var pol)) chan.Polarity = pol == "0" ? 'H' : 'V'; chan.Hidden |= data.TryGet("SystemHidden") == "1"; chan.Encrypted = data.TryGet("Scrambled") == "1"; // doesn't exist in all format versions } #endregion #region ParseChannelFormat2 private void ParseChannelFormat2(Dictionary data, Channel chan) { chan.Format = 2; chan.OldProgramNr = ParseInt(data.TryGet("presetnumber")); chan.Name = data.TryGet("name"); chan.RawName = chan.Name; chan.FreqInMhz = ParseInt(data.TryGet("frequency")); //if ((chan.SignalSource & SignalSource.Analog) != 0) // chan.FreqInMhz /= 16; if (chan.FreqInMhz > 1200) chan.FreqInMhz /= 1000; chan.ServiceId = ParseInt(data.TryGet("serviceID")); chan.OriginalNetworkId = ParseInt(data.TryGet("ONID")); chan.TransportStreamId = ParseInt(data.TryGet("TSID")); chan.ServiceType = ParseInt(data.TryGet("serviceType")); chan.SymbolRate = ParseInt(data.TryGet("symbolrate")) / 1000; } #endregion #region ReadFavList private void ReadFavList(XmlNode node) { int index = ParseInt(node.Attributes["Index"].InnerText); string name = DecodeName(node.Attributes["Name"].InnerText); this.Features.SupportedFavorites |= (Favorites) (1 << (index - 1)); this.Features.SortedFavorites = true; this.Features.MixedSourceFavorites = true; this.DataRoot.SetFavListCaption(index - 1, name); if (this.favChannels.Count == 0) { foreach (var rootList in this.DataRoot.ChannelLists) { if (rootList.IsMixedSourceFavoritesList) continue; foreach (var chan in rootList.Channels) { favChannels.Channels.Add(chan); for (int i=0; i ch.RecordIndex == uniqueId); chan?.SetOldPosition(index, favNumber + 1); } } } #endregion #region DecodeName() private string DecodeName(string input) { if (input == null || !input.StartsWith("0x")) // fallback for unknown input return input; var hexParts = input.Split(' '); var buffer = new MemoryStream(); foreach (var part in hexParts) { if (part == "" || part == "0x00") continue; buffer.WriteByte((byte)ParseInt(part)); } return this.DefaultEncoding.GetString(buffer.GetBuffer(), 0, (int)buffer.Length); } #endregion #region DefaultEncoding public override Encoding DefaultEncoding { get => base.DefaultEncoding; set { if (value == this.DefaultEncoding) return; base.DefaultEncoding = value; this.ChangeEncoding(); } } #endregion #region ChangeEncoding private void ChangeEncoding() { foreach (var list in this.DataRoot.ChannelLists) { foreach (var channel in list.Channels) { if (!(channel is Channel ch)) continue; ch.Name = this.DecodeName(ch.RawName); ch.Satellite = this.DecodeName(ch.RawSatellite); } } } #endregion #region GetDataFilePaths() public override IEnumerable GetDataFilePaths() { return this.fileDataList.Select(f => f.path); } #endregion #region Save() public override void Save(string tvOutputFile) { // "Save As..." is not supported by this loader foreach (var list in this.DataRoot.ChannelLists) { if (list.IsMixedSourceFavoritesList) this.UpdateFavList(); else this.UpdateChannelList(list); } foreach (var file in this.fileDataList) this.SaveFile(file); } #endregion #region SaveFile() private void SaveFile(FileData file) { // by default .NET reformats the whole XML. These settings produce almost same format as the TV xml files use var xmlSettings = new XmlWriterSettings(); xmlSettings.Encoding = this.DefaultEncoding; xmlSettings.CheckCharacters = false; xmlSettings.Indent = true; xmlSettings.IndentChars = " "; xmlSettings.NewLineHandling = NewLineHandling.None; xmlSettings.NewLineChars = file.newline; xmlSettings.OmitXmlDeclaration = false; string xml; using (var sw = new StringWriter()) using (var w = new CustomXmlWriter(sw, xmlSettings, false)) { file.doc.WriteTo(w); w.Flush(); xml = sw.ToString(); } var enc = new UTF8Encoding(false, false); File.WriteAllText(file.path, xml, enc); } #endregion #region UpdateChannelList() private void UpdateChannelList(ChannelList list) { foreach (var channel in list.Channels) { var ch = channel as Channel; if (ch == null) continue; // might be a proxy channel from a reference list if (ch.IsDeleted || ch.NewProgramNr < 0) { ch.SetupNode.ParentNode.ParentNode.RemoveChild(ch.SetupNode.ParentNode); continue; } if (ch.Format == 1) this.UpdateChannelFormat1(ch); else if (ch.Format == 2) this.UpdateChannelFormat2(ch); } } #endregion #region UpdateChannelFormat1 and 2 private void UpdateChannelFormat1(Channel ch) { ch.SetupNode.Attributes["ChannelNumber"].Value = ch.NewProgramNr.ToString(); if (ch.IsNameModified) ch.SetupNode.Attributes["ChannelName"].Value = EncodeName(ch.Name); ch.SetupNode.Attributes["FavoriteNumber"].Value = Math.Max(ch.FavIndex[0], 0).ToString(); } private void UpdateChannelFormat2(Channel ch) { ch.SetupNode.Attributes["presetnumber"].Value = ch.NewProgramNr.ToString(); if (ch.IsNameModified) ch.SetupNode.Attributes["name"].Value = ch.Name; } #endregion #region UpdateFavList private void UpdateFavList() { var favFile = this.fileDataList.FirstOrDefault(fd => Path.GetFileName(fd.path).ToLower() == "favorite.xml"); if (favFile == null) return; int index = 0; foreach(XmlNode favListNode in favFile.doc["FavoriteListMAP"].ChildNodes) { ++index; favListNode.InnerXml = ""; // clear all child elements but keep the attributes of the current node var attr = favListNode.Attributes?["Name"]; if (attr != null) attr.InnerText = EncodeName(this.DataRoot.GetFavListCaption(index - 1)); foreach (var ch in favChannels.Channels.OrderBy(ch => ch.GetPosition(index))) { var nr = ch.GetPosition(index); if (nr <= 0) continue; var uniqueIdNode = favFile.doc.CreateElement("UniqueID"); uniqueIdNode.InnerText = ch.RecordIndex.ToString(); var favNrNode = favFile.doc.CreateElement("FavNumber"); favNrNode.InnerText = (nr-1).ToString(); var channelNode = favFile.doc.CreateElement("FavoriteChannel"); channelNode.AppendChild(uniqueIdNode); channelNode.AppendChild(favNrNode); favListNode.AppendChild(channelNode); } } } #endregion #region EncodeName private string EncodeName(string name) { var bytes = this.DefaultEncoding.GetBytes(name); var sb = new StringBuilder(); foreach (var b in bytes) sb.Append($"0x{b:X2} 0x00 "); sb.Remove(sb.Length - 1, 1); return sb.ToString(); } #endregion #region class FileData private class FileData { public string path; public XmlDocument doc; public byte[] content; public string textContent; public string newline; public int formatVersion; } #endregion } }