mirror of
https://github.com/PredatH0r/ChanSort.git
synced 2026-01-13 10:52:05 +01:00
- fixed .scm format detection based on CloneInfo content when .scm file was renamed by user - upgraded DevExpress
541 lines
18 KiB
C#
541 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
using System.Xml;
|
|
using ChanSort.Api;
|
|
|
|
namespace ChanSort.Loader.GlobalClone
|
|
{
|
|
class GcSerializer : SerializerBase
|
|
{
|
|
private readonly ChannelList atvChannels = new ChannelList(SignalSource.AnalogCT | SignalSource.TvAndRadio, "Analog");
|
|
private readonly ChannelList dtvTvChannels = new ChannelList(SignalSource.DvbCT | SignalSource.Tv, "DTV");
|
|
private readonly ChannelList dtvRadioChannels = new ChannelList(SignalSource.DvbCT | SignalSource.Radio, "Radio");
|
|
private readonly ChannelList satTvChannels = new ChannelList(SignalSource.DvbS | SignalSource.Tv, "Sat-TV");
|
|
private readonly ChannelList satRadioChannels = new ChannelList(SignalSource.DvbS | SignalSource.Radio, "Sat-Radio");
|
|
private XmlDocument doc;
|
|
private readonly DvbStringDecoder dvbStringDecoder = new DvbStringDecoder(Encoding.Default);
|
|
private string modelName;
|
|
private readonly Dictionary<int, string> satPositionByIndex = new Dictionary<int, string>();
|
|
private bool usesBinaryDataInUtf8Envelope = false;
|
|
|
|
#region ctor()
|
|
public GcSerializer(string inputFile) : base(inputFile)
|
|
{
|
|
this.Features.ChannelNameEdit = ChannelNameEditMode.All;
|
|
//this.Features.CanDeleteChannels = false;
|
|
|
|
this.DataRoot.AddChannelList(this.atvChannels);
|
|
this.DataRoot.AddChannelList(this.dtvTvChannels);
|
|
this.DataRoot.AddChannelList(this.dtvRadioChannels);
|
|
this.DataRoot.AddChannelList(this.satTvChannels);
|
|
this.DataRoot.AddChannelList(this.satRadioChannels);
|
|
}
|
|
#endregion
|
|
|
|
#region DisplayName
|
|
public override string DisplayName => "LG GlobalClone loader";
|
|
|
|
#endregion
|
|
|
|
|
|
#region Load()
|
|
|
|
public override void Load()
|
|
{
|
|
bool fail = false;
|
|
try
|
|
{
|
|
this.doc = new XmlDocument();
|
|
string textContent = File.ReadAllText(this.FileName, Encoding.UTF8);
|
|
textContent = ReplaceInvalidXmlCharacters(textContent);
|
|
var settings = new XmlReaderSettings { CheckCharacters = false };
|
|
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 != "TLLDATA")
|
|
throw new FileLoadException("\"" + this.FileName + "\" is not a supported GlobalClone XML file");
|
|
|
|
foreach (XmlNode child in root.ChildNodes)
|
|
{
|
|
switch (child.LocalName)
|
|
{
|
|
case "ModelInfo":
|
|
this.ReadModelInfo(child);
|
|
break;
|
|
case "SatelliteDB":
|
|
this.ReadSatelliteDB(child);
|
|
break;
|
|
case "CHANNEL":
|
|
this.ReadChannelLists(child);
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.Features.ChannelNameEdit = usesBinaryDataInUtf8Envelope ? ChannelNameEditMode.Analog : ChannelNameEditMode.All;
|
|
}
|
|
#endregion
|
|
|
|
#region ReadModelInfo()
|
|
private void ReadModelInfo(XmlNode modelInfoNode)
|
|
{
|
|
// show warning about broken import function in early webOS firmware
|
|
var regex = new System.Text.RegularExpressions.Regex(@"\d{2}([A-Z]{2})(\d{2})\d[0-9A-Z].*");
|
|
var series = "";
|
|
foreach (XmlNode child in modelInfoNode.ChildNodes)
|
|
{
|
|
switch (child.LocalName)
|
|
{
|
|
case "ModelName":
|
|
this.modelName = child.InnerText;
|
|
var match = regex.Match(this.modelName);
|
|
if (match.Success)
|
|
{
|
|
series = match.Groups[1].Value;
|
|
if ((series == "LB" || series == "UB") && StringComparer.InvariantCulture.Compare(match.Groups[2].Value, "60") >= 0)
|
|
MessageBox.Show(Resources.GcSerializer_webOsFirmwareWarning, "LG GlobalClone", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ask whether binary TLL file should be deleted
|
|
var dir = Path.GetDirectoryName(this.FileName) ?? ".";
|
|
var binTlls = Directory.GetFiles(dir, "xx" + series + "*.tll");
|
|
if (binTlls.Length > 0)
|
|
{
|
|
var txt = Resources.GcSerializer_ReadModelInfo_ModelWarning;
|
|
if (MessageBox.Show(txt, "LG GlobalClone", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes)
|
|
{
|
|
foreach (var file in binTlls)
|
|
File.Move(file, file + "_bak");
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region ReadSatelliteDB()
|
|
private void ReadSatelliteDB(XmlNode node)
|
|
{
|
|
foreach (XmlNode child in node.ChildNodes)
|
|
{
|
|
switch (child.LocalName)
|
|
{
|
|
case "SATDBInfo":
|
|
this.ReadSatDbInfo(child);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReadSatDbInfo(XmlNode node)
|
|
{
|
|
foreach (XmlNode child in node.ChildNodes)
|
|
{
|
|
switch (child.LocalName)
|
|
{
|
|
case "SatRecordInfo":
|
|
int i = 0;
|
|
foreach (XmlNode satNode in child.ChildNodes)
|
|
this.ReadSatRecordInfo(i++, satNode);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReadSatRecordInfo(int i, XmlNode satRecordInfoNode)
|
|
{
|
|
string orbitalPos = "";
|
|
foreach (XmlNode child in satRecordInfoNode.ChildNodes)
|
|
{
|
|
switch (child.LocalName)
|
|
{
|
|
case "Angle":
|
|
orbitalPos += child.InnerText;
|
|
break;
|
|
case "AnglePrec":
|
|
orbitalPos += "." + child.InnerText;
|
|
break;
|
|
case "DirEastWest":
|
|
orbitalPos += child.InnerText == "0" ? "W" : "E";
|
|
break;
|
|
}
|
|
}
|
|
this.satPositionByIndex[i] = orbitalPos;
|
|
}
|
|
#endregion
|
|
|
|
#region ReadChannelLists()
|
|
private void ReadChannelLists(XmlNode channelNode)
|
|
{
|
|
foreach (XmlNode chanListNode in channelNode.ChildNodes)
|
|
{
|
|
switch (chanListNode.LocalName)
|
|
{
|
|
case "ATV":
|
|
this.ReadChannelList(chanListNode, true);
|
|
break;
|
|
case "DTV":
|
|
this.ReadChannelList(chanListNode, false);
|
|
break;
|
|
case "DTVATV":
|
|
// TODO: US DTV_ATSC files contain such lists
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region ReadChannelList()
|
|
private void ReadChannelList(XmlNode node, bool analog)
|
|
{
|
|
int i = -1;
|
|
foreach (XmlNode itemNode in node.ChildNodes)
|
|
{
|
|
if (itemNode.LocalName != "ITEM")
|
|
continue;
|
|
++i;
|
|
GcChannel ch = new GcChannel(analog ? SignalSource.AnalogCT | SignalSource.Tv : SignalSource.Digital, i, itemNode);
|
|
this.ParseChannelInfoNodes(itemNode, ch);
|
|
|
|
var list = this.DataRoot.GetChannelList(ch.SignalSource);
|
|
this.DataRoot.AddChannel(list, ch);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region ParseChannelInfoNode()
|
|
private void ParseChannelInfoNodes(XmlNode itemNode, ChannelInfo ch, bool onlyNames = false)
|
|
{
|
|
bool hasHexName = false;
|
|
int mapType = 0;
|
|
foreach (XmlNode info in itemNode.ChildNodes)
|
|
{
|
|
if (onlyNames && info.LocalName != "vchName" && info.LocalName != "hexVchName")
|
|
continue;
|
|
|
|
switch (info.LocalName)
|
|
{
|
|
// common to ATV and DTV
|
|
case "prNum":
|
|
ch.OldProgramNr = int.Parse(info.InnerText) & 0x3FFF;
|
|
break;
|
|
case "vchName":
|
|
// In old file format versions, this field contains binary data stuffed into UTF8 envelopes. that data is correct
|
|
// In newer file formats, this field contains plain text but fails to hold localized characters. The hexVchName field, if present, contains the correct data then.
|
|
if (!hasHexName)
|
|
ch.Name = ParseName(info.InnerText);
|
|
break;
|
|
case "sourceIndex":
|
|
var source = int.Parse(info.InnerText);
|
|
if (source == 2)
|
|
ch.SignalSource |= SignalSource.Cable;
|
|
else if (source == 7)
|
|
ch.SignalSource |= SignalSource.Sat;
|
|
else
|
|
ch.SignalSource |= SignalSource.Antenna;
|
|
break;
|
|
case "mapType":
|
|
mapType = int.Parse(info.InnerText);
|
|
break;
|
|
case "mapAttr":
|
|
if (mapType == 1)
|
|
ch.Favorites = (Favorites) int.Parse(info.InnerText);
|
|
break;
|
|
case "isBlocked":
|
|
ch.Lock = int.Parse(info.InnerText) == 1;
|
|
break;
|
|
case "isSkipped":
|
|
ch.Skip = int.Parse(info.InnerText) == 1;
|
|
break;
|
|
|
|
// ATV
|
|
case "pllData":
|
|
ch.FreqInMhz = (decimal) int.Parse(info.InnerText)/20;
|
|
break;
|
|
|
|
// DTV
|
|
case "original_network_id":
|
|
ch.OriginalNetworkId = int.Parse(info.InnerText);
|
|
break;
|
|
case "transport_id":
|
|
ch.TransportStreamId = int.Parse(info.InnerText);
|
|
break;
|
|
case "service_id":
|
|
ch.ServiceId = int.Parse(info.InnerText);
|
|
break;
|
|
case "serviceType":
|
|
ch.ServiceType = int.Parse(info.InnerText);
|
|
ch.SignalSource |= LookupData.Instance.IsRadioOrTv(ch.ServiceType);
|
|
break;
|
|
case "frequency":
|
|
ch.FreqInMhz = int.Parse(info.InnerText);
|
|
if ((ch.SignalSource & SignalSource.Sat) == 0)
|
|
ch.FreqInMhz /= 1000;
|
|
break;
|
|
case "isInvisable": // that spelling error is part of the XML
|
|
ch.Hidden = int.Parse(info.InnerText) == 1;
|
|
break;
|
|
case "isNumUnSel":
|
|
// ?
|
|
break;
|
|
case "isDisabled":
|
|
ch.IsDeleted = int.Parse(info.InnerText) != 0;
|
|
break;
|
|
case "usSatelliteHandle":
|
|
int satIndex = int.Parse(info.InnerText);
|
|
string satPos = this.satPositionByIndex.TryGet(satIndex);
|
|
ch.SatPosition = satPos ?? satIndex.ToString(); // fallback to ensure unique UIDs
|
|
ch.Satellite = satPos;
|
|
break;
|
|
|
|
// not present in all XML files. if present, the <vchName> might be empty or corrupted
|
|
case "hexVchName":
|
|
var bytes = Tools.HexDecode(info.InnerText);
|
|
string longName, shortName;
|
|
dvbStringDecoder.GetChannelNames(bytes, 0, bytes.Length, out longName, out shortName);
|
|
ch.Name = longName;
|
|
ch.ShortName = shortName;
|
|
hasHexName = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region ParseName()
|
|
private string ParseName(string input)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(input);
|
|
if (bytes.Length == 0 || bytes[0] < 0xC0)
|
|
return input;
|
|
|
|
this.usesBinaryDataInUtf8Envelope = true;
|
|
|
|
// older GlobalClone files look like as if the <vchName> is Chinese, but it's a weired "binary inside UTF8 envelope" encoding:
|
|
// A 3 byte UTF-8 envelope is used to encode 2 input bytes: 1110aaaa 10bbbbcc 10ccdddd represents the 16bit little endian integer aaaabbbbccccdddd, which represents bytes ccccdddd, aaaabbbb
|
|
// If a remaining byte is >= 0x80, it is encoded in a 2 byte UTF-8 envelope: 110000aa 10aabbbb represents the byte aaaabbbb
|
|
// If a remaining byte is < 0x80, it is encoded directly into a 1 byte UTF-8 char. (This can cause invalid XML files for values < 0x20.)
|
|
using (MemoryStream ms = new MemoryStream(40))
|
|
{
|
|
for (int i = 0, c = bytes.Length; i < c; i++)
|
|
{
|
|
int b0 = bytes[i + 0];
|
|
if (b0 >= 0xE0) // 3-byte UTF envelope for 2 input bytes
|
|
{
|
|
int b1 = bytes[i + 1];
|
|
int b2 = bytes[i + 2];
|
|
int ch1 = ((b1 & 0x03) << 6) | (b2 & 0x3F);
|
|
int ch2 = ((b0 & 0x0F) << 4) | ((b1 & 0x3C) >> 2);
|
|
ms.WriteByte((byte) ch1);
|
|
ms.WriteByte((byte) ch2);
|
|
i += 2;
|
|
}
|
|
else if (b0 >= 0xC0) // 2-byte UTF envelope for 1 input byte >= 0x80
|
|
{
|
|
int b1 = bytes[i + 1];
|
|
int ch = ((b0 & 0x03) << 6) | (b1 & 0x3F);
|
|
ms.WriteByte((byte)ch);
|
|
i++;
|
|
}
|
|
else if (b0 < 0x80) // 1-byte UTF envelope for 1 input byte < 0x80
|
|
ms.WriteByte(bytes[i]);
|
|
}
|
|
|
|
string longName, shortName;
|
|
this.dvbStringDecoder.GetChannelNames(ms.GetBuffer(), 0, (int)ms.Length, out longName, out shortName);
|
|
return longName;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Save()
|
|
public override void Save(string tvOutputFile)
|
|
{
|
|
foreach (var list in this.DataRoot.ChannelLists)
|
|
{
|
|
foreach (var channel in list.Channels)
|
|
{
|
|
var ch = channel as GcChannel;
|
|
if (ch == null) continue; // ignore proxy channels from reference lists
|
|
var nameBytes = Encoding.UTF8.GetBytes(ch.Name);
|
|
bool nameNeedsEncoding = nameBytes.Length != ch.Name.Length;
|
|
string mapType = "";
|
|
|
|
foreach (XmlNode node in ch.XmlNode.ChildNodes)
|
|
{
|
|
switch (node.LocalName)
|
|
{
|
|
case "prNum":
|
|
var nr = ch.NewProgramNr;
|
|
if ((ch.SignalSource & SignalSource.Radio) != 0)
|
|
nr |= 0x4000;
|
|
node.InnerText = nr.ToString();
|
|
break;
|
|
case "hexVchName":
|
|
if (channel.IsNameModified)
|
|
node.InnerText = (nameNeedsEncoding ? "15" : "") + Tools.HexEncode(nameBytes); // 0x15 = DVB encoding indicator for UTF-8
|
|
break;
|
|
case "notConvertedLengthOfVchName":
|
|
if (channel.IsNameModified)
|
|
node.InnerText = ((nameNeedsEncoding ? 1 : 0) + ch.Name.Length).ToString();
|
|
break;
|
|
case "vchName":
|
|
if (channel.IsNameModified)
|
|
node.InnerText = nameNeedsEncoding ? " " : ch.Name;
|
|
if (node.InnerText == "") // XmlTextReader removed the required space from empty channel names
|
|
node.InnerText = " ";
|
|
break;
|
|
case "isInvisable":
|
|
node.InnerText = ch.Hidden ? "1" : "0";
|
|
break;
|
|
case "isBlocked":
|
|
node.InnerText = ch.Lock ? "1" : "0";
|
|
break;
|
|
case "isSkipped":
|
|
node.InnerText = ch.Skip ? "1" : "0";
|
|
break;
|
|
case "isNumUnSel":
|
|
// ?
|
|
break;
|
|
case "isDisabled":
|
|
case "isDeleted":
|
|
node.InnerText = ch.IsDeleted ? "1" : "0";
|
|
break;
|
|
case "isUserSelCHNo":
|
|
if (ch.NewProgramNr != ch.OldProgramNr)
|
|
node.InnerText = "1";
|
|
break;
|
|
case "mapType":
|
|
mapType = node.InnerText;
|
|
break;
|
|
case "mapAttr":
|
|
if (mapType == "1")
|
|
node.InnerText = ((int) ch.Favorites).ToString();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// by default .NET reformats the whole XML. These settings produce the same format as the TV xml files use
|
|
var settings = new XmlWriterSettings();
|
|
settings.Encoding = new UTF8Encoding(false);
|
|
settings.Indent = true;
|
|
settings.NewLineChars = "\r\n";
|
|
settings.NewLineHandling = NewLineHandling.Replace;
|
|
settings.OmitXmlDeclaration = true;
|
|
settings.IndentChars = "";
|
|
settings.CheckCharacters = false;
|
|
using (StringWriter sw = new StringWriter())
|
|
using (XmlWriter xw = XmlWriter.Create(sw, settings))
|
|
{
|
|
doc.Save(xw);
|
|
xw.Flush();
|
|
string xml = RestoreInvalidXmlCharacters(sw.ToString());
|
|
xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\r\n" + xml;
|
|
xml = xml.Replace("<ATV></ATV>\r\n", "<ATV>\r\n</ATV>\r\n");
|
|
xml = xml.Replace("<DTV></DTV>\r\n", "<DTV>\r\n</DTV>\r\n");
|
|
if (!xml.EndsWith("\r\n"))
|
|
xml += "\r\n";
|
|
File.WriteAllText(tvOutputFile, xml, settings.Encoding);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region DefaultEncoding
|
|
public override Encoding DefaultEncoding
|
|
{
|
|
get { return base.DefaultEncoding; }
|
|
set
|
|
{
|
|
if (ReferenceEquals(value, this.DefaultEncoding))
|
|
return;
|
|
base.DefaultEncoding = value;
|
|
this.dvbStringDecoder.DefaultEncoding = value;
|
|
this.ChangeEncoding();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region ChangeEncoding()
|
|
private void ChangeEncoding()
|
|
{
|
|
foreach (var list in this.DataRoot.ChannelLists)
|
|
{
|
|
foreach (var channel in list.Channels)
|
|
{
|
|
var gcChannel = channel as GcChannel;
|
|
if (gcChannel != null)
|
|
this.ParseChannelInfoNodes(gcChannel.XmlNode, channel, true);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
#region ReplaceInvalidXmlCharacters()
|
|
private string ReplaceInvalidXmlCharacters(string input)
|
|
{
|
|
StringBuilder output = new StringBuilder();
|
|
foreach (var c in input)
|
|
{
|
|
if (c >= ' ' || c == '\r' || c == '\n' || c == '\t')
|
|
output.Append(c);
|
|
else
|
|
output.AppendFormat("&#x{0:d}{1:d};", c >> 4, c & 0x0F);
|
|
}
|
|
return output.ToString();
|
|
}
|
|
#endregion
|
|
|
|
#region RestoreInvalidXmlCharacters()
|
|
private string RestoreInvalidXmlCharacters(string input)
|
|
{
|
|
StringBuilder output = new StringBuilder();
|
|
int prevIdx = 0;
|
|
while(true)
|
|
{
|
|
int nextIdx = input.IndexOf("&#", prevIdx);
|
|
if (nextIdx < 0)
|
|
break;
|
|
output.Append(input, prevIdx, nextIdx - prevIdx);
|
|
|
|
int numBase = 10;
|
|
char inChar;
|
|
int outChar = 0;
|
|
for (nextIdx += 2; (inChar=input[nextIdx]) != ';'; nextIdx++)
|
|
{
|
|
if (inChar == 'x' || inChar == 'X')
|
|
numBase = 16;
|
|
else
|
|
outChar = outChar*numBase + HexNibble(inChar);
|
|
}
|
|
var binChar = (char)outChar;
|
|
output.Append(binChar);
|
|
prevIdx = nextIdx + 1;
|
|
}
|
|
output.Append(input, prevIdx, input.Length - prevIdx);
|
|
return output.ToString();
|
|
}
|
|
|
|
private int HexNibble(char hexDigit)
|
|
{
|
|
return hexDigit >= '0' && hexDigit <= '9' ? hexDigit - '0' : (Char.ToUpper(hexDigit) - 'A') + 10;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|