2021-01-02 13:18:37 +01:00
using System ;
using System.Collections.Generic ;
2022-04-19 10:36:52 +02:00
using System.Globalization ;
2021-01-02 13:18:37 +01:00
using System.IO ;
using System.Linq ;
using System.Text ;
using System.Xml ;
using System.Xml.Schema ;
using ChanSort.Api ;
namespace ChanSort.Loader.Grundig
{
class Serializer : SerializerBase
{
2022-04-19 10:36:52 +02:00
private readonly ChannelList terrChannels = new ChannelList ( SignalSource . Antenna | SignalSource . TvAndData , "Antenna TV" ) ;
private readonly ChannelList cableChannels = new ChannelList ( SignalSource . Cable | SignalSource . TvAndData , "Cable TV" ) ;
private readonly ChannelList satChannels = new ChannelList ( SignalSource . Sat | SignalSource . TvAndData , "Satellite TV" ) ;
private readonly ChannelList terrChannelsRadio = new ChannelList ( SignalSource . Antenna | SignalSource . Radio , "Antenna Radio" ) ;
private readonly ChannelList cableChannelsRadio = new ChannelList ( SignalSource . Cable | SignalSource . Radio , "Cable Radio" ) ;
private readonly ChannelList satChannelsRadio = new ChannelList ( SignalSource . Sat | SignalSource . Radio , "Satellite Radio" ) ;
2021-01-02 13:18:37 +01:00
private readonly List < FileData > fileDataList = new List < FileData > ( ) ;
private readonly StringBuilder logMessages = new StringBuilder ( ) ;
#region ctor ( )
public Serializer ( string inputFile ) : base ( inputFile )
{
this . Features . ChannelNameEdit = ChannelNameEditMode . All ;
this . Features . CanSkipChannels = true ;
this . Features . CanLockChannels = true ;
this . Features . CanHideChannels = true ;
this . Features . DeleteMode = DeleteMode . Physically ;
this . Features . AllowGapsInFavNumbers = false ;
this . Features . CanEditFavListNames = false ;
2021-03-14 22:13:22 +01:00
this . Features . FavoritesMode = FavoritesMode . OrderedPerSource ;
this . Features . MaxFavoriteLists = 4 ;
2021-01-02 13:18:37 +01:00
this . DataRoot . AddChannelList ( this . terrChannels ) ;
this . DataRoot . AddChannelList ( this . cableChannels ) ;
this . DataRoot . AddChannelList ( this . satChannels ) ;
2022-04-19 10:36:52 +02:00
this . DataRoot . AddChannelList ( this . terrChannelsRadio ) ;
this . DataRoot . AddChannelList ( this . cableChannelsRadio ) ;
this . DataRoot . AddChannelList ( this . satChannelsRadio ) ;
2021-01-02 13:18:37 +01:00
foreach ( var list in this . DataRoot . ChannelLists )
{
list . VisibleColumnFieldNames . Remove ( nameof ( ChannelInfo . ServiceTypeName ) ) ;
list . VisibleColumnFieldNames . Remove ( nameof ( ChannelInfo . PcrPid ) ) ;
list . VisibleColumnFieldNames . Remove ( nameof ( ChannelInfo . VideoPid ) ) ;
list . VisibleColumnFieldNames . Remove ( nameof ( ChannelInfo . AudioPid ) ) ;
list . VisibleColumnFieldNames . Remove ( nameof ( ChannelInfo . ShortName ) ) ;
}
this . terrChannels . VisibleColumnFieldNames . Add ( nameof ( ChannelInfo . Source ) ) ;
this . cableChannels . VisibleColumnFieldNames . Add ( nameof ( ChannelInfo . Source ) ) ;
2022-04-19 10:36:52 +02:00
this . terrChannelsRadio . VisibleColumnFieldNames . Add ( nameof ( ChannelInfo . Source ) ) ;
this . cableChannelsRadio . VisibleColumnFieldNames . Add ( nameof ( ChannelInfo . Source ) ) ;
2021-01-02 13:18:37 +01:00
}
#endregion
#region Load ( )
public override void Load ( )
{
// read all files from a directory structure that looks like
// My_Channellist\dvbs_config.xml
// My_Channellist\dvbc_config.xml
// My_Channellist\dvbt_config.xml
var dataFiles = new [ ] { "dvbt_config.xml" , "dvbc_config.xml" , "dvbs_config.xml" } ;
var dir = Path . GetDirectoryName ( this . FileName ) ? ? "" ;
foreach ( var file in dataFiles )
{
var fullPath = Path . GetFullPath ( Path . Combine ( dir , file ) ) ;
this . LoadFile ( fullPath ) ;
}
if ( this . fileDataList . Count = = 0 )
2022-11-29 14:56:23 +01:00
throw LoaderException . TryNext ( "No dvb*_config.xml files found in folder structure" ) ;
2021-01-02 13:18:37 +01:00
}
#endregion
#region LoadFile ( )
private void LoadFile ( string fileName )
{
if ( ! File . Exists ( fileName ) )
return ;
bool fail = false ;
var fileData = new FileData ( ) ;
try
{
var content = File . ReadAllBytes ( fileName ) ;
fileData . path = fileName ;
fileData . hasBom = content . Length > = 3 & & content [ 0 ] = = 0xef & & content [ 1 ] = = 0xbb & & content [ 2 ] = = 0xbf ;
var textContent = Encoding . UTF8 . GetString ( content , fileData . hasBom ? 3 : 0 , content . Length - ( fileData . hasBom ? 3 : 0 ) ) ;
2022-04-19 10:36:52 +02:00
// some files contain unescaped characters like \x10, which causes XML parsing to fail
var sb = new StringBuilder ( textContent . Length ) ;
foreach ( var ch in textContent )
{
if ( ch < 32 & & ch ! = '\n' & & ch ! = '\r' & & ch ! = '\t' )
sb . Append ( $"&#x{(int)ch:x2};" ) ;
else
sb . Append ( ch ) ;
}
textContent = sb . ToString ( ) ;
2021-01-02 13:18:37 +01:00
fileData . newline = textContent . Contains ( "\r\n" ) ? "\r\n" : "\n" ;
fileData . indent = textContent . Contains ( " <" ) ;
fileData . doc = new XmlDocument ( ) ;
fileData . doc . PreserveWhitespace = true ;
var settings = new XmlReaderSettings
{
CheckCharacters = false ,
IgnoreProcessingInstructions = true ,
ValidationFlags = XmlSchemaValidationFlags . None ,
DtdProcessing = DtdProcessing . Ignore
} ;
using var reader = XmlReader . Create ( new StringReader ( textContent ) , settings ) ;
fileData . doc . Load ( reader ) ;
}
catch
{
fail = true ;
}
var root = fileData . doc . FirstChild ;
if ( root is XmlDeclaration )
root = root . NextSibling ;
while ( root . LocalName = = "#whitespace" )
root = root . NextSibling ;
if ( fail | | root = = null | | root . LocalName ! = "CONFIG" )
2022-11-29 14:56:23 +01:00
throw LoaderException . Fail ( "\"" + fileName + "\" is not a supported Grundig XML file" ) ;
2021-01-02 13:18:37 +01:00
int transponderId = 0 ;
int chanId = 0 ;
foreach ( XmlNode child in root . ChildNodes )
{
switch ( child . LocalName )
{
case "Digital" :
ReadDigitalChannels ( child , transponderId , ref chanId ) ;
break ;
case "Analog" :
ReadAnalogChannels ( child , ref chanId ) ;
break ;
}
}
this . fileDataList . Add ( fileData ) ;
}
#endregion
#region ReadAnalogChannels
private void ReadAnalogChannels ( XmlNode analog , ref int chanId )
{
SignalSource src ;
var type = analog . Attributes ? [ "type" ] ? . InnerText ;
switch ( type )
{
case "DVBT_ANALOG" :
src = SignalSource . AnalogT ;
break ;
case "DVBC_ANALOG" :
src = SignalSource . AnalogC ;
break ;
default :
logMessages . AppendFormat ( "skipped unsupported analog source type: " + type ) ;
return ;
}
foreach ( XmlNode service in analog [ "channels" ] . ChildNodes )
{
if ( service . LocalName ! = "service" )
continue ;
var c = new Channel ( src , chanId , chanId , service ) ;
c . Source = type ;
c . FreqInMhz = Int32 . Parse ( service . Attributes [ "frq" ] . InnerText ) / 20 m ;
c . Hidden = service . Attributes [ "hid" ] . InnerText = = "1" ;
ReadCommonChannelData ( c , service ) ;
var list = this . DataRoot . GetChannelList ( src ) ;
this . DataRoot . AddChannel ( list , c ) ;
}
}
#endregion
#region ReadDigitalChannels
private void ReadDigitalChannels ( XmlNode digital , int transponderId , ref int chanId )
{
SignalSource src ;
decimal freqDivider ;
var type = digital . Attributes ? [ "type" ] ? . InnerText ;
switch ( type )
{
case "DVBC" :
src = SignalSource . DvbC ;
freqDivider = 1000 ;
break ;
case "DVBT" :
src = SignalSource . DvbT ;
freqDivider = 1000 ;
break ;
case "DVBS" :
src = SignalSource . DvbS ;
freqDivider = 1 ;
break ;
default :
logMessages . AppendFormat ( "skipped unsupported digital source type: " + type ) ;
return ;
}
2021-03-13 18:11:30 +01:00
var channels = digital [ "channels" ] ;
if ( channels = = null )
return ;
foreach ( XmlNode networkNode in channels . ChildNodes )
2021-01-02 13:18:37 +01:00
{
if ( networkNode . LocalName ! = "network" )
continue ;
var provider = networkNode . Attributes [ "nwname" ] ? . InnerText ;
foreach ( XmlNode mux in networkNode . ChildNodes )
{
if ( mux . LocalName ! = "mux" )
continue ;
var t = CreateTransponder ( mux , ref transponderId , freqDivider ) ;
foreach ( XmlNode service in mux . ChildNodes )
{
if ( service . LocalName ! = "service" )
continue ;
var c = CreateChannel ( service , src , t , ref chanId ) ;
if ( c = = null )
continue ;
c . Source = type ;
c . Provider = provider ;
2022-04-19 10:36:52 +02:00
var list = this . DataRoot . GetChannelList ( c . SignalSource ) ;
2021-01-02 13:18:37 +01:00
this . DataRoot . AddChannel ( list , c ) ;
+ + chanId ;
}
}
}
}
#endregion
#region CreateChannel
private Channel CreateChannel ( XmlNode service , SignalSource src , Transponder transponder , ref int chanId )
{
var c = new Channel ( src , chanId , chanId , service ) ;
c . Transponder = transponder ;
c . Polarity = transponder . Polarity ;
c . FreqInMhz = transponder . FrequencyInMhz ;
c . SymbolRate = transponder . SymbolRate ;
c . OriginalNetworkId = transponder . OriginalNetworkId ;
c . TransportStreamId = transponder . TransportStreamId ;
ReadCommonChannelData ( c , service ) ;
c . Hidden = service . Attributes [ "vis" ] . InnerText = = "0" ;
c . ServiceId = Int32 . Parse ( service . Attributes [ "sid" ] . InnerText ) ;
c . Encrypted = service . Attributes [ "ca" ] . InnerText = = "1" ;
c . IsDeleted = service . Attributes [ "del" ] . InnerText = = "1" ;
2022-04-19 10:36:52 +02:00
var typ = service . Attributes [ "typ" ] . InnerText ;
c . SignalSource | = typ = = "1" ? SignalSource . Tv : typ = = "2" ? SignalSource . Radio : SignalSource . Data ;
2021-01-02 13:18:37 +01:00
return c ;
}
#endregion
#region ReadCommonChannelData
private void ReadCommonChannelData ( Channel c , XmlNode service )
{
c . OldProgramNr = Int32 . Parse ( service . Attributes [ "num" ] . InnerText ) ;
for ( int f = 1 ; f < = 4 ; f + + )
{
var n = Int32 . Parse ( service . Attributes [ "f" + f ] . InnerText ) ;
c . SetOldPosition ( f , n = = 0 ? - 1 : n ) ;
}
c . Lock = service . Attributes [ "lck" ] . InnerText = = "1" ;
c . Skip = service . Attributes [ "skp" ] . InnerText = = "1" ;
c . Name = service . Attributes [ "name" ] . InnerText ;
}
#endregion
#region CreateTransponder
private Transponder CreateTransponder ( XmlNode mux , ref int transponderId , decimal freqDivider )
{
var t = new Transponder ( + + transponderId ) ;
t . Polarity = mux . Attributes [ "pol" ] . InnerText = = "1" ? 'H' : 'V' ;
t . SymbolRate = Int32 . Parse ( mux . Attributes [ "sym" ] . InnerText ) ;
t . FrequencyInMhz = Int32 . Parse ( mux . Attributes [ "frq" ] . InnerText ) / freqDivider ;
t . OriginalNetworkId = Int32 . Parse ( mux . Attributes [ "onid" ] . InnerText ) ;
t . TransportStreamId = Int32 . Parse ( mux . Attributes [ "tsid" ] . InnerText ) ;
return t ;
}
#endregion
#region Save ( )
2022-11-29 22:00:16 +01:00
public override void Save ( )
2021-01-02 13:18:37 +01:00
{
foreach ( var list in this . DataRoot . ChannelLists )
this . UpdateChannelList ( list ) ;
foreach ( var file in this . fileDataList )
this . SaveFile ( file ) ;
}
#endregion
#region SaveFile ( )
private void SaveFile ( FileData file )
{
// use xmlWriterSettings and some post-processing to maintain the original white spacing as much as possible (new line characters, indentation, empty element close tag, ...),
// so that the original and modified files can be hex-compared
// From the 2 test files available so far, one only has \n after the XML processing instruction and the document end, all other white spaces are removed.
// The other file uses \r\n after all start/end tags and 2 spaces for indentation
var xmlSettings = new XmlWriterSettings ( ) ;
xmlSettings . Encoding = this . DefaultEncoding ;
xmlSettings . CheckCharacters = false ;
xmlSettings . Indent = file . indent ;
xmlSettings . IndentChars = " " ;
xmlSettings . NewLineHandling = NewLineHandling . Replace ;
xmlSettings . NewLineChars = file . newline ;
xmlSettings . OmitXmlDeclaration = false ;
using var sw = new StringWriter ( ) ;
using var w = XmlWriter . Create ( sw , xmlSettings ) ;
file . doc . WriteTo ( w ) ;
w . Flush ( ) ;
var xml = sw . ToString ( ) ;
if ( ! file . indent )
2022-04-19 10:36:52 +02:00
{
xml = xml . Replace ( " />" , "/>" ) ;
// replace escaped characters with unescaped ones (invalid XML, but that's how Grundig does it)
var sb = new StringBuilder ( xml . Length ) ;
for ( int i = 0 , c = xml . Length - 5 ; i < c ; i + + )
{
if ( xml [ i ] = = '&' & & xml [ i + 1 ] = = '#' & & xml [ i + 2 ] = = 'x' & & xml [ i + 5 ] = = ';' )
{
sb . Append ( ( char ) int . Parse ( xml . Substring ( i + 3 , 2 ) , NumberStyles . HexNumber ) ) ;
i + = 5 ;
}
else
sb . Append ( xml [ i ] ) ;
}
var trail = Math . Min ( 5 , xml . Length ) ;
sb . Append ( xml , xml . Length - trail , trail ) ;
xml = sb . ToString ( ) ;
}
2021-01-02 13:18:37 +01:00
var enc = new UTF8Encoding ( file . hasBom , false ) ;
File . WriteAllText ( file . path , xml , enc ) ;
}
#endregion
#region UpdateChannelList ( )
private void UpdateChannelList ( ChannelList list )
{
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 )
continue ;
if ( ch . NewProgramNr < 0 )
{
2023-06-03 10:38:11 +02:00
if ( ( ch . SignalSource & SignalSource . Dvb ) ! = 0 )
2021-01-02 13:18:37 +01:00
ch . IsDeleted = true ;
else
{
// analog channels can only be physically removed (no "del" attribute)
ch . Node . ParentNode . RemoveChild ( ch . Node ) ;
continue ;
}
}
this . UpdateChannel ( ch ) ;
}
}
#endregion
#region UpdateChannel
private void UpdateChannel ( Channel ch )
{
var att = ch . Node . Attributes ;
if ( ch . IsDeleted )
{
att [ "del" ] . InnerText = "1" ;
return ; // "num" stays as-is and can be a dupe
}
att [ "num" ] . InnerText = ch . NewProgramNr . ToString ( ) ;
if ( ch . IsNameModified )
att [ "name" ] . Value = ch . Name ;
for ( int i = 1 ; i < = 4 ; i + + )
2021-03-13 18:11:30 +01:00
att [ "f" + i ] . Value = Math . Max ( 0 , ch . GetPosition ( i ) ) . ToString ( ) ; // convert -1 to 0
2021-01-02 13:18:37 +01:00
att [ "skp" ] . InnerText = ch . Skip ? "1" : "0" ;
att [ "lck" ] . InnerText = ch . Lock ? "1" : "0" ;
2023-06-03 10:38:11 +02:00
if ( ( ch . SignalSource & SignalSource . Dvb ) ! = 0 )
2021-01-02 13:18:37 +01:00
att [ "vis" ] . InnerText = ch . Hidden ? "0" : "1" ;
else
att [ "hid" ] . InnerText = ch . Hidden ? "1" : "0" ;
}
#endregion
#region GetDataFilePaths ( )
public override IEnumerable < string > GetDataFilePaths ( )
{
return this . fileDataList . Select ( fd = > fd . path ) ;
}
#endregion
#region GetFileInformation ( )
public override string GetFileInformation ( )
{
return base . GetFileInformation ( ) + this . logMessages . Replace ( "\n" , "\r\n" ) ;
}
#endregion
#region class FileData
private class FileData
{
public string path ;
public bool hasBom ;
public string newline ;
public bool indent ;
public XmlDocument doc ;
}
#endregion
}
}