2014-05-29 19:36:29 +02:00
using System ;
2015-01-14 21:38:01 +01:00
using System.Collections.Generic ;
2014-05-29 19:36:29 +02:00
using System.IO ;
2014-05-25 16:13:15 +02:00
using System.Text ;
2014-05-29 19:36:29 +02:00
using System.Windows.Forms ;
2014-05-25 16:13:15 +02:00
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 ;
2014-05-29 19:36:29 +02:00
private readonly DvbStringDecoder dvbStringDecoder = new DvbStringDecoder ( Encoding . Default ) ;
2015-01-14 21:38:01 +01:00
private string modelName ;
private readonly Dictionary < int , string > satPositionByIndex = new Dictionary < int , string > ( ) ;
2015-09-19 23:24:31 +02:00
private bool usesBinaryDataInUtf8Envelope = false ;
2014-05-25 16:13:15 +02:00
#region ctor ( )
public GcSerializer ( string inputFile ) : base ( inputFile )
{
2015-06-13 18:37:59 +02:00
this . Features . ChannelNameEdit = ChannelNameEditMode . All ;
2015-01-14 21:38:01 +01:00
//this.Features.CanDeleteChannels = false;
2014-05-25 16:13:15 +02:00
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
2015-09-19 23:24:31 +02:00
public override string DisplayName = > "LG GlobalClone loader" ;
2014-05-25 16:13:15 +02:00
#endregion
#region Load ( )
2014-05-29 19:36:29 +02:00
2014-05-25 16:13:15 +02:00
public override void Load ( )
{
bool fail = false ;
try
{
this . doc = new XmlDocument ( ) ;
2015-09-19 23:24:31 +02:00
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 ) )
{
2014-05-29 19:36:29 +02:00
doc . Load ( reader ) ;
2015-09-19 23:24:31 +02:00
}
2014-05-25 16:13:15 +02:00
}
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 )
{
2014-05-29 19:36:29 +02:00
case "ModelInfo" :
this . ReadModelInfo ( child ) ;
break ;
2015-01-14 21:38:01 +01:00
case "SatelliteDB" :
this . ReadSatelliteDB ( child ) ;
break ;
2014-05-25 16:13:15 +02:00
case "CHANNEL" :
this . ReadChannelLists ( child ) ;
break ;
}
}
2015-09-19 23:24:31 +02:00
this . Features . ChannelNameEdit = usesBinaryDataInUtf8Envelope ? ChannelNameEditMode . Analog : ChannelNameEditMode . All ;
2014-05-25 16:13:15 +02:00
}
#endregion
2014-05-29 19:36:29 +02:00
#region ReadModelInfo ( )
private void ReadModelInfo ( XmlNode modelInfoNode )
{
2015-09-19 23:24:31 +02:00
// show warning about broken import function in early webOS firmware
2014-05-29 19:36:29 +02:00
var regex = new System . Text . RegularExpressions . Regex ( @"\d{2}([A-Z]{2})(\d{2})\d[0-9A-Z].*" ) ;
2015-09-19 23:24:31 +02:00
var series = "" ;
2014-05-29 19:36:29 +02:00
foreach ( XmlNode child in modelInfoNode . ChildNodes )
{
switch ( child . LocalName )
{
case "ModelName" :
2015-01-14 21:38:01 +01:00
this . modelName = child . InnerText ;
var match = regex . Match ( this . modelName ) ;
2014-05-29 19:36:29 +02:00
if ( match . Success )
{
2015-09-19 23:24:31 +02:00
series = match . Groups [ 1 ] . Value ;
2015-01-14 21:38:01 +01:00
if ( ( series = = "LB" | | series = = "UB" ) & & StringComparer . InvariantCulture . Compare ( match . Groups [ 2 ] . Value , "60" ) > = 0 )
2015-09-19 23:24:31 +02:00
MessageBox . Show ( Resources . GcSerializer_webOsFirmwareWarning , "LG GlobalClone" , MessageBoxButtons . OK , MessageBoxIcon . Information ) ;
2014-05-29 19:36:29 +02:00
}
break ;
}
}
2015-09-19 23:24:31 +02:00
// 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 ;
2015-10-04 17:47:38 +02:00
if ( MessageBox . Show ( txt , "LG GlobalClone" , MessageBoxButtons . YesNo , MessageBoxIcon . Information ) = = DialogResult . Yes )
2015-09-19 23:24:31 +02:00
{
foreach ( var file in binTlls )
File . Move ( file , file + "_bak" ) ;
}
}
2014-05-29 19:36:29 +02:00
}
#endregion
2015-01-14 21:38:01 +01:00
#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
2014-05-25 16:13:15 +02:00
#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 ;
2015-01-14 21:38:01 +01:00
case "DTVATV" :
// TODO: US DTV_ATSC files contain such lists
break ;
2014-05-25 16:13:15 +02:00
}
}
}
#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 ) ;
2014-05-29 19:36:29 +02:00
this . ParseChannelInfoNodes ( itemNode , ch ) ;
2014-05-25 16:13:15 +02:00
var list = this . DataRoot . GetChannelList ( ch . SignalSource ) ;
this . DataRoot . AddChannel ( list , ch ) ;
}
}
#endregion
#region ParseChannelInfoNode ( )
2014-05-29 19:36:29 +02:00
private void ParseChannelInfoNodes ( XmlNode itemNode , ChannelInfo ch , bool onlyNames = false )
2014-05-25 16:13:15 +02:00
{
2014-05-29 19:36:29 +02:00
bool hasHexName = false ;
int mapType = 0 ;
foreach ( XmlNode info in itemNode . ChildNodes )
2014-05-25 16:13:15 +02:00
{
2014-05-29 19:36:29 +02:00
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 ;
2015-01-14 21:38:01 +01:00
case "isNumUnSel" :
// ?
break ;
2014-05-29 19:36:29 +02:00
case "isDisabled" :
ch . IsDeleted = int . Parse ( info . InnerText ) ! = 0 ;
break ;
2015-01-14 21:38:01 +01:00
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 ;
2014-05-29 19:36:29 +02:00
// 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 ;
}
2014-05-25 16:13:15 +02:00
}
}
#endregion
2014-05-29 19:36:29 +02:00
#region ParseName ( )
2014-05-25 16:13:15 +02:00
private string ParseName ( string input )
{
2014-05-29 19:36:29 +02:00
var bytes = Encoding . UTF8 . GetBytes ( input ) ;
if ( bytes . Length = = 0 | | bytes [ 0 ] < 0xC0 )
return input ;
2015-09-19 23:24:31 +02:00
this . usesBinaryDataInUtf8Envelope = true ;
2014-05-29 19:36:29 +02:00
// 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
2015-09-19 23:24:31 +02:00
// 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.)
2014-05-29 19:36:29 +02:00
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 ;
}
2014-05-25 16:13:15 +02:00
}
2014-05-29 19:36:29 +02:00
#endregion
2014-05-25 16:13:15 +02:00
#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
2015-09-19 23:24:31 +02:00
var nameBytes = Encoding . UTF8 . GetBytes ( ch . Name ) ;
bool nameNeedsEncoding = nameBytes . Length ! = ch . Name . Length ;
2015-11-26 15:56:52 +01:00
string mapType = "" ;
2015-09-19 23:24:31 +02:00
2014-05-25 16:13:15 +02:00
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 ;
2015-09-19 23:24:31 +02:00
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 )
2016-08-10 22:27:52 +02:00
node . InnerText = nameNeedsEncoding ? " " : ch . Name ;
if ( node . InnerText = = "" ) // XmlTextReader removed the required space from empty channel names
node . InnerText = " " ;
2015-09-19 23:24:31 +02:00
break ;
2015-01-14 21:38:01 +01:00
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 ;
2014-05-25 16:13:15 +02:00
case "isDisabled" :
2015-01-14 21:38:01 +01:00
case "isDeleted" :
2014-05-25 16:13:15 +02:00
node . InnerText = ch . IsDeleted ? "1" : "0" ;
break ;
case "isUserSelCHNo" :
2016-08-10 22:27:52 +02:00
if ( ch . NewProgramNr ! = ch . OldProgramNr )
node . InnerText = "1" ;
2014-05-25 16:13:15 +02:00
break ;
2015-11-26 15:56:52 +01:00
case "mapType" :
mapType = node . InnerText ;
break ;
case "mapAttr" :
if ( mapType = = "1" )
node . InnerText = ( ( int ) ch . Favorites ) . ToString ( ) ;
break ;
2014-05-25 16:13:15 +02:00
}
}
}
}
2014-05-30 00:24:29 +02:00
// 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 = "" ;
2015-09-19 23:24:31 +02:00
settings . CheckCharacters = false ;
2014-05-30 00:24:29 +02:00
using ( StringWriter sw = new StringWriter ( ) )
using ( XmlWriter xw = XmlWriter . Create ( sw , settings ) )
{
doc . Save ( xw ) ;
2016-08-10 22:27:52 +02:00
xw . Flush ( ) ;
2015-09-19 23:24:31 +02:00
string xml = RestoreInvalidXmlCharacters ( sw . ToString ( ) ) ;
xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n\r\n" + xml ;
2016-08-10 22:27:52 +02:00
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" ;
2014-05-30 00:24:29 +02:00
File . WriteAllText ( tvOutputFile , xml , settings . Encoding ) ;
}
2014-05-25 16:13:15 +02:00
}
#endregion
2014-05-29 19:36:29 +02:00
#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
2015-09-19 23:24:31 +02:00
#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
2014-05-25 16:13:15 +02:00
}
}