2015-04-17 21:59:44 +02:00
using System ;
using System.Collections.Generic ;
using System.Data ;
using System.IO ;
2019-11-08 02:31:44 +01:00
using System.Linq ;
2015-04-22 09:51:21 +02:00
using System.Text ;
2021-04-25 18:31:05 +02:00
using Microsoft.Data.Sqlite ;
2015-04-17 21:59:44 +02:00
using ChanSort.Api ;
2021-01-23 14:22:18 +01:00
namespace ChanSort.Loader.Samsung.Zip
2015-04-17 21:59:44 +02:00
{
2019-02-10 15:56:03 +01:00
/// <summary>
2022-11-30 19:01:11 +01:00
/// Loader for Samsung .zip files starting with model J in 2015 (and still valid as of 2022 for current models)
///
/// The .zip file contains various SQLite database files without file extensions.
///
/// In theory SQLite is neutral to bit-ness (32/64) and endian-ness (MSB/LSB first) and should handle strings without issues.
/// SQLite also has a dynamic type system, allowing individual row values to have a different type than the column's default.
/// All observed Samsung databases are set to encoding "UTF-16le".
///
/// Samsung somehow manages to store strings in columns/cells with data type TEXT in reversed UTF16 byte-order (as BE instead LE).
/// Reading such a TEXT column returns an object of type "string" looking Chinese due to the swapped high/low-order bytes
/// One solution is to explicitly cast the column to BLOB in the query and manually decode it as UTF16BE. (Always works)
/// Another approach is to encode the string to a byte[] and decode it again as UTF16BE. (This doesn't work for format 1242)
///
/// While it's easy to ready strings by casting them to BLOBs, there is a severe catch writing strings to the database.
/// Saving a byte[] as BLOB changes the value's data type in the database to BLOB and the TV receives byte[] instead of string - booom!
/// Saving a byte[] as TEXT leads to automatic conversion in the Sqlite library, decoding it as UTF16LE and writing it in LE byte-order - booom!
/// The hack is to pass a "Chinese"-ified string to the DB, manually swapping byte order through chained LE-encode + BE-decode
/// That does NOT work for format 1242 though.
///
/// Up until Microsoft.Data.Sqlite version 5.0.8 with SQLitePCLRaw 2.0.4 a workaround was to pass the SQL parameter as BLOB
/// with the expected byte order and cast the value to TEXT in the SQL update statement.
/// With Microsoft.Data.Sqlite version 7.0.0 and SQLitePCLRaw 2.1.2 this no longer work and the TEXT value ends up in the
/// database column as a readable UTF16-LE string instead of the expected reversed UTF16-BE.
///
/// Format "1242"
///
/// To make things even more complicated, there is file format version _1242, which stores channel names not as UTF16, but
/// instead as a raw byte sequence that encodes 16 UTF16BE bits in 3 byte UTF8 sequences, which also looks "Chinese".
/// In this format it is not possible to query the string as TEXT and then later re-encode/decode in code, because the
/// DB library already corrupts the raw data in the returned string with invalid-utf16-characters at the end (0xFD, 0xFF).
/// This format can only be read properly by casting the TEXT column to BLOB in the query.
/// There is NO WAY with Microsoft.Data.Sqlite 7.0.0 to store an arbitrary byte sequence and keep its data type TEXT.
/// Therefore changing channel names is disabled for this format and no updates are made to string values.
2019-02-10 15:56:03 +01:00
/// </summary>
2022-11-29 14:56:23 +01:00
internal class DbSerializer : SerializerBase
2015-04-17 21:59:44 +02:00
{
2022-11-30 19:01:11 +01:00
private readonly Dictionary < long , DbChannel > channelById = new ( ) ;
private readonly Dictionary < ChannelList , string > dbPathByChannelList = new ( ) ;
private readonly List < string > tableNames = new ( ) ;
2020-07-12 02:39:43 +02:00
private Encoding encoding ;
2015-04-17 21:59:44 +02:00
2020-03-20 01:25:27 +01:00
private enum FileType { Unknown , SatDb , ChannelDbDvb , ChannelDbAnalog , ChannelDbIp }
2015-04-22 09:51:21 +02:00
2015-04-17 21:59:44 +02:00
#region ctor ( )
public DbSerializer ( string inputFile ) : base ( inputFile )
{
2015-06-13 18:37:59 +02:00
this . Features . ChannelNameEdit = ChannelNameEditMode . All ;
2019-11-08 02:31:44 +01:00
this . Features . DeleteMode = DeleteMode . Physically ;
2019-11-24 20:00:48 +01:00
this . Features . CanSkipChannels = true ;
this . Features . CanLockChannels = true ;
this . Features . CanHideChannels = true ;
2021-03-14 22:13:22 +01:00
this . Features . FavoritesMode = FavoritesMode . OrderedPerSource ;
this . Features . MaxFavoriteLists = 5 ;
2020-03-20 01:25:27 +01:00
this . Features . AllowGapsInFavNumbers = true ;
2015-04-17 21:59:44 +02:00
}
#endregion
#region Load ( )
2022-11-29 18:21:52 +01:00
2015-04-17 21:59:44 +02:00
public override void Load ( )
{
2022-11-29 18:21:52 +01:00
this . UnzipFileToTempFolder ( ) ;
if ( File . Exists ( this . TempPath + "\\sat" ) )
2015-06-05 06:08:40 +02:00
{
2022-11-29 18:21:52 +01:00
try
2015-06-05 06:08:40 +02:00
{
2025-06-05 18:35:10 +02:00
using var conn = new SqliteConnection ( $"Data Source=\" { this . TempPath + "\\sat" } \ ";Pooling=False" ) ;
2022-11-29 18:21:52 +01:00
conn . Open ( ) ;
this . ReadSatDatabase ( conn ) ;
}
catch
{
// not all files in the folder are SQLite databases
2015-06-05 06:08:40 +02:00
}
2022-11-29 18:21:52 +01:00
}
2015-06-05 06:08:40 +02:00
2022-11-29 18:21:52 +01:00
var files = Directory . GetFiles ( this . TempPath , "*." ) ;
if ( files . Length = = 0 )
throw LoaderException . TryNext ( "The Samsung .zip channel list archive does not contain any supported files." ) ;
2017-06-08 20:01:42 +02:00
2022-11-29 18:21:52 +01:00
foreach ( var filePath in files )
{
var filename = Path . GetFileName ( filePath ) ? ? "" ;
2022-11-29 22:00:16 +01:00
if ( filename . StartsWith ( "vconf_" ) | | filename . EndsWith ( "-shm" ) )
2022-11-29 18:21:52 +01:00
continue ;
FileType type ;
try
2015-04-17 21:59:44 +02:00
{
2025-06-05 18:35:10 +02:00
using var conn = new SqliteConnection ( $"Data Source=\" { filePath } \ ";Pooling=False" ) ;
2022-11-29 18:21:52 +01:00
conn . Open ( ) ;
using ( var cmd = conn . CreateCommand ( ) )
2015-04-22 09:51:21 +02:00
{
2022-11-29 18:21:52 +01:00
this . RepairCorruptedDatabaseImage ( cmd ) ;
type = this . DetectFileType ( cmd ) ;
2015-04-22 09:51:21 +02:00
}
2022-11-29 18:21:52 +01:00
switch ( type )
2019-11-08 02:31:44 +01:00
{
2022-11-29 18:21:52 +01:00
case FileType . SatDb :
break ;
case FileType . ChannelDbAnalog :
case FileType . ChannelDbDvb :
case FileType . ChannelDbIp :
ReadChannelDatabase ( conn , filePath , type ) ;
break ;
2019-11-08 02:31:44 +01:00
}
2015-04-17 21:59:44 +02:00
}
2022-11-29 18:21:52 +01:00
catch
{
// ignore non-SQLite files in the folder
}
2015-04-17 21:59:44 +02:00
}
}
#endregion
#region RepairCorruptedDatabaseImage ( )
2021-04-25 18:31:05 +02:00
private void RepairCorruptedDatabaseImage ( IDbCommand cmd )
2015-04-17 21:59:44 +02:00
{
cmd . CommandText = "REINDEX" ;
cmd . ExecuteNonQuery ( ) ;
}
#endregion
2015-04-22 09:51:21 +02:00
#region DetectFileType ( )
2021-04-25 18:31:05 +02:00
private FileType DetectFileType ( IDbCommand cmd )
2015-04-22 09:51:21 +02:00
{
2020-03-14 16:54:42 +01:00
this . tableNames . Clear ( ) ;
cmd . CommandText = "select name from sqlite_master where type='table'" ;
using var r = cmd . ExecuteReader ( ) ;
while ( r . Read ( ) )
2021-08-31 22:13:28 +02:00
this . tableNames . Add ( r . GetString ( 0 ) . ToUpperInvariant ( ) ) ;
2020-03-14 16:54:42 +01:00
if ( tableNames . Contains ( "SAT" ) & & tableNames . Contains ( "SAT_TP" ) )
2015-04-22 09:51:21 +02:00
return FileType . SatDb ;
2020-03-14 16:54:42 +01:00
if ( tableNames . Contains ( "CHNL" ) & & tableNames . Contains ( "SRV" ) & & tableNames . Contains ( "SRV_DVB" ) )
2015-06-05 06:08:40 +02:00
return FileType . ChannelDbDvb ;
2020-03-14 16:54:42 +01:00
if ( tableNames . Contains ( "CHNL" ) & & tableNames . Contains ( "SRV" ) & & tableNames . Contains ( "SRV_ANL" ) )
2015-06-05 06:08:40 +02:00
return FileType . ChannelDbAnalog ;
2015-04-22 09:51:21 +02:00
2020-05-02 19:04:43 +02:00
if ( tableNames . Contains ( "CHNL" ) & & tableNames . Contains ( "SRV" ) & & tableNames . Contains ( "SRV_IP" ) )
return FileType . ChannelDbIp ;
2020-03-20 01:25:27 +01:00
2015-04-22 09:51:21 +02:00
return FileType . Unknown ;
}
#endregion
#region ReadSatDatabase ( )
2021-04-25 18:31:05 +02:00
private void ReadSatDatabase ( SqliteConnection conn )
2015-04-22 09:51:21 +02:00
{
2022-11-29 22:00:16 +01:00
using var cmd = conn . CreateCommand ( ) ;
this . RepairCorruptedDatabaseImage ( cmd ) ;
this . ReadSatellites ( cmd ) ;
this . ReadTransponders ( cmd ) ;
2015-04-22 09:51:21 +02:00
}
#endregion
2015-04-17 21:59:44 +02:00
#region ReadSatellites ( )
2021-04-25 18:31:05 +02:00
private void ReadSatellites ( IDbCommand cmd )
2015-04-17 21:59:44 +02:00
{
2015-06-05 06:08:40 +02:00
cmd . CommandText = "select distinct satId, cast(satName as blob), satPos, satDir from SAT" ;
2022-11-29 22:00:16 +01:00
using var r = cmd . ExecuteReader ( ) ;
while ( r . Read ( ) )
2015-04-17 21:59:44 +02:00
{
2022-11-29 22:00:16 +01:00
Satellite sat = new Satellite ( r . GetInt32 ( 0 ) ) ;
int pos = Math . Abs ( r . GetInt32 ( 2 ) ) ;
// 171027 - ohuseyinoglu: For user-defined satellites, the direction may be -1
// (and not just 1 for "E", 0 for "W")
int dir = r . GetInt32 ( 3 ) ;
sat . OrbitalPosition = $"{pos / 10}.{pos % 10}{(dir == 1 ? " E " : dir == 0 ? " W " : " ")}" ;
sat . Name = ReadUtf16 ( r , 1 ) ;
this . DataRoot . AddSatellite ( sat ) ;
2015-04-17 21:59:44 +02:00
}
}
#endregion
#region ReadTransponders ( )
2021-04-25 18:31:05 +02:00
private void ReadTransponders ( IDbCommand cmd )
2015-04-17 21:59:44 +02:00
{
cmd . CommandText = "select satId, tpFreq, tpPol, tpSr, tpId from SAT_TP" ;
2022-11-29 22:00:16 +01:00
using var r = cmd . ExecuteReader ( ) ;
while ( r . Read ( ) )
2015-04-17 21:59:44 +02:00
{
2022-11-29 22:00:16 +01:00
// 171027 - ohuseyinoglu: tpId is the primary key of this table, we should be able to use it as "id/dict. index"
// It will also be our lookup value for the CHNL table
int id = r . GetInt32 ( 4 ) ;
Transponder tp = new Transponder ( id ) ;
tp . FrequencyInMhz = ( decimal ) r . GetInt32 ( 1 ) / 1000 ;
tp . Number = id ;
tp . Polarity = r . GetInt32 ( 2 ) = = 0 ? 'H' : 'V' ;
tp . Satellite = this . DataRoot . Satellites . TryGet ( r . GetInt32 ( 0 ) ) ;
tp . SymbolRate = r . GetInt32 ( 3 ) ;
this . DataRoot . AddTransponder ( tp . Satellite , tp ) ;
2015-04-17 21:59:44 +02:00
}
}
#endregion
2015-04-22 09:51:21 +02:00
#region ReadChannelDatabase ( )
2021-04-25 18:31:05 +02:00
private void ReadChannelDatabase ( SqliteConnection conn , string dbPath , FileType fileType )
2015-04-17 21:59:44 +02:00
{
2015-04-22 09:51:21 +02:00
this . channelById . Clear ( ) ;
2022-11-29 22:00:16 +01:00
using var cmd = conn . CreateCommand ( ) ;
var providers = fileType = = FileType . ChannelDbDvb ? this . ReadProviders ( cmd ) : null ;
var channelList = this . ReadChannels ( cmd , dbPath , providers , fileType ) ;
this . ReadFavorites ( cmd ) ;
this . dbPathByChannelList . Add ( channelList , dbPath ) ;
2015-04-17 21:59:44 +02:00
}
#endregion
2015-04-22 09:51:21 +02:00
#region ReadProviders ( )
2021-04-25 18:31:05 +02:00
private Dictionary < long , string > ReadProviders ( IDbCommand cmd )
2015-04-17 21:59:44 +02:00
{
2015-04-22 09:51:21 +02:00
var dict = new Dictionary < long , string > ( ) ;
try
2015-04-17 21:59:44 +02:00
{
2015-04-22 09:51:21 +02:00
cmd . CommandText = "select provId, cast(provName as blob) from PROV" ;
2020-07-13 10:00:41 +02:00
var prevEncoding = this . encoding ;
this . encoding = Encoding . BigEndianUnicode ; // while Sat and Service names might be utf16 binary data inside an utf8 envelope, the providers are always plain utf16
2015-04-22 09:51:21 +02:00
using ( var r = cmd . ExecuteReader ( ) )
2015-04-17 21:59:44 +02:00
{
2015-04-22 09:51:21 +02:00
while ( r . Read ( ) )
dict . Add ( r . GetInt64 ( 0 ) , ReadUtf16 ( r , 1 ) ) ;
2015-04-17 21:59:44 +02:00
}
2020-07-13 10:00:41 +02:00
this . encoding = prevEncoding ;
2015-04-17 21:59:44 +02:00
}
2015-04-22 09:51:21 +02:00
catch
{
}
return dict ;
2015-04-17 21:59:44 +02:00
}
#endregion
2015-06-05 06:08:40 +02:00
#region ReadChannels ( )
2021-04-25 18:31:05 +02:00
private ChannelList ReadChannels ( IDbCommand cmd , string dbPath , Dictionary < long , string > providers , FileType fileType )
2015-04-17 21:59:44 +02:00
{
2020-03-20 01:25:27 +01:00
var signalSource = DetectSignalSource ( cmd , fileType ) ;
2015-04-17 21:59:44 +02:00
2015-04-22 09:51:21 +02:00
string name = Path . GetFileName ( dbPath ) ;
2020-05-02 19:04:43 +02:00
ChannelList channelList = this . CreateChannelList ( signalSource , name ) ;
2020-03-20 01:25:27 +01:00
string table = fileType = = FileType . ChannelDbDvb ? "SRV_DVB" : fileType = = FileType . ChannelDbAnalog ? "SRV_ANL" : "SRV_IP" ;
2015-06-05 06:08:40 +02:00
List < string > fieldNames = new List < string > {
"chType" , "chNum" , "freq" , // CHNL
2020-03-20 01:25:27 +01:00
"SRV.srvId" , "major" , "progNum" , "cast(srvName as blob) srvName" , "srvType" , "hidden" , "scrambled" , "lockMode" , "numSel" , "elim" // SRV
2015-06-05 06:08:40 +02:00
} ;
2020-03-20 01:25:27 +01:00
if ( fileType = = FileType . ChannelDbDvb )
fieldNames . AddRange ( new [ ] { "onid" , "tsid" , "vidPid" , "provId" , "cast(shrtSrvName as blob) shrtSrvName" , "lcn" } ) ; // SRV_DVB
2023-08-14 10:46:10 +02:00
if ( fileType = = FileType . ChannelDbIp )
fieldNames . AddRange ( new [ ] { "cast(jsonMeta as blob) jsonMeta" } ) ;
2015-11-27 01:51:08 +01:00
2015-06-05 06:08:40 +02:00
var sql = this . BuildQuery ( table , fieldNames ) ;
2015-04-17 21:59:44 +02:00
var fields = this . GetFieldMap ( fieldNames ) ;
cmd . CommandText = sql ;
using ( var r = cmd . ExecuteReader ( ) )
{
2020-03-20 01:25:27 +01:00
int prevNr = 0 ;
2015-04-17 21:59:44 +02:00
while ( r . Read ( ) )
{
2020-03-20 01:25:27 +01:00
if ( r . GetInt32 ( fields [ "elim" ] ) ! = 0 )
continue ;
2017-10-28 13:20:39 +02:00
// 171027 - ohuseyinoglu: With our change in transponder indexing, we can directly look it up by "chNum" now!
var tp = this . DataRoot . Transponder . TryGet ( r . GetInt32 ( 1 ) ) ;
// ... and get the satellite from that transponder - if set
// Note that we can have channels from multiple satellites in the same list, so this is a loop variable now
var sat = tp ? . Satellite ;
2020-07-12 02:39:43 +02:00
var channel = new DbChannel ( r , fields , this , providers , sat , tp ) ;
2020-03-20 01:25:27 +01:00
if ( channel . OldProgramNr = = prevNr ) // when there is a SRV_EXT_APP table in the database, the service with the highest ext_app "recState" takes priority
continue ;
2019-11-08 02:31:44 +01:00
this . DataRoot . AddChannel ( channelList , channel ) ;
this . channelById . Add ( channel . RecordIndex , channel ) ;
2020-03-20 01:25:27 +01:00
prevNr = channel . OldProgramNr ;
2015-04-17 21:59:44 +02:00
}
}
2015-06-05 06:08:40 +02:00
this . DataRoot . AddChannelList ( channelList ) ;
return channelList ;
}
#endregion
2020-07-12 02:39:43 +02:00
#region CreateChannelList ( )
2020-05-02 19:04:43 +02:00
private ChannelList CreateChannelList ( SignalSource signalSource , string name )
{
var list = new ChannelList ( signalSource , name ) ;
2023-06-03 10:38:11 +02:00
if ( ( list . SignalSource & SignalSource . Ip ) ! = 0 )
2020-05-02 19:04:43 +02:00
{
list . VisibleColumnFieldNames = new List < string >
{
2023-08-14 10:46:10 +02:00
"OldPosition" , "Position" , "PrNr" , "Name" , "Favorites" , "SymbolRate" , "+JsonDefaultUrl" , "+JsonLogoUrl"
2020-05-02 19:04:43 +02:00
} ;
}
return list ;
}
2020-07-12 02:39:43 +02:00
#endregion
2020-05-02 19:04:43 +02:00
2015-06-05 06:08:40 +02:00
#region DetectSignalSource ( )
2021-04-25 18:31:05 +02:00
private static SignalSource DetectSignalSource ( IDbCommand cmd , FileType fileType )
2015-06-05 06:08:40 +02:00
{
2020-03-20 01:25:27 +01:00
if ( fileType = = FileType . ChannelDbIp )
2023-06-03 10:38:11 +02:00
return SignalSource . Ip ;
var signalSource = fileType = = FileType . ChannelDbAnalog ? SignalSource . Analog : SignalSource . Dvb ;
2015-06-05 06:08:40 +02:00
cmd . CommandText = "select distinct chType from CHNL" ;
2022-11-29 22:00:16 +01:00
using var r = cmd . ExecuteReader ( ) ;
if ( r . Read ( ) )
2015-06-05 06:08:40 +02:00
{
2022-11-29 22:00:16 +01:00
var ss = ChTypeToSignalSource ( r . GetInt32 ( 0 ) ) ;
if ( ss ! = 0 )
signalSource = ss ;
2015-06-05 06:08:40 +02:00
}
2022-11-29 22:00:16 +01:00
2019-08-29 16:57:20 +02:00
return signalSource ;
2015-06-05 06:08:40 +02:00
}
#endregion
#region ChTypeToSignalSource ( )
internal static SignalSource ChTypeToSignalSource ( int chType )
{
switch ( chType )
{
case 1 : return SignalSource . AnalogT ;
case 2 : return SignalSource . DvbT ;
case 3 : return SignalSource . AnalogC ;
case 4 : return SignalSource . DvbC ;
case 7 : return SignalSource . DvbS ;
default : return 0 ;
}
}
#endregion
#region BuildQuery ( )
private string BuildQuery ( string table , IList < string > fieldNames )
2015-04-17 21:59:44 +02:00
{
string sql = "select " ;
2015-06-05 06:08:40 +02:00
for ( int i = 0 ; i < fieldNames . Count ; i + + )
2015-04-17 21:59:44 +02:00
{
if ( i > 0 )
sql + = "," ;
sql + = fieldNames [ i ] ;
}
sql + = " from " + table + " inner join SRV on SRV.srvId=" + table + ".srvId inner join CHNL on CHNL.chId=SRV.chId" ;
2020-03-14 16:54:42 +01:00
2020-03-20 01:25:27 +01:00
if ( this . tableNames . Contains ( "SRV_EXT_APP" ) ) // in format 1352.0 there are duplicate "major" values in SRV and this recState seems to be the only difference
2021-04-10 10:08:45 +02:00
sql + = " left outer join SRV_EXT_APP on SRV_EXT_APP.srvId=SRV.srvId order by SRV.major, ifnull(SRV_EXT_APP.recState,0) desc" ;
2020-03-14 16:54:42 +01:00
2015-04-17 21:59:44 +02:00
return sql ;
}
#endregion
#region GetFieldMap ( )
2015-06-05 06:08:40 +02:00
private IDictionary < string , int > GetFieldMap ( IList < string > fieldNames )
2015-04-17 21:59:44 +02:00
{
Dictionary < string , int > field = new Dictionary < string , int > ( ) ;
2015-06-05 06:08:40 +02:00
for ( int i = 0 ; i < fieldNames . Count ; i + + )
2020-03-20 01:25:27 +01:00
{
var idx = fieldNames [ i ] . LastIndexOf ( ' ' ) + 1 ;
field [ fieldNames [ i ] . Substring ( idx ) ] = i ;
}
2015-04-17 21:59:44 +02:00
return field ;
}
#endregion
#region ReadFavorites ( )
2021-04-25 18:31:05 +02:00
private void ReadFavorites ( IDbCommand cmd )
2015-04-17 21:59:44 +02:00
{
cmd . CommandText = "select srvId, fav, pos from SRV_FAV" ;
var r = cmd . ExecuteReader ( ) ;
2020-03-20 01:25:27 +01:00
int favPosAdjust = tableNames . Contains ( "SRV_EXT_APP" ) ? 0 : 1 ;
2015-04-17 21:59:44 +02:00
while ( r . Read ( ) )
{
var channel = this . channelById . TryGet ( r . GetInt64 ( 0 ) ) ;
if ( channel = = null )
continue ;
2015-11-29 20:36:56 +01:00
int fav = r . GetInt32 ( 1 ) - 1 ; // fav values start with 1 in the table
2020-03-20 01:25:27 +01:00
int pos = r . GetInt32 ( 2 ) + favPosAdjust ; // pos values start with 0 or 1
2015-04-22 09:51:21 +02:00
if ( pos > = 0 )
2015-11-29 20:36:56 +01:00
{
2015-04-17 21:59:44 +02:00
channel . Favorites | = ( Favorites ) ( 1 < < fav ) ;
2021-03-13 18:11:30 +01:00
channel . SetOldPosition ( fav + 1 , pos ) ;
2015-11-29 20:36:56 +01:00
}
2015-04-17 21:59:44 +02:00
}
}
#endregion
2015-04-22 09:51:21 +02:00
#region ReadUtf16 ( )
2021-04-25 18:31:05 +02:00
internal string ReadUtf16 ( IDataReader r , int fieldIndex )
2015-04-22 09:51:21 +02:00
{
if ( r . IsDBNull ( fieldIndex ) )
return null ;
2022-11-30 19:01:11 +01:00
byte [ ] nameBytes = new byte [ 1000 ] ;
// Microsoft.Data.SqlDataReader (and the underlying native DLLs) are throwing a memory access violation when using r.GetBytes(...)
//int nameLen = (int)r.GetBytes(fieldIndex, 0, nameBytes, 0, nameBytes.Length);
int nameLen = 0 ;
2021-04-25 18:31:05 +02:00
var obj = r . GetValue ( fieldIndex ) ;
2022-11-30 19:01:11 +01:00
if ( obj is byte [ ] buffer ) // DB returned a BLOB in correct byte order
2021-04-25 18:31:05 +02:00
{
nameBytes = buffer ;
nameLen = buffer . Length ;
}
2022-11-30 19:01:11 +01:00
else if ( obj is string str )
{
// SQLite library decoded the stored utf16be as utf16le, making everything look Chinese due to reversed byte order
// a 1242 format file with utf16be-inside-utf8-envelope encoding can also be decoded this way, but depending on the string length, the last 1-3 characters may be garbled
nameBytes = Encoding . Unicode . GetBytes ( str ) ;
nameLen = nameBytes . Length ;
}
2021-04-25 18:31:05 +02:00
2020-07-13 10:00:41 +02:00
this . encoding ? ? = AutoDetectUtf16Encoding ( nameBytes , nameLen ) ;
2020-07-12 02:39:43 +02:00
if ( this . encoding = = null )
return string . Empty ;
return encoding . GetString ( nameBytes , 0 , nameLen ) . Replace ( "\0" , "" ) ; // remove trailing \0 characters found in Samsung "_T_..." channel list
}
#endregion
#region AutoDetectUtf16Endian ( )
2020-07-13 10:00:41 +02:00
private Encoding AutoDetectUtf16Encoding ( byte [ ] nameBytes , int nameLen )
2020-07-12 02:39:43 +02:00
{
2022-11-30 19:01:11 +01:00
//return Encoding.BigEndianUnicode;
2020-07-12 02:39:43 +02:00
int evenBytesZero = 0 ;
int oddBytesZero = 0 ;
2020-07-13 10:00:41 +02:00
int bytesAbove128 = 0 ;
2020-07-12 02:39:43 +02:00
for ( int i = 0 ; i < nameLen ; i + = 2 )
{
if ( nameBytes [ i ] = = 0 )
+ + evenBytesZero ;
2020-07-13 10:00:41 +02:00
if ( nameBytes [ i ] > = 128 )
+ + bytesAbove128 ;
2020-07-12 02:39:43 +02:00
if ( nameBytes [ i + 1 ] = = 0 )
+ + oddBytesZero ;
2020-07-13 10:00:41 +02:00
if ( nameBytes [ i + 1 ] > = 128 )
+ + bytesAbove128 ;
2020-07-12 02:39:43 +02:00
}
if ( evenBytesZero + oddBytesZero = = nameLen )
return null ;
2022-11-30 19:01:11 +01:00
// in case of the 1242 format with 16 bits UTF16BE encoded inside 3-byte UTF8 sequences, every raw data byte has a value > 128
2020-07-13 10:00:41 +02:00
if ( bytesAbove128 + 1 > = nameLen )
{
2022-11-30 19:01:11 +01:00
this . Features . ChannelNameEdit = ChannelNameEditMode . None ; // impossible to write the arbitrary byte sequence needed and at the same time maintain data type TEXT
2020-07-13 10:00:41 +02:00
return new Utf16InsideUtf8EnvelopeEncoding ( ) ;
}
2022-11-30 19:01:11 +01:00
// so far only UTF16BE has been seen across all sample files
2020-07-12 02:39:43 +02:00
return evenBytesZero > = oddBytesZero ? Encoding . BigEndianUnicode : Encoding . Unicode ;
}
#endregion
#region DefaultEncoding
public override Encoding DefaultEncoding
{
get = > base . DefaultEncoding ;
set
{
if ( ! ( value is UnicodeEncoding ) )
return ;
var oldEncoding = base . DefaultEncoding ;
if ( oldEncoding ! = null )
{
// change encoding of channel names
foreach ( var list in this . DataRoot . ChannelLists )
{
foreach ( var chan in list . Channels )
{
byte [ ] bytes ;
if ( chan . Name ! = null )
{
bytes = oldEncoding . GetBytes ( chan . Name ) ;
chan . Name = value . GetString ( bytes ) ;
}
if ( chan . ShortName ! = null )
{
bytes = oldEncoding . GetBytes ( chan . ShortName ) ;
chan . ShortName = value . GetString ( bytes ) ;
}
}
}
}
base . DefaultEncoding = value ;
}
2015-04-22 09:51:21 +02:00
}
#endregion
2022-11-30 19:01:11 +01:00
#region EncodingInfo
/// <summary>
/// The actually used encoding to decypher utf-8, utf16-le, utf16-be and utf16-inside-utf8-envelope
/// </summary>
internal string EncodingInfo = >
this . encoding = = Encoding . BigEndianUnicode ? "uc16be" :
this . encoding = = Encoding . Unicode ? "uc16le" :
this . encoding is Utf16InsideUtf8EnvelopeEncoding ? "16in8" :
this . encoding . GetType ( ) . Name ;
#endregion
2015-04-17 21:59:44 +02:00
#region Save ( )
2022-11-29 22:00:16 +01:00
public override void Save ( )
2015-04-17 21:59:44 +02:00
{
2019-11-08 02:31:44 +01:00
foreach ( var channelList in this . DataRoot . ChannelLists )
2015-04-17 21:59:44 +02:00
{
2019-11-08 02:31:44 +01:00
var dbPath = this . dbPathByChannelList [ channelList ] ;
SaveChannelList ( channelList , dbPath ) ;
2015-04-17 21:59:44 +02:00
}
2022-11-29 22:00:16 +01:00
this . ZipToOutputFile ( ) ;
2015-04-22 09:51:21 +02:00
}
#endregion
#region SaveChannelList ( )
private void SaveChannelList ( ChannelList channelList , string dbPath )
{
2025-06-05 18:35:10 +02:00
using var conn = new SqliteConnection ( $"Data Source=\" { dbPath } \ ";Pooling=False" ) ;
2022-11-29 22:00:16 +01:00
conn . Open ( ) ;
using ( var trans = conn . BeginTransaction ( ) )
2015-04-17 21:59:44 +02:00
{
2022-11-29 22:00:16 +01:00
using var cmdUpdateSrv = PrepareUpdateCommand ( conn ) ;
2023-06-03 10:38:11 +02:00
using var cmdDeleteSrv = PrepareDeleteCommand ( conn , ( channelList . SignalSource & SignalSource . Dvb ) ! = 0 ) ;
2022-11-29 22:00:16 +01:00
using var cmdInsertFav = PrepareInsertFavCommand ( conn ) ;
using var cmdUpdateFav = PrepareUpdateFavCommand ( conn ) ;
using var cmdDeleteFav = PrepareDeleteFavCommand ( conn ) ;
2023-08-14 10:46:10 +02:00
using var cmdUpdateIp = ( channelList . SignalSource & SignalSource . Ip ) ! = 0 ? PrepareUpdateIpCommand ( conn ) : null ;
2022-11-29 22:00:16 +01:00
Editor . SequentializeFavPos ( channelList , 5 ) ;
2023-08-14 10:46:10 +02:00
this . WriteChannels ( cmdUpdateSrv , cmdDeleteSrv , cmdInsertFav , cmdUpdateFav , cmdDeleteFav , cmdUpdateIp , channelList ) ;
2022-11-29 22:00:16 +01:00
trans . Commit ( ) ;
2022-11-29 18:21:52 +01:00
}
2022-11-29 22:00:16 +01:00
using var cmd = conn . CreateCommand ( ) ;
this . RepairCorruptedDatabaseImage ( cmd ) ;
2015-04-17 21:59:44 +02:00
}
2015-04-22 09:51:21 +02:00
2015-04-17 21:59:44 +02:00
#endregion
2015-09-19 23:24:31 +02:00
#region Prepare * Command ( )
2021-04-25 18:31:05 +02:00
private SqliteCommand PrepareUpdateCommand ( SqliteConnection conn )
2015-04-17 21:59:44 +02:00
{
2020-07-13 10:00:41 +02:00
var canUpdateNames = this . Features . ChannelNameEdit ! = ChannelNameEditMode . None ;
2015-09-19 23:24:31 +02:00
var cmd = conn . CreateCommand ( ) ;
2022-11-30 19:01:11 +01:00
var updateSrvName = canUpdateNames ? ", srvName=@srvname" : "" ;
2020-07-13 10:00:41 +02:00
cmd . CommandText = "update SRV set major=@nr, lockMode=@lock, hideGuide=@hidden, hidden=@hidden, numSel=@numsel" + updateSrvName + " where srvId=@id" ;
2021-04-25 18:31:05 +02:00
cmd . Parameters . Add ( "@id" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@nr" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@lock" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@hidden" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@numsel" , SqliteType . Integer ) ;
2020-07-13 10:00:41 +02:00
if ( canUpdateNames )
2022-11-30 19:01:11 +01:00
cmd . Parameters . Add ( "@srvname" , SqliteType . Text ) ;
2015-09-19 23:24:31 +02:00
cmd . Prepare ( ) ;
return cmd ;
2015-04-17 21:59:44 +02:00
}
2015-09-19 23:24:31 +02:00
2023-08-14 10:46:10 +02:00
private SqliteCommand PrepareUpdateIpCommand ( SqliteConnection conn )
{
var cmd = conn . CreateCommand ( ) ;
cmd . CommandText = "update SRV_IP set jsonMeta=@jsonMeta where srvId=@id" ;
cmd . Parameters . Add ( "@id" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@jsonMeta" , SqliteType . Blob ) ;
cmd . Prepare ( ) ;
return cmd ;
}
2021-04-25 18:31:05 +02:00
private SqliteCommand PrepareDeleteCommand ( SqliteConnection conn , bool digital )
2015-09-19 23:24:31 +02:00
{
var cmd = conn . CreateCommand ( ) ;
2015-11-27 01:51:08 +01:00
var sql = new StringBuilder ( ) ;
cmd . CommandText = "select name from sqlite_master where sql like '%srvId integer%' order by name desc" ;
using ( var r = cmd . ExecuteReader ( ) )
2015-11-21 19:34:30 +01:00
{
2015-11-27 01:51:08 +01:00
while ( r . Read ( ) )
sql . AppendLine ( $"; delete from {r.GetString(0)} where srvId=@id" ) ;
2015-11-21 19:34:30 +01:00
}
2015-11-27 01:51:08 +01:00
cmd . CommandText = sql . ToString ( ) ;
2021-04-25 18:31:05 +02:00
cmd . Parameters . Add ( "@id" , SqliteType . Integer ) ;
2015-09-19 23:24:31 +02:00
cmd . Prepare ( ) ;
return cmd ;
}
2021-04-25 18:31:05 +02:00
private SqliteCommand PrepareInsertFavCommand ( SqliteConnection conn )
2015-09-19 23:24:31 +02:00
{
var cmd = conn . CreateCommand ( ) ;
cmd . CommandText = "insert into SRV_FAV (srvId, fav, pos) values (@id, @fav, @pos)" ;
2021-04-25 18:31:05 +02:00
cmd . Parameters . Add ( "@id" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@fav" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@pos" , SqliteType . Integer ) ;
2015-09-19 23:24:31 +02:00
cmd . Prepare ( ) ;
return cmd ;
}
2019-11-08 02:31:44 +01:00
2021-04-25 18:31:05 +02:00
private SqliteCommand PrepareUpdateFavCommand ( SqliteConnection conn )
2015-09-19 23:24:31 +02:00
{
var cmd = conn . CreateCommand ( ) ;
cmd . CommandText = "update SRV_FAV set pos=@pos where srvId=@id and fav=@fav" ;
2021-04-25 18:31:05 +02:00
cmd . Parameters . Add ( "@id" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@fav" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@pos" , SqliteType . Integer ) ;
2015-09-19 23:24:31 +02:00
cmd . Prepare ( ) ;
return cmd ;
}
2021-04-25 18:31:05 +02:00
private SqliteCommand PrepareDeleteFavCommand ( SqliteConnection conn )
2015-09-19 23:24:31 +02:00
{
var cmd = conn . CreateCommand ( ) ;
cmd . CommandText = "delete from SRV_FAV where srvId=@id and fav=@fav" ;
2021-04-25 18:31:05 +02:00
cmd . Parameters . Add ( "@id" , SqliteType . Integer ) ;
cmd . Parameters . Add ( "@fav" , SqliteType . Integer ) ;
2015-09-19 23:24:31 +02:00
cmd . Prepare ( ) ;
return cmd ;
}
2015-04-17 21:59:44 +02:00
#endregion
#region WriteChannels ( )
2023-08-14 10:46:10 +02:00
private void WriteChannels ( SqliteCommand cmdUpdateSrv , SqliteCommand cmdDeleteSrv , SqliteCommand cmdInsertFav , SqliteCommand cmdUpdateFav , SqliteCommand cmdDeleteFav , SqliteCommand cmdUpdateIp ,
2015-06-13 18:37:59 +02:00
ChannelList channelList , bool analog = false )
2015-11-29 20:36:56 +01:00
{
2020-07-13 10:00:41 +02:00
bool canUpdateNames = this . Features . ChannelNameEdit ! = ChannelNameEditMode . None ;
2019-11-08 02:31:44 +01:00
foreach ( ChannelInfo channelInfo in channelList . Channels . ToList ( ) )
2015-04-17 21:59:44 +02:00
{
var channel = channelInfo as DbChannel ;
if ( channel = = null ) // ignore reference list proxy channels
continue ;
2019-02-10 15:56:03 +01:00
2019-11-08 02:31:44 +01:00
if ( channel . IsDeleted )
2015-06-13 18:37:59 +02:00
{
2015-11-29 20:36:56 +01:00
// delete channel from all tables that have a reference to srvId
2015-06-13 18:37:59 +02:00
cmdDeleteSrv . Parameters [ "@id" ] . Value = channel . RecordIndex ;
cmdDeleteSrv . ExecuteNonQuery ( ) ;
2019-11-08 02:31:44 +01:00
channelList . Channels . Remove ( channelInfo ) ;
2015-11-29 20:36:56 +01:00
continue ;
2015-06-13 18:37:59 +02:00
}
2015-11-29 20:36:56 +01:00
// update channel record
cmdUpdateSrv . Parameters [ "@id" ] . Value = channel . RecordIndex ;
2019-08-11 10:04:29 +02:00
cmdUpdateSrv . Parameters [ "@nr" ] . Value = channel . NewProgramNr ;
2015-11-29 20:36:56 +01:00
cmdUpdateSrv . Parameters [ "@lock" ] . Value = channel . Lock ;
cmdUpdateSrv . Parameters [ "@hidden" ] . Value = channel . Hidden ;
cmdUpdateSrv . Parameters [ "@numsel" ] . Value = ! channel . Skip ;
2020-07-13 10:00:41 +02:00
if ( canUpdateNames )
2022-11-30 19:01:11 +01:00
cmdUpdateSrv . Parameters [ "@srvname" ] . Value = channel . Name = = null ? ( object ) DBNull . Value : encoding . GetString ( Encoding . Unicode . GetBytes ( channel . Name ) ) ; // convert string => UTF16LE => string with flipped byte order (looking "Chinese")
2015-11-29 20:36:56 +01:00
cmdUpdateSrv . ExecuteNonQuery ( ) ;
2023-08-14 10:46:10 +02:00
if ( cmdUpdateIp ! = null & & channel . JsonModified )
{
cmdUpdateIp . Parameters [ "@id" ] . Value = channel . RecordIndex ;
cmdUpdateIp . Parameters [ "@jsonMeta" ] . Value = channel . GetRawJson ( ) ;
cmdUpdateIp . ExecuteNonQuery ( ) ;
}
2015-11-29 20:36:56 +01:00
// update favorites
2015-06-13 18:37:59 +02:00
for ( int i = 0 , mask = 1 ; i < 5 ; i + + , mask < < = 1 )
2015-04-17 21:59:44 +02:00
{
2021-03-07 16:12:21 +01:00
int oldPos = channel . GetOldPosition ( 1 + i ) ;
int newPos = ( ( int ) channel . Favorites & mask ) ! = 0 ? channel . GetPosition ( 1 + i ) : - 1 ;
2015-11-29 20:36:56 +01:00
if ( newPos > = 0 )
2015-04-17 21:59:44 +02:00
{
2015-06-13 18:37:59 +02:00
var c = oldPos < 0 ? cmdInsertFav : cmdUpdateFav ;
2015-04-17 21:59:44 +02:00
c . Parameters [ "@id" ] . Value = channel . RecordIndex ;
2015-04-22 09:51:21 +02:00
c . Parameters [ "@fav" ] . Value = i + 1 ;
2015-11-29 20:36:56 +01:00
c . Parameters [ "@pos" ] . Value = newPos - 1 ;
2015-04-17 21:59:44 +02:00
c . ExecuteNonQuery ( ) ;
}
else
{
2015-06-13 18:37:59 +02:00
cmdDeleteFav . Parameters [ "@id" ] . Value = channel . RecordIndex ;
cmdDeleteFav . Parameters [ "@fav" ] . Value = i + 1 ;
cmdDeleteFav . ExecuteNonQuery ( ) ;
2015-04-17 21:59:44 +02:00
}
2015-11-29 20:36:56 +01:00
2021-03-13 18:11:30 +01:00
channel . SetPosition ( i + 1 , newPos ) ;
2015-04-17 21:59:44 +02:00
}
}
}
#endregion
}
}