From 25b2df77347ea4c8b44f6ad986d5607fd4690db8 Mon Sep 17 00:00:00 2001 From: hbeham Date: Mon, 5 Aug 2019 23:50:53 +0200 Subject: [PATCH] - added partial support for Philips .xml channel lists (There are MANY different file formats, only a few are currently supported) - fixed "most-recently-used" getting reversed every time the program was started - added "UTF-8 (Unicode)" character set to menu - fixed disappearing columns when loading different channel lists without restarting the application --- readme.md | 6 +- readme_de.md | 6 +- source/ChanSort.Api/Model/ChannelList.cs | 2 +- source/ChanSort.Api/Model/Enums.cs | 2 + .../ChanSort.Loader.PhilipsXml.csproj | 74 ++++ source/ChanSort.Loader.PhilipsXml/Channel.cs | 20 + .../CustomXmlWriter.cs | 170 ++++++++ .../Properties/AssemblyInfo.cs | 36 ++ .../ChanSort.Loader.PhilipsXml/Serializer.cs | 408 ++++++++++++++++++ .../SerializerPlugin.cs | 16 + source/ChanSort.Loader.Sony/Serializer.cs | 2 +- source/ChanSort.sln | 14 + source/ChanSort/ChanSort.csproj | 4 + source/ChanSort/MainForm.Designer.cs | 31 +- source/ChanSort/MainForm.cs | 19 +- source/ChanSort/MainForm.resx | 19 +- source/changelog.md | 7 + 17 files changed, 815 insertions(+), 21 deletions(-) create mode 100644 source/ChanSort.Loader.PhilipsXml/ChanSort.Loader.PhilipsXml.csproj create mode 100644 source/ChanSort.Loader.PhilipsXml/Channel.cs create mode 100644 source/ChanSort.Loader.PhilipsXml/CustomXmlWriter.cs create mode 100644 source/ChanSort.Loader.PhilipsXml/Properties/AssemblyInfo.cs create mode 100644 source/ChanSort.Loader.PhilipsXml/Serializer.cs create mode 100644 source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs diff --git a/readme.md b/readme.md index 3f2b061..4454214 100644 --- a/readme.md +++ b/readme.md @@ -11,7 +11,7 @@ About ChanSort -------------- ChanSort is a Windows application that allows you to reorder your TV's channel list. Most modern TVs can transfer channel lists via USB stick, which you can plug into your PC. -ChanSort supports various file formats from **Sony** (new), **ITT, Medion, Nabo, ok., PEAQ, Schaub-Lorenz, Silva-Schneider, Telefunken** (new), +ChanSort supports various file formats from **Philips** (new), **Sony** (new), **ITT, Medion, Nabo, ok., PEAQ, Schaub-Lorenz, Silva-Schneider, Telefunken** (new), Hisense, Samsung, LG, Panasonic, Toshiba and the Linux VDR project. ![screenshot](http://beham.biz/chansort/ChanSort-en.png) @@ -96,6 +96,10 @@ Models that export a .zip file containing chmgt.db, dvbSysData.db and dvbMainDat **ITT, Medion, Nabo, ok., PEAQ, Schaub-Lorenz, Silva-Schneider, Telefunken** These brands use .sdx files (currently only satellite lists are supported) +**Philips** +Philips uses countless incompatible file formats for various TV models. +ChanSort currently supports 2 different versions of .xml files, other formats are not supported. + **VDR (Linux Video Disk Recorder)** Supports the channels.conf file format. Implementation for this was provided by TCr82 from the VDR project. diff --git a/readme_de.md b/readme_de.md index fadc284..54e794e 100644 --- a/readme_de.md +++ b/readme_de.md @@ -11,7 +11,7 @@ Links -------------- ChanSort ist eine Windows-Anwendung, die das Sortieren von Fernsehsenderlisten erlaubt. Die meisten modernen Fernseher können Senderlisten auf einen USB-Stick übertragen, den man danach am PC anschließt. -ChanSort unterstützt diverse Dateiformate von **Sony** (new), **ITT, Medion, Nabo, ok., PEAQ, Schaub-Lorenz, Silva-Schneider, Telefunken** (new), +ChanSort unterstützt diverse Dateiformate von **Philips** (neu), **Sony** (new), **ITT, Medion, Nabo, ok., PEAQ, Schaub-Lorenz, Silva-Schneider, Telefunken** (new), Hisense, Samsung, LG, Panasonic, Toshiba und dem Linux VDR Projekt. ![screenshot](http://beham.biz/chansort/ChanSort-de.png) @@ -91,6 +91,10 @@ Die Marken nutzen .sdx Dateien (derzeit wird nur Satellitenempfang unterst Modelle, die eine .zip-Datei mit folgendem Inhalt: chmgt.db, dvbSysData.db und dvbMainData.db. (z.B. RL, SL, TL, UL, VL, WL, XL, YL models of series 8xx/9xx) +**Philips** +Philips verwendet unzählige unterschiedliche Dateiformate für diverse TV-Modelle. +ChanSort unterstützt derzeit 2 Varianten von .xml-Dateien. Andere Formate werden nicht unterstützt. + **VDR (Linux Video Disk Recorder)** Unterstützung des channels.conf Dateiformats. Die Implementation hierfür wurde vom Mitglied "TCr82" des VDR Projekts beigesteuert. diff --git a/source/ChanSort.Api/Model/ChannelList.cs b/source/ChanSort.Api/Model/ChannelList.cs index b94fea4..6587008 100644 --- a/source/ChanSort.Api/Model/ChannelList.cs +++ b/source/ChanSort.Api/Model/ChannelList.cs @@ -20,7 +20,7 @@ namespace ChanSort.Api this.SignalSource = source; this.ShortCaption = caption; this.FirstProgramNumber = (source & SignalSource.Digital) != 0 ? 1 : 0; - this.VisibleColumnFieldNames = DefaultVisibleColumns; + this.VisibleColumnFieldNames = DefaultVisibleColumns.ToList(); // create copy of default list, so it can be modified } public string ShortCaption { get; } diff --git a/source/ChanSort.Api/Model/Enums.cs b/source/ChanSort.Api/Model/Enums.cs index 69466f5..d27c479 100644 --- a/source/ChanSort.Api/Model/Enums.cs +++ b/source/ChanSort.Api/Model/Enums.cs @@ -24,6 +24,8 @@ namespace ChanSort.Api Sat = 0x0040, IP = 0x0080, + MaskAdInput = MaskAnalogDigital | MaskAntennaCableSat, + // bit 9+10: TV/Radio MaskTvRadio = 0x0300, Tv = 0x0100, diff --git a/source/ChanSort.Loader.PhilipsXml/ChanSort.Loader.PhilipsXml.csproj b/source/ChanSort.Loader.PhilipsXml/ChanSort.Loader.PhilipsXml.csproj new file mode 100644 index 0000000..03a9391 --- /dev/null +++ b/source/ChanSort.Loader.PhilipsXml/ChanSort.Loader.PhilipsXml.csproj @@ -0,0 +1,74 @@ + + + + + Debug + AnyCPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F} + Library + Properties + ChanSort.Loader.PhilipsXml + ChanSort.Loader.PhilipsXml + v4.0 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + + + + + + + + + + + + + + + + + + {dccffa08-472b-4d17-bb90-8f513fc01392} + ChanSort.Api + + + + \ No newline at end of file diff --git a/source/ChanSort.Loader.PhilipsXml/Channel.cs b/source/ChanSort.Loader.PhilipsXml/Channel.cs new file mode 100644 index 0000000..a33f8e7 --- /dev/null +++ b/source/ChanSort.Loader.PhilipsXml/Channel.cs @@ -0,0 +1,20 @@ +using System.Xml; +using ChanSort.Api; + +namespace ChanSort.Loader.PhilipsXml +{ + internal class Channel : ChannelInfo + { + public readonly XmlNode SetupNode; + public string RawName; + public string RawSatellite; + + internal Channel(SignalSource source, int order, int rowId, XmlNode setupNode) + { + this.SignalSource = source; + this.RecordOrder = order; + this.RecordIndex = rowId; + this.SetupNode = setupNode; + } + } +} diff --git a/source/ChanSort.Loader.PhilipsXml/CustomXmlWriter.cs b/source/ChanSort.Loader.PhilipsXml/CustomXmlWriter.cs new file mode 100644 index 0000000..43f445e --- /dev/null +++ b/source/ChanSort.Loader.PhilipsXml/CustomXmlWriter.cs @@ -0,0 +1,170 @@ +using System; +using System.IO; +using System.Xml; + +namespace ChanSort.Loader.PhilipsXml +{ + /// + /// This XmlWriter replaces some characters with Char- or Entity- references the same way + /// they are escaped in the original Sony XML files + /// + class CustomXmlWriter : XmlWriter + { + private static readonly char[] CharsToEscape = { '\'', '\"', '&', '<', '>' }; + private static readonly string[] CharEntites = { "apos", "quot", "amp", "lt", "gt" }; + + private XmlWriter w; + private readonly bool escapeAsEntityRef; // if true, use & otherwise " + + public CustomXmlWriter(TextWriter tw, XmlWriterSettings settings, bool useEntityRef) + { + this.w = XmlWriter.Create(tw, settings); + this.escapeAsEntityRef = useEntityRef; + } + + public override void WriteString(string text) + { + int i = 0, j; + while ((j = text.IndexOfAny(CharsToEscape, i)) >= 0) + { + this.w.WriteString(text.Substring(i, j - i)); + if (this.escapeAsEntityRef) + { + // => & + int k = Array.IndexOf(CharsToEscape, text[j]); + this.w.WriteEntityRef(CharEntites[k]); + } + else + { + // => & + //this.w.WriteCharEntity(text[j]); + this.w.WriteRaw("&#" + (int)text[j] + ";"); + } + + i = j + 1; + } + this.w.WriteString(text.Substring(i)); + } + + #region 1:1 delegation + + public override void WriteStartDocument() + { + this.w.WriteStartDocument(); + } + + public override void WriteStartDocument(bool standalone) + { + this.w.WriteStartDocument(standalone); + } + + public override void WriteEndDocument() + { + this.w.WriteEndDocument(); + } + + public override void WriteDocType(string name, string pubid, string sysid, string subset) + { + this.w.WriteDocType(name, pubid, sysid, subset); + } + + public override void WriteStartElement(string prefix, string localName, string ns) + { + this.w.WriteStartElement(prefix, localName, ns); + } + + public override void WriteEndElement() + { + this.w.WriteEndElement(); + } + + public override void WriteFullEndElement() + { + this.w.WriteFullEndElement(); + } + + public override void WriteStartAttribute(string prefix, string localName, string ns) + { + this.w.WriteStartAttribute(prefix, localName, ns); + } + + public override void WriteEndAttribute() + { + this.w.WriteEndAttribute(); + } + + public override void WriteCData(string text) + { + this.w.WriteCData(text); + } + + public override void WriteComment(string text) + { + this.w.WriteComment(text); + } + + public override void WriteProcessingInstruction(string name, string text) + { + this.w.WriteProcessingInstruction(name, text); + } + + public override void WriteEntityRef(string name) + { + this.w.WriteEntityRef(name); + } + + public override void WriteCharEntity(char ch) + { + this.w.WriteCharEntity(ch); + } + + public override void WriteWhitespace(string ws) + { + this.w.WriteWhitespace(ws); + } + + public override void WriteSurrogateCharEntity(char lowChar, char highChar) + { + this.w.WriteSurrogateCharEntity(lowChar, highChar); + } + + public override void WriteChars(char[] buffer, int index, int count) + { + this.w.WriteChars(buffer, index, count); + } + + public override void WriteRaw(char[] buffer, int index, int count) + { + this.w.WriteRaw(buffer, index, count); + } + + public override void WriteRaw(string data) + { + this.w.WriteRaw(data); + } + + public override void WriteBase64(byte[] buffer, int index, int count) + { + this.w.WriteBase64(buffer, index, count); + } + + public override void Close() + { + this.w.Close(); + } + + public override void Flush() + { + this.w.Flush(); + } + + public override string LookupPrefix(string ns) + { + return this.w.LookupPrefix(ns); + } + + public override WriteState WriteState => this.w.WriteState; + + #endregion + } +} \ No newline at end of file diff --git a/source/ChanSort.Loader.PhilipsXml/Properties/AssemblyInfo.cs b/source/ChanSort.Loader.PhilipsXml/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c8b3306 --- /dev/null +++ b/source/ChanSort.Loader.PhilipsXml/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ChanSort.Loader.PhilipsXml")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ChanSort.Loader.PhilipsXml")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d7bafd55-50f5-46c3-a76b-2193bed5358f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/source/ChanSort.Loader.PhilipsXml/Serializer.cs b/source/ChanSort.Loader.PhilipsXml/Serializer.cs new file mode 100644 index 0000000..a68989b --- /dev/null +++ b/source/ChanSort.Loader.PhilipsXml/Serializer.cs @@ -0,0 +1,408 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +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. + + I had a test file "DVBS.xml" with satellite channels and entries like: + + + + + + The other file was "CM_TPM1013E_LA_CK.xml" with entries like: + + + + + + */ + class Serializer : SerializerBase + { + private readonly ChannelList terrChannels = new ChannelList(SignalSource.DvbT | SignalSource.Tv | SignalSource.Radio, "DVB-T"); + private readonly ChannelList cableChannels = new ChannelList(SignalSource.DvbC | SignalSource.Tv | SignalSource.Radio, "DVB-C"); + private readonly ChannelList satChannels = new ChannelList(SignalSource.DvbS | SignalSource.Tv | SignalSource.Radio, "DVB-S"); + + private ChannelList curList; + + 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.CanDeleteChannels = true; + + this.DataRoot.AddChannelList(this.terrChannels); + this.DataRoot.AddChannelList(this.cableChannels); + this.DataRoot.AddChannelList(this.satChannels); + + 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"); + } + } + #endregion + + #region DisplayName + public override string DisplayName => "Philips *.xml loader"; + + #endregion + + + #region Load() + + public override void Load() + { + bool fail = false; + 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"; + + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + ValidationFlags = XmlSchemaValidationFlags.None, + DtdProcessing = DtdProcessing.Ignore + }; + using (var reader = XmlReader.Create(new StringReader(textContent), settings)) + { + doc.Load(reader); + } + } + catch + { + fail = true; + } + + var root = 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"); + + + int rowId = 0; + foreach (XmlNode child in root.ChildNodes) + { + switch (child.LocalName) + { + case "Channel": + if (rowId == 0) + this.DetectFormatAndFeatures(child); + if (this.curList != null) + this.ReadChannel(child, rowId++); + break; + } + } + } + #endregion + + #region DetectFormatAndFeatures() + + private void DetectFormatAndFeatures(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(this.FileName).ToLower(); + var medium = bcastNode.GetAttribute("medium"); + if (medium == "" && fname.Length == 4 && fname.StartsWith("dvb")) + medium = fname; + bool hasEncrypt = false; + + if (setupNode.HasAttribute("ChannelName")) + { + this.formatVersion = 1; + this.DataRoot.SupportedFavorites = Favorites.A; + this.DataRoot.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")) + { + this.formatVersion = 2; + this.DataRoot.SupportedFavorites = 0; + this.DataRoot.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"); + + switch (medium) + { + case "dvbt": + this.curList = this.terrChannels; + break; + case "dvbc": + this.curList = this.cableChannels; + break; + case "dvbs": + this.curList = this.satChannels; + break; + default: + this.curList = null; + break; + } + + if (!hasEncrypt) + this.curList?.VisibleColumnFieldNames.Remove("Encrypted"); + } + #endregion + + #region ReadChannel() + private void ReadChannel(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); + } + + var chan = new Channel(this.curList.SignalSource, rowId, rowId, setupNode); + chan.OldProgramNr = -1; + chan.IsDeleted = false; + if (this.formatVersion == 1) + this.ParseChannelFormat1(data, chan); + else if (this.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(this.curList, chan); + } + #endregion + + #region ParseChannelFormat1 + private void ParseChannelFormat1(Dictionary data, Channel chan) + { + 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.FavIndex[0] = fav == 0 ? -1 : fav; + chan.OriginalNetworkId = ParseInt(data.TryGet("Onid")); + chan.TransportStreamId = ParseInt(data.TryGet("Tsid")); + chan.ServiceId = ParseInt(data.TryGet("Sid")); + var freq = ParseInt(data.TryGet("Frequency")); + chan.FreqInMhz = freq; + chan.ServiceType = ParseInt(data.TryGet("ServiceType")); + chan.SignalSource |= LookupData.Instance.IsRadioOrTv(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.OldProgramNr = ParseInt(data.TryGet("presetnumber")); + chan.Name = data.TryGet("name"); + chan.RawName = chan.Name; + chan.FreqInMhz = ParseInt(data.TryGet("frequency")); + if (chan.FreqInMhz > 100000) + 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 ParseInt() + private int ParseInt(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return 0; + if (input.Length > 2 && input[0] == '0' && char.ToLower(input[1]) == 'x') + return int.Parse(input.Substring(2), NumberStyles.HexNumber); + if (int.TryParse(input, out var value)) + return value; + return 0; + } + #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 Save() + public override void Save(string tvOutputFile) + { + foreach (var list in this.DataRoot.ChannelLists) + this.UpdateChannelList(list); + + // 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 = this.newline; + xmlSettings.OmitXmlDeclaration = false; + + string xml; + using (var sw = new StringWriter()) + using (var w = new CustomXmlWriter(sw, xmlSettings, false)) + { + this.doc.WriteTo(w); + w.Flush(); + xml = sw.ToString(); + } + + var enc = new UTF8Encoding(false, false); + File.WriteAllText(tvOutputFile, 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 (this.formatVersion == 1) + this.UpdateChannelFormat1(ch); + else if (this.formatVersion == 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 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 "); + return sb.ToString(); + } + #endregion + } +} diff --git a/source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs b/source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs new file mode 100644 index 0000000..c47b78a --- /dev/null +++ b/source/ChanSort.Loader.PhilipsXml/SerializerPlugin.cs @@ -0,0 +1,16 @@ +using ChanSort.Api; + +namespace ChanSort.Loader.PhilipsXml +{ + public class SerializerPlugin : ISerializerPlugin + { + public string DllName { get; set; } + public string PluginName => "Philips .xml"; + public string FileFilter => "*.xml"; + + public SerializerBase CreateSerializer(string inputFile) + { + return new Serializer(inputFile); + } + } +} diff --git a/source/ChanSort.Loader.Sony/Serializer.cs b/source/ChanSort.Loader.Sony/Serializer.cs index a269fd4..0fa19df 100644 --- a/source/ChanSort.Loader.Sony/Serializer.cs +++ b/source/ChanSort.Loader.Sony/Serializer.cs @@ -23,7 +23,7 @@ namespace ChanSort.Loader.Sony private const string SupportedFormatVersions = " e1.1.0 1.0.0 1.1.0 1.2.0 "; - private readonly ChannelList terrChannels = new ChannelList(SignalSource.DvbC | SignalSource.Tv | SignalSource.Radio, "DVB-T"); + private readonly ChannelList terrChannels = new ChannelList(SignalSource.DvbT | SignalSource.Tv | SignalSource.Radio, "DVB-T"); private readonly ChannelList cableChannels = new ChannelList(SignalSource.DvbC | SignalSource.Tv | SignalSource.Radio, "DVB-C"); private readonly ChannelList satChannels = new ChannelList(SignalSource.DvbS | SignalSource.Tv | SignalSource.Radio, "DVB-S"); private readonly ChannelList satChannelsP = new ChannelList(SignalSource.DvbS | SignalSource.Tv | SignalSource.Radio, "DVB-S Preset"); diff --git a/source/ChanSort.sln b/source/ChanSort.sln index fb494e8..37a1345 100644 --- a/source/ChanSort.sln +++ b/source/ChanSort.sln @@ -52,6 +52,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChanSort.Loader.SilvaSchnei EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChanSort.Loader.Sony", "ChanSort.Loader.Sony\ChanSort.Loader.Sony.csproj", "{70E29C6B-B926-4859-9548-23375BF1E1B5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChanSort.Loader.PhilipsXml", "ChanSort.Loader.PhilipsXml\ChanSort.Loader.PhilipsXml.csproj", "{D7BAFD55-50F5-46C3-A76B-2193BED5358F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -254,6 +256,18 @@ Global {70E29C6B-B926-4859-9548-23375BF1E1B5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {70E29C6B-B926-4859-9548-23375BF1E1B5}.Release|x86.ActiveCfg = Release|x86 {70E29C6B-B926-4859-9548-23375BF1E1B5}.Release|x86.Build.0 = Release|x86 + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Debug|x86.ActiveCfg = Debug|x86 + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Debug|x86.Build.0 = Debug|x86 + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Release|Any CPU.Build.0 = Release|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Release|x86.ActiveCfg = Release|Any CPU + {D7BAFD55-50F5-46C3-A76B-2193BED5358F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/ChanSort/ChanSort.csproj b/source/ChanSort/ChanSort.csproj index 0a2d275..6413713 100644 --- a/source/ChanSort/ChanSort.csproj +++ b/source/ChanSort/ChanSort.csproj @@ -386,6 +386,10 @@ {68da8072-3a29-4076-9f64-d66f38349585} ChanSort.Loader.Panasonic + + {d7bafd55-50f5-46c3-a76b-2193bed5358f} + ChanSort.Loader.PhilipsXml + {33897002-0537-49a4-b963-a18d17311b3d} ChanSort.Loader.SamsungJ diff --git a/source/ChanSort/MainForm.Designer.cs b/source/ChanSort/MainForm.Designer.cs index d3ba2d2..cf38039 100644 --- a/source/ChanSort/MainForm.Designer.cs +++ b/source/ChanSort/MainForm.Designer.cs @@ -161,6 +161,7 @@ this.miShowWarningsAfterLoad = new DevExpress.XtraBars.BarCheckItem(); this.miAllowEditPredefinedLists = new DevExpress.XtraBars.BarButtonItem(); this.miExplorerIntegration = new DevExpress.XtraBars.BarButtonItem(); + this.miCheckUpdates = new DevExpress.XtraBars.BarButtonItem(); this.mnuAccessibility = new DevExpress.XtraBars.BarSubItem(); this.mnuGotoChannelList = new DevExpress.XtraBars.BarSubItem(); this.mnuInputSource = new DevExpress.XtraBars.BarLinkContainerItem(); @@ -206,7 +207,7 @@ this.pageProgNr = new DevExpress.XtraTab.XtraTabPage(); this.popupInputSource = new DevExpress.XtraBars.PopupMenu(this.components); this.popupFavList = new DevExpress.XtraBars.PopupMenu(this.components); - this.miCheckUpdates = new DevExpress.XtraBars.BarButtonItem(); + this.miUtf8Charset = new DevExpress.XtraBars.BarButtonItem(); ((System.ComponentModel.ISupportInitialize)(this.splitContainerControl1)).BeginInit(); this.splitContainerControl1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.grpOutputList)).BeginInit(); @@ -1026,9 +1027,10 @@ this.miCzech, this.miRomanian, this.miExplorerIntegration, - this.miCheckUpdates}); + this.miCheckUpdates, + this.miUtf8Charset}); this.barManager1.MainMenu = this.bar1; - this.barManager1.MaxItemId = 99; + this.barManager1.MaxItemId = 100; this.barManager1.ShowFullMenus = true; // // bar1 @@ -1608,7 +1610,8 @@ this.mnuCharset.ImageOptions.ImageIndex = ((int)(resources.GetObject("mnuCharset.ImageOptions.ImageIndex"))); this.mnuCharset.LinksPersistInfo.AddRange(new DevExpress.XtraBars.LinkPersistInfo[] { new DevExpress.XtraBars.LinkPersistInfo(this.miCharsetForm), - new DevExpress.XtraBars.LinkPersistInfo(this.miIsoCharSets, true)}); + new DevExpress.XtraBars.LinkPersistInfo(this.miUtf8Charset, true), + new DevExpress.XtraBars.LinkPersistInfo(this.miIsoCharSets)}); this.mnuCharset.Name = "mnuCharset"; this.mnuCharset.PaintStyle = DevExpress.XtraBars.BarItemPaintStyle.CaptionInMenu; // @@ -1653,6 +1656,14 @@ this.miExplorerIntegration.Name = "miExplorerIntegration"; this.miExplorerIntegration.ItemClick += new DevExpress.XtraBars.ItemClickEventHandler(this.miExplorerIntegration_ItemClick); // + // miCheckUpdates + // + this.miCheckUpdates.ButtonStyle = DevExpress.XtraBars.BarButtonStyle.Check; + resources.ApplyResources(this.miCheckUpdates, "miCheckUpdates"); + this.miCheckUpdates.Id = 98; + this.miCheckUpdates.Name = "miCheckUpdates"; + this.miCheckUpdates.ItemClick += new DevExpress.XtraBars.ItemClickEventHandler(this.miCheckUpdates_ItemClick); + // // mnuAccessibility // resources.ApplyResources(this.mnuAccessibility, "mnuAccessibility"); @@ -2055,13 +2066,12 @@ this.popupFavList.Name = "popupFavList"; this.popupFavList.ShowCaption = true; // - // miCheckUpdates + // miUtf8Charset // - this.miCheckUpdates.ButtonStyle = DevExpress.XtraBars.BarButtonStyle.Check; - resources.ApplyResources(this.miCheckUpdates, "miCheckUpdates"); - this.miCheckUpdates.Id = 98; - this.miCheckUpdates.Name = "miCheckUpdates"; - this.miCheckUpdates.ItemClick += new DevExpress.XtraBars.ItemClickEventHandler(this.miCheckUpdates_ItemClick); + resources.ApplyResources(this.miUtf8Charset, "miUtf8Charset"); + this.miUtf8Charset.Id = 99; + this.miUtf8Charset.Name = "miUtf8Charset"; + this.miUtf8Charset.ItemClick += new DevExpress.XtraBars.ItemClickEventHandler(this.MiUtf8Charset_ItemClick); // // MainForm // @@ -2304,6 +2314,7 @@ private DevExpress.XtraGrid.Columns.GridColumn colPcrPid; private DevExpress.XtraBars.BarButtonItem miExplorerIntegration; private DevExpress.XtraBars.BarButtonItem miCheckUpdates; + private DevExpress.XtraBars.BarButtonItem miUtf8Charset; } } diff --git a/source/ChanSort/MainForm.cs b/source/ChanSort/MainForm.cs index 0bacc46..0f25985 100644 --- a/source/ChanSort/MainForm.cs +++ b/source/ChanSort/MainForm.cs @@ -412,6 +412,9 @@ namespace ChanSort.Ui private void UpdateFavoritesEditor(Favorites favorites) { + var miSet = new[] {this.miFavASet, this.miFavBSet, this.miFavCSet, this.miFavDSet, this.miFavESet}; + var miUnset = new[] { this.miFavAUnset, this.miFavBUnset, this.miFavCUnset, this.miFavDUnset, this.miFavEUnset }; + this.repositoryItemCheckedComboBoxEdit1.Items.Clear(); this.repositoryItemCheckedComboBoxEdit2.Items.Clear(); byte mask = 0x01; @@ -424,9 +427,16 @@ namespace ChanSort.Ui var c = (char) ('A' + bit); this.repositoryItemCheckedComboBoxEdit1.Items.Add(c); this.repositoryItemCheckedComboBoxEdit2.Items.Add(c); + miSet[bit].Visibility = BarItemVisibility.Always; + miUnset[bit].Visibility = BarItemVisibility.Always; regex += c; ++favCount; } + else + { + miSet[bit].Visibility = BarItemVisibility.Never; + miUnset[bit].Visibility = BarItemVisibility.Never; + } } regex += "]*"; this.repositoryItemCheckedComboBoxEdit1.Mask.EditMask = regex; @@ -1257,8 +1267,8 @@ namespace ChanSort.Ui this.cbCloseGap.Checked = Config.Default.CloseGaps; this.ClearLeftFilter(); this.ClearRightFilter(); - foreach(var path in Config.Default.MruFiles) - this.AddFileToMruList(path); + this.mruFiles.Clear(); + this.mruFiles.AddRange(Config.Default.MruFiles); this.UpdateMruMenu(); this.miExplorerIntegration.Down = Config.Default.ExplorerIntegration; @@ -2872,6 +2882,11 @@ namespace ChanSort.Ui #region Character set menu + private void MiUtf8Charset_ItemClick(object sender, ItemClickEventArgs e) + { + TryExecute(() => this.SetDefaultEncoding(Encoding.UTF8)); + } + private void miIsoCharSets_ListItemClick(object sender, ListItemClickEventArgs e) { TryExecute(() => this.SetDefaultEncoding(Encoding.GetEncoding(this.isoEncodings[e.Index]))); diff --git a/source/ChanSort/MainForm.resx b/source/ChanSort/MainForm.resx index 5e3d81a..d74b6bc 100644 --- a/source/ChanSort/MainForm.resx +++ b/source/ChanSort/MainForm.resx @@ -647,6 +647,9 @@ 9 + + UTF-8 (Unicode) + ISO character sets @@ -1255,7 +1258,7 @@ globalImageCollection1 - ChanSort.Ui.GlobalImageCollection, ChanSort, Version=1.0.7134.23381, Culture=neutral, PublicKeyToken=null + ChanSort.Ui.GlobalImageCollection, ChanSort, Version=1.0.7156.40088, Culture=neutral, PublicKeyToken=null gviewRight @@ -1809,6 +1812,12 @@ DevExpress.XtraBars.BarButtonItem, DevExpress.XtraBars.v19.1, Version=19.1.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a + + miCheckUpdates + + + DevExpress.XtraBars.BarButtonItem, DevExpress.XtraBars.v19.1, Version=19.1.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a + mnuAccessibility @@ -1971,10 +1980,10 @@ DevExpress.XtraBars.PopupMenu, DevExpress.XtraBars.v19.1, Version=19.1.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a - - miCheckUpdates + + miUtf8Charset - + DevExpress.XtraBars.BarButtonItem, DevExpress.XtraBars.v19.1, Version=19.1.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a @@ -1984,7 +1993,7 @@ DevExpress.XtraEditors.XtraForm, DevExpress.Utils.v19.1, Version=19.1.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a - 07/14/2019 13:16:32 + 08/05/2019 22:23:17 16, 16 diff --git a/source/changelog.md b/source/changelog.md index dd4b574..fdc7689 100644 --- a/source/changelog.md +++ b/source/changelog.md @@ -1,6 +1,13 @@ ChanSort Change Log =================== +2019-08-05 +- added partial support for Philips .xml channel lists + (There are MANY different file formats, only a few are currently supported) +- fixed "most-recently-used" getting reversed every time the program was started +- added "UTF-8 (Unicode)" character set to menu +- fixed disappearing columns when loading different channel lists without restarting the application + 2019-07-25 - fix: Application failed to save config and didn't exit when the folder %LOCALAPPDATA%\ChanSort doesn't exist