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%