diff --git a/source/ChanSort.Api/Controller/SerializerBase.cs b/source/ChanSort.Api/Controller/SerializerBase.cs index 89504443..7ee7d029 100644 --- a/source/ChanSort.Api/Controller/SerializerBase.cs +++ b/source/ChanSort.Api/Controller/SerializerBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Compression; @@ -27,6 +28,7 @@ public class SupportedFeatures public bool CanHaveGaps { get; set; } = true; public bool EncryptedFlagEdit { get; set; } public DeleteMode DeleteMode { get; set; } = DeleteMode.NotSupported; + public bool CanSaveAs { get; set; } = true; public Favorites SupportedFavorites { get; set; } = Favorites.A | Favorites.B | Favorites.C | Favorites.D; @@ -39,7 +41,7 @@ public class SupportedFeatures private Encoding defaultEncoding; - public string FileName { get; set; } + public string FileName { get; protected set; } public DataRoot DataRoot { get; protected set; } public SupportedFeatures Features { get; } = new SupportedFeatures(); @@ -59,6 +61,16 @@ public virtual Encoding DefaultEncoding set { this.defaultEncoding = value; } } + #region GetDataFilePaths + /// + /// returns the list of all data files that need to be copied for backup/restore + /// + public virtual IEnumerable GetDataFilePaths() + { + return new List { this.FileName }; + } + #endregion + #region GetFileInformation() public virtual string GetFileInformation() { @@ -192,6 +204,5 @@ protected long ParseLong(string input) return 0; } #endregion - } } diff --git a/source/ChanSort.Loader.PhilipsXml/Serializer.cs b/source/ChanSort.Loader.PhilipsXml/Serializer.cs index c5cce809..1dd40126 100644 --- a/source/ChanSort.Loader.PhilipsXml/Serializer.cs +++ b/source/ChanSort.Loader.PhilipsXml/Serializer.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Xml; using System.Xml.Schema; @@ -31,17 +31,19 @@ class Serializer : SerializerBase private readonly ChannelList cableChannels = new ChannelList(SignalSource.DvbC, "DVB-C"); private readonly ChannelList satChannels = new ChannelList(SignalSource.DvbS, "DVB-S"); - private XmlDocument doc; - private byte[] content; - private string textContent; - private string newline; - private int formatVersion; + 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.DeleteMode = DeleteMode.Physically; + this.Features.CanSaveAs = false; this.DataRoot.AddChannelList(this.terrChannels); this.DataRoot.AddChannelList(this.cableChannels); @@ -59,18 +61,61 @@ public Serializer(string inputFile) : base(inputFile) } #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" }; + + // support for files in a ChannelMap_xxx directory structure + var dir = Path.GetDirectoryName(this.FileName); + var dirName = Path.GetFileName(dir).ToLower(); + if (dirName == "channellib" || dirName == "s2channellib") + dir = Path.GetDirectoryName(dir); + + 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; + foreach (var file in dataFiles) + { + var fullPath = Path.GetFullPath(Path.Combine(dir, file)); + this.LoadFile(fullPath); + } + + return; + } + + // otherwise load the single file that was originally selected by the user + LoadFile(this.FileName); + } + #endregion + + #region LoadFile() + + private void LoadFile(string fileName) { bool fail = false; + var fileData = new FileData(); try { - this.doc = new XmlDocument(); - this.content = File.ReadAllBytes(this.FileName); - this.textContent = Encoding.UTF8.GetString(this.content); - this.newline = this.textContent.Contains("\r\n") ? "\r\n" : "\n"; + 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 { @@ -79,9 +124,9 @@ public override void Load() ValidationFlags = XmlSchemaValidationFlags.None, DtdProcessing = DtdProcessing.Ignore }; - using (var reader = XmlReader.Create(new StringReader(textContent), settings)) + using (var reader = XmlReader.Create(new StringReader(fileData.textContent), settings)) { - doc.Load(reader); + fileData.doc.Load(reader); } } catch @@ -89,11 +134,11 @@ public override void Load() fail = true; } - var root = doc.FirstChild; + var root = fileData.doc.FirstChild; if (root is XmlDeclaration) root = root.NextSibling; if (fail || root == null || root.LocalName != "ChannelMap") - throw new FileLoadException("\"" + this.FileName + "\" is not a supported Philips XML file"); + throw new FileLoadException("\"" + fileName + "\" is not a supported Philips XML file"); int rowId = 0; @@ -104,18 +149,19 @@ public override void Load() { case "Channel": if (rowId == 0) - curList = this.DetectFormatAndFeatures(child); + curList = this.DetectFormatAndFeatures(fileData, child); if (curList != null) - this.ReadChannel(curList, child, rowId++); + this.ReadChannel(fileData, curList, child, rowId++); break; } } + this.fileDataList.Add(fileData); } #endregion #region DetectFormatAndFeatures() - private ChannelList DetectFormatAndFeatures(XmlNode node) + 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"); @@ -128,7 +174,7 @@ private ChannelList DetectFormatAndFeatures(XmlNode node) if (setupNode.HasAttribute("ChannelName")) { - this.formatVersion = 1; + file.formatVersion = 1; this.Features.SupportedFavorites = Favorites.A; this.Features.SortedFavorites = true; @@ -144,7 +190,7 @@ private ChannelList DetectFormatAndFeatures(XmlNode node) } else if (setupNode.HasAttribute("name")) { - this.formatVersion = 2; + file.formatVersion = 2; this.Features.SupportedFavorites = 0; this.Features.SortedFavorites = false; foreach (var list in this.DataRoot.ChannelLists) @@ -182,7 +228,7 @@ private ChannelList DetectFormatAndFeatures(XmlNode node) #endregion #region ReadChannel() - private void ReadChannel(ChannelList curList, XmlNode node, int rowId) + 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"); @@ -196,9 +242,9 @@ private void ReadChannel(ChannelList curList, XmlNode node, int rowId) var chan = new Channel(curList.SignalSource & SignalSource.MaskAdInput, rowId, rowId, setupNode); chan.OldProgramNr = -1; chan.IsDeleted = false; - if (this.formatVersion == 1) + if (file.formatVersion == 1) this.ParseChannelFormat1(data, chan); - else if (this.formatVersion == 2) + else if (file.formatVersion == 2) this.ParseChannelFormat2(data, chan); if ((chan.SignalSource & SignalSource.MaskAdInput) == SignalSource.DvbT) @@ -226,7 +272,9 @@ private void ParseChannelFormat1(Dictionary data, Channel chan) chan.TransportStreamId = ParseInt(data.TryGet("Tsid")); chan.ServiceId = ParseInt(data.TryGet("Sid")); chan.FreqInMhz = ParseInt(data.TryGet("Frequency")); ; - if (chan.FreqInMhz > 100000) + if (chan.FreqInMhz > 2000) + chan.FreqInMhz /= 1000; + if (chan.FreqInMhz > 2000) chan.FreqInMhz /= 1000; chan.ServiceType = ParseInt(data.TryGet("ServiceType")); chan.SignalSource |= LookupData.Instance.IsRadioTvOrData(chan.ServiceType); @@ -246,7 +294,7 @@ private void ParseChannelFormat2(Dictionary data, Channel chan) chan.Name = data.TryGet("name"); chan.RawName = chan.Name; chan.FreqInMhz = ParseInt(data.TryGet("frequency")); - if (chan.FreqInMhz > 100000) + if (chan.FreqInMhz > 2000) chan.FreqInMhz /= 1000; chan.ServiceId = ParseInt(data.TryGet("serviceID")); chan.OriginalNetworkId = ParseInt(data.TryGet("ONID")); @@ -306,11 +354,31 @@ private void ChangeEncoding() } #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 file in this.fileDataList) + this.SaveFile(file); + } + + #endregion + + #region SaveFile() + private void SaveFile(FileData file) { foreach (var list in this.DataRoot.ChannelLists) - this.UpdateChannelList(list); + this.UpdateChannelList(file, list); // by default .NET reformats the whole XML. These settings produce almost same format as the TV xml files use var xmlSettings = new XmlWriterSettings(); @@ -319,25 +387,25 @@ public override void Save(string tvOutputFile) xmlSettings.Indent = true; xmlSettings.IndentChars = ""; xmlSettings.NewLineHandling = NewLineHandling.None; - xmlSettings.NewLineChars = this.newline; + xmlSettings.NewLineChars = file.newline; xmlSettings.OmitXmlDeclaration = false; string xml; using (var sw = new StringWriter()) using (var w = new CustomXmlWriter(sw, xmlSettings, false)) { - this.doc.WriteTo(w); + file.doc.WriteTo(w); w.Flush(); xml = sw.ToString(); } var enc = new UTF8Encoding(false, false); - File.WriteAllText(tvOutputFile, xml, enc); + File.WriteAllText(file.path, xml, enc); } #endregion #region UpdateChannelList() - private void UpdateChannelList(ChannelList list) + private void UpdateChannelList(FileData file, ChannelList list) { foreach (var channel in list.Channels) { @@ -351,9 +419,9 @@ private void UpdateChannelList(ChannelList list) continue; } - if (this.formatVersion == 1) + if (file.formatVersion == 1) this.UpdateChannelFormat1(ch); - else if (this.formatVersion == 2) + else if (file.formatVersion == 2) this.UpdateChannelFormat2(ch); } } @@ -386,5 +454,18 @@ private string EncodeName(string name) 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 } } diff --git a/source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs b/source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs index c47b78ac..e372e8db 100644 --- a/source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs +++ b/source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs @@ -6,7 +6,7 @@ public class SerializerPlugin : ISerializerPlugin { public string DllName { get; set; } public string PluginName => "Philips .xml"; - public string FileFilter => "*.xml"; + public string FileFilter => "*.xml;*.bin"; public SerializerBase CreateSerializer(string inputFile) { diff --git a/source/ChanSort/MainForm.cs b/source/ChanSort/MainForm.cs index 6e921680..98a5c5f4 100644 --- a/source/ChanSort/MainForm.cs +++ b/source/ChanSort/MainForm.cs @@ -302,6 +302,7 @@ private void LoadFiles(ISerializerPlugin plugin, string tvDataFile) //this.SetControlsEnabled(!this.dataRoot.IsEmpty); this.UpdateFavoritesEditor(this.DataRoot.SupportedFavorites); this.colEncrypted.OptionsColumn.AllowEdit = this.currentTvSerializer.Features.EncryptedFlagEdit; + this.UpdateMenu(true); if (this.DataRoot.Warnings.Length > 0 && this.miShowWarningsAfterLoad.Checked) this.BeginInvoke((Action) this.ShowFileInformation); @@ -336,6 +337,9 @@ private void LoadFiles(ISerializerPlugin plugin, string tvDataFile) internal bool DetectCommonFileCorruptions(string tvDataFile) { + if (!File.Exists(tvDataFile)) // a loader (like Philips) may use internal file names that don't match the one in the UI, i.e. tvDataFile might be a directory path + return true; + var content = File.ReadAllBytes(tvDataFile); var isAllZero = true; for (int i = 0, c = content.Length; i < c; i++) @@ -558,11 +562,11 @@ private bool LoadTvDataFile(ISerializerPlugin plugin, string tvDataFile) this.currentTvSerializer?.Dispose(); serializer.DataRoot.ValidateAfterLoad(); - this.SetFileName(tvDataFile); + this.SetFileName(serializer.FileName); this.currentPlugin = plugin; this.currentTvSerializer = serializer; this.DataRoot = serializer.DataRoot; - this.AddFileToMruList(this.currentTvFile); + this.AddFileToMruList(tvDataFile); this.UpdateMruMenu(); return true; @@ -740,7 +744,7 @@ private void UpdateGridReadOnly() private void ShowSaveFileDialog() { - var extension = Path.GetExtension(this.currentTvSerializer.FileName) ?? "."; + var extension = Path.GetExtension(this.currentTvFile) ?? "."; using (var dlg = new SaveFileDialog()) { dlg.InitialDirectory = Path.GetDirectoryName(this.currentTvFile); @@ -778,7 +782,7 @@ private void SaveFiles() this.SaveTvDataFile(); this.DataRoot.NeedsSaving = false; this.RefreshGrid(this.gviewLeft, this.gviewRight); - this.UpdateMenu(); + this.UpdateMenu(true); } catch (IOException ex) { @@ -926,13 +930,16 @@ private void SaveTvDataFile() this.splashScreenManager1.ShowWaitForm(); try { - // create backup file if none exists - if (File.Exists(currentTvFile)) + foreach (var filePath in this.currentTvSerializer.GetDataFilePaths()) { - var bakFile = currentTvFile + ".bak"; - if (!File.Exists(bakFile)) - File.Copy(currentTvFile, bakFile); + if (File.Exists(filePath)) + { + var bakFile = filePath + ".bak"; + if (!File.Exists(bakFile)) + File.Copy(filePath, bakFile); + } } + this.currentTvSerializer.Save(this.currentTvFile); this.DataRoot.ValidateAfterSave(); } @@ -1587,7 +1594,7 @@ private void SetActiveGrid(GridView grid) #region UpdateMenu - private void UpdateMenu() + private void UpdateMenu(bool afterFileLoad = false) { var fileLoaded = this.DataRoot != null; var isRight = this.lastFocusedGrid == this.gviewRight; @@ -1611,15 +1618,19 @@ private void UpdateMenu() this.btnToggleFavH.Enabled = mayEdit && (this.DataRoot.SupportedFavorites & Favorites.H) != 0 && this.subListIndex != 8; this.btnToggleLock.Enabled = mayEdit; - this.miReload.Enabled = fileLoaded; - this.miFileInformation.Enabled = fileLoaded; - this.miRestoreOriginal.Enabled = fileLoaded && File.Exists(this.currentTvFile + ".bak"); - this.miSave.Enabled = fileLoaded; - this.miSaveAs.Enabled = fileLoaded; - this.miOpenReferenceFile.Enabled = fileLoaded; - this.miSaveReferenceFile.Enabled = fileLoaded; - this.miExcelExport.Enabled = fileLoaded; - this.miPrint.Enabled = fileLoaded; + if (afterFileLoad) + { + // this block may contain some time-expensive checks that only need to be done after loading a file + this.miReload.Enabled = fileLoaded; + this.miFileInformation.Enabled = fileLoaded; + this.miRestoreOriginal.Enabled = fileLoaded && this.GetPathOfMissingBackupFile() == null; + this.miSave.Enabled = fileLoaded; + this.miSaveAs.Enabled = fileLoaded && this.currentTvSerializer.Features.CanSaveAs; + this.miOpenReferenceFile.Enabled = fileLoaded; + this.miSaveReferenceFile.Enabled = fileLoaded; + this.miExcelExport.Enabled = fileLoaded; + this.miPrint.Enabled = fileLoaded; + } this.miAddChannel.Enabled = fileLoaded && isRight; @@ -1666,12 +1677,32 @@ private void UpdateMruMenu() #endregion + #region GetPathOfMissingBackupFile() + /// + /// If any backup file exists, return NULL. Otherwise the name of any expected .bak file (in case the loader has multiple data files) + /// + /// + private string GetPathOfMissingBackupFile() + { + var files = this.currentTvSerializer.GetDataFilePaths().ToList(); + string bakFile = null; + foreach (var dataFilePath in files) + { + bakFile = dataFilePath + ".bak"; + if (File.Exists(bakFile)) + return null; + } + + return bakFile; + } + #endregion + #region RestoreBackupFile() private void RestoreBackupFile() { - var bakFile = this.currentTvFile + ".bak"; - if (!File.Exists(bakFile)) + var bakFile = this.GetPathOfMissingBackupFile(); + if (bakFile != null) { XtraMessageBox.Show(this, string.Format(Resources.MainForm_miRestoreOriginal_ItemClick_NoBackup, bakFile), this.miRestoreOriginal.Caption, @@ -1690,9 +1721,14 @@ private void RestoreBackupFile() try { - File.Copy(bakFile, this.currentTvFile, true); - var attr = File.GetAttributes(this.currentTvFile); - File.SetAttributes(this.currentTvFile, attr & ~FileAttributes.ReadOnly); + foreach (var dataFilePath in this.currentTvSerializer.GetDataFilePaths()) + { + bakFile = dataFilePath + ".bak"; + File.Copy(bakFile, dataFilePath, true); + var attr = File.GetAttributes(dataFilePath); + File.SetAttributes(dataFilePath, attr & ~FileAttributes.ReadOnly); + } + this.currentTvSerializer.DataRoot.NeedsSaving = false; if (this.currentPlugin != null) this.LoadFiles(this.currentPlugin, this.currentTvFile); @@ -2310,7 +2346,7 @@ private void gviewLeft_RowClick(object sender, RowClickEventArgs e) private void gviewLeft_EndSorting(object sender, EventArgs e) { - TryExecute(this.UpdateMenu); + TryExecute(() => this.UpdateMenu()); } #endregion diff --git a/source/changelog.md b/source/changelog.md index 7b24d02b..22149051 100644 --- a/source/changelog.md +++ b/source/changelog.md @@ -1,6 +1,11 @@ ChanSort Change Log =================== +2019-11-17 +- Philips: Improved support for ChannelMap_xxx channel lists directory structure. + Selecting any .xml or .bin file in the folder will now load all DVB\*.xml files from the + channellib and s2channellib sub folders. + 2019-11-11 - LG hospitality TVs using files names like xx[modelname].TLL can now be loaded (They use the naming pattern of binary TLL files, but contain GlobalClone/XML text data)