Files
ChanSort/source/ChanSort.Loader.Sony/Serializer.cs
Horst Beham 3139f3d9f4 Various changes and refactorings to integrate the suggested changes from pull-request https://github.com/PredatH0r/ChanSort/pull/358 to handle the distinction between IP-antenna, IP-cable, IP-sat and DVB-T, DVB-C, DVB-S for Panasonic TVs.
- SignalSource.IP is now treated as a broadcast system (distinguishing Analog/Dvb/Ip) and no longer a broadcast medium (like antenna/cable/sat).
- SignalSource.Digital was renamed to DVB
2023-06-03 10:38:11 +02:00

827 lines
32 KiB
C#

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: <FormateVer>1.1.0</FormateVer>, 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 <FormatVer>...</FormatVer> 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<SignalSource, ChannelListNodes> channeListNodes = new Dictionary<SignalSource, ChannelListNodes>();
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<Transponder>();
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("&amp;", "&");
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<int, Channel>();
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<string, string[]> SplitLines(XmlNode parent)
{
var dict = new Dictionary<string, string[]>();
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<string, string[]> svcData, int i, Dictionary<string, string> 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 </SdbXml> in the checksum
if (this.newline == "\n")
{
start = FindMarker(data, "<SdbXml>");
end = FindMarker(data, "</SdbXml>") + 9 + (isEFormat ? 1 : 0); // e-Format includes the \n at the end
}
else
{
start = dataAsText.IndexOf("<SdbXml>", StringComparison.Ordinal);
end = dataAsText.IndexOf("</SdbXml>", 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 + "</", tag + this.newline + "</");
if (isEFormat)
xml = xml.Replace(" />", "/>");
xml += this.newline;
// put new checksum in place
var newContent = Encoding.UTF8.GetBytes(xml);
var crc = this.CalcChecksum(newContent, xml);
var i1 = xml.LastIndexOf("</CheckSum>", 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<string, StringBuilder> CreateStringBuilderDict(XmlNode parentNode)
{
if (parentNode == null)
return null;
var sbDict = new Dictionary<string, StringBuilder>();
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<string, StringBuilder> sbDictService, Dictionary<string, StringBuilder> sbDictProgramme,
ref int serviceCount, ref int programmeCount, IList<ChannelInfo> channels)
{
if (this.isEFormat)
{
// keep original record order in the <Service> element so that we don't need to reorder data in <Service><dvb_info> and its
// <t_svc_replmnt_info>, <t_ca_replmnt_info>, <t_cmplt_eit_replmnt_info>, <t_hd_simulcat_info>, <t_orig_simulcat_info> 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<string, StringBuilder> sbDict,
ref int count,
IEnumerable<ChannelInfo> channels,
Predicate<ChannelInfo> accept,
Func<Channel,Dictionary<string,string>> getChannelData,
Func<Channel, string, string, string> 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("&", "&amp;") : value; // TV has the XML element double-escaped like &amp;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<string, StringBuilder> 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;
}
}