From 56676748b7b64b9262af2d1e3b79884af489a348 Mon Sep 17 00:00:00 2001 From: Horst Beham Date: Sun, 17 Sep 2023 20:07:55 +0200 Subject: [PATCH] - added support for Philips Repair channel list format 2.0 - added experimental support for amdb\*.db Android STB channel lists --- source/ChanSort.Loader.Amdb/AmdbPlugin.cs | 16 + source/ChanSort.Loader.Amdb/AmdbSerializer.cs | 273 ++++++++++++++++++ .../ChanSort.Loader.Amdb.csproj | 23 ++ .../BinarySerializer.cs | 15 +- .../ChanSort.Loader.Philips.ini | 11 + .../ChanSort.Loader.Philips/PhilipsPlugin.cs | 2 +- source/ChanSort.sln | 32 ++ source/ChanSort/ChanSort.csproj | 1 + source/changelog.md | 4 + 9 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 source/ChanSort.Loader.Amdb/AmdbPlugin.cs create mode 100644 source/ChanSort.Loader.Amdb/AmdbSerializer.cs create mode 100644 source/ChanSort.Loader.Amdb/ChanSort.Loader.Amdb.csproj diff --git a/source/ChanSort.Loader.Amdb/AmdbPlugin.cs b/source/ChanSort.Loader.Amdb/AmdbPlugin.cs new file mode 100644 index 0000000..1a1a518 --- /dev/null +++ b/source/ChanSort.Loader.Amdb/AmdbPlugin.cs @@ -0,0 +1,16 @@ +using ChanSort.Api; + +namespace ChanSort.Loader.Amdb +{ + public class AmdbPlugin : ISerializerPlugin + { + public string DllName { get; set; } + public string PluginName => "AMDB (*.db)"; + public string FileFilter => "amdb*.db"; + + public SerializerBase CreateSerializer(string inputFile) + { + return new AmdbSerializer(inputFile); + } + } +} \ No newline at end of file diff --git a/source/ChanSort.Loader.Amdb/AmdbSerializer.cs b/source/ChanSort.Loader.Amdb/AmdbSerializer.cs new file mode 100644 index 0000000..7cc0b02 --- /dev/null +++ b/source/ChanSort.Loader.Amdb/AmdbSerializer.cs @@ -0,0 +1,273 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Data.Sqlite; +using ChanSort.Api; + +namespace ChanSort.Loader.Amdb +{ + /* + * This class loads amdb*.db files from unknown Android set-top-boxes. + */ + class AmdbSerializer : SerializerBase + { + private readonly HashSet tableNames = new(); + + private readonly ChannelList tv = new (SignalSource.Tv, "TV"); + private readonly ChannelList radio = new(SignalSource.Radio, "Radio"); + private readonly ChannelList data = new(SignalSource.Data, "Data"); + + #region ctor() + public AmdbSerializer(string inputFile) : base(inputFile) + { + this.Features.ChannelNameEdit = ChannelNameEditMode.All; + this.Features.DeleteMode = DeleteMode.Physically; + this.Features.CanSkipChannels = true; + this.Features.CanLockChannels = true; + this.Features.CanHideChannels = true; + this.Features.CanHaveGaps = false; + this.Features.AllowGapsInFavNumbers = false; + this.Features.FavoritesMode = FavoritesMode.None; + + this.DataRoot.AddChannelList(tv); + this.DataRoot.AddChannelList(radio); + this.DataRoot.AddChannelList(data); + } + #endregion + + #region Load() + public override void Load() + { + string connString = $"Data Source={this.FileName};Pooling=False"; + using var conn = new SqliteConnection(connString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + + this.RepairCorruptedDatabaseImage(cmd); + + cmd.CommandText = "SELECT name FROM sqlite_master WHERE type = 'table'"; + using (var r = cmd.ExecuteReader()) + { + while (r.Read()) + this.tableNames.Add(r.GetString(0).ToLowerInvariant()); + } + + if (new[] { "sat_para_table", "ts_table", "srv_table" }.Any(tbl => !tableNames.Contains(tbl))) + throw LoaderException.TryNext("File doesn't contain the expected tables"); + + this.ReadSatellites(cmd); + this.ReadTransponders(cmd); + this.ReadChannels(cmd); + this.AdjustColumns(); + } + #endregion + + #region RepairCorruptedDatabaseImage() + private void RepairCorruptedDatabaseImage(SqliteCommand cmd) + { + cmd.CommandText = "REINDEX"; + cmd.ExecuteNonQuery(); + } + #endregion + + #region ReadSatellites() + private void ReadSatellites(SqliteCommand cmd) + { + cmd.CommandText = "select db_id, sat_name, sat_longitude from sat_para_table order by db_id"; + using var r = cmd.ExecuteReader(); + while (r.Read()) + { + Satellite sat = new Satellite(r.GetInt32(0)); + string eastWest = "E"; + int pos = r.GetInt32(2); + // i haven't seen a file containing satellites on the west side. could be either negative or > 180° + if (pos < 0) + { + pos = -pos; + eastWest = "W"; + } + else if (pos > 180) + { + pos = 360 - pos; + eastWest = "W"; + } + sat.OrbitalPosition = $"{pos / 10}.{pos % 10}{eastWest}"; + sat.Name = r.GetString(1); + this.DataRoot.AddSatellite(sat); + } + } + #endregion + + #region ReadTransponders() + private void ReadTransponders(SqliteCommand cmd) + { + cmd.CommandText = "select db_id, db_sat_para_id, freq, polar, symb from ts_table"; + using var r = cmd.ExecuteReader(); + while (r.Read()) + { + int id = r.GetInt32(0); + int satId = r.GetInt32(1); + int freq = r.GetInt32(2); + + if (this.DataRoot.Transponder.TryGet(id) != null) + continue; + Transponder tp = new Transponder(id); + tp.FrequencyInMhz = (int)(freq/1000); + tp.Polarity = r.GetInt32(3) == 0 ? 'H' : 'V'; + tp.Satellite = this.DataRoot.Satellites.TryGet(satId); + tp.SymbolRate = r.GetInt32(4) / 1000; + this.DataRoot.AddTransponder(tp.Satellite, tp); + } + } + #endregion + + #region ReadChannels() + private void ReadChannels(SqliteCommand cmd) + { + int ixP = 0; + int ixST = ixP + 12; + + cmd.CommandText = @" +select + p.db_id, p.chan_num, p.name, p.service_id, p.vid_pid, p.pcr_pid, p.service_type, p.vid_fmt, p.free_ca_mode, p.lock, p.skip, p.hidden, + t.db_sat_para_id, 0, t.ts_id, t.freq, t.polar, t.symb +from srv_table p +left outer join ts_table t on t.db_id=p.db_ts_id +order by p.chan_num"; + + using var r = cmd.ExecuteReader(); + while (r.Read()) + { + var handle = r.GetInt32(ixP + 0); + var oldProgNr = r.GetInt32(ixP + 1); + var name = r.GetString(ixP + 2); + if (name.StartsWith("xxx")) + name = name.Substring(3); + ChannelInfo channel = new ChannelInfo(0, handle, oldProgNr, name); + channel.ServiceId = r.GetInt32(ixP + 3) & 0x7FFF; + channel.VideoPid = r.GetInt32(ixP + 4); + channel.PcrPid = r.IsDBNull(ixP + 5) ? 0 : r.GetInt32(ixP + 5); + var serviceType = r.GetInt32(ixP + 6); + var vidFmt = r.GetInt32(ixP + 7); + channel.ServiceType = serviceType; + if (serviceType == 1) + { + channel.ServiceTypeName = vidFmt == 2 ? "HD TV" : "TV"; + channel.SignalSource |= SignalSource.Tv; + } + else if (serviceType == 2) + { + channel.ServiceTypeName = "Radio"; + channel.SignalSource |= SignalSource.Radio; + } + else + { + channel.ServiceTypeName = "Data"; + channel.SignalSource |= SignalSource.Data; + } + channel.Encrypted = r.GetInt32(ixP + 8) != 0; + channel.Lock = r.GetBoolean(ixP + 9); + channel.Skip = r.GetBoolean(ixP + 10); + channel.Hidden = r.GetBoolean(ixP + 11); + + // DVB-S + if (!r.IsDBNull(ixST + 0)) + { + var satId = r.GetInt32(ixST + 0); + var sat = this.DataRoot.Satellites.TryGet(satId); + channel.Satellite = sat?.Name; + channel.SatPosition = sat?.OrbitalPosition; + channel.OriginalNetworkId = r.GetInt32(ixST + 1) & 0x7FFF; + channel.TransportStreamId = r.GetInt32(ixST + 2) & 0x7FFF; + channel.FreqInMhz = r.GetInt32(ixST + 3); + if (channel.FreqInMhz > 20000) // DVB-S is in MHz already, DVB-C/T in kHz + channel.FreqInMhz /= 1000; + channel.Polarity = r.GetInt32(ixST + 4) == 0 ? 'H' : 'V'; + channel.SymbolRate = r.GetInt32(ixST + 5)/1000; + } + + var list = this.DataRoot.GetChannelList(channel.SignalSource); + if (list != null) + this.DataRoot.AddChannel(list, channel); + } + } + #endregion + + #region AdjustColumns() + private void AdjustColumns() + { + foreach (var list in this.DataRoot.ChannelLists) + { + list.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.AudioPid)); + list.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.ShortName)); + list.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.Provider)); + list.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.NetworkName)); + list.VisibleColumnFieldNames.Remove(nameof(ChannelInfo.NetworkOperator)); + } + } + #endregion + + + #region Save() + public override void Save() + { + string channelConnString = $"Data Source={this.FileName};Pooling=False"; + using var conn = new SqliteConnection(channelConnString); + conn.Open(); + using var trans = conn.BeginTransaction(); + using var cmd = conn.CreateCommand(); + using var cmd2 = conn.CreateCommand(); + + this.WriteChannels(cmd, cmd2); + trans.Commit(); + + cmd.Transaction = null; + this.RepairCorruptedDatabaseImage(cmd); + } + #endregion + + #region WriteChannels() + private void WriteChannels(SqliteCommand cmd, SqliteCommand cmdDelete) + { + cmd.CommandText = "update srv_table set chan_num=@nr, chan_order=@nr, name=@name, skip=@skip, lock=@lock, hidden=@hide where db_id=@handle"; + cmd.Parameters.Add("@handle", SqliteType.Integer); + cmd.Parameters.Add("@nr", SqliteType.Integer); + cmd.Parameters.Add("@name", SqliteType.Text); + cmd.Parameters.Add("@skip", SqliteType.Integer); + cmd.Parameters.Add("@lock", SqliteType.Integer); + cmd.Parameters.Add("@hide", SqliteType.Integer); + cmd.Prepare(); + + cmdDelete.CommandText = "delete from srv_table where db_id=@handle"; + cmdDelete.Parameters.Add("@handle", SqliteType.Integer); + cmdDelete.Prepare(); + + foreach (var list in this.DataRoot.ChannelLists) + { + foreach (ChannelInfo channel in list.Channels.OrderBy(ch => (ch.SignalSource & SignalSource.Tv) != 0 ? 0 : 1).ThenBy(ch => ch.NewProgramNr)) + { + if (channel.IsProxy) // ignore reference list proxy channels + continue; + + if (channel.IsDeleted) + { + cmdDelete.Parameters["@handle"].Value = channel.RecordIndex; + cmdDelete.ExecuteNonQuery(); + } + else + { + channel.UpdateRawData(); + cmd.Parameters["@handle"].Value = channel.RecordIndex; + cmd.Parameters["@nr"].Value = channel.NewProgramNr; + cmd.Parameters["@name"].Value = "xxx" + channel.Name; + cmd.Parameters["@skip"].Value = channel.Skip ? 1 : 0; + cmd.Parameters["@lock"].Value = channel.Lock ? 1 : 0; + cmd.Parameters["@hide"].Value = channel.Hidden ? 1 : 0; + cmd.ExecuteNonQuery(); + } + } + } + } + #endregion + } +} diff --git a/source/ChanSort.Loader.Amdb/ChanSort.Loader.Amdb.csproj b/source/ChanSort.Loader.Amdb/ChanSort.Loader.Amdb.csproj new file mode 100644 index 0000000..8995035 --- /dev/null +++ b/source/ChanSort.Loader.Amdb/ChanSort.Loader.Amdb.csproj @@ -0,0 +1,23 @@ + + + + net48 + disable + latest + + + + ..\Debug\ + + + ..\Release\ + + + + + + + + + + diff --git a/source/ChanSort.Loader.Philips/BinarySerializer.cs b/source/ChanSort.Loader.Philips/BinarySerializer.cs index 172965c..74903de 100644 --- a/source/ChanSort.Loader.Philips/BinarySerializer.cs +++ b/source/ChanSort.Loader.Philips/BinarySerializer.cs @@ -11,7 +11,7 @@ namespace ChanSort.Loader.Philips { /* - This loader handles the file format versions 1.x (*Table and *.dat files), version 25.x-45.x (*Db.bin files + tv.db and list.db) + This loader handles the file format versions 1.x, 2.0 (*Table and *.dat files), version 25.x-45.x (*Db.bin files + tv.db and list.db) Version 30.x and 45.x were tested, version 25 is untested due to the lack of any sample files. Based on what I read online, the files are pretty much the same as with Format 45. @@ -458,13 +458,17 @@ private void LoadDvbS(ChannelList list, string path, string mappingName) throw LoaderException.Fail("Unsupported file content: " + path); } + var channelCount = recordCount; + if (version == 2) + channelCount = BitConverter.ToInt16(data, 2); // number of used channels + this.dataFilePaths.Add(path); var dvbStringDecoder = new DvbStringDecoder(this.DefaultEncoding); var mapping = new DataMapping(this.ini.GetSection(mappingName)); mapping.SetDataPtr(data, 12 + (chanLstBin.VersionMajor <= 11 ? recordCount * 4 : 0)); - for (int i = 0; i < recordCount; i++, mapping.BaseOffset += recordSize) + for (int i = 0; i < channelCount; i++, mapping.BaseOffset += recordSize) { var ch = LoadDvbsChannel(list, mapping, i, dvbStringDecoder); this.DataRoot.AddChannel(list, ch); @@ -542,8 +546,11 @@ private ChannelInfo LoadDvbsChannel(ChannelList list, DataMapping mapping, int r ch.Satellite = t.Satellite?.Name; if (t.OriginalNetworkId != 0) ch.OriginalNetworkId = t.OriginalNetworkId; - if (t.TransportStreamId != 0) - ch.TransportStreamId = t.TransportStreamId; + if (chanLstBin.VersionMajor != 2) + { + if (t.TransportStreamId != 0) // does not work in version 2.0 + ch.TransportStreamId = t.TransportStreamId; + } } return ch; diff --git a/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini b/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini index 0c2bf63..04fa5cb 100644 --- a/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini +++ b/source/ChanSort.Loader.Philips/ChanSort.Loader.Philips.ini @@ -262,6 +262,17 @@ lenName=32 offProvider=60 lenProvider=32 +[tuneinfo.dat] +offSymRate=0 +offFreq=2 +maskFreq=0x3FFF +offPolarity=2 +maskPolarity=0x4000 +offSatIndex=6 +maskSatIndex=0xFFF0 +offOnid=16 +offTsid=18 + [CableDigSrvTable_entry] offChecksum=0 offSymbolRate=24 diff --git a/source/ChanSort.Loader.Philips/PhilipsPlugin.cs b/source/ChanSort.Loader.Philips/PhilipsPlugin.cs index 98db765..9665f99 100644 --- a/source/ChanSort.Loader.Philips/PhilipsPlugin.cs +++ b/source/ChanSort.Loader.Philips/PhilipsPlugin.cs @@ -133,7 +133,7 @@ public SerializerBase CreateSerializer(string inputFile) if (majorVersion == 0 || majorVersion >= 100 && majorVersion <= 115) return new XmlSerializer(inputFile); - if (majorVersion == 1 || majorVersion == 30 || majorVersion == 45) // || majorVersion == 11 // format version 11 is similar to 1.x, but not (yet) supported + if (majorVersion == 1 || majorVersion == 2 || majorVersion == 30 || majorVersion == 45) // || majorVersion == 11 // format version 11 is similar to 1.x, but not (yet) supported return new BinarySerializer(inputFile); if (majorVersion == -1) return new DbSerializer(inputFile); diff --git a/source/ChanSort.sln b/source/ChanSort.sln index 2011200..ce1e8c0 100644 --- a/source/ChanSort.sln +++ b/source/ChanSort.sln @@ -156,6 +156,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChanSort.Loader.Medion", "C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChanSort.Loader.TechniSat", "ChanSort.Loader.TechniSat\ChanSort.Loader.TechniSat.csproj", "{A5C22199-1C51-4265-89CA-A7183F1BDB8B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChanSort.Loader.Amdb", "ChanSort.Loader.Amdb\ChanSort.Loader.Amdb.csproj", "{30E9D084-6F3C-41A9-9B46-846178C91BDB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution All_Debug|Any CPU = All_Debug|Any CPU @@ -1433,6 +1435,36 @@ Global {A5C22199-1C51-4265-89CA-A7183F1BDB8B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A5C22199-1C51-4265-89CA-A7183F1BDB8B}.Release|x86.ActiveCfg = Release|Any CPU {A5C22199-1C51-4265-89CA-A7183F1BDB8B}.Release|x86.Build.0 = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Debug|Any CPU.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Debug|x86.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Debug|x86.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Release|Any CPU.ActiveCfg = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Release|Any CPU.Build.0 = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Release|Mixed Platforms.Build.0 = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Release|x86.ActiveCfg = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.All_Release|x86.Build.0 = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Debug|x86.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Debug|x86.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.NoDevExpress_Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.NoDevExpress_Debug|Any CPU.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.NoDevExpress_Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.NoDevExpress_Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.NoDevExpress_Debug|x86.ActiveCfg = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.NoDevExpress_Debug|x86.Build.0 = Debug|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Release|Any CPU.Build.0 = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Release|x86.ActiveCfg = Release|Any CPU + {30E9D084-6F3C-41A9-9B46-846178C91BDB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/ChanSort/ChanSort.csproj b/source/ChanSort/ChanSort.csproj index 9d37f6e..76c81b2 100644 --- a/source/ChanSort/ChanSort.csproj +++ b/source/ChanSort/ChanSort.csproj @@ -125,6 +125,7 @@ + diff --git a/source/changelog.md b/source/changelog.md index 1cb331b..a7704c6 100644 --- a/source/changelog.md +++ b/source/changelog.md @@ -1,6 +1,10 @@ ChanSort Change Log =================== +2023-09-17 +- added support for Philips Repair channel list format 2.0 +- added experimental support for amdb\*.db Android STB channel lists + 2023-08-17 - fixed error when opening the reference list dialog