- basic support for Enigma2 (Dreambox, Vu+,...) channel lists

- dynamic number of favorite lists (still limited to 64 due to bitmask)
This commit is contained in:
Horst Beham
2021-03-07 16:12:21 +01:00
parent bc4b650f20
commit cb1fb9db5d
17 changed files with 608 additions and 23 deletions

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{4AD7F77E-617C-4741-82AE-E7A41C85EE4D}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ChanSort.Loader.Enigma2</RootNamespace>
<AssemblyName>ChanSort.Loader.Enigma2</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>latest</LangVersion>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<LangVersion>latest</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<LangVersion>latest</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Channel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Enigma2Plugin.cs" />
<Compile Include="Serializer.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChanSort.Api\ChanSort.Api.csproj">
<Project>{dccffa08-472b-4d17-bb90-8f513fc01392}</Project>
<Name>ChanSort.Api</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,29 @@
using ChanSort.Api;
namespace ChanSort.Loader.Enigma2
{
internal class Channel : ChannelInfo
{
/// <summary>
/// first two fields of the lamedb entry
/// </summary>
public string Prefix { get; set; }
/// <summary>
/// For DVB-S it is the orbital position * 10 (e.g. 192 for Astra 19.2E) * 65536
/// </summary>
public int DvbNamespace { get; set; }
public int ServiceNumber { get; set; }
/// <summary>
/// all fields after the DVB-namespace in the lamedb entry
/// </summary>
public string Suffix { get; set; }
/// <summary>
/// #DESCRIPTION of the userbouquet entry
/// </summary>
public string Description { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using ChanSort.Api;
namespace ChanSort.Loader.Enigma2
{
public class Enigma2Plugin : ISerializerPlugin
{
public string DllName { get; set; }
public string PluginName => "Enigma2 (Linux Receiver)";
public string FileFilter => "bouquets.*";
public SerializerBase CreateSerializer(string inputFile)
{
return new Serializer(inputFile);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("ChanSort.Loader.Enigma2")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ChanSort.Loader.Enigma2")]
[assembly: AssemblyCopyright("Copyright © 2021")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("4ad7f77e-617c-4741-82ae-e7a41c85ee4d")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using ChanSort.Api;
namespace ChanSort.Loader.Enigma2
{
/*
* This class loads "userbouquet.*" channel lists from Enigma2 Linux set-top-boxes (Dreambox, Vu+, ...)
*
* lamedb version 4 format: https://www.satsupreme.com/showthread.php/194074-Lamedb-format-explained
* userbouqet.* format: https://www.opena.tv/english-section/43964-iptv-service-4097-service-1-syntax.html#post376271
*/
internal class Serializer : SerializerBase
{
private static readonly Encoding utf8WithoutBom = new UTF8Encoding(false);
private ChannelList tv = new ChannelList(SignalSource.Digital | SignalSource.Tv, "TV");
private ChannelList radio = new ChannelList(SignalSource.Digital | SignalSource.Radio, "Radio");
private readonly List<string> favListFileNames = new();
private readonly Dictionary<string, Transponder> transponderByLamedbId = new();
private readonly Dictionary<string, Channel> channelsByBouquetId = new();
private DvbStringDecoder decoder;
private readonly StringBuilder log = new();
#region ctor()
public Serializer(string inputFile) : base(inputFile)
{
this.FileName = Path.Combine(Path.GetDirectoryName(inputFile), "lamedb");
this.Features.ChannelNameEdit = ChannelNameEditMode.None;
this.Features.DeleteMode = DeleteMode.Physically;
this.Features.CanSkipChannels = false;
this.Features.CanLockChannels = false;
this.Features.CanHideChannels = false;
this.Features.MixedSourceFavorites = true;
this.Features.SortedFavorites = true;
this.Features.SupportedFavorites = 0; // dynamically added
this.Features.CanSaveAs = false;
this.tv.IsMixedSourceFavoritesList = true;
this.DataRoot.AddChannelList(this.tv);
this.radio.IsMixedSourceFavoritesList = true;
this.DataRoot.AddChannelList(this.radio);
// hide columns for fields that don't exist in Silva-Schneider channel list
foreach (var list in this.DataRoot.ChannelLists)
{
list.VisibleColumnFieldNames.Remove("PcrPid");
list.VisibleColumnFieldNames.Remove("VideoPid");
list.VisibleColumnFieldNames.Remove("AudioPid");
list.VisibleColumnFieldNames.Remove("Lock");
list.VisibleColumnFieldNames.Remove("Skip");
list.VisibleColumnFieldNames.Remove("Hidden");
list.VisibleColumnFieldNames.Remove("Encrypted");
list.VisibleColumnFieldNames.Remove("Favorites");
list.VisibleColumnFieldNames.Remove("ServiceType");
list.VisibleColumnFieldNames.Add("ServiceTypeName");
}
}
#endregion
#region Load()
public override void Load()
{
this.decoder = new DvbStringDecoder(this.DefaultEncoding);
this.LoadLamedb();
int favId = 0;
foreach(var file in Directory.GetFiles(Path.GetDirectoryName(this.FileName), "userbouquet.*"))
this.LoadBouquet(file, ref favId);
}
#endregion
#region LoadLamedb()
private void LoadLamedb()
{
var path = Path.Combine(Path.GetDirectoryName(this.FileName), "lamedb");
if (!File.Exists(path))
throw new FileLoadException($"Could not find required file \"{path}\"");
using var r = new StreamReader(File.OpenRead(path), utf8WithoutBom);
var line = r.ReadLine();
if (line != "eDVB services /4/")
throw new FileLoadException($"lamedb version 4 is required");
string mode = null;
Transponder tp = null;
int tpId = 0, chanId = 0;
while ((line = r.ReadLine()) != null)
{
if (line.Trim() == "")
continue;
if (line == "transponders")
mode = line;
else if (line == "services")
mode = "services";
else if (line == "end")
mode = null;
else if (mode == "transponders")
tp = ReadLamedbTransponderLine(line, tp, ref tpId);
else if (mode == "services")
ReadLamedbServiceLine(line, r, ref chanId);
}
}
#endregion
#region ReadLamedbTransponderLine
private Transponder ReadLamedbTransponderLine(string line, Transponder tp, ref int tpId)
{
if (line == "/")
return tp;
if (!line.StartsWith("\t"))
{
tp = new Transponder(++tpId);
this.transponderByLamedbId[line] = tp;
var parts = line.Split(':');
tp.Number = FromHex(parts[0]);
tp.TransportStreamId = FromHex(parts[1]);
tp.OriginalNetworkId = FromHex(parts[2]);
}
else
{
if (line[1] == 's')
{
var parts = line.Substring(3).Split(':');
tp.FrequencyInMhz = int.Parse(parts[0]) / 1000;
tp.SymbolRate = int.Parse(parts[1]) / 1000;
tp.Polarity = "HVLR"[int.Parse(parts[2])];
tp.Satellite = GetOrCreateSatellite(int.Parse(parts[4]));
}
}
return tp;
}
#endregion
#region GetOrCreateSatellite
private Satellite GetOrCreateSatellite(int orbitalPos)
{
if (this.DataRoot.Satellites.TryGetValue(orbitalPos, out var sat))
return sat;
sat = new Satellite(orbitalPos);
sat.OrbitalPosition = $"{(float) Math.Abs(orbitalPos) / 10:0.0}{(orbitalPos<0?'W':'E')}";
sat.Name = sat.OrbitalPosition;
this.DataRoot.Satellites.Add(orbitalPos, sat);
return sat;
}
#endregion
#region ReadLamedbServiceLine
private void ReadLamedbServiceLine(string line, StreamReader r, ref int chanId)
{
var line2 = r.ReadLine();
var line3 = r.ReadLine();
if (line2 == null || line3 == null)
return;
var ch = new Channel();
ch.SignalSource = SignalSource.Digital;
// line 1: SID:DvbNamespace:TSID:ONID:ServiceType:ServiceNumber
var parts = line.Split(':');
ch.RecordIndex = chanId;
ch.RecordOrder = chanId;
ch.OldProgramNr = ++chanId;
ch.NewProgramNr = chanId;
ch.IsDeleted = false;
ch.ServiceId = FromHex(parts[0]);
ch.DvbNamespace = FromHex(parts[1]);
ch.TransportStreamId = FromHex(parts[2]);
ch.OriginalNetworkId = FromHex(parts[3]);
ch.ServiceType = int.Parse(parts[4]);
ch.ServiceNumber = int.Parse(parts[5]);
ch.SignalSource |= LookupData.Instance.IsRadioTvOrData(ch.ServiceType);
var tpId = parts[1] + ":" + parts[2] + ":" + parts[3];
if (this.transponderByLamedbId.TryGetValue(tpId, out var tp))
{
ch.Satellite = tp.Satellite?.Name;
ch.SymbolRate = tp.SymbolRate;
ch.FreqInMhz = tp.FrequencyInMhz;
ch.Polarity = tp.Polarity;
}
// line 2: channel name (in raw DVB encoding)
var rawName = new byte[line2.Length];
for (int i = 0, c = rawName.Length; i < c; i++)
rawName[i] = (byte)line2[i];
this.decoder.GetChannelNames(rawName, 0, rawName.Length, out var longName, out var shortName);
ch.Name = longName;
ch.ShortName = shortName;
// line 3: provider and other info
parts = line3.Split(',');
foreach (var part in parts)
{
var keyVal = part.Split(new char[] {':'}, 2);
switch (keyVal[0])
{
case "p":
ch.Provider = keyVal[1];
break;
}
}
this.DataRoot.AddChannel(this.tv, ch);
this.channelsByBouquetId[$"{ch.DvbNamespace}:{ch.OriginalNetworkId}:{ch.TransportStreamId}:{ch.ServiceId}"] = ch;
}
#endregion
#region LoadBoquet
private void LoadBouquet(string file, ref int favIndex)
{
ChannelList list;
if (file.EndsWith(".tv"))
list = this.tv;
else if (file.EndsWith(".radio"))
list = this.radio;
else
return;
using var r = new StreamReader(File.OpenRead(file), utf8WithoutBom);
var line = r.ReadLine();
if (line == null || !line.StartsWith("#NAME "))
{
log.AppendLine($"{file} does not start with #NAME");
return;
}
this.DataRoot.SetFavListCaption(favIndex, line.Substring(6));
this.Features.SupportedFavorites = (Favorites)((int)this.Features.SupportedFavorites<<1)|Favorites.A;
int lineNr = 0;
int progNr = 0;
Channel ch = null;
while ((line = r.ReadLine()) != null)
{
++lineNr;
if (line.Trim() == "")
continue;
if (line.Contains(":FROM") && line.Contains("ORDER BY")) // ignore the root-level bouquet that only references other bouquet files
return;
if (line.StartsWith("#DESCRIPTION "))
{
if (ch != null)
ch.Description = line.Substring(13);
continue;
}
if (!line.StartsWith("#SERVICE "))
continue;
var parts = line.Substring(9).Split(':');
if (parts[0] != "1") // ignore non-DVB
continue;
if (parts[1] != "0") // ignore special-purpose rows
continue;
var prefix = parts[0] + ":" + parts[1];
// parts[2] = DVB service type
var sid = FromHex(parts[3]);
var tsid = FromHex(parts[4]);
var onid = FromHex(parts[5]);
var dvbNamespace = FromHex(parts[6]);
var suffix = "";
for (int i = 7; i < parts.Length; i++)
suffix += ":" + parts[i];
var key = $"{dvbNamespace}:{onid}:{tsid}:{sid}";
if (!this.channelsByBouquetId.TryGetValue(key, out ch))
{
log.AppendLine($"{file} line {lineNr}: service not found in lamedb");
continue;
}
ch.Prefix = prefix;
ch.Suffix = suffix;
ch.SetOldPosition(1+favIndex, ++progNr);
}
this.favListFileNames.Add(file);
++favIndex;
}
#endregion
#region FromHex()
private int FromHex(string str)
{
int result = 0;
foreach (var ch in str)
{
if (Char.IsWhiteSpace(ch))
continue;
result <<= 4;
if (ch >= '0' && ch <= '9')
result += ch - '0';
else if (ch >= 'A' && ch <= 'F')
result += ch - 'A' + 10;
else if (ch >= 'a' && ch <= 'f')
result += ch - 'a' + 10;
else
throw new ArgumentException(str + " contains invalid hex characters");
}
return result;
}
#endregion
#region GetDataFilePaths()
public override IEnumerable<string> GetDataFilePaths()
{
var list = new List<string>(this.favListFileNames.Count + 1);
list.Add(this.FileName); // lamedb
list.AddRange(this.favListFileNames); // userbouquet*
return list;
}
#endregion
#region Save()
public override void Save(string tvOutputFile)
{
for (int favIndex = 0; favIndex < this.favListFileNames.Count; favIndex++)
{
var file = this.favListFileNames[favIndex];
using var w = new StreamWriter(File.OpenWrite(file), utf8WithoutBom);
w.WriteLine($"#NAME {this.DataRoot.GetFavListCaption(favIndex)}");
foreach (var ch in this.tv.Channels.OrderBy(c => c.GetPosition(favIndex+1)))
{
if (!(ch is Channel c) || c.GetPosition(favIndex + 1) < 0)
continue;
w.WriteLine($"#SERVICE {c.Prefix}:{c.ServiceType:X}:{c.ServiceId:X}:{c.TransportStreamId:X}:{c.OriginalNetworkId:X}:{c.DvbNamespace:X}{c.Suffix}");
if (c.Description != null)
w.WriteLine($"#DESCRIPTION {c.Description}");
}
}
}
#endregion
#region GetFileInformation()
public override string GetFileInformation()
{
var sb = new StringBuilder();
sb.Append(base.GetFileInformation());
sb.AppendLine();
sb.Append(this.log);
return sb.ToString();
}
#endregion
}
}