diff --git a/source/ChanSort.Api/Utils/Tools.cs b/source/ChanSort.Api/Utils/Tools.cs index 5f9fb0c..45fa712 100644 --- a/source/ChanSort.Api/Utils/Tools.cs +++ b/source/ChanSort.Api/Utils/Tools.cs @@ -131,5 +131,51 @@ namespace ChanSort.Api return sb.ToString(); } #endregion + + #region IsUtf8() + /// + /// This method tests whether the binary data can be interpreted as valid UTF-8. If not, it might be encoded with a locale specific encoding + /// + public static bool IsUtf8(byte[] buffer) + { + int followBytes = 0; + foreach (byte b in buffer) + { + if (followBytes > 0) + { + if ((b & 0xC0) != 0x80) // follow-up bytes must be 10xx xxxx + return false; + --followBytes; + continue; + } + + if (b < 0x80) // standard ASCII characters + continue; + + if (b < 0xC0) // [0x80-0xBF] is only allowed for UTF-8 follow-up bytes + return false; + + if (b < 0xE0) // 110x xxxx + followBytes = 1; + else if (b < 0xF0) // 1110 xxxx + followBytes = 2; + else if (b < 0xF8) // 1111 0xxx + followBytes = 3; + else + return false; // can't be more than 3 follow-up bytes + } + + return followBytes == 0; + } + #endregion + + #region HasUtf8Bom() + public static bool HasUtf8Bom(byte[] content) + { + if (content == null || content.Length < 3) + return false; + return content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF; + } + #endregion } } diff --git a/source/ChanSort.Loader.Hisense/ChanSort.Loader.Hisense.csproj b/source/ChanSort.Loader.Hisense/ChanSort.Loader.Hisense.csproj index f9eba53..02b80e0 100644 --- a/source/ChanSort.Loader.Hisense/ChanSort.Loader.Hisense.csproj +++ b/source/ChanSort.Loader.Hisense/ChanSort.Loader.Hisense.csproj @@ -24,6 +24,7 @@ 4 x86 false + latest pdbonly @@ -33,6 +34,7 @@ prompt 4 false + latest true @@ -43,6 +45,7 @@ prompt MinimumRecommendedRules.ruleset false + latest ..\Release\ @@ -53,6 +56,7 @@ prompt MinimumRecommendedRules.ruleset false + latest @@ -70,6 +74,7 @@ + diff --git a/source/ChanSort.Loader.Hisense/Channel.cs b/source/ChanSort.Loader.Hisense/Channel.cs new file mode 100644 index 0000000..fd79a8b --- /dev/null +++ b/source/ChanSort.Loader.Hisense/Channel.cs @@ -0,0 +1,14 @@ +using ChanSort.Api; + +namespace ChanSort.Loader.Hisense +{ + internal class Channel : ChannelInfo + { + public int ChannelId; + public int NwMask; + + public Channel(SignalSource ssource, long id, int prNr, string name): base(ssource, id, prNr, name) + { + } + } +} diff --git a/source/ChanSort.Loader.Hisense/HisDbSerializer.cs b/source/ChanSort.Loader.Hisense/HisDbSerializer.cs index 9076d59..026281d 100644 --- a/source/ChanSort.Loader.Hisense/HisDbSerializer.cs +++ b/source/ChanSort.Loader.Hisense/HisDbSerializer.cs @@ -81,9 +81,11 @@ namespace ChanSort.Loader.Hisense #endregion private readonly List channelLists = new List(); - private readonly Dictionary channelsById = new Dictionary(); + private readonly Dictionary channelsById = new Dictionary(); private List tableNames; + // the fav_1 - fav_4 tables in channel.db of a H50B7700UW has different column names and a primary key/unique constraint which requires specific handling + private bool hasCamelCaseFavSchema = false; #region ctor() @@ -212,7 +214,7 @@ namespace ChanSort.Loader.Hisense { var sat = new Satellite(r.GetInt32(0)); var pos = r.GetInt32(1); - sat.OrbitalPosition = $"{(decimal) Math.Abs(pos)/10:n1}{(pos < 0 ? 'W' : 'E')}"; + sat.OrbitalPosition = $"{(decimal) Math.Abs(pos) / 10:n1}{(pos < 0 ? 'W' : 'E')}"; sat.Name = r.GetString(2); this.DataRoot.AddSatellite(sat); } @@ -234,26 +236,20 @@ namespace ChanSort.Loader.Hisense continue; int x = int.Parse(match.Groups[1].Value); - this.LoadTslData(cmd, x, "tsl_#_data_ter_dig", ", freq", (t, r, i0) => - { - t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0)/1000000; - }); + this.LoadTslData(cmd, x, "tsl_#_data_ter_dig", ", freq", + (t, r, i0) => { t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0) / 1000000; }); - this.LoadTslData(cmd, x, "tsl_#_data_ter_ana", ", freq", (t, r, i0) => - { - t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0)/1000000; - }); + this.LoadTslData(cmd, x, "tsl_#_data_ter_ana", ", freq", + (t, r, i0) => { t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0) / 1000000; }); this.LoadTslData(cmd, x, "tsl_#_data_cab_dig", ", freq, sym_rate", (t, r, i0) => { - t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0)/1000000; + t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0) / 1000000; t.SymbolRate = r.GetInt32(i0 + 1); }); - this.LoadTslData(cmd, x, "tsl_#_data_cab_ana", ", freq", (t, r, i0) => - { - t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0)/1000000; - }); + this.LoadTslData(cmd, x, "tsl_#_data_cab_ana", ", freq", + (t, r, i0) => { t.FrequencyInMhz = (decimal) r.GetInt32(i0 + 0) / 1000000; }); this.LoadTslData(cmd, x, "tsl_#_data_sat_dig", ", freq, sym_rate, orb_pos", (t, r, i0) => { @@ -268,22 +264,25 @@ namespace ChanSort.Loader.Hisense if (sat == null) { sat = new Satellite(opos); - var pos = (decimal) opos/10; + var pos = (decimal) opos / 10; sat.Name = pos < 0 ? (-pos).ToString("n1") + "W" : pos.ToString("n1") + "E"; } + t.Satellite = sat; } }); } } - private void LoadTslData(SQLiteCommand cmd, int tableNr, string joinTable, string joinFields, Action enhanceTransponderInfo) + private void LoadTslData(SQLiteCommand cmd, int tableNr, string joinTable, string joinFields, + Action enhanceTransponderInfo) { if (!this.tableNames.Contains(joinTable.Replace("#", tableNr.ToString()))) return; - cmd.CommandText = $"select tsl_#.tsl_rec_id, `t_desc.on_id`, `t_desc.ts_id`, `t_ref.satl_rec_id`, `t_desc.e_bcst_medium` {joinFields} " - + $" from tsl_# inner join {joinTable} on {joinTable}.tsl_rec_id=tsl_#.tsl_rec_id"; + cmd.CommandText = + $"select tsl_#.tsl_rec_id, `t_desc.on_id`, `t_desc.ts_id`, `t_ref.satl_rec_id`, `t_desc.e_bcst_medium` {joinFields} " + + $" from tsl_# inner join {joinTable} on {joinTable}.tsl_rec_id=tsl_#.tsl_rec_id"; cmd.CommandText = cmd.CommandText.Replace("#", tableNr.ToString()); using (var r = cmd.ExecuteReader()) { @@ -322,18 +321,19 @@ namespace ChanSort.Loader.Hisense } this.LoadSvlData(cmd, x, "svl_#_data_analog", "", (ci, r, i0) => { }); - this.LoadSvlData(cmd, x, "svl_#_data_dvb", ", b_free_ca_mode, s_svc_name, sdt_service_type, cur_lcn", (ci, r, i0) => - { - ci.Encrypted = r.GetBoolean(i0 + 0); - ci.ShortName = r.GetString(i0 + 1); - ci.ServiceType = r.GetInt32(i0 + 2); - if (ci.ServiceType != 0) - ci.ServiceTypeName = LookupData.Instance.GetServiceTypeDescription(ci.ServiceType); + this.LoadSvlData(cmd, x, "svl_#_data_dvb", ", b_free_ca_mode, s_svc_name, sdt_service_type, cur_lcn", + (ci, r, i0) => + { + ci.Encrypted = r.GetBoolean(i0 + 0); + ci.ShortName = r.GetString(i0 + 1); + ci.ServiceType = r.GetInt32(i0 + 2); + if (ci.ServiceType != 0) + ci.ServiceTypeName = LookupData.Instance.GetServiceTypeDescription(ci.ServiceType); - if ((ci.SignalSource & SignalSource.DvbT) == SignalSource.DvbT) - ci.ChannelOrTransponder = LookupData.Instance.GetDvbtTransponder(ci.FreqInMhz).ToString(); - else if ((ci.SignalSource & SignalSource.DvbC) == SignalSource.DvbC) - ci.ChannelOrTransponder = LookupData.Instance.GetDvbcTransponder(ci.FreqInMhz).ToString(); + if ((ci.SignalSource & SignalSource.DvbT) == SignalSource.DvbT) + ci.ChannelOrTransponder = LookupData.Instance.GetDvbtTransponder(ci.FreqInMhz).ToString(); + else if ((ci.SignalSource & SignalSource.DvbC) == SignalSource.DvbC) + ci.ChannelOrTransponder = LookupData.Instance.GetDvbcTransponder(ci.FreqInMhz).ToString(); #if LOCK_LCN_LISTS // make the current list read-only if LCN is used @@ -342,17 +342,19 @@ namespace ChanSort.Loader.Hisense this.channelLists[x - 1].ReadOnly = true; } #endif - }); + }); } } - private void LoadSvlData(SQLiteCommand cmd, int tableNr, string joinTable, string joinFields, Action enhanceChannelInfo) + private void LoadSvlData(SQLiteCommand cmd, int tableNr, string joinTable, string joinFields, + Action enhanceChannelInfo) { if (!this.tableNames.Contains(joinTable.Replace("#", tableNr.ToString()))) return; - cmd.CommandText = $"select svl_#.svl_rec_id, channel_id, svl_#.tsl_id, svl_#.tsl_rec_id, e_serv_type, ac_name, nw_mask, prog_id, `t_desc.e_bcst_medium` {joinFields}" - + $" from svl_# inner join {joinTable} on {joinTable}.svl_rec_id=svl_#.svl_rec_id inner join tsl_# on tsl_#.tsl_rec_id=svl_#.tsl_rec_id"; + cmd.CommandText = + $"select svl_#.svl_rec_id, channel_id, svl_#.tsl_id, svl_#.tsl_rec_id, e_serv_type, ac_name, nw_mask, prog_id, `t_desc.e_bcst_medium` {joinFields}" + + $" from svl_# inner join {joinTable} on {joinTable}.svl_rec_id=svl_#.svl_rec_id inner join tsl_# on tsl_#.tsl_rec_id=svl_#.tsl_rec_id"; cmd.CommandText = cmd.CommandText.Replace("#", tableNr.ToString()); using (var r = cmd.ExecuteReader()) { @@ -368,7 +370,7 @@ namespace ChanSort.Loader.Hisense var bmedium = (BroadcastMedium) r.GetInt32(8); var ssource = DetermineSignalSource(bmedium, stype); - var ci = new ChannelInfo(ssource, id, prNr, name); + var ci = new Channel(ssource, id, prNr, name); if (trans != null) { ci.Transponder = trans; @@ -380,6 +382,8 @@ namespace ChanSort.Loader.Hisense } ci.ServiceId = sid; + ci.ChannelId = r.GetInt32(1); + ci.NwMask = (int)nwMask; //ci.Skip = (nwMask & NwMask.Active) == 0; ci.Lock = (nwMask & NwMask.Lock) != 0; @@ -412,20 +416,34 @@ namespace ChanSort.Loader.Hisense private void LoadFavorites(SQLiteCommand cmd) { + // detect schema used by fav_x tables + if (tableNames.Contains("fav_1")) + { + cmd.CommandText = "pragma table_info('fav_1')"; + using var r = cmd.ExecuteReader(); + while (r.Read()) + { + if (r.GetString(1) == "sortId") + this.hasCamelCaseFavSchema = true; + } + } + + // load the actual favorites data for (int i = 1; i <= 4; i++) { if (!this.tableNames.Contains($"fav_{i}")) continue; - cmd.CommandText = $"select ui2_svc_id, ui2_svc_rec_id, user_defined_ch_num from fav_{i}"; - using (var r = cmd.ExecuteReader()) + cmd.CommandText = hasCamelCaseFavSchema + ? $"select svlId, svlRecId, sortId from fav_{i}" + : $"select ui2_svc_id, ui2_svc_rec_id, cast(user_defined_ch_num as integer) from fav_{i}"; + + using var r = cmd.ExecuteReader(); + while (r.Read()) { - while (r.Read()) - { - var id = ((long) r.GetInt32(0) << 32) | (uint) r.GetInt32(1); - var ci = this.channelsById.TryGet(id); - if (ci != null) - ci.OldFavIndex[i - 1] = int.Parse(r.GetString(2)); - } + var id = ((long) r.GetInt32(0) << 32) | (uint) r.GetInt32(1); + var ci = this.channelsById.TryGet(id); + if (ci != null) + ci.OldFavIndex[i - 1] = r.GetInt32(2); } } } @@ -466,35 +484,43 @@ namespace ChanSort.Loader.Hisense if (tvOutputFile != this.FileName) File.Copy(this.FileName, tvOutputFile, true); - using (var conn = new SQLiteConnection("Data Source=" + tvOutputFile)) + using var conn = new SQLiteConnection("Data Source=" + tvOutputFile); + conn.Open(); + using var trans = conn.BeginTransaction(); + using var cmd = conn.CreateCommand(); + cmd.Transaction = trans; + try { - conn.Open(); - using (var trans = conn.BeginTransaction()) - using (var cmd = conn.CreateCommand()) + this.CreateFavTables(cmd); + + // must truncate and re-fill this table because there is a unique primary key constraint on a data column that needs to be edited + if (this.hasCamelCaseFavSchema) { - cmd.Transaction = trans; - try + for (int i = 1; i <= 4; i++) { - this.CreateFavTables(cmd); -#if !LOCK_LCN_LISTS - this.ResetLcn(cmd); -#endif - foreach (var list in this.DataRoot.ChannelLists) - { - if (list.ReadOnly) - continue; - foreach (var ci in list.Channels) - this.UpdateChannel(cmd, ci); - } - trans.Commit(); - this.FileName = tvOutputFile; - } - catch - { - trans.Rollback(); - throw; + cmd.CommandText = $"delete from fav_{i}"; + cmd.ExecuteNonQuery(); } } + +#if !LOCK_LCN_LISTS + this.ResetLcn(cmd); +#endif + foreach (var list in this.DataRoot.ChannelLists) + { + if (list.ReadOnly || list.IsMixedSourceFavoritesList) + continue; + foreach (var ci in list.Channels) + this.UpdateChannel(cmd, ci as Channel); + } + + trans.Commit(); + this.FileName = tvOutputFile; + } + catch + { + trans.Rollback(); + throw; } } @@ -508,7 +534,9 @@ namespace ChanSort.Loader.Hisense { if (!this.tableNames.Contains("fav_" + i)) { - cmd.CommandText = $"CREATE TABLE fav_{i} (ui2_svc_id INTEGER, ui2_svc_rec_id INTEGER, user_defined_ch_num VARCHAR, user_defined_ch_name VARCHAR)"; + cmd.CommandText = hasCamelCaseFavSchema + ? $"CREATE TABLE fav_{i} (sortId INTEGER, channelId INTEGER, svlId INTEGER, channelName VARCHAR(), svlRecId INTEGER, nwMask INTEGER, PREIMARY KEY (sortId)" + : $"CREATE TABLE fav_{i} (ui2_svc_id INTEGER, ui2_svc_rec_id INTEGER, user_defined_ch_num VARCHAR, user_defined_ch_name VARCHAR)"; cmd.ExecuteNonQuery(); this.tableNames.Add($"fav_{i}"); } @@ -535,10 +563,10 @@ namespace ChanSort.Loader.Hisense #region UpdateChannel() - private void UpdateChannel(SQLiteCommand cmd, ChannelInfo ci) + private void UpdateChannel(SQLiteCommand cmd, Channel ci) { - if (ci.IsProxy) - return; + if (ci == null || ci.IsProxy) + return; int x = (int) ((ulong) ci.RecordIndex >> 32); // the table number is kept in the higher 32 bits int id = (int) (ci.RecordIndex & 0xFFFFFFFF); // the record id is kept in the lower 32 bits @@ -547,26 +575,38 @@ namespace ChanSort.Loader.Hisense var setFlags = (NwMask) (((int) ci.Favorites & 0x0F) << 4); if (ci.Lock) setFlags |= NwMask.Lock; if (!ci.Hidden && ci.NewProgramNr >= 0) setFlags |= NwMask.Visible; + var nwMask = (int)(((NwMask)ci.NwMask & ~resetFlags) | setFlags); cmd.CommandText = $"update svl_{x} set channel_id=(channel_id&{0x3FFFF})|(@chnr << 18)" + $", ch_id_txt=@chnr || ' 0'" + $", ac_name=@name" + $", option_mask=option_mask|{(int) (OptionMask.ChNumEdited | OptionMask.NameEdited)}" + - $", nw_mask=(nw_mask&@resetFlags)|@setFlags" + + $", nw_mask=@nwMask" + $" where svl_rec_id=@id"; cmd.Parameters.Clear(); cmd.Parameters.Add("@id", DbType.Int32); cmd.Parameters.Add("@chnr", DbType.Int32); cmd.Parameters.Add("@name", DbType.String); - cmd.Parameters.Add("@resetFlags", DbType.Int32); - cmd.Parameters.Add("@setFlags", DbType.Int32); + cmd.Parameters.Add("@nwMask", DbType.Int32); cmd.Parameters["@id"].Value = id; cmd.Parameters["@chnr"].Value = ci.NewProgramNr; cmd.Parameters["@name"].Value = ci.Name; - cmd.Parameters["@resetFlags"].Value = ~(int) resetFlags; - cmd.Parameters["@setFlags"].Value = (int) setFlags; + cmd.Parameters["@nwMask"].Value = nwMask; cmd.ExecuteNonQuery(); + ci.NwMask = nwMask; + + if (this.hasCamelCaseFavSchema) + this.UpdateFavoritesWithCamelCaseColumnNames(cmd, ci); + else + this.UpdateFavoritesWithUnderlinedColumnNames(cmd, ci); + } + + #endregion + + #region UpdateFavoritesWithUnderlinedColumnNames() + private void UpdateFavoritesWithUnderlinedColumnNames(SQLiteCommand cmd, ChannelInfo ci) + { for (int i = 0; i < 4; i++) { if (ci.FavIndex[i] <= 0) @@ -594,7 +634,34 @@ namespace ChanSort.Loader.Hisense } } } - #endregion + + #region UpdateFavoritesWithCamelCaseColumnNames() + private void UpdateFavoritesWithCamelCaseColumnNames(SQLiteCommand cmd, Channel ci) + { + for (int i = 0; i < 4; i++) + { + if (ci.FavIndex[i] <= 0) + continue; + + cmd.CommandText = $"insert into fav_{i + 1} (sortId, channelId, svlId, channelName, svlRecId, nwMask) values (@chnr,@chanid,@svcid,@name,@recid,@nwmask)"; + cmd.Parameters.Clear(); + cmd.Parameters.Add("@chnr", DbType.Int32); + cmd.Parameters.Add("@chanid", DbType.Int32); + cmd.Parameters.Add("@svcid", DbType.Int32); + cmd.Parameters.Add("@name", DbType.String); + cmd.Parameters.Add("@recid", DbType.Int32); + cmd.Parameters.Add("@nwmask", DbType.Int32); + cmd.Parameters["@chnr"].Value = ci.FavIndex[i]; + cmd.Parameters["@chanid"].Value = ci.ChannelId; + cmd.Parameters["@name"].Value = ci.Name; + cmd.Parameters["@svcid"].Value = ci.RecordIndex >> 32; + cmd.Parameters["@recid"].Value = ci.RecordIndex & 0xFFFF; + cmd.Parameters["@nwmask"].Value = ci.NwMask; + cmd.ExecuteNonQuery(); + } + } + #endregion + } } diff --git a/source/ChanSort.Loader.M3u/Channel.cs b/source/ChanSort.Loader.M3u/Channel.cs index a309f69..4eac44e 100644 --- a/source/ChanSort.Loader.M3u/Channel.cs +++ b/source/ChanSort.Loader.M3u/Channel.cs @@ -1,14 +1,15 @@ -using ChanSort.Api; +using System.Collections.Generic; +using ChanSort.Api; namespace ChanSort.Loader.M3u { internal class Channel : ChannelInfo { - public string Uri { get; } + public List Lines { get; } - public Channel(int index, int progNr, string name, string uri) : base(SignalSource.IP, index, progNr, name) + public Channel(int index, int progNr, string name, List lines) : base(SignalSource.IP, index, progNr, name) { - this.Uri = uri; + this.Lines = lines; } } } diff --git a/source/ChanSort.Loader.M3u/Serializer.cs b/source/ChanSort.Loader.M3u/Serializer.cs index 002946f..bad1f1c 100644 --- a/source/ChanSort.Loader.M3u/Serializer.cs +++ b/source/ChanSort.Loader.M3u/Serializer.cs @@ -8,11 +8,21 @@ using ChanSort.Api; namespace ChanSort.Loader.M3u { + /* + * This serializer reads .m3u files that are used for SAT>IP lists. Some hardware SAT>IP servers use this format as well as VNC. + * There is no official standard for .m3u and files may have a UTF-8 BOM or not, may be encoded in UTF-8 or a locale specific encoding and my have different new-line sequences. + * This loader attempts to maintain the original file as much as possible, including comment lines that are not directly understood by ChanSort. + */ class Serializer : SerializerBase { + private static readonly Regex ExtInfRegex = new Regex(@"^#EXTINF:\d+,(?:(\d+)\. )?(.*)$"); + private readonly ChannelList allChannels = new ChannelList(SignalSource.IP, "All"); - private List lines = new List(); + private Encoding overrideEncoding; + private string newLine = "\r\n"; + private string headerLine; + private List trailingLines = new List(); // comment and blank lines after the last URI line #region ctor() public Serializer(string inputFile) : base(inputFile) @@ -38,113 +48,142 @@ namespace ChanSort.Loader.M3u #region Load() public override void Load() { - var rdr = new StreamReader(this.FileName); - var header = rdr.ReadLine()?.TrimEnd(); - if (header != "#EXTM3U") + // read file as binary and detect optional BOM and UTF-8 encoding + var content = File.ReadAllBytes(this.FileName); + if (Tools.HasUtf8Bom(content)) + overrideEncoding = Encoding.UTF8; + else if (Tools.IsUtf8(content)) + overrideEncoding = new UTF8Encoding(false); + + // detect line separator + int idx = Array.IndexOf(content, '\n'); + this.newLine = idx >= 1 && content[idx] - 1 == '\r' ? "\r\n" : "\n"; + + var rdr = new StreamReader(new MemoryStream(content), overrideEncoding ?? this.DefaultEncoding); + this.headerLine = rdr.ReadLine()?.TrimEnd(); + if (this.headerLine == null || this.headerLine != "#EXTM3U") throw new FileLoadException("Unsupported .m3u file: " + this.FileName); - int lineNr=0; - string line1, line2; - while ((line1 = rdr.ReadLine()) != null) + // read lines until a non-comment non-empty line is found and then create a channel for the block + int lineNr = 1; + string line, extInfLine = null; + var lines = new List(); + while ((line = rdr.ReadLine()) != null) { ++lineNr; - if (line1.Trim() == "") + lines.Add(line); + + if (line.Trim() == "") continue; - var lineNr1 = lineNr; - while ((line2 = rdr.ReadLine()) != null) + if (line.StartsWith("#EXTINF:")) + extInfLine = line; + + if (!line.StartsWith("#")) { - ++lineNr; - if (line2.Trim() != "") - { - ReadChannel(lineNr1, line1, line2); - break; - } + ReadChannel(lineNr, line, extInfLine, lines); + lines = new List(); + extInfLine = null; } } + + this.trailingLines = lines; } #endregion #region ReadChannel() - private static readonly Regex ExtInfRegex = new Regex(@"^#EXTINF:\d+,(?:(\d+)\. )?(.*)$"); - private void ReadChannel(int lineNr, string line1, string line2) + private void ReadChannel(int uriLineNr, string uriLine, string extInfLine, List allLines) { - var match = ExtInfRegex.Match(line1); - if (!match.Success) - throw new FileLoadException($"Unsupported #EXTINF line #{lineNr}: {line1}"); + int progNr = 0; + string name = ""; - int progNr = string.IsNullOrEmpty(match.Groups[2].Value) - ? this.allChannels.Count + 1 - : this.ParseInt(match.Groups[1].Value); + if (extInfLine != null) + { + var match = ExtInfRegex.Match(extInfLine); + if (match.Success) + { + if (!string.IsNullOrEmpty(match.Groups[2].Value)) + progNr = this.ParseInt(match.Groups[1].Value); + name = match.Groups[2].Value; + } + } + + if (progNr == 0) + progNr = this.allChannels.Count + 1; + + var chan = new Channel(uriLineNr, progNr, name, allLines); - Uri uri; try { - uri = new Uri(line2); + var uri = new Uri(uriLine); + chan.Satellite = uri.GetLeftPart(UriPartial.Path); + var parms = HttpUtility.ParseQueryString(uri.Query); + foreach (var key in parms.AllKeys) + { + var val = parms.Get(key); + switch (key) + { + case "freq": + chan.FreqInMhz = this.ParseInt(val); + break; + case "pol": + if (val.Length == 1) + chan.Polarity = Char.ToUpper(val[0]); + break; + case "sr": + chan.SymbolRate = this.ParseInt(val); + break; + case "pids": + var pids = val.Split(','); + //if (pids.Length > 3) + // chan.PcrPid = this.ParseInt(pids[3]); + if (pids.Length > 4) + chan.VideoPid = this.ParseInt(pids[4]); + if (pids.Length > 5) + chan.AudioPid = this.ParseInt(pids[5]); + break; + } + } + + if (name == "") + chan.Name = chan.FreqInMhz + chan.Polarity + " " + chan.VideoPid; } catch { - throw new FileLoadException($"Unsupported URI in line #{lineNr}: {line2}"); - } - - var chan = new Channel(lineNr, progNr, match.Groups[2].Value, line2); - chan.Satellite = uri.GetLeftPart(UriPartial.Path); - var parms = HttpUtility.ParseQueryString(uri.Query); - foreach (var key in parms.AllKeys) - { - var val = parms.Get(key); - switch (key) - { - case "freq": - chan.FreqInMhz = this.ParseInt(val); - break; - case "pol": - if (val.Length == 1) - chan.Polarity = Char.ToUpper(val[0]); - break; - case "sr": - chan.SymbolRate = this.ParseInt(val); - break; - case "pids": - var pids = val.Split(','); - //if (pids.Length > 3) - // chan.PcrPid = this.ParseInt(pids[3]); - if (pids.Length > 4) - chan.VideoPid = this.ParseInt(pids[4]); - if (pids.Length > 5) - chan.AudioPid = this.ParseInt(pids[5]); - break; - } + if (name == "") + chan.Name = uriLine; } this.DataRoot.AddChannel(this.allChannels, chan); } #endregion - #region DefaultEncoding - public override Encoding DefaultEncoding - { - get => base.DefaultEncoding; // set to UTF-8 without BOM in constructor - set { } // can't be changed - } - #endregion - - #region Save() public override void Save(string tvOutputFile) { - using var file = new StreamWriter(new FileStream(tvOutputFile, FileMode.Create), this.DefaultEncoding); - file.WriteLine("#EXTM3U"); + using var file = new StreamWriter(new FileStream(tvOutputFile, FileMode.Create), this.overrideEncoding ?? this.DefaultEncoding); + file.NewLine = this.newLine; + + file.WriteLine(this.headerLine); + foreach (ChannelInfo channel in this.allChannels.GetChannelsByNewOrder()) { // when a reference list was applied, the list may contain proxy entries for deleted channels, which must be ignored if (channel is Channel chan && !channel.IsDeleted) { - file.WriteLine($"#EXTINF:0,{chan.NewProgramNr}. {chan.Name}"); - file.WriteLine(chan.Uri); + foreach (var line in chan.Lines) + { + if (line.StartsWith("#EXTINF:")) + file.WriteLine($"#EXTINF:0,{chan.NewProgramNr}. {chan.Name}"); + else + file.WriteLine(line); + } } } + foreach(var line in this.trailingLines) + file.WriteLine(line); + this.FileName = tvOutputFile; } #endregion diff --git a/source/changelog.md b/source/changelog.md index 9560f72..0c1a667 100644 --- a/source/changelog.md +++ b/source/changelog.md @@ -3,6 +3,9 @@ ChanSort Change Log 2020-01-02 - added support for m3u lists (SAT>IP, VLC, WinAmp, ...) +- added support for Hisense H50B7700UW (and maybe others which use the same favorite list table schema) +- fixed missing DLLs with spanish translation +- fixed polarity display for Samsung (caused by a stale .ini file in the package) - disabled "Lock" toggle button when the list does not support parental locks 2020-01-01 diff --git a/source/makeDistribZip.cmd b/source/makeDistribZip.cmd index 2fd2790..dff0302 100644 --- a/source/makeDistribZip.cmd +++ b/source/makeDistribZip.cmd @@ -16,11 +16,13 @@ mkdir "%target%\de" 2>nul mkdir "%target%\pt" 2>nul mkdir "%target%\ru" 2>nul mkdir "%target%\cs" 2>nul +mkdir "%target%\es" 2>nul mkdir "%target%\ReferenceLists" 2>nul xcopy /siy debug\de "%target%\de" xcopy /siy debug\pt "%target%\pt" xcopy /siy debug\ru "%target%\ru" xcopy /siy debug\cs "%target%\cs" +xcopy /siy debug\es "%target%\es" xcopy /siy ChanSort\ReferenceLists\* "%target%\ReferenceLists" copy ..\readme.md "%target%\readme.txt" copy changelog.md "%target%\changelog.txt" @@ -45,7 +47,7 @@ rem ----------------------------- set signtool="C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe" set oldcd=%cd% cd %target% -set files=ChanSort.exe ChanSort*.dll de\ChanSort*.dll ru\ChanSort*.dll pt\ChanSort*.dll cs\ChanSort*.dll +set files=ChanSort.exe ChanSort*.dll de\ChanSort*.dll ru\ChanSort*.dll pt\ChanSort*.dll cs\ChanSort*.dll es\ChanSort*.dll %signtool% sign /a /t "http://timestamp.comodoca.com/authenticode" %files% if errorlevel 1 goto :error cd %oldcd%