2020-05-03 12:12:09 +02:00
using System ;
2021-03-28 20:07:35 +02:00
using System.Globalization ;
2020-05-03 12:12:09 +02:00
using System.IO ;
using System.Text ;
using ChanSort.Api ;
2020-05-03 18:07:28 +02:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2020-05-03 12:12:09 +02:00
namespace ChanSort.Loader.GlobalClone
{
2020-12-26 16:51:33 +01:00
/ *
* LG ' s webOS 5 firmware has severe limitations ( or bugs ) regarding the import of channel lists ( at the time of this writing , end of 2020 ) .
* There have been a few reports about successful imports , but no conclusions about steps that guarantee success .
* More information can be found on https : //github.com/PredatH0r/ChanSort/discussions/207
* /
2020-05-03 12:12:09 +02:00
internal class GcJsonSerializer : SerializerBase
{
2020-05-03 18:07:28 +02:00
private readonly string content ;
2021-10-24 22:35:32 +02:00
private string xmlPrefix ;
private string xmlSuffix ;
2020-05-03 18:07:28 +02:00
private JObject doc ;
2021-10-24 22:35:32 +02:00
private readonly IniFile . Section ini ;
2020-05-03 12:12:09 +02:00
2020-05-03 18:07:28 +02:00
public GcJsonSerializer ( string filename , string content ) : base ( filename )
2020-05-03 12:12:09 +02:00
{
2020-05-03 18:07:28 +02:00
this . content = content ;
2020-12-26 16:51:33 +01:00
this . Features . DeleteMode = DeleteMode . NotSupported ; //.FlagWithoutPrNr;
2020-05-03 18:07:28 +02:00
this . Features . ChannelNameEdit = ChannelNameEditMode . All ;
2021-03-14 22:13:22 +01:00
this . Features . FavoritesMode = FavoritesMode . None ;
2020-05-03 18:07:28 +02:00
this . Features . CanHaveGaps = true ;
this . Features . CanHideChannels = true ;
this . Features . CanSkipChannels = true ;
this . Features . CanLockChannels = true ;
2021-01-02 13:18:37 +01:00
this . Features . CanEditAudioPid = false ;
2020-05-03 18:07:28 +02:00
2020-08-27 12:48:37 +02:00
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . AnalogT | SignalSource . Tv | SignalSource . Data , "Analog Antenna" ) ) ;
2020-05-11 20:47:28 +02:00
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . DvbT | SignalSource . Tv | SignalSource . Data , "DVB-T TV" ) ) ;
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . DvbT | SignalSource . Radio , "DVB-T Radio" ) ) ;
2020-08-27 12:48:37 +02:00
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . AnalogC | SignalSource . Tv | SignalSource . Data , "Analog Cable" ) ) ;
2020-05-11 20:47:28 +02:00
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . DvbC | SignalSource . Tv | SignalSource . Data , "DVB-C TV" ) ) ;
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . DvbC | SignalSource . Radio , "DVB-C Radio" ) ) ;
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . DvbS | SignalSource . Tv | SignalSource . Data , "DVB-S TV" ) ) ;
this . DataRoot . AddChannelList ( new ChannelList ( SignalSource . DvbS | SignalSource . Radio , "DVB-S Radio" ) ) ;
2021-10-24 22:35:32 +02:00
string iniFile = this . GetType ( ) . Assembly . Location . ToLowerInvariant ( ) . Replace ( ".dll" , ".ini" ) ;
this . ini = new IniFile ( iniFile ) . GetSection ( "webOS 5" , false ) ;
2020-05-03 12:12:09 +02:00
}
2021-03-28 20:07:35 +02:00
#region Load ( )
2020-05-03 12:12:09 +02:00
public override void Load ( )
{
2020-05-03 18:07:28 +02:00
var startTag = "<legacybroadcast>" ;
var endTag = "</legacybroadcast>" ;
var start = content . IndexOf ( startTag ) ;
string json = null ;
if ( start > = 0 )
{
this . xmlPrefix = content . Substring ( 0 , start + startTag . Length ) ;
var end = content . IndexOf ( endTag , start ) ;
if ( end > = 0 )
{
json = content . Substring ( start + startTag . Length , end - start - startTag . Length ) ;
2021-03-28 20:07:35 +02:00
json = UnescapeXml ( json ) ;
2020-05-03 18:07:28 +02:00
this . xmlSuffix = content . Substring ( end ) ;
}
}
if ( json = = null )
2022-11-29 14:56:23 +01:00
throw LoaderException . Fail ( $"File does not contain a {startTag}...{endTag} node" ) ;
2020-05-03 18:07:28 +02:00
this . doc = JObject . Parse ( json ) ;
LoadSatellites ( ) ;
LoadChannels ( ) ;
2020-12-26 16:51:33 +01:00
if ( View . Default = = null ) // can't show dialog while unit-testing
return ;
2021-01-23 18:55:39 +01:00
var dlg = View . Default . CreateActionBox ( ChanSort . Loader . LG . Resource . LG_BlindscanInfo ) ;
dlg . AddAction ( ChanSort . Loader . LG . Resource . LG_BlindscanInfo_OpenWebpage , 1 ) ;
dlg . AddAction ( ChanSort . Loader . LG . Resource . LG_BlindscanInfo_Continue , 2 ) ;
dlg . AddAction ( ChanSort . Loader . LG . Resource . LG_BlindscanInfo_Cancel , 0 ) ;
2020-12-26 16:51:33 +01:00
while ( true )
{
dlg . ShowDialog ( ) ;
switch ( dlg . SelectedAction )
{
case 0 :
2022-11-29 14:56:23 +01:00
throw LoaderException . Fail ( ChanSort . Loader . LG . Resource . LG_BlindscanInfo_Rejected ) ;
2020-12-26 16:51:33 +01:00
case 1 :
System . Diagnostics . Process . Start ( "https://github.com/PredatH0r/ChanSort/discussions/207" ) ;
break ;
case 2 :
return ;
}
}
2020-05-03 18:07:28 +02:00
}
2021-03-28 20:07:35 +02:00
#endregion
#region UnescapeXml ( )
private string UnescapeXml ( string json )
{
var sb = new StringBuilder ( json . Length ) ;
int i = 0 , j = 0 ;
for ( i = json . IndexOf ( '&' , j ) ; i ! = - 1 ; i = json . IndexOf ( '&' , j ) )
{
sb . Append ( json , j , i - j ) ;
j = json . IndexOf ( ';' , i ) ;
var entity = json . Substring ( i + 1 , j - i - 1 ) ;
switch ( entity )
{
case "amp" : sb . Append ( "&" ) ; break ;
case "lt" : sb . Append ( "<" ) ; break ;
case "gt" : sb . Append ( ">" ) ; break ;
default :
if ( entity . StartsWith ( "#x" ) )
sb . Append ( ( char ) int . Parse ( "0x" + entity . Substring ( 2 ) , NumberStyles . AllowHexSpecifier ) ) ;
else if ( entity . StartsWith ( "#" ) )
sb . Append ( ( char ) int . Parse ( entity . Substring ( 1 ) ) ) ;
else
sb . Append ( "&" ) . Append ( entity ) . Append ( ";" ) ;
break ;
}
+ + j ;
}
sb . Append ( json , j , json . Length - j ) ;
return sb . ToString ( ) ;
}
#endregion
#region EscapeXml ( )
private string EscapeXml ( string json )
{
var sb = new StringBuilder ( json . Length ) ;
foreach ( var c in json )
{
switch ( c )
{
case '&' : sb . Append ( "&" ) ; break ;
case '<' : sb . Append ( "<" ) ; break ;
case '>' : sb . Append ( ">" ) ; break ;
default :
if ( c < 32 & & ! char . IsWhiteSpace ( c ) )
sb . Append ( $"&#x{(int) c:x4}" ) ;
else
sb . Append ( c ) ;
break ;
}
}
return sb . ToString ( ) ;
}
#endregion
2020-05-03 18:07:28 +02:00
#region LoadSatellites ( )
private void LoadSatellites ( )
{
var satList = this . doc [ "satelliteList" ] ;
if ( satList = = null )
return ;
foreach ( var node in satList )
{
if ( ! ( bool ) node [ "tpListLoad" ] )
continue ;
var id = int . Parse ( ( string ) node [ "satelliteId" ] ) ;
var sat = new Satellite ( id ) ;
sat . Name = ( string ) node [ "satelliteName" ] ;
sat . OrbitalPosition = ( string ) node [ "satLocation" ] ;
this . DataRoot . AddSatellite ( sat ) ;
LoadTransponders ( node [ "TransponderList" ] , sat ) ;
}
}
#endregion
#region LoadTransponders ( )
private void LoadTransponders ( JToken transponderList , Satellite sat )
{
foreach ( var node in transponderList )
{
var id = ( sat . Id < < 16 ) + ( int ) node [ "channelIdx" ] ;
var tp = new Transponder ( id ) ;
tp . Satellite = sat ;
sat . Transponder . Add ( id , tp ) ;
tp . FrequencyInMhz = ( int ) node [ "frequency" ] ;
tp . Number = ( int ) node [ "channelIdx" ] ;
2021-08-31 22:13:28 +02:00
var pol = ( ( string ) node [ "polarization" ] ) . ToLowerInvariant ( ) ;
2020-05-03 18:07:28 +02:00
tp . Polarity = pol . StartsWith ( "h" ) ? 'H' : pol . StartsWith ( "V" ) ? 'V' : '\0' ;
tp . TransportStreamId = ( int ) node [ "TSID" ] ;
tp . OriginalNetworkId = ( int ) node [ "ONID" ] ;
tp . SymbolRate = ( int ) node [ "symbolRate" ] ;
}
2020-05-03 12:12:09 +02:00
}
2020-05-03 18:07:28 +02:00
#endregion
#region LoadChannels ( )
private void LoadChannels ( )
{
if ( this . doc [ "channelList" ] = = null )
2022-11-29 14:56:23 +01:00
throw LoaderException . Fail ( "JSON does not contain a channelList node" ) ;
2020-05-03 18:07:28 +02:00
var dec = new DvbStringDecoder ( this . DefaultEncoding ) ;
int i = 0 ;
foreach ( var node in this . doc [ "channelList" ] )
{
2020-12-05 21:55:43 +01:00
var ch = new GcChannel < JToken > ( 0 , i + + , node ) ;
2020-11-16 20:43:56 +01:00
var major = ( int ) node [ "majorNumber" ] ;
2020-08-27 12:48:37 +02:00
ch . Source = ( string ) node [ "sourceIndex" ] ;
2020-05-11 20:47:28 +02:00
if ( ch . Source = = "SATELLITE DIGITAL" )
ch . SignalSource | = SignalSource . DvbS ;
else if ( ch . Source = = "CABLE DIGITAL" )
ch . SignalSource | = SignalSource . DvbC ;
2020-08-27 12:48:37 +02:00
else if ( ch . Source . Contains ( "ANTENNA DIGITAL" ) )
2020-05-11 20:47:28 +02:00
ch . SignalSource | = SignalSource . DvbT ;
2020-08-27 12:48:37 +02:00
else if ( ch . Source . Contains ( "ANTENNA ANALOG" ) )
ch . SignalSource | = SignalSource . AnalogT ;
else if ( ch . Source . Contains ( "CABLE ANALOG" ) )
ch . SignalSource | = SignalSource . AnalogC ;
else
{
// TODO: add some log for skipped channels
continue ;
}
2020-05-11 20:47:28 +02:00
2020-08-27 12:48:37 +02:00
ch . IsDisabled = ( bool ) node [ "disabled" ] ;
2020-05-03 18:07:28 +02:00
ch . Skip = ( bool ) node [ "skipped" ] ;
2020-08-27 12:48:37 +02:00
ch . Lock = ( bool ) node [ "locked" ] ;
2020-05-03 18:07:28 +02:00
ch . Hidden = ( bool ) node [ "Invisible" ] ;
var nameBytes = Convert . FromBase64String ( ( string ) node [ "chNameBase64" ] ) ;
dec . GetChannelNames ( nameBytes , 0 , nameBytes . Length , out var name , out var shortName ) ;
ch . ShortName = shortName ;
2020-11-16 20:43:56 +01:00
ch . Name = name ;
var chName = ( string ) node [ "channelName" ] ;
if ( ! string . IsNullOrWhiteSpace ( chName ) ) // chNameBase64 may contain special characters without proper code page ID, so we prefer the UTF8 "channelName" if available
ch . Name = chName ;
2020-08-27 12:48:37 +02:00
ch . TransportStreamId = ( int ) node [ "TSID" ] ;
2023-06-03 10:38:11 +02:00
if ( ( ch . SignalSource & SignalSource . Dvb ) ! = 0 )
2020-08-27 12:48:37 +02:00
{
var transSystem = ( string ) node [ "transSystem" ] ;
//if (int.TryParse((string) node["satelliteId"], out var satId))
ch . Satellite = ( string ) node [ "satelliteId" ] ; //this.DataRoot.Satellites.TryGet(satId);
ch . Encrypted = ( bool ) node [ "scrambled" ] ;
ch . FreqInMhz = ( int ) node [ "frequency" ] ;
if ( ch . FreqInMhz > = 100000 & & ch . FreqInMhz < 1000000 ) // DVBS is given in MHz, DVBC/T in kHz
ch . FreqInMhz / = 1000 ;
var tpId = ( string ) node [ "tpId" ] ;
if ( tpId ! = null & & tpId . Length = = 10 )
ch . Transponder = this . DataRoot . Transponder . TryGet ( ( int . Parse ( tpId . Substring ( 0 , 4 ) ) < < 16 ) + int . Parse ( tpId . Substring ( 4 ) ) ) ; // satId + freq, e.g. 0192126041
ch . IsDeleted = ( bool ) node [ "deleted" ] ;
2021-04-11 13:17:53 +02:00
ch . PcrPid = ( int ) node [ "pcrPid" ] & 0x1FFF ;
ch . AudioPid = ( int ) node [ "audioPid" ] & 0x1FFF ;
ch . VideoPid = ( int ) node [ "videoPid" ] & 0x1FFF ;
2020-08-27 12:48:37 +02:00
ch . ServiceId = ( int ) node [ "SVCID" ] ;
if ( ch . ServiceId = = 0 )
ch . ServiceId = ( int ) node [ "programNum" ] ;
ch . ServiceType = ( int ) node [ "serviceType" ] ;
ch . OriginalNetworkId = ( int ) node [ "ONID" ] ;
ch . SignalSource | = LookupData . Instance . IsRadioTvOrData ( ch . ServiceType ) ;
}
else
{
ch . ChannelOrTransponder = ( string ) node [ "TSID" ] ;
ch . SignalSource | = SignalSource . Tv ;
}
2020-05-03 12:12:09 +02:00
2021-07-27 01:25:04 +02:00
if ( ch . IsDeleted )
ch . OldProgramNr = - 1 ;
else
2020-05-03 18:07:28 +02:00
{
2021-07-27 01:25:04 +02:00
ch . OldProgramNr = major ;
if ( ( major & 0x4000 ) ! = 0 )
{
ch . OldProgramNr & = 0x3FFF ;
ch . SignalSource | = SignalSource . Radio ;
}
2020-05-03 18:07:28 +02:00
}
2021-07-27 01:25:04 +02:00
2024-08-18 17:41:46 +02:00
ch . AddDebug ( node [ "minorNumber" ] ? . ToString ( ) ) ;
//ch.AddDebug(", userEditChNumber=" + node["userEditChNumber"]);
2020-05-11 20:47:28 +02:00
var list = this . DataRoot . GetChannelList ( ch . SignalSource ) ;
this . DataRoot . AddChannel ( list , ch ) ;
2020-05-03 18:07:28 +02:00
}
}
#endregion
2020-05-03 12:12:09 +02:00
2020-05-03 18:07:28 +02:00
// saving
2020-05-03 12:12:09 +02:00
2020-05-03 18:07:28 +02:00
#region Save ( )
2022-11-29 22:00:16 +01:00
public override void Save ( )
2020-05-03 12:12:09 +02:00
{
2020-05-03 18:07:28 +02:00
this . UpdateJsonDoc ( ) ;
2021-03-28 20:07:35 +02:00
var sb = new StringBuilder ( ) ;
sb . Append ( xmlPrefix ) ;
2020-05-03 18:07:28 +02:00
using var sw = new StringWriter ( ) ;
var jw = new JsonTextWriter ( sw ) ;
{
doc . WriteTo ( jw ) ;
}
2021-03-28 20:07:35 +02:00
var json = EscapeXml ( sw . ToString ( ) ) ;
sb . Append ( json ) ;
sb . Append ( xmlSuffix ) ;
2022-11-29 22:00:16 +01:00
File . WriteAllText ( this . FileName , sb . ToString ( ) ) ;
2020-05-03 18:07:28 +02:00
}
#endregion
#region UpdateJsonDoc ( )
private void UpdateJsonDoc ( )
{
foreach ( var list in this . DataRoot . ChannelLists )
{
2020-07-12 02:39:43 +02:00
int radioMask = ( list . SignalSource & SignalSource . Radio ) ! = 0 ? 0x4000 : 0 ;
2021-10-24 22:35:32 +02:00
var updateUserEditChNumber = ini . GetBool ( "set_userEditChNumber" ) ;
2020-05-03 18:07:28 +02:00
foreach ( var chan in list . Channels )
{
if ( ! ( chan is GcChannel < JToken > ch ) )
continue ;
var node = ch . Node ;
if ( ch . IsNameModified )
{
node [ "channelName" ] = ch . Name ;
node [ "chNameBase64" ] = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( ch . Name ) ) ;
}
2021-10-24 22:35:32 +02:00
//node["deleted"] = ch.IsDeleted; // we don't support deleting in this serializer
2020-12-05 21:55:43 +01:00
var nr = Math . Max ( ch . NewProgramNr , 0 ) | radioMask ; // radio channels have 0x4000 added to the majorNumber
2020-05-15 22:02:15 +02:00
node [ "majorNumber" ] = nr ;
2020-05-03 18:07:28 +02:00
node [ "skipped" ] = ch . Skip ;
node [ "locked" ] = ch . Lock ;
node [ "Invisible" ] = ch . Hidden ;
2021-04-11 13:17:53 +02:00
if ( this . Features . CanEditAudioPid )
node [ "audioPid" ] = ( ( int ) node [ "audioPid" ] & ~ 0x1FFF ) | ch . AudioPid ;
2020-07-12 02:39:43 +02:00
2020-12-26 16:51:33 +01:00
// the only successfully imported file was one where these flags were NOT set by ChanSort
// these flags do get set when changing numbers through the TV's menu, but then prevent further modifications, e.g. through an import
2021-10-24 22:35:32 +02:00
if ( updateUserEditChNumber & & ch . NewProgramNr ! = Math . Max ( ch . OldProgramNr , 0 ) )
{
node [ "userEditChNumber" ] = true ;
node [ "userSelCHNo" ] = true ;
}
2020-12-26 16:51:33 +01:00
//node["disableUpdate"] = true; // No-Go! This blocked the whole list and required a factory reset. Regardless of the setting, the TV showed wrong numbers.
2021-01-02 13:18:37 +01:00
2020-12-26 16:51:33 +01:00
//node["factoryDefault"] = true; // an exported file after manually changing numbers through the TV-menu had all channels set to userEditChNumber=true, userSelCHNo=true, factoryDefault=true;
2020-05-03 18:07:28 +02:00
}
}
2020-05-03 12:12:09 +02:00
}
2020-05-03 18:07:28 +02:00
#endregion
2020-05-03 12:12:09 +02:00
}
}