From bc4b650f20e581e64396581d06241a33af211e6e Mon Sep 17 00:00:00 2001 From: Horst Beham Date: Wed, 24 Feb 2021 11:05:47 +0100 Subject: [PATCH] - Philips ChannelMap_45: TV did not remember last selected favorite list when first fav list was created by ChanSort. - Philips ChannelMap_100 and later: "Channel" XML elements inside the DVB*.xml files are now reordered by program nr. - Philips ChannelMap_105 and 110: fixed saving favorite lists (keeping FavoriteNumber="0" in DVB*.xml and only setting the numbers in Favorites.xml) - m3u: keep original end-of-line characters (CRLF or LF) - m3u: detect whether channel names are prefixed with a program number or not, and save the file in the same way. --- source/ChanSort.Api/Utils/IniFile.cs | 14 ++ source/ChanSort.Loader.M3u/Serializer.cs | 12 +- .../BinarySerializer.cs | 88 ++++++------ .../ChanSort.Loader.Philips.ini | 14 ++ .../ChanSort.Loader.Philips/XmlSerializer.cs | 127 ++++++++++++------ source/changelog.md | 8 ++ 6 files changed, 182 insertions(+), 81 deletions(-) diff --git a/source/ChanSort.Api/Utils/IniFile.cs b/source/ChanSort.Api/Utils/IniFile.cs index 13e282c7..f1bad617 100644 --- a/source/ChanSort.Api/Utils/IniFile.cs +++ b/source/ChanSort.Api/Utils/IniFile.cs @@ -90,6 +90,20 @@ public int[] GetIntList(string key) } #endregion + #region GetBool() + public bool GetBool(string key, bool defaultValue = false) + { + var val = GetString(key)?.ToLowerInvariant(); + if (val == null) + return defaultValue; + if (val == "false" || val == "off" || val == "no" || val == "0") + return false; + if (val == "true" || val == "on" || val == "yes" || val == "1") + return true; + return defaultValue; + } + #endregion + #region ParseNumber() private int ParseNumber(string value) { diff --git a/source/ChanSort.Loader.M3u/Serializer.cs b/source/ChanSort.Loader.M3u/Serializer.cs index 00b02171..6472b8df 100644 --- a/source/ChanSort.Loader.M3u/Serializer.cs +++ b/source/ChanSort.Loader.M3u/Serializer.cs @@ -20,6 +20,7 @@ class Serializer : SerializerBase private Encoding overrideEncoding; private string newLine = "\r\n"; + private bool allChannelsPrefixedWithProgNr = true; private List headerLines = new List(); private List trailingLines = new List(); // comment and blank lines after the last URI line @@ -55,7 +56,7 @@ public override void Load() overrideEncoding = new UTF8Encoding(false); // detect line separator - int idx = Array.IndexOf(content, '\n'); + int idx = Array.IndexOf(content, (byte)'\n'); this.newLine = idx >= 1 && content[idx-1] == '\r' ? "\r\n" : "\n"; var rdr = new StreamReader(new MemoryStream(content), overrideEncoding ?? this.DefaultEncoding); @@ -126,6 +127,7 @@ private void ReadChannel(int uriLineNr, string uriLine, string extInfLine, strin if (extInfLine != null) { + bool extInfContainsProgNr = false; extInfTrackNameIndex = FindExtInfTrackName(extInfLine); if (extInfTrackNameIndex >= 0) { @@ -135,10 +137,13 @@ private void ReadChannel(int uriLineNr, string uriLine, string extInfLine, strin { progNr = this.ParseInt(match.Groups[1].Value); name = match.Groups[2].Value; + extInfContainsProgNr = true; } } + this.allChannelsPrefixedWithProgNr &= extInfContainsProgNr; } + if (progNr == 0) progNr = this.allChannels.Count + 1; @@ -231,7 +236,10 @@ public override void Save(string tvOutputFile) foreach (var line in chan.Lines) { if (line.StartsWith("#EXTINF:")) - file.WriteLine($"{line.Substring(0, chan.ExtInfTrackNameIndex)}{chan.NewProgramNr}. {chan.Name}"); + { + var progNrPrefix = this.allChannelsPrefixedWithProgNr ? chan.NewProgramNr + ". " : ""; + file.WriteLine($"{line.Substring(0, chan.ExtInfTrackNameIndex)}{progNrPrefix}{chan.Name}"); + } else file.WriteLine(line); } diff --git a/source/ChanSort.Loader.Philips/BinarySerializer.cs b/source/ChanSort.Loader.Philips/BinarySerializer.cs index fd194fd8..5aa5811c 100644 --- a/source/ChanSort.Loader.Philips/BinarySerializer.cs +++ b/source/ChanSort.Loader.Philips/BinarySerializer.cs @@ -56,9 +56,9 @@ class BinarySerializer : SerializerBase private ChanLstBin chanLstBin; private readonly StringBuilder logMessages = new StringBuilder(); - private readonly List favListIndexToId = new(); - private readonly Dictionary favListIdToIndex = new(); private readonly ChannelList favChannels = new ChannelList(SignalSource.All, "Favorites"); + private const int FavListCount = 8; + private bool mustFixFavListIds; #region ctor() public BinarySerializer(string inputFile) : base(inputFile) @@ -647,40 +647,45 @@ private void LoadMap45Favorites(string listDb) this.favChannels.AddChannel(chan); } } + this.Features.SupportedFavorites = (Favorites)0xFF; + this.Features.SortedFavorites = true; this.Features.MixedSourceFavorites = true; this.Features.AllowGapsInFavNumbers = false; - using var conn = new SQLiteConnection($"Data Source={listDb}"); conn.Open(); + + // older versions of ChanSort wrote invalid "list_id" values starting at 0 instead of 1 and going past 8. + // if everything is in the range of 1-8, this code keeps the current ids. otherwise it remaps them to 1-8. using var cmd = conn.CreateCommand(); - cmd.CommandText = "select list_id, list_name from List"; - this.Features.SupportedFavorites = 0; - this.Features.SortedFavorites = true; + cmd.CommandText = "select min(list_id), max(list_id) from List"; using (var r = cmd.ExecuteReader()) { - int i = 0; - while (r.Read() && i < ChannelInfo.MAX_FAV_LISTS) - { - this.favListIndexToId.Add(r.GetInt32(0)); - this.favListIdToIndex.Add(r.GetInt32(0), i); - this.DataRoot.SetFavListCaption(i, r.GetString(1)); - this.Features.SupportedFavorites |= (Favorites) (1 << i); - i++; - } + r.Read(); + mustFixFavListIds = !r.IsDBNull(0) && (r.GetInt16(0) < 1 || r.GetInt16(1) > 8); + if (mustFixFavListIds) + logMessages.AppendLine("invalid list_id values in list.db will be corrected"); + } - for (; i < 8; i++) + cmd.CommandText = "select list_id, list_name from List order by list_id"; + var listIds = new List(); + using (var r = cmd.ExecuteReader()) + { + var listIndex = 0; + while (r.Read()) { - this.favListIndexToId.Add(-i-1); - this.favListIdToIndex.Add(-i-1, i); - this.DataRoot.SetFavListCaption(i, "Fav " + (i+1)); - this.Features.SupportedFavorites |= (Favorites)(1 << i); + var listId = r.GetInt16(0); + listIds.Add(listId); + if (!this.mustFixFavListIds) + listIndex = listId - 1; + this.DataRoot.SetFavListCaption(listIndex, r.GetString(1)); + ++listIndex; } } - for (int listIndex = 0; listIndex < this.favListIndexToId.Count; listIndex++) + for (int listIndex = 0; listIndex < listIds.Count; listIndex++) { - cmd.CommandText = $"select channel_id from FavoriteChannels where fav_list_id={favListIndexToId[listIndex]} order by rank"; + cmd.CommandText = $"select channel_id from FavoriteChannels where fav_list_id={listIds[listIndex]} order by rank"; using var r = cmd.ExecuteReader(); int seq = 0; while (r.Read()) @@ -997,29 +1002,30 @@ private void UpdateChannelMap45ListDb() conn.Open(); using var trans = conn.BeginTransaction(); using var cmd = conn.CreateCommand(); - for (int favListIndex = 0; favListIndex < this.favListIndexToId.Count; favListIndex++) + cmd.CommandText = "delete from FavoriteChannels"; + cmd.ExecuteNonQuery(); + if (this.mustFixFavListIds) { - var favListId = favListIndexToId[favListIndex]; - string sqlInsertOrUpdateList; - if (favListId >= 0) - { - cmd.CommandText = $"delete from FavoriteChannels where fav_list_id={favListId}"; - cmd.ExecuteNonQuery(); - sqlInsertOrUpdateList = "update List set list_name=@name, list_version=list_version+1 where list_id=@id"; - } - else - { - favListId = favListIndexToId.Count == 0 ? 1 : favListIndexToId.Max() + 1; - favListIndexToId[favListIndex] = favListId; - favListIdToIndex[favListId] = favListIndex; - sqlInsertOrUpdateList = "insert into List (list_id, list_name, list_version) values (@id,@name,1)"; - } + cmd.CommandText = "delete from List"; + cmd.ExecuteNonQuery(); + } + + var incFavList = (ini.GetSection("Map" + chanLstBin.VersionMajor)?.GetBool("incrementFavListVersion", true) ?? true) + ? ", list_version=list_version+1" + : ""; + + for (int favListIndex = 0; favListIndex < FavListCount; favListIndex++) + { + var favListId = favListIndex + 1; + cmd.CommandText = $"select count(1) from List where list_id={favListId}"; + cmd.CommandText = (long) cmd.ExecuteScalar() == 0 ? + "insert into List (list_id, list_name, list_version) values (@id,@name,1)" : + "update List set list_name=@name" + incFavList + " where list_id=@id"; - cmd.CommandText = sqlInsertOrUpdateList; cmd.Parameters.Add(new SQLiteParameter("@id", DbType.Int16)); cmd.Parameters.Add(new SQLiteParameter("@name", DbType.String)); cmd.Parameters["@id"].Value = favListId; - cmd.Parameters["@name"].Value = DataRoot.GetFavListCaption(favListIndex); + cmd.Parameters["@name"].Value = DataRoot.GetFavListCaption(favListIndex) ?? "Fav " + (favListIndex + 1); cmd.ExecuteNonQuery(); cmd.CommandText = "insert into FavoriteChannels(fav_list_id, channel_id, rank) values (@listId,@channelId,@rank)"; @@ -1049,7 +1055,7 @@ private void UpdateChannelMap45ListDb() cmd.ExecuteNonQuery(); // make sure the last_watched_channel_id is valid in the list - cmd.CommandText = "update List set last_watched_channel_id=(select min(channel_id) from FavoriteChannels f where f.fav_list_id=List.list_id)"; + cmd.CommandText = @"update List set last_watched_channel_id=(select channel_id from FavoriteChannels f where f.fav_list_id=List.list_id order by rank limit 1) where last_watched_channel_id not in (select channel_id from FavoriteChannels f where f.fav_list_id=List.list_id)"; cmd.ExecuteNonQuery(); trans.Commit(); diff --git a/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini b/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini index cca23947..21181d18 100644 --- a/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini +++ b/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini @@ -88,3 +88,17 @@ lenSatName=64 offUnk1=48 offUnk2=68 offPolarity=72 + +[Map45] +incrementFavListVersion=true + +[Map100] +setFavoriteNumber=true + +[Map105] +incrementFavListVersion=true +setFavoriteNumber=false + +[Map110] +incrementFavListVersion=true +setFavoriteNumber=false diff --git a/source/ChanSort.Loader.Philips/XmlSerializer.cs b/source/ChanSort.Loader.Philips/XmlSerializer.cs index fcb18c17..0317c9b6 100644 --- a/source/ChanSort.Loader.Philips/XmlSerializer.cs +++ b/source/ChanSort.Loader.Philips/XmlSerializer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Xml; using System.Xml.Schema; @@ -59,6 +60,7 @@ class XmlSerializer : SerializerBase private readonly List fileDataList = new List(); private ChanLstBin chanLstBin; private readonly StringBuilder logMessages = new StringBuilder(); + private readonly IniFile ini; #region ctor() @@ -103,6 +105,9 @@ public XmlSerializer(string inputFile) : base(inputFile) this.favChannels.IsMixedSourceFavoritesList = true; + + string iniFile = Assembly.GetExecutingAssembly().Location.Replace(".dll", ".ini"); + this.ini = new IniFile(iniFile); } #endregion @@ -490,42 +495,23 @@ public override void Save(string tvOutputFile) } foreach (var file in this.fileDataList) - this.SaveFile(file); - this.chanLstBin?.Save(this.FileName); - } - - #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 = new UTF8Encoding(false); - xmlSettings.CheckCharacters = false; - xmlSettings.Indent = true; - xmlSettings.IndentChars = file.indent; - 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(); + if (Path.GetFileName(file.path).ToLowerInvariant().StartsWith("dvb")) + this.ReorderNodes(file); + this.SaveFile(file); } - var enc = new UTF8Encoding(false, false); - File.WriteAllText(file.path, xml, enc); + this.chanLstBin?.Save(this.FileName); } + #endregion #region UpdateChannelList() private void UpdateChannelList(ChannelList list) { + var sec = ini.GetSection("Map" + (this.chanLstBin?.VersionMajor ?? 0)); + var setFavoriteNumber = sec?.GetBool("setFavoriteNumber", false) ?? false; + foreach (var channel in list.Channels) { var ch = channel as Channel; @@ -539,7 +525,7 @@ private void UpdateChannelList(ChannelList list) } if (ch.Format == 1) - this.UpdateChannelFormat1(ch); + this.UpdateChannelFormat1(ch, setFavoriteNumber); else if (ch.Format == 2) this.UpdateChannelFormat2(ch); } @@ -547,22 +533,28 @@ private void UpdateChannelList(ChannelList list) #endregion #region UpdateChannelFormat1 and 2 - private void UpdateChannelFormat1(Channel ch) + private void UpdateChannelFormat1(Channel ch, bool setFavoriteNumber) { ch.SetupNode.Attributes["ChannelNumber"].Value = ch.NewProgramNr.ToString(); - var attr = ch.SetupNode.Attributes["UserReorderChannel"]; // introduced with format 110 - if (attr != null) - attr.InnerText = "1"; if (ch.IsNameModified) { ch.SetupNode.Attributes["ChannelName"].InnerText = EncodeName(ch.Name, (ch.SetupNode.Attributes["ChannelName"].InnerText.Length + 1) / 5, true); - attr = ch.SetupNode.Attributes["UserModifiedName"]; + var attr = ch.SetupNode.Attributes["UserModifiedName"]; if (attr != null) attr.InnerText = "1"; } - ch.SetupNode.Attributes["FavoriteNumber"].Value = Math.Max(ch.FavIndex[0], 0).ToString(); + // ChannelMap_100 supports a single fav list and stores the favorite number directly here in the channel. + // ChannelMap_105 and later always store the value 0 in the channel and instead use a separate Favorites.xml file. + ch.SetupNode.Attributes["FavoriteNumber"].Value = setFavoriteNumber ? Math.Max(ch.FavIndex[0], 0).ToString() : "0"; + + if (ch.OldProgramNr != ch.NewProgramNr) + { + var attr = ch.SetupNode.Attributes["UserReorderChannel"]; // introduced with format 110, but not always present + if (attr != null) + attr.InnerText = "1"; + } } private void UpdateChannelFormat2(Channel ch) @@ -588,10 +580,14 @@ private void UpdateFavList() var attr = favListNode.Attributes?["Name"]; if (attr != null) attr.InnerText = EncodeName(this.DataRoot.GetFavListCaption(index - 1), (attr.InnerText.Length + 1)/5, false); - - attr = favListNode.Attributes?["Version"]; - if (attr != null && int.TryParse(attr.Value, out var version)) - attr.InnerText = (version + 1).ToString(); + + // increment fav list version, unless disabled in .ini file + if (chanLstBin != null && (ini.GetSection("Map" + chanLstBin.VersionMajor)?.GetBool("incrementFavListVersion", true) ?? true)) + { + attr = favListNode.Attributes?["Version"]; + if (attr != null && int.TryParse(attr.Value, out var version)) + attr.InnerText = (version + 1).ToString(); + } foreach (var ch in favChannels.Channels.OrderBy(ch => ch.GetPosition(index))) { @@ -628,6 +624,61 @@ private string EncodeName(string name, int numBytes, bool upperCaseHexDigits) } #endregion + #region ReorderNodes + private void ReorderNodes(FileData file) + { + if (file.formatVersion != 1) + return; + + var nodes = file.doc.DocumentElement.GetElementsByTagName("Channel"); + var list = new List(); + foreach(var node in nodes) + list.Add((XmlElement)node); + foreach (var node in list) + file.doc.DocumentElement.RemoveChild(node); + foreach(var node in list.OrderBy(elem => int.Parse(elem["Setup"].Attributes["ChannelNumber"].InnerText))) + file.doc.DocumentElement.AppendChild(node); + } + #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 = new UTF8Encoding(false); + xmlSettings.CheckCharacters = false; + xmlSettings.Indent = true; + xmlSettings.IndentChars = file.indent; + xmlSettings.NewLineHandling = NewLineHandling.None; + xmlSettings.NewLineChars = file.newline; + xmlSettings.OmitXmlDeclaration = true; + + string xml; + using (var sw = new StringWriter()) + { + // write unmodified XML declaration (the DVB*.xml files use a different one than the Favorite.xml file) + var i = file.textContent.IndexOf("?>"); + if (i >= 0) + sw.Write(file.textContent.Substring(0, i + 2 + file.newline.Length)); + + using (var w = new CustomXmlWriter(sw, xmlSettings, false)) + { + file.doc.WriteTo(w); + w.Flush(); + xml = sw.ToString(); + } + } + + // append trailing newline, if the original file had one + if (file.textContent.EndsWith(file.newline) && !xml.EndsWith(file.newline)) + xml += file.newline; + + var enc = new UTF8Encoding(false, false); + File.WriteAllText(file.path, xml, enc); + } + #endregion + public override string GetFileInformation() { return base.GetFileInformation() + this.logMessages.Replace("\n", "\r\n"); diff --git a/source/changelog.md b/source/changelog.md index 7a074a69..92386a94 100644 --- a/source/changelog.md +++ b/source/changelog.md @@ -1,6 +1,14 @@ ChanSort Change Log =================== +2021-02-24 +- Philips ChannelMap\_45: TV did not remember last selected favorite list when first fav list was created by ChanSort. +- Philips ChannelMap\_100 and later: "Channel" XML elements inside the DVB\*.xml files are now reordered by program nr. +- Philips ChannelMap\_105 and 110: fixed saving favorite lists (keeping FavoriteNumber="0" in DVB\*.xml and only + setting the numbers in Favorites.xml) +- m3u: keep original end-of-line characters (CRLF or LF) +- m3u: detect whether channel names are prefixed with a program number or not, and save the file in the same way. + 2021-02-17_2 - Philips ChannelMap\_105 and 110: fixed broken favorites.xml file and DVB\*.xml when channels were renamed