using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using ChanSort.Api;
namespace ChanSort.Loader.Philips
{
/*
This loader supports 2 different kinds of XML files from Philips, the first in a "Repair" folder, the others in a "ChannelMap_100" (or later) folder
Example from Repair\CM_TPM1013E_LA_CK.xml:
Example from a ChannelMap_100\ChannelList\channellib\DVBC.xml:
Example from a ChannelMap_105\ChannelList\s2channellib\DVBS.xml:
Example from a ChannelMap_110\ChannelList\channellib\DVBC.xml:
DVB-T and DVB-C share the same number range, so they are treated as a unified logical list
*/
class XmlSerializer : SerializerBase
{
private readonly ChannelList analogChannels = new ChannelList(SignalSource.AnalogCT, "Analog C/T");
private readonly ChannelList dvbtChannels = new ChannelList(SignalSource.DvbT, "DVB-T");
private readonly ChannelList dvbcChannels = new ChannelList(SignalSource.DvbC, "DVB-C");
private readonly ChannelList satChannels = new ChannelList(SignalSource.DvbS, "DVB-S");
private readonly ChannelList allSatChannels = new ChannelList(SignalSource.DvbS, "DVB-S all");
private readonly ChannelList favChannels = new ChannelList(SignalSource.All, "Favorites");
private readonly List fileDataList = new List();
private ChanLstBin chanLstBin;
private readonly StringBuilder logMessages = new StringBuilder();
private readonly IniFile ini;
#region ctor()
public XmlSerializer(string inputFile) : base(inputFile)
{
this.Features.ChannelNameEdit = ChannelNameEditMode.All;
this.Features.CanSkipChannels = false;
this.Features.CanLockChannels = true;
this.Features.CanHideChannels = true;
this.Features.DeleteMode = DeleteMode.Physically;
this.Features.CanSaveAs = false;
this.Features.AllowGapsInFavNumbers = false;
this.Features.CanEditFavListNames = true;
this.DataRoot.AddChannelList(this.analogChannels);
this.DataRoot.AddChannelList(this.dvbtChannels);
this.DataRoot.AddChannelList(this.dvbcChannels);
this.DataRoot.AddChannelList(this.satChannels);
this.DataRoot.AddChannelList(this.allSatChannels);
this.DataRoot.AddChannelList(this.favChannels);
this.dvbtChannels.VisibleColumnFieldNames.Add("Source");
this.dvbcChannels.VisibleColumnFieldNames.Add("Source");
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");
}
this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.OriginalNetworkId));
this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.TransportStreamId));
this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.ServiceId));
this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.SymbolRate));
this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.ChannelOrTransponder));
this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.NetworkName));
this.analogChannels.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.NetworkOperator));
this.favChannels.IsMixedSourceFavoritesList = true;
string iniFile = Assembly.GetExecutingAssembly().Location.Replace(".dll", ".ini");
this.ini = new IniFile(iniFile);
}
#endregion
#region Load()
public override void Load()
{
// read all files from a directory structure that looks like
// ./CM_TPM1013E_LA_CK.xml
// - or -
// ChannelMap_100/ChannelList/channellib/DVBC.xml
// ChannelMap_100/ChannelList/channellib/DVBT.xml
// ChannelMap_100/ChannelList/s2channellib/DVBS.xml
// ChannelMap_100/ChannelList/s2channellib/DVBSall.xml
// ChannelMap_100/ChannelList/chanLst.bin
// + optionally
// ChannelMap_100/ChannelList/channelFile.bin
// ChannelMap_100/ChannelList/Favorite.xml
// ChannelMap_100/ChannelList/satInfo.bin
var dataFiles = new[] { @"channellib\DVBC.xml", @"channellib\DVBT.xml", @"s2channellib\DVBS.xml", @"s2channellib\DVBSall.xml", @"Favorite.xml" };
// support for files in a ChannelMap_xxx directory structure
bool isChannelMapFolderStructure = false;
var dir = Path.GetDirectoryName(this.FileName);
var dirName = Path.GetFileName(dir).ToLower();
if (dirName == "channellib" || dirName == "s2channellib")
{
dir = Path.GetDirectoryName(dir);
isChannelMapFolderStructure = true;
}
var binFile = Path.Combine(dir, "chanLst.bin"); // the .bin file is used as a proxy for the whole directory structure
if (File.Exists(binFile))
{
this.FileName = binFile;
isChannelMapFolderStructure = true;
this.chanLstBin = new ChanLstBin();
this.chanLstBin.Load(this.FileName, msg => this.logMessages.AppendLine(msg));
}
else if (Path.GetExtension(this.FileName).ToLower() == ".bin")
{
// older Philips models export a visible file like Repair\CM_T911_LA_CK.BIN and an invisible (hidden+system) .xml file with the same name
var xmlPath = Path.Combine(dir, Path.GetFileNameWithoutExtension(this.FileName) + ".xml");
if (File.Exists(xmlPath))
{
try { File.SetAttributes(xmlPath, FileAttributes.Archive);}
catch { /**/ }
this.FileName = xmlPath;
}
}
if (isChannelMapFolderStructure)
{
foreach (var file in dataFiles)
{
var fullPath = Path.GetFullPath(Path.Combine(dir, file));
this.LoadFile(fullPath);
}
if (this.fileDataList.Count == 0)
throw new FileLoadException("No XML files found in folder structure");
}
else
{
// otherwise load the single file that was originally selected by the user
LoadFile(this.FileName);
}
}
#endregion
#region LoadFile()
private void LoadFile(string fileName)
{
if (!File.Exists(fileName))
return;
// skip read-only files (like hidden read-only DVBSall.xml on a Philips 24PFS5535 from 2020 (along with)
var info = new FileInfo(fileName);
if ((info.Attributes & FileAttributes.ReadOnly) != 0)
{
this.logMessages.AppendLine($"Skipping read-only file {fileName}");
return;
}
bool fail = false;
var fileData = new FileData();
try
{
fileData.path = fileName;
fileData.doc = new XmlDocument();
fileData.content = File.ReadAllBytes(fileName);
fileData.textContent = Encoding.UTF8.GetString(fileData.content);
fileData.newline = fileData.textContent.Contains("\r\n") ? "\r\n" : "\n";
// indentation can be 0, 2 or 4 spaces
var idx1 = fileData.textContent.IndexOf("");
var idx0 = fileData.textContent.LastIndexOf("\n", idx1+1);
if (idx0 >= 0 && idx1 >= 0)
fileData.indent = fileData.textContent.Substring(idx0 + 1, idx1 - idx0 - 1);
else
fileData.indent = fileData.textContent.Contains(" <") ? " " : "";
var settings = new XmlReaderSettings
{
CheckCharacters = false,
IgnoreProcessingInstructions = true,
ValidationFlags = XmlSchemaValidationFlags.None,
DtdProcessing = DtdProcessing.Ignore
};
using (var reader = XmlReader.Create(new StringReader(fileData.textContent), settings))
{
fileData.doc.Load(reader);
}
}
catch
{
fail = true;
}
var root = fileData.doc.FirstChild;
if (root is XmlDeclaration)
root = root.NextSibling;
if (fail || root == null || (root.LocalName != "ChannelMap" && root.LocalName != "FavoriteListMAP"))
throw new FileLoadException("\"" + fileName + "\" is not a supported Philips XML file");
int rowId = 0;
ChannelList curList = null;
foreach (XmlNode child in root.ChildNodes)
{
switch (child.LocalName)
{
case "Channel":
if (rowId == 0)
curList = this.DetectFormatAndFeatures(fileData, child);
if (curList != null)
this.ReadChannel(fileData, curList, child, rowId++);
break;
case "FavoriteList":
this.ReadFavList(child);
break;
}
}
this.fileDataList.Add(fileData);
}
#endregion
#region DetectFormatAndFeatures()
private ChannelList DetectFormatAndFeatures(FileData file, 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(file.path).ToLower();
var medium = bcastNode.GetAttribute("medium");
if (medium == "" && fname.Length >= 4 && fname.StartsWith("dvb"))
medium = fname;
bool hasEncrypt = false;
foreach (var list in this.DataRoot.ChannelLists)
{
list.VisibleColumnFieldNames.Remove("ServiceType");
list.VisibleColumnFieldNames.Add("ServiceTypeName");
}
if (setupNode.HasAttribute("ChannelName"))
{
file.formatVersion = 1;
this.Features.FavoritesMode = FavoritesMode.OrderedPerSource;
this.Features.MaxFavoriteLists = 1;
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"))
{
file.formatVersion = 2;
this.Features.FavoritesMode = FavoritesMode.None;
foreach (var list in this.DataRoot.ChannelLists)
{
list.VisibleColumnFieldNames.Remove("Favorites");
list.VisibleColumnFieldNames.Remove("Lock");
list.VisibleColumnFieldNames.Remove("Hidden");
list.VisibleColumnFieldNames.Remove("ServiceTypeName");
list.VisibleColumnFieldNames.Remove("Encrypted");
}
}
else
throw new FileLoadException("Unknown data format");
ChannelList chList = null;
switch (medium)
{
case "analog":
chList = this.analogChannels;
break;
case "dvbc":
chList = this.dvbcChannels;
break;
case "dvbt":
chList = this.dvbtChannels;
break;
case "dvbs":
chList = this.satChannels;
break;
case "dvbsall":
chList = this.allSatChannels;
break;
}
if (!hasEncrypt)
chList?.VisibleColumnFieldNames.Remove("Encrypted");
return chList;
}
#endregion
#region ReadChannel()
private void ReadChannel(FileData file, ChannelList curList, 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);
}
if (!data.ContainsKey("UniqueID") || !int.TryParse(data["UniqueID"], out var uniqueId)) // UniqueId only exists in ChannelMap_105 and later
uniqueId = rowId;
var chan = new Channel(curList.SignalSource & SignalSource.MaskAdInput, rowId, uniqueId, setupNode);
chan.OldProgramNr = -1;
chan.IsDeleted = false;
if (file.formatVersion == 1)
this.ParseChannelFormat1(data, chan);
else if (file.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(curList, chan);
}
#endregion
#region ParseChannelFormat1
private void ParseChannelFormat1(Dictionary data, Channel chan)
{
chan.Format = 1;
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.SetOldPosition(1, fav == 0 ? -1 : fav);
chan.OriginalNetworkId = ParseInt(data.TryGet("Onid"));
chan.TransportStreamId = ParseInt(data.TryGet("Tsid"));
chan.ServiceId = ParseInt(data.TryGet("Sid"));
chan.FreqInMhz = ParseInt(data.TryGet("Frequency")); ;
if (chan.FreqInMhz > 2000 && (chan.SignalSource & SignalSource.Sat) == 0)
chan.FreqInMhz /= 1000;
var st = ParseInt(data.TryGet("ServiceType"));
chan.ServiceTypeName = st == 1 ? "TV" : "Radio";
if (st == 1)
chan.SignalSource |= SignalSource.Tv;
else
chan.SignalSource |= SignalSource.Radio;
var decoderType = data.TryGet("DecoderType");
if (decoderType == "1")
chan.Source = "DVB-T";
else if (decoderType == "2")
chan.Source = "DVB-C";
chan.SignalSource |= LookupData.Instance.IsRadioTvOrData(chan.ServiceType);
chan.SymbolRate = ParseInt(data.TryGet("SymbolRate"));
if (chan.SymbolRate > 100000) // DVB-S stores values in kSym, DVB-C stores it in Sym, DVB-T stores 0
chan.SymbolRate /= 1000;
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"; // introduced in ChannelMap_105 format
}
#endregion
#region ParseChannelFormat2
private void ParseChannelFormat2(Dictionary data, Channel chan)
{
chan.Format = 2;
chan.OldProgramNr = ParseInt(data.TryGet("presetnumber"));
chan.Name = data.TryGet("name");
chan.RawName = chan.Name;
chan.FreqInMhz = ParseInt(data.TryGet("frequency"));
//if ((chan.SignalSource & SignalSource.Analog) != 0) // analog channels have some really strange values (e.g. 00080 - 60512) that I can't convert to a plausible freq range (48-856 MHz)
// chan.FreqInMhz /= 16;
if (chan.FreqInMhz > 1200 && (chan.SignalSource & SignalSource.Sat) == 0)
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 ReadFavList
private void ReadFavList(XmlNode node)
{
int index = ParseInt(node.Attributes["Index"].InnerText);
string name = DecodeName(node.Attributes["Name"].InnerText);
this.Features.FavoritesMode = FavoritesMode.MixedSource;
this.Features.MaxFavoriteLists = Math.Max(this.Features.MaxFavoriteLists, index);
this.favChannels.SetFavListCaption(index - 1, name);
if (this.favChannels.Count == 0)
{
foreach (var rootList in this.DataRoot.ChannelLists)
{
if (rootList.IsMixedSourceFavoritesList)
continue;
foreach (var chan in rootList.Channels)
{
favChannels.Channels.Add(chan);
for (int i=0; i ch.RecordIndex == uniqueId);
chan?.SetOldPosition(index, favNumber + 1);
}
}
}
#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 == "")
continue;
buffer.WriteByte((byte)ParseInt(part));
}
return Encoding.Unicode.GetString(buffer.GetBuffer(), 0, (int) buffer.Length).TrimEnd('\x0');
}
#endregion
#region GetDataFilePaths()
public override IEnumerable GetDataFilePaths()
{
return this.fileDataList.Select(f => f.path);
}
#endregion
#region Save()
public override void Save(string tvOutputFile)
{
// "Save As..." is not supported by this loader
foreach (var list in this.DataRoot.ChannelLists)
{
if (list.IsMixedSourceFavoritesList)
this.UpdateFavList();
else
this.UpdateChannelList(list);
}
foreach (var file in this.fileDataList)
{
if (Path.GetFileName(file.path).ToLowerInvariant().StartsWith("dvb"))
this.ReorderNodes(file);
this.SaveFile(file);
}
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;
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 (ch.Format == 1)
this.UpdateChannelFormat1(ch, setFavoriteNumber);
else if (ch.Format == 2)
this.UpdateChannelFormat2(ch);
}
}
#endregion
#region UpdateChannelFormat1 and 2
private void UpdateChannelFormat1(Channel ch, bool setFavoriteNumber)
{
ch.SetupNode.Attributes["ChannelNumber"].Value = ch.NewProgramNr.ToString();
if (ch.IsNameModified)
{
ch.SetupNode.Attributes["ChannelName"].InnerText = EncodeName(ch.Name, (ch.SetupNode.Attributes["ChannelName"].InnerText.Length + 1) / 5, true);
var attr = ch.SetupNode.Attributes["UserModifiedName"];
if (attr != null)
attr.InnerText = "1";
}
// 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.GetPosition(1), 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)
{
ch.SetupNode.Attributes["presetnumber"].Value = ch.NewProgramNr.ToString();
if (ch.IsNameModified)
ch.SetupNode.Attributes["name"].Value = ch.Name;
}
#endregion
#region UpdateFavList
private void UpdateFavList()
{
var favFile = this.fileDataList.FirstOrDefault(fd => Path.GetFileName(fd.path).ToLower() == "favorite.xml");
if (favFile == null)
return;
int index = 0;
foreach(XmlNode favListNode in favFile.doc["FavoriteListMAP"].ChildNodes)
{
++index;
favListNode.InnerXml = ""; // clear all child elements but keep the attributes of the current node
var attr = favListNode.Attributes?["Name"];
if (attr != null)
attr.InnerText = EncodeName(this.favChannels.GetFavListCaption(index - 1), (attr.InnerText.Length + 1)/5, false);
// 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)))
{
var nr = ch.GetPosition(index);
if (nr <= 0)
continue;
var uniqueIdNode = favFile.doc.CreateElement("UniqueID");
uniqueIdNode.InnerText = ch.RecordIndex.ToString();
var favNrNode = favFile.doc.CreateElement("FavNumber");
favNrNode.InnerText = (nr-1).ToString();
var channelNode = favFile.doc.CreateElement("FavoriteChannel");
channelNode.AppendChild(uniqueIdNode);
channelNode.AppendChild(favNrNode);
favListNode.AppendChild(channelNode);
}
}
}
#endregion
#region EncodeName
private string EncodeName(string name, int numBytes, bool upperCaseHexDigits)
{
var bytes = Encoding.Unicode.GetBytes(name);
var sb = new StringBuilder();
var pattern = upperCaseHexDigits ? "0x{0:X2} " : "0x{0:x2} ";
for (int i = 0; i < numBytes - 2; i++)
{
var b = i < bytes.Length ? bytes[i] : 0;
sb.AppendFormat(pattern, b);
}
sb.Append("0x00 0x00"); // always add an end-of-string
return sb.ToString();
}
#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");
}
#region class FileData
private class FileData
{
public string path;
public XmlDocument doc;
public byte[] content;
public string textContent;
public string newline;
public string indent;
public int formatVersion;
}
#endregion
}
}