mirror of
https://github.com/PredatH0r/ChanSort.git
synced 2026-02-20 21:36:49 +01:00
- rewrote m3u loader to handle arbitrary lines in the file and preserve encoding and line endings
- support for Hisense channel.db format of the H50B7700UW, which has a different schema in the fav_x tables - added spanish translation to distribution packages
This commit is contained in:
@@ -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<string> lines = new List<string>();
|
||||
private Encoding overrideEncoding;
|
||||
private string newLine = "\r\n";
|
||||
private string headerLine;
|
||||
private List<string> trailingLines = new List<string>(); // 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<string>();
|
||||
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<string>();
|
||||
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<string> 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
|
||||
|
||||
Reference in New Issue
Block a user