using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Xml; using System.Xml.Schema; using ChanSort.Api; // ReSharper disable UnusedMember.Local namespace ChanSort.Loader.Sony { class Serializer : SerializerBase { /* * At the time of this writing, there seem to be 4 different versions of this format. * One defines an element with a typo: 1.1.0, which has different XML elements and checksum calculation than all other versions. * This format is identified as "e1.1.0" here, with the leading "e" and it is assumed to be generated by the "Android" firmware models. * * * The other formats define ... with versions 1.0.0, 1.1.0 and 1.2.0, which are otherwise identical. This format is likely used by the "KDL" models. * * NOTE: Even within the same version, there are some files using CRLF and some using LF for newlines. * * A couple anomalies that I encountered in some test files: * - for the "e" format with independent fav list numbers, the fav-flag can be inconsistent (e.g. the flag for FAV1 is set, but in the aui1_custom_data there is a 0 for that channel in fav list 1) * - encrypted flags are sometimes inconsistent (in ui4_nw_mask and t_free_ca_mode) * - "deleted" flags are inconsistent (or not fully understood)... there is one flag in the ui4_nw_mask and also a b_deleted_by_user */ private const string SupportedFormatVersions = " e1.1.0 1.0.0 1.1.0 1.2.0 "; private XmlDocument doc; private byte[] content; private string textContent; private string format; private bool isEFormat; private string newline; private readonly StringBuilder fileInfo = new(); private readonly Dictionary channeListNodes = new Dictionary(); private ChannelList mixedFavList; private readonly IniFile ini; private readonly IniFile.Section iniSection; #region enum NwMask // ui4_nw_mask for the Android "e110"-format [Flags] private enum NwMask { //Active = 0x0002, // guess based on values from Hisense Visible = 0x0008, FavMask = 0x00F0, Fav1 = 0x0010, Fav2 = 0x0020, Fav3 = 0x0040, Fav4 = 0x0080, // Skip = 0x0100, // guess based on values from Hisense NotDeletedByUserOption = 0x0200, Radio = 0x0400, Encrypted = 0x0800, Tv = 0x2000, MaskWhenDeleted = 0x0206 } // ui4_nw_option_mask for the Android "e110"-format [Flags] private enum NwOptionMask : uint { NameEdited = 1 << 3, // 8, 0x0008 - guess based on values from Hisense ChNumEdited = 1 << 10, // 1024, 0x0400 - used by Sony Channel Editor 1.2.0, SetEdit 1.21 and Hisense DeletedByUser = 1 << 13 // 8192, 0x2000 - used by Sony Channel Editor 1.2.0 and Hisense } #endregion #region ctor() public Serializer(string inputFile) : base(inputFile) { this.Features.ChannelNameEdit = ChannelNameEditMode.All; this.Features.DeleteMode = DeleteMode.FlagWithoutPrNr; // in Android/e-format, this will be changed to FlagWithPrNr this.Features.FavoritesMode = FavoritesMode.Flags; // MixedSource for Android/e-format this.Features.CanSkipChannels = false; this.Features.CanLockChannels = false; this.Features.CanHideChannels = false; // true in Android/e-format this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbT | SignalSource.Tv, "DVB-T TV")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbT | SignalSource.Radio, "DVB-T Radio")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbT | SignalSource.Data, "DVB-T Other")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbC | SignalSource.Tv, "DVB-C TV")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbC | SignalSource.Radio, "DVB-C Radio")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbC | SignalSource.Data, "DVB-C Other")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbS | SignalSource.Provider0, "DVB-S")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbS | SignalSource.Provider1, "DVB-S Preset")); this.DataRoot.AddChannelList(new ChannelList(SignalSource.DvbS | SignalSource.Provider2, "DVB-S Ci")); foreach (var list in this.DataRoot.ChannelLists) { list.VisibleColumnFieldNames.Remove("PcrPid"); list.VisibleColumnFieldNames.Remove("VideoPid"); list.VisibleColumnFieldNames.Remove("AudioPid"); list.VisibleColumnFieldNames.Remove("Lock"); list.VisibleColumnFieldNames.Remove("Skip"); list.VisibleColumnFieldNames.Remove("ShortName"); list.VisibleColumnFieldNames.Remove("Provider"); } string iniFile = Assembly.GetExecutingAssembly().Location.Replace(".dll", ".ini"); this.ini = new IniFile(iniFile); this.iniSection = ini.GetSection("sdb.xml"); } #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 != "SdbRoot") throw LoaderException.TryNext("\"" + this.FileName + "\" is not a supported Sony XML file"); foreach (XmlNode child in root.ChildNodes) { switch (child.LocalName) { case "SdbXml": this.ReadSdbXml(child); break; case "CheckSum": this.ReadChecksum(child); break; } } if (!this.isEFormat) { foreach (var list in this.DataRoot.ChannelLists) { if ((list.SignalSource & SignalSource.Sat) != 0) { list.VisibleColumnFieldNames.Remove("Hidden"); list.VisibleColumnFieldNames.Remove("Satellite"); } } } } #endregion #region ReadSdbXml() private void ReadSdbXml(XmlNode node) { this.format = ""; this.isEFormat = false; var formatNode = node["FormatVer"]; if (formatNode != null) this.format = formatNode.InnerText; else if ((formatNode = node["FormateVer"]) != null) { this.format = "e" + formatNode.InnerText; this.isEFormat = true; this.Features.DeleteMode = DeleteMode.FlagWithPrNr; this.Features.CanHideChannels = true; this.Features.FavoritesMode = FavoritesMode.MixedSource; this.mixedFavList = new ChannelList(SignalSource.All, "Favorites"); this.mixedFavList.IsMixedSourceFavoritesList = true; this.DataRoot.AddChannelList(this.mixedFavList); } if (SupportedFormatVersions.IndexOf(" " + this.format + " ", StringComparison.Ordinal) < 0) throw LoaderException.TryNext("Unsupported file format version: " + this.format); foreach(XmlNode child in node.ChildNodes) { var name = child.LocalName.ToLowerInvariant(); if (name == "sdbt") ReadSdb(child, SignalSource.DvbT, 0, "DvbT"); else if (name == "sdbc") ReadSdb(child, SignalSource.DvbC, 0x10000, "DvbC"); else if (name == "sdbgs") ReadSdb(child, SignalSource.DvbS | SignalSource.Provider0, 0x20000, "DvbS"); else if (name == "sdbps") ReadSdb(child, SignalSource.DvbS | SignalSource.Provider1, 0x30000, "DvbS"); else if (name == "sdbcis") ReadSdb(child, SignalSource.DvbS | SignalSource.Provider2, 0x40000, "DvbS"); } } #endregion #region ReadSdb() private void ReadSdb(XmlNode node, SignalSource signalSource, int idAdjustment, string dvbSystem) { if (node["Editable"]?.InnerText == "F") { foreach (var list in this.DataRoot.ChannelLists) { if ((list.SignalSource & (SignalSource.MaskBcast | SignalSource.MaskProvider)) == signalSource) list.ReadOnly = true; } } this.ReadSatellites(node, idAdjustment); this.ReadTransponder(node, idAdjustment, dvbSystem); if (this.isEFormat) this.ReadServicesE110(node, signalSource, idAdjustment); else this.ReadServices(node, signalSource, idAdjustment); } #endregion #region ReadSatellites() private void ReadSatellites(XmlNode node, int satIdAdjustment) { var satlRec = node["SATL_REC"]; if (satlRec == null) return; var data = this.SplitLines(satlRec); var ids = data["ui2_satl_rec_id"]; for (int i = 0, c = ids.Length; i < c; i++) { var sat = new Satellite(int.Parse(ids[i]) + satIdAdjustment); sat.Name = data["ac_sat_name"][i]; var pos = int.Parse(data["i2_orb_pos"][i]); sat.OrbitalPosition = Math.Abs((decimal) pos / 10) + (pos < 0 ? "W" : "E"); this.DataRoot.AddSatellite(sat); } } #endregion #region ReadTransponder() private void ReadTransponder(XmlNode node, int idAdjustment, string dvbSystem) { var mux = node["Multiplex"] ?? throw LoaderException.Fail("Missing Multiplex XML element"); var transpList = new List(); var muxData = SplitLines(mux); var muxIds = isEFormat ? muxData["MuxID"] : muxData["MuxRowId"]; var rfParmData = isEFormat ? null : SplitLines(mux["RfParam"]); var dvbsData = isEFormat ? null : SplitLines(mux["RfParam"]?[dvbSystem]); var polarity = dvbsData?.ContainsKey("Pola") ?? false ? dvbsData["Pola"] : null; for (int i = 0, c = muxIds.Length; i < c; i++) { Satellite sat = null; var transp = new Transponder(int.Parse(muxIds[i]) + idAdjustment); if (isEFormat) { var freq = muxData.ContainsKey("ui4_freq") ? muxData["ui4_freq"] : muxData["SysFreq"]; transp.FrequencyInMhz = int.Parse(freq[i]); if (muxData.ContainsKey("ui4_sym_rate")) transp.SymbolRate = int.Parse(muxData["ui4_sym_rate"][i]); if (Char.ToLowerInvariant(dvbSystem[dvbSystem.Length - 1]) == 's') // "DvbGs", "DvbPs", "DvbCis" { transp.Polarity = muxData["e_pol"][i] == "1" ? 'H' : 'V'; var satId = int.Parse(muxData["ui2_satl_rec_id"][i]) + idAdjustment; sat = DataRoot.Satellites[satId]; } else { transp.FrequencyInMhz /= 1000000; transp.SymbolRate /= 1000; } } else { transp.OriginalNetworkId = this.ParseInt(muxData["Onid"][i]); transp.TransportStreamId = this.ParseInt(muxData["Tsid"][i]); transp.FrequencyInMhz = int.Parse(rfParmData["Freq"][i]) / 1000; transp.Polarity = polarity == null ? ' ' : polarity[i] == "H_L" ? 'H' : 'V'; if (dvbsData.ContainsKey("SymbolRate")) transp.SymbolRate = int.Parse(dvbsData["SymbolRate"][i]) / 1000; } this.DataRoot.AddTransponder(sat, transp); transpList.Add(transp); } // in the "E"-Format, there is a TS_Descr element that holds ONID and TSID, but lacks any sort of key (like "ui4_tsl_rec_id" or similar) // However, it seems like the entries correlate with the entries in the Multiplex element (same number and order) if (this.isEFormat) { var tsDescr = node["TS_Descr"]; if (tsDescr == null) return; var tsData = SplitLines(tsDescr); var onids = tsData["Onid"]; var tsids = tsData["Tsid"]; if (onids.Length != muxIds.Length) return; for (int i = 0, c = onids.Length; i < c; i++) { var transp = transpList[i]; transp.OriginalNetworkId = this.ParseInt(onids[i]); transp.TransportStreamId = this.ParseInt(tsids[i]); } } } #endregion #region ReadServicesE110() private void ReadServicesE110(XmlNode node, SignalSource signalSource, int idAdjustment) { var serviceNode = node["Service"] ?? throw LoaderException.Fail("Missing Service XML element"); var svcData = SplitLines(serviceNode); var dvbData = SplitLines(serviceNode["dvb_info"]); // remember the nodes that need to be updated when saving var nodes = new ChannelListNodes(); nodes.Service = serviceNode; this.channeListNodes[signalSource] = nodes; for (int i = 0, c = svcData["ui2_svl_rec_id"].Length; i < c; i++) { var recId = int.Parse(svcData["ui2_svl_rec_id"][i]); var chan = new Channel(signalSource, i, recId); var no = ParseInt(svcData["No"][i]); // the lower 18 bits always have 0x80 set and bits 0-1 seem to encode a service type like 1=TV, 2=radio, 3=data chan.AddDebug("No.low=").AddDebug((uint)no & 0x3FFFF); chan.OldProgramNr = (int)((uint)no >> 18); chan.RecordOrder = chan.OldProgramNr; var nwMask = (NwMask)uint.Parse(svcData["ui4_nw_mask"][i]); chan.AddDebug("NW=").AddDebug((uint)nwMask); chan.AddDebug("OPT=").AddDebug(uint.Parse(svcData["ui4_nw_option_mask"][i])); chan.IsDeleted = (nwMask & NwMask.NotDeletedByUserOption) == 0; chan.IsDeleted |= svcData["b_deleted_by_user"][i] != "1"; // reverse logic: 0=deleted, 1=NOT deleted chan.Hidden = (nwMask & NwMask.Visible) == 0; chan.Encrypted = (nwMask & NwMask.Encrypted) != 0; chan.Encrypted |= dvbData["t_free_ca_mode"][i] == "1"; chan.Favorites = (Favorites) ((uint)(nwMask & NwMask.FavMask) >> 4); chan.ServiceId = int.Parse(svcData["ui2_prog_id"][i]); chan.Name = svcData["Name"][i].Replace("&", "&"); var favNumbers = svcData["aui1_custom_data"][i]?.Split(' '); if (favNumbers != null) { for (int j = 0; j < 4 && j < favNumbers.Length; j++) { if (int.TryParse(favNumbers[j], out var favNr) && favNr > 0) chan.SetOldPosition(j+1, favNr); } } var muxId = int.Parse(svcData["MuxID"][i]) + idAdjustment; var transp = this.DataRoot.Transponder[muxId]; chan.Transponder = transp; if (transp != null) { chan.FreqInMhz = transp.FrequencyInMhz; chan.SymbolRate = transp.SymbolRate; chan.OriginalNetworkId = transp.OriginalNetworkId; chan.TransportStreamId = transp.TransportStreamId; chan.Polarity = transp.Polarity; chan.Satellite = transp.Satellite?.Name; chan.SatPosition = transp.Satellite?.OrbitalPosition; if ((signalSource & SignalSource.Cable) != 0) chan.ChannelOrTransponder = LookupData.Instance.GetDvbcChannelName(chan.FreqInMhz); if ((signalSource & SignalSource.Antenna) != 0) chan.ChannelOrTransponder = LookupData.Instance.GetDvbtTransponder(chan.FreqInMhz).ToString(); } else { // this block should never be entered // only DVB-C and -T (in the E-format) contain non-0 values in these fields chan.OriginalNetworkId = this.ParseInt(dvbData["ui2_on_id"][i]); chan.TransportStreamId = this.ParseInt(dvbData["ui2_ts_id"][i]); } chan.ServiceType = int.Parse(dvbData["ui1_sdt_service_type"][i]); if ((no & 0x07) == 1) chan.SignalSource |= SignalSource.Tv; else if ((no & 0x07) == 2) chan.SignalSource |= SignalSource.Radio; else chan.SignalSource |= SignalSource.Data; CopyDataValues(serviceNode, svcData, i, chan.ServiceData); var list = this.DataRoot.GetChannelList(chan.SignalSource); chan.Source = list.ShortCaption; this.DataRoot.AddChannel(list, chan); this.mixedFavList.Channels.Add(chan); } } #endregion #region ReadServices() private void ReadServices(XmlNode node, SignalSource signalSource, int idAdjustment) { var serviceNode = node["Service"] ?? throw LoaderException.Fail("Missing Service XML element"); var svcData = SplitLines(serviceNode); var progNode = node["Programme"] ?? throw LoaderException.Fail("Missing Programme XML element"); var progData = SplitLines(progNode); // remember the nodes that need to be updated when saving var nodes = new ChannelListNodes(); nodes.Service = serviceNode; nodes.Programme = progNode; this.channeListNodes[signalSource] = nodes; var map = new Dictionary(); for (int i = 0, c = svcData["ServiceRowId"].Length; i < c; i++) { var rowId = int.Parse(svcData["ServiceRowId"][i]); var chan = new Channel(signalSource, i, rowId); map[rowId] = chan; chan.OldProgramNr = -1; chan.IsDeleted = true; chan.ServiceType = int.Parse(svcData["Type"][i]); chan.OriginalNetworkId = this.ParseInt(svcData["Onid"][i]); chan.TransportStreamId = this.ParseInt(svcData["Tsid"][i]); chan.ServiceId = this.ParseInt(svcData["Sid"][i]); chan.Name = svcData["Name"][i]; var muxId = int.Parse(svcData["MuxRowId"][i]) + idAdjustment; var transp = this.DataRoot.Transponder[muxId]; chan.Transponder = transp; if (transp != null) { chan.FreqInMhz = transp.FrequencyInMhz; chan.SymbolRate = transp.SymbolRate; chan.Polarity = transp.Polarity; if ((signalSource & SignalSource.Cable) != 0) chan.ChannelOrTransponder = LookupData.Instance.GetDvbcChannelName(chan.FreqInMhz); else if ((signalSource & SignalSource.Antenna) != 0) chan.ChannelOrTransponder = LookupData.Instance.GetDvbtTransponder(chan.FreqInMhz).ToString(); } chan.SignalSource |= LookupData.Instance.IsRadioTvOrData(chan.ServiceType); var att = this.ParseInt(svcData["Attribute"][i]); chan.Encrypted = (att & 8) != 0; CopyDataValues(serviceNode, svcData, i, chan.ServiceData); var list = this.DataRoot.GetChannelList(chan.SignalSource); this.DataRoot.AddChannel(list, chan); } for (int i = 0, c = progData["ServiceRowId"].Length; i < c; i++) { var rowId = int.Parse(progData["ServiceRowId"][i]); var chan = map.TryGet(rowId); if (chan == null) continue; chan.IsDeleted = false; chan.OldProgramNr = int.Parse(progData["No"][i]); var flag = int.Parse(progData["Flag"][i]); chan.Favorites = (Favorites)(flag & 0x0F); CopyDataValues(progNode, progData, i, chan.ProgrammeData); } } #endregion #region SplitLines() private Dictionary SplitLines(XmlNode parent) { var dict = new Dictionary(); foreach (XmlNode node in parent.ChildNodes) { if (node.Attributes?["loop"] == null) continue; var inner = node.InnerText; if (inner.Length >= 2) inner = inner.Substring(1, inner.Length - 2); // remove new-lines that follow/lead the XML tag var lines = inner.Split('\n'); dict[node.LocalName] = lines.Length == 1 && lines[0] == "" ? new string[0] : lines; } return dict; } #endregion #region CopyDataValues() private void CopyDataValues(XmlNode parentNode, Dictionary svcData, int i, Dictionary target) { // copy of data values from all child nodes into the channel. // this inverts the [field,channel] data presentation from the file to [channel,field] and is later used for saving channels foreach (XmlNode child in parentNode.ChildNodes) { var field = child.LocalName; if (svcData.ContainsKey(field)) target[field] = svcData[field][i]; } } #endregion #region ReadChecksum() private void ReadChecksum(XmlNode node) { // skip "0x" prefix ("e"-format doesn't have it) uint expectedCrc = uint.Parse(this.isEFormat ? node.InnerText : node.InnerText.Substring(2), NumberStyles.HexNumber); uint crc = CalcChecksum(this.content, this.textContent); // the official Sony editor ignores wrong checksums, writes wrong checksums and according to user feedback, the TV imports files with wrong checksums. so no error, just an info msg if (crc != expectedCrc) this.fileInfo.AppendLine($"Invalid checksum: expected 0x{expectedCrc:x8}, calculated 0x{crc:x8}. This could indicate that the file is corrupted or it was modified with the Sony channel editor."); } #endregion #region CalcChecksum() private uint CalcChecksum(byte[] data, string dataAsText) { int start; int end; // files with CRLF as line separator will calculate the checksum as if the line separator was just LF // files in the e-format include a trailing LF after in the checksum if (this.newline == "\n") { start = FindMarker(data, ""); end = FindMarker(data, "") + 9 + (isEFormat ? 1 : 0); // e-Format includes the \n at the end } else { start = dataAsText.IndexOf("", StringComparison.Ordinal); end = dataAsText.IndexOf("", StringComparison.Ordinal) + 9 + (isEFormat ? 2 : 0); // e-Format with CRLF separator includes the newline in the checksum var text = dataAsText.Substring(start, end - start); text = text.Replace("\r\n", "\n"); data = Encoding.UTF8.GetBytes(text); start = 0; end = data.Length; } return ~Crc32.Normal.CalcCrc32(data, start, end - start); } #endregion #region FindMarker() private int FindMarker(byte[] data, string marker) { var bytes = Encoding.ASCII.GetBytes(marker); var len = bytes.Length; int i = -1; for (;;) { i = Array.IndexOf(data, bytes[0], i + 1); if (i < 0) return -1; int j; for (j = 1; j < len; j++) { if (data[i + j] != bytes[j]) break; } if (j == len) return i; i += j - 1; } } #endregion #region GetFileInformation() public override string GetFileInformation() { var txt = base.GetFileInformation(); return txt + "\n\n" + this.fileInfo; } #endregion #region Save() public override void Save() { // sdbT if (this.channeListNodes.TryGetValue(SignalSource.DvbT, out var nodes)) { this.UpdateChannelListNode(nodes, this.DataRoot.GetChannelList(SignalSource.DvbT | SignalSource.Tv), this.DataRoot.GetChannelList(SignalSource.DvbT | SignalSource.Radio), this.DataRoot.GetChannelList(SignalSource.DvbT | SignalSource.Data)); } // sdbC if (this.channeListNodes.TryGetValue(SignalSource.DvbC, out nodes)) { this.UpdateChannelListNode(nodes, this.DataRoot.GetChannelList(SignalSource.DvbC | SignalSource.Tv), this.DataRoot.GetChannelList(SignalSource.DvbC | SignalSource.Radio), this.DataRoot.GetChannelList(SignalSource.DvbC | SignalSource.Data)); } // sdbGs, sdbPs, sdbCis foreach (var list in this.DataRoot.ChannelLists) { if ((list.SignalSource & SignalSource.DvbS) == SignalSource.DvbS && this.channeListNodes.TryGetValue(list.SignalSource & ~SignalSource.MaskTvRadioData, out nodes)) this.UpdateChannelListNode(nodes, 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, isEFormat)) { this.doc.WriteTo(w); w.Flush(); xml = sw.ToString(); } // elements with a 'loop="0"' attribute must contain a newline instead of <...> var emptyTagsWithNewline = new[] { "loop=\"0\">", "loop=\"0\" notation=\"DEC\">", "loop=\"0\" notation=\"HEX\">" }; foreach (var tag in emptyTagsWithNewline) xml = xml.Replace(tag + "", "/>"); xml += this.newline; // put new checksum in place var newContent = Encoding.UTF8.GetBytes(xml); var crc = this.CalcChecksum(newContent, xml); var i1 = xml.LastIndexOf("", StringComparison.Ordinal); var i0 = xml.LastIndexOf(">", i1, StringComparison.Ordinal); var hexCrc = this.isEFormat ? crc.ToString("x") : "0x" + crc.ToString("X"); xml = xml.Substring(0, i0 + 1) + hexCrc + xml.Substring(i1); var enc = new UTF8Encoding(false, false); File.WriteAllText(this.FileName, xml, enc); } #endregion #region UpdateChannelListNode() private void UpdateChannelListNode(ChannelListNodes nodes, params ChannelList[] channelLists) { int serviceCount = 0, programmeCount = 0; var sbService = this.CreateStringBuilderDict(nodes.Service); var sbProgramme = this.CreateStringBuilderDict(nodes.Programme); foreach(var list in channelLists) this.UpdateChannelList(sbService, sbProgramme, ref serviceCount, ref programmeCount, list.Channels); this.ApplyStringBuilderDictToXmlNodes(nodes.Service, sbService, serviceCount); this.ApplyStringBuilderDictToXmlNodes(nodes.Programme, sbProgramme, programmeCount); } #endregion #region CreateStringBuilderDict() private Dictionary CreateStringBuilderDict(XmlNode parentNode) { if (parentNode == null) return null; var sbDict = new Dictionary(); foreach (XmlNode node in parentNode.ChildNodes) { if (node.Attributes["loop"] != null) sbDict[node.LocalName] = new StringBuilder(this.newline); } return sbDict; } #endregion #region UpdateChannelList() private void UpdateChannelList(Dictionary sbDictService, Dictionary sbDictProgramme, ref int serviceCount, ref int programmeCount, IList channels) { if (this.isEFormat) { // keep original record order in the element so that we don't need to reorder data in and its // , , , , child nodes // (Sony Channel Editor 1.2.0 does it the same way, but that tool is questionable since it generates an invalid checksum) // however, as some sample files suggest, when the TV re-exports a modified list, it re-orders the channels by "ServiceFilter"+"No" this.AddDataToStringBuilders(sbDictService, ref serviceCount, channels.OrderBy(c => c.RecordOrder), ch => true, ch => ch.ServiceData, this.GetNewValueForServiceNode); } else { this.AddDataToStringBuilders(sbDictService, ref serviceCount, channels.OrderBy(c => c.RecordOrder), ch => true, ch => ch.ServiceData, this.GetNewValueForServiceNode); this.AddDataToStringBuilders(sbDictProgramme, ref programmeCount, channels.OrderBy(c => c.NewProgramNr), ch => !(ch.IsDeleted || ch.NewProgramNr < 0), ch => ch.ProgrammeData, this.GetNewValueForProgrammeNode); } } #endregion #region AddDataToStringBuilders() void AddDataToStringBuilders( Dictionary sbDict, ref int count, IEnumerable channels, Predicate accept, Func> getChannelData, Func getNewValue) { foreach (var channel in channels) { var ch = channel as Channel; if (ch == null) continue; // ignore proxy channels from reference lists if (!accept(ch)) continue; foreach (var field in getChannelData(ch)) { var sb = sbDict[field.Key]; var value = getNewValue(ch, field.Key, field.Value); sb.Append(value).Append(this.newline); } ++count; } } #endregion #region GetNewValueForServiceNode() private string GetNewValueForServiceNode(Channel ch, string field, string value) { if (field == "Name") return ch.IsNameModified ? ch.Name.Replace("&", "&") : value; // TV has the XML element double-escaped like &amp; if (this.isEFormat) { if (field == "b_deleted_by_user") return ch.IsDeleted ? "0" : "1"; // file seems to contain reverse logic (1 = not deleted) if (field == "No") return ((ch.NewProgramNr << 18) | (int.Parse(value) & 0x3FFFF)).ToString(); // Sony Channel Editor 1.2.0 exports 9999 as new No for all deleted channels, we use unique numbers if (field == "ui4_nw_mask") { var mask = ((uint) ch.Favorites << 4) | (ch.Hidden ? 0u : (uint) NwMask.Visible) | (uint.Parse(value) & ~(uint) (NwMask.FavMask | NwMask.Visible)); // for deleted channels in the e110 format SDBEdit 0.9 removes only 0x200 from this mask, Sony Channel Editor 1.2.0 clears 0x206 if (ch.IsDeleted) mask &= ~(uint)NwMask.MaskWhenDeleted; return mask.ToString(); } if (field == "ui4_nw_option_mask") { // SDBEdit 0.9 does not change this field at all (in the e110 format) // Sony Channel Editor 1.2.0 sets the DeletedByUser flag + ChNumEdited flag var mask = (NwOptionMask)uint.Parse(value); if (this.iniSection.GetBool("setProgNrEditedFlag", true)) mask = (mask & ~NwOptionMask.ChNumEdited) | (ch.IsNameModified ? NwOptionMask.ChNumEdited : 0); if (this.iniSection.GetBool("setProgNameEditedFlag", true)) mask = (mask & ~NwOptionMask.NameEdited) | (ch.IsNameModified ? NwOptionMask.NameEdited : 0); if (this.iniSection.GetBool("setDeletedFlagInNwOptionMask", true)) mask = mask & ~NwOptionMask.DeletedByUser | (ch.IsDeleted ? NwOptionMask.DeletedByUser : 0); return ((uint)mask).ToString(); } if (field == "aui1_custom_data") // mixed favorite list position { var vals = value.Split(' '); for (int i = 0; i < 4; i++) vals[i] = ch.GetPosition(i+1) <= 0 ? "0" : ch.GetPosition(i+1).ToString(); return string.Join(" ", vals); } } return value; } #endregion #region GetNewValueForProgrammeNode() private string GetNewValueForProgrammeNode(Channel ch, string field, string value) { if (field == "No") return ch.NewProgramNr.ToString(); if (field == "Flag") return ((int)ch.Favorites & 0x0F).ToString(); return value; } #endregion #region ApplyStringBuilderDictToXmlNodes() private void ApplyStringBuilderDictToXmlNodes(XmlNode parentNode, Dictionary sbDict, int count) { if (parentNode == null) return; foreach (XmlNode node in parentNode.ChildNodes) { if (sbDict.TryGetValue(node.LocalName, out var sb)) { node.InnerText = sb.ToString(); node.Attributes["loop"].InnerText = count.ToString(); } } } #endregion } class ChannelListNodes { public XmlNode Service; public XmlNode Programme; } }