diff --git a/source/ChanSort.Loader.LG/ChanSort.Loader.LG.ini b/source/ChanSort.Loader.LG/ChanSort.Loader.LG.ini index c2526c1..7ad94fa 100644 --- a/source/ChanSort.Loader.LG/ChanSort.Loader.LG.ini +++ b/source/ChanSort.Loader.LG/ChanSort.Loader.LG.ini @@ -1032,3 +1032,7 @@ [FirmwareData:39680] ; LB580V offSize = 0 + + +[webOS 5] + set_userEditChNumber=true diff --git a/source/ChanSort.Loader.LG/GlobalClone/GcJsonSerializer.cs b/source/ChanSort.Loader.LG/GlobalClone/GcJsonSerializer.cs index e32d56b..b3ca2e3 100644 --- a/source/ChanSort.Loader.LG/GlobalClone/GcJsonSerializer.cs +++ b/source/ChanSort.Loader.LG/GlobalClone/GcJsonSerializer.cs @@ -16,9 +16,10 @@ namespace ChanSort.Loader.GlobalClone internal class GcJsonSerializer : SerializerBase { private readonly string content; - string xmlPrefix; - string xmlSuffix; + private string xmlPrefix; + private string xmlSuffix; private JObject doc; + private readonly IniFile.Section ini; public GcJsonSerializer(string filename, string content) : base(filename) { @@ -42,6 +43,9 @@ namespace ChanSort.Loader.GlobalClone this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbC | SignalSource.Radio, "DVB-C Radio")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbS | SignalSource.Tv | SignalSource.Data, "DVB-S TV")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbS | SignalSource.Radio, "DVB-S Radio")); + + string iniFile = this.GetType().Assembly.Location.ToLowerInvariant().Replace(".dll", ".ini"); + this.ini = new IniFile(iniFile).GetSection("webOS 5", false); } #region Load() @@ -315,6 +319,7 @@ namespace ChanSort.Loader.GlobalClone foreach (var list in this.DataRoot.ChannelLists) { int radioMask = (list.SignalSource & SignalSource.Radio) != 0 ? 0x4000 : 0; + var updateUserEditChNumber = ini.GetBool("set_userEditChNumber"); foreach (var chan in list.Channels) { if (!(chan is GcChannel ch)) @@ -326,7 +331,8 @@ namespace ChanSort.Loader.GlobalClone node["chNameBase64"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(ch.Name)); } - node["deleted"] = ch.IsDeleted; + //node["deleted"] = ch.IsDeleted; // we don't support deleting in this serializer + var nr = Math.Max(ch.NewProgramNr, 0) | radioMask; // radio channels have 0x4000 added to the majorNumber node["majorNumber"] = nr; node["skipped"] = ch.Skip; @@ -337,11 +343,11 @@ namespace ChanSort.Loader.GlobalClone // the only successfully imported file was one where these flags were NOT set by ChanSort // these flags do get set when changing numbers through the TV's menu, but then prevent further modifications, e.g. through an import - //if (ch.NewProgramNr != Math.Max(ch.OldProgramNr, 0)) - //{ - // node["userEditChNumber"] = false; - // node["userSelCHNo"] = false; - //} + if (updateUserEditChNumber && ch.NewProgramNr != Math.Max(ch.OldProgramNr, 0)) + { + node["userEditChNumber"] = true; + node["userSelCHNo"] = true; + } //node["disableUpdate"] = true; // No-Go! This blocked the whole list and required a factory reset. Regardless of the setting, the TV showed wrong numbers. diff --git a/source/ChanSort.Loader.M3u/Serializer.cs b/source/ChanSort.Loader.M3u/Serializer.cs index 4851a54..bfd3ce1 100644 --- a/source/ChanSort.Loader.M3u/Serializer.cs +++ b/source/ChanSort.Loader.M3u/Serializer.cs @@ -39,7 +39,7 @@ namespace ChanSort.Loader.M3u base.DefaultEncoding = new UTF8Encoding(false); this.allChannels.VisibleColumnFieldNames = new List() { - "+OldPosition", "+Position", "+Name", "+FreqInMhz", "+Polarity", "+SymbolRate", "+VideoPid", "+AudioPid", "+Satellite", "+Provider" + "+OldPosition", "+Position", "+Name", "+SatPosition", "+Source", "+FreqInMhz", "+Polarity", "+SymbolRate", "+Satellite", "+Provider", "+Debug" }; } #endregion @@ -60,7 +60,7 @@ namespace ChanSort.Loader.M3u var rdr = new StreamReader(new MemoryStream(content), overrideEncoding ?? this.DefaultEncoding); string line = rdr.ReadLine()?.TrimEnd(); - if (line == null || line != "#EXTM3U") + if (line == null || !(line == "#EXTM3U" || line.StartsWith("#EXTM3U "))) throw new FileLoadException("Unsupported .m3u file: " + this.FileName); this.headerLines.Add(line); @@ -122,15 +122,14 @@ namespace ChanSort.Loader.M3u { int progNr = 0; string name = ""; - int extInfTrackNameIndex = -1; + int extInfTrackNameIndex = -1; if (extInfLine != null) { bool extInfContainsProgNr = false; - extInfTrackNameIndex = FindExtInfTrackName(extInfLine); - if (extInfTrackNameIndex >= 0) + ParseExtInf(extInfLine, out name, out extInfTrackNameIndex, out var param); + if (name != "") { - name = extInfLine.Substring(extInfTrackNameIndex); var match = ExtInfTrackName.Match(name); if (!string.IsNullOrEmpty(match.Groups[1].Value)) { @@ -139,6 +138,10 @@ namespace ChanSort.Loader.M3u extInfContainsProgNr = true; } } + + if (string.IsNullOrEmpty(group)) + param.TryGetValue("group-title", out group); + this.allChannelsPrefixedWithProgNr &= extInfContainsProgNr; } @@ -156,11 +159,15 @@ namespace ChanSort.Loader.M3u var uri = new Uri(uriLine); chan.Satellite = uri.GetLeftPart(UriPartial.Path); var parms = HttpUtility.ParseQueryString(uri.Query); + chan.AddDebug(uri.Query); foreach (var key in parms.AllKeys) { var val = parms.Get(key); switch (key) { + case "src": + chan.SatPosition = "src=" + val; + break; case "freq": chan.FreqInMhz = this.ParseInt(val); break; @@ -168,18 +175,12 @@ namespace ChanSort.Loader.M3u if (val.Length == 1) chan.Polarity = Char.ToUpperInvariant(val[0]); break; + case "msys": + chan.Source = val; + break; case "sr": chan.SymbolRate = this.ParseInt(val); break; - case "pids": - var pids = val.Split(','); - //if (pids.Length > 3) - // chan.PcrPid = this.ParseInt(pids[3]); - if (pids.Length > 4) - chan.VideoPid = this.ParseInt(pids[4]); - if (pids.Length > 5) - chan.AudioPid = this.ParseInt(pids[5]); - break; } } @@ -196,24 +197,78 @@ namespace ChanSort.Loader.M3u } #endregion - #region FindExtInfTrackName() + #region ParseExtInf() + + enum ExtInfParsePhase + { + Header, + Length, + Key, + Value, + Name + } + /// /// parse track name from lines that may look like: /// #EXTINF:<length>[ key="value" ...],<TrackName> /// - private int FindExtInfTrackName(string extInfLine) + private void ParseExtInf(string extInfLine, out string name, out int nameIndex, out Dictionary param) { + name = ""; + nameIndex = -1; + param = new Dictionary(); bool inQuote = false; + var key = ""; + var value = ""; + ExtInfParsePhase phase = ExtInfParsePhase.Header; for (int i = 0, c = extInfLine.Length; i < c; i++) { var ch = extInfLine[i]; - if (ch == ',' && !inQuote) - return i + 1; - if (ch == '"') - inQuote = !inQuote; - } + switch (phase) + { + case ExtInfParsePhase.Header: + if (ch == ':') + phase = ExtInfParsePhase.Length; + break; + case ExtInfParsePhase.Length: + if (ch == ' ') + phase = ExtInfParsePhase.Key; + else if (ch == ',') + { + phase = ExtInfParsePhase.Name; + nameIndex = i + 1; + } + break; + case ExtInfParsePhase.Key: + if (ch == '=') + phase = ExtInfParsePhase.Value; + else + key += ch; + break; + case ExtInfParsePhase.Value: + if (ch == '"') + inQuote = !inQuote; + else if (ch == ' ' && !inQuote) + { + param[key] = value; + key = ""; + value = ""; - return -1; + } + else if (ch == ',' && !inQuote) + { + phase = ExtInfParsePhase.Name; + param[key] = value; + } + else + value += ch; + break; + case ExtInfParsePhase.Name: + if (ch != ' ' || name.Length > 0) + name += ch; + break; + } + } } #endregion diff --git a/source/ChanSort.Loader.Panasonic/XmlSerializer.cs b/source/ChanSort.Loader.Panasonic/XmlSerializer.cs index 7579809..b511ecf 100644 --- a/source/ChanSort.Loader.Panasonic/XmlSerializer.cs +++ b/source/ChanSort.Loader.Panasonic/XmlSerializer.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; using System.Xml; using System.Xml.Schema; using ChanSort.Api; @@ -70,6 +71,7 @@ namespace ChanSort.Loader.Panasonic doc = new XmlDocument(); var content = File.ReadAllBytes(this.FileName); var textContent = Encoding.UTF8.GetString(content); + textContent = FixUnescapedXmlChars(textContent); var settings = new XmlReaderSettings { @@ -104,6 +106,30 @@ namespace ChanSort.Loader.Panasonic } #endregion + #region FixUnescapedXmlChars() + private string FixUnescapedXmlChars(string textContent) + { + var sb = new StringBuilder((int)(textContent.Length * 1.1)); + var inQuotes = false; + foreach (var c in textContent) + { + if (c == '\"') + inQuotes = !inQuotes; + + if (c == '&' && inQuotes) + sb.Append("&"); + else if (c == '<' && inQuotes) + sb.Append("<"); + else if (c == '>' && inQuotes) + sb.Append(">"); + else + sb.Append(c); + } + + return sb.ToString(); + } + #endregion + #region ReadChannel() private void ReadChannel(XmlNode node) { @@ -152,13 +178,75 @@ namespace ChanSort.Loader.Panasonic xmlSettings.NewLineHandling = NewLineHandling.None; xmlSettings.NewLineChars = "\n"; xmlSettings.OmitXmlDeclaration = true; - using var w = XmlWriter.Create(tvOutputFile, xmlSettings); + + // write to a string so that we can patch the result to be binary identical to the original file (if there are no changes) + using var stringWriter = new StringWriter(); + using var w = XmlWriter.Create(stringWriter, xmlSettings); doc.WriteTo(w); + w.Flush(); + stringWriter.Write('\n'); // original file has a trailing \x0A + var xml = stringWriter.ToString(); + xml = UnescapeXmlChars(xml); // create same broken XML as the original export with unescaped entities + xml = xml.Replace(" />", "/>"); // original file has no space before the element end + File.WriteAllText(tvOutputFile, xml, xmlSettings.Encoding); this.FileName = tvOutputFile; } #endregion + #region UnescapeXmlChars() + /// + /// Generate the same broken XML with unescaped XML-entities as the original Panasonic XML export does (i.e. literal '&' character in channel names) + /// + private string UnescapeXmlChars(string xml) + { + bool inQuotes = false; + bool inEntity = false; + string entity = ""; + var sb = new StringBuilder(xml.Length); + foreach (var c in xml) + { + if (inEntity) + { + if (c == ';') + { + switch (entity) + { + case "lt": + sb.Append("<"); + break; + case "gt": + sb.Append(">"); + break; + case "amp": + sb.Append("&"); + break; + } + + inEntity = false; + } + else + entity += c; + continue; + } + + if (c == '"') + inQuotes = !inQuotes; + + if (c == '&' && inQuotes) + { + inEntity = true; + entity = ""; + continue; + } + + sb.Append(c); + } + + return sb.ToString(); + } + #endregion + #region class XmlChannel diff --git a/source/ChanSort/MainForm.cs b/source/ChanSort/MainForm.cs index 97010c4..94bec8c 100644 --- a/source/ChanSort/MainForm.cs +++ b/source/ChanSort/MainForm.cs @@ -502,9 +502,13 @@ namespace ChanSort.Ui regex += c; } - regex += "]*"; + if (favorites == 0) + regex = ""; + else + regex += "]*"; this.repositoryItemCheckedComboBoxEdit1.Mask.EditMask = regex; this.repositoryItemCheckedComboBoxEdit2.Mask.EditMask = regex; + this.repositoryItemCheckedComboBoxEdit1.ReadOnly = this.repositoryItemCheckedComboBoxEdit2.ReadOnly = favorites == 0; this.tabSubList.BeginUpdate(); while (this.tabSubList.TabPages.Count > favCount + 1) diff --git a/source/ChanSort/SkinPickerForm.de.resx b/source/ChanSort/SkinPickerForm.de.resx index 2a78e46..037a817 100644 --- a/source/ChanSort/SkinPickerForm.de.resx +++ b/source/ChanSort/SkinPickerForm.de.resx @@ -126,4 +126,7 @@ Ok + + Standard-Skin verwenden + \ No newline at end of file diff --git a/source/Translation.xlsx b/source/Translation.xlsx index 2eedbeb..14936ed 100644 Binary files a/source/Translation.xlsx and b/source/Translation.xlsx differ diff --git a/source/Translation_ru.xlsx b/source/Translation_ru.xlsx deleted file mode 100644 index 00f7491..0000000 Binary files a/source/Translation_ru.xlsx and /dev/null differ diff --git a/source/changelog.md b/source/changelog.md index a520737..d5c5cee 100644 --- a/source/changelog.md +++ b/source/changelog.md @@ -1,6 +1,16 @@ ChanSort Change Log =================== +2021-10-24 +- LG webOS 5 and 6: Improved support for DVB-C lists which changed channel numbers after import + (Now setting the "userEditChNumber" and "userSelCHNo" flags when channel numbers are changed. + This can be disabled in ChanSort.Loader.LG.ini, section \[webOS 5\], setting set_userEditChNumber=false) +- Sat>IP/.m3u: Support for files with extra information after the #EXTM3U header + Also capturing the group-title from #EXTINF, showing the msys value in the Source column (dvbs/dvbt/...), + and showing all URL-parameters in the Debug column +- Panasonic .xml: files with "&" characters in channel names can now be loaded + (Panasonic does not escape special XML characters and produces invalid XML syntax) + 2021-09-23_1945 - Philips: disabled deleting of channels for ChannelMap\_100 - 115, except for version 100 without any .bin files. (Lists with .bin files require that the .xml file contains all the same channels to override all channel numbers)