- added partial support for Philips .xml channel lists

(There are MANY different file formats, only a few are currently supported)
- fixed "most-recently-used" getting reversed every time the program was started
- added "UTF-8 (Unicode)" character set to menu
- fixed disappearing columns when loading different channel lists without restarting the application
This commit is contained in:
hbeham
2019-08-05 23:50:53 +02:00
parent 55a7a1a048
commit 25b2df7734
17 changed files with 815 additions and 21 deletions

View File

@@ -0,0 +1,74 @@
<?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>{D7BAFD55-50F5-46C3-A76B-2193BED5358F}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ChanSort.Loader.PhilipsXml</RootNamespace>
<AssemblyName>ChanSort.Loader.PhilipsXml</AssemblyName>
<TargetFrameworkVersion>v4.0</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>
</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>
</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>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</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.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Channel.cs" />
<Compile Include="CustomXmlWriter.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Serializer.cs" />
<Compile Include="SerializerPlugin.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,20 @@
using System.Xml;
using ChanSort.Api;
namespace ChanSort.Loader.PhilipsXml
{
internal class Channel : ChannelInfo
{
public readonly XmlNode SetupNode;
public string RawName;
public string RawSatellite;
internal Channel(SignalSource source, int order, int rowId, XmlNode setupNode)
{
this.SignalSource = source;
this.RecordOrder = order;
this.RecordIndex = rowId;
this.SetupNode = setupNode;
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.IO;
using System.Xml;
namespace ChanSort.Loader.PhilipsXml
{
/// <summary>
/// This XmlWriter replaces some characters with Char- or Entity- references the same way
/// they are escaped in the original Sony XML files
/// </summary>
class CustomXmlWriter : XmlWriter
{
private static readonly char[] CharsToEscape = { '\'', '\"', '&', '<', '>' };
private static readonly string[] CharEntites = { "apos", "quot", "amp", "lt", "gt" };
private XmlWriter w;
private readonly bool escapeAsEntityRef; // if true, use &amp; otherwise &#34;
public CustomXmlWriter(TextWriter tw, XmlWriterSettings settings, bool useEntityRef)
{
this.w = XmlWriter.Create(tw, settings);
this.escapeAsEntityRef = useEntityRef;
}
public override void WriteString(string text)
{
int i = 0, j;
while ((j = text.IndexOfAny(CharsToEscape, i)) >= 0)
{
this.w.WriteString(text.Substring(i, j - i));
if (this.escapeAsEntityRef)
{
// => &amp;
int k = Array.IndexOf(CharsToEscape, text[j]);
this.w.WriteEntityRef(CharEntites[k]);
}
else
{
// => &#38;
//this.w.WriteCharEntity(text[j]);
this.w.WriteRaw("&#" + (int)text[j] + ";");
}
i = j + 1;
}
this.w.WriteString(text.Substring(i));
}
#region 1:1 delegation
public override void WriteStartDocument()
{
this.w.WriteStartDocument();
}
public override void WriteStartDocument(bool standalone)
{
this.w.WriteStartDocument(standalone);
}
public override void WriteEndDocument()
{
this.w.WriteEndDocument();
}
public override void WriteDocType(string name, string pubid, string sysid, string subset)
{
this.w.WriteDocType(name, pubid, sysid, subset);
}
public override void WriteStartElement(string prefix, string localName, string ns)
{
this.w.WriteStartElement(prefix, localName, ns);
}
public override void WriteEndElement()
{
this.w.WriteEndElement();
}
public override void WriteFullEndElement()
{
this.w.WriteFullEndElement();
}
public override void WriteStartAttribute(string prefix, string localName, string ns)
{
this.w.WriteStartAttribute(prefix, localName, ns);
}
public override void WriteEndAttribute()
{
this.w.WriteEndAttribute();
}
public override void WriteCData(string text)
{
this.w.WriteCData(text);
}
public override void WriteComment(string text)
{
this.w.WriteComment(text);
}
public override void WriteProcessingInstruction(string name, string text)
{
this.w.WriteProcessingInstruction(name, text);
}
public override void WriteEntityRef(string name)
{
this.w.WriteEntityRef(name);
}
public override void WriteCharEntity(char ch)
{
this.w.WriteCharEntity(ch);
}
public override void WriteWhitespace(string ws)
{
this.w.WriteWhitespace(ws);
}
public override void WriteSurrogateCharEntity(char lowChar, char highChar)
{
this.w.WriteSurrogateCharEntity(lowChar, highChar);
}
public override void WriteChars(char[] buffer, int index, int count)
{
this.w.WriteChars(buffer, index, count);
}
public override void WriteRaw(char[] buffer, int index, int count)
{
this.w.WriteRaw(buffer, index, count);
}
public override void WriteRaw(string data)
{
this.w.WriteRaw(data);
}
public override void WriteBase64(byte[] buffer, int index, int count)
{
this.w.WriteBase64(buffer, index, count);
}
public override void Close()
{
this.w.Close();
}
public override void Flush()
{
this.w.Flush();
}
public override string LookupPrefix(string ns)
{
return this.w.LookupPrefix(ns);
}
public override WriteState WriteState => this.w.WriteState;
#endregion
}
}

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.PhilipsXml")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ChanSort.Loader.PhilipsXml")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[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("d7bafd55-50f5-46c3-a76b-2193bed5358f")]
// 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,408 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using ChanSort.Api;
namespace ChanSort.Loader.PhilipsXml
{
/*
This loader supports 2 different kinds of XML files from Philips.
I had a test file "DVBS.xml" with satellite channels and entries like:
<Channel>
<Setup SatelliteName="0x54 0x00 0x55 0x00 0x52 0x00 0x4B 0x00 0x53 0x00 0x41 0x00 0x54 0x00 0x20 0x00 0x34 0x00 0x32 0x00 0x45 0x00 " ChannelNumber="1" ChannelName="0x54 0x00 0xC4 0x00 0xB0 0x00 0x56 0x00 0xC4 0x00 0xB0 0x00 0x42 0x00 0x55 0x00 0x20 0x00 0x53 0x00 0x50 0x00 0x4F 0x00 0x52 0x00 " ChannelLock="0" UserModifiedName="0" LogoID="0" UserModifiedLogo="0" LogoLock="0" UserHidden="0" FavoriteNumber="0" />
<Broadcast ChannelType="3" Onid="1070" Tsid="43203" Sid="16001" Frequency="11794" Modulation="0" ServiceType="1" SymbolRate="27507" LNBNumber="38" Polarization="0" SystemHidden="0" />
</Channel>
The other file was "CM_TPM1013E_LA_CK.xml" with entries like:
<Channel>
<Setup oldpresetnumber="1" presetnumber="1" name="Das Erste" ></Setup>
<Broadcast medium="dvbc" frequency="410000" system="west" serviceID="1" ONID="41985" TSID="1101" modulation="256" symbolrate="6901000" bandwidth="Unknown"></Broadcast>
</Channel>
*/
class Serializer : SerializerBase
{
private readonly ChannelList terrChannels = new ChannelList(SignalSource.DvbT | SignalSource.Tv | SignalSource.Radio, "DVB-T");
private readonly ChannelList cableChannels = new ChannelList(SignalSource.DvbC | SignalSource.Tv | SignalSource.Radio, "DVB-C");
private readonly ChannelList satChannels = new ChannelList(SignalSource.DvbS | SignalSource.Tv | SignalSource.Radio, "DVB-S");
private ChannelList curList;
private XmlDocument doc;
private byte[] content;
private string textContent;
private string newline;
private int formatVersion;
#region ctor()
public Serializer(string inputFile) : base(inputFile)
{
this.Features.ChannelNameEdit = ChannelNameEditMode.All;
this.Features.CanDeleteChannels = true;
this.DataRoot.AddChannelList(this.terrChannels);
this.DataRoot.AddChannelList(this.cableChannels);
this.DataRoot.AddChannelList(this.satChannels);
foreach (var list in this.DataRoot.ChannelLists)
{
list.VisibleColumnFieldNames.Remove("PcrPid");
list.VisibleColumnFieldNames.Remove("VideoPid");
list.VisibleColumnFieldNames.Remove("AudioPid");
list.VisibleColumnFieldNames.Remove("Skip");
list.VisibleColumnFieldNames.Remove("ShortName");
list.VisibleColumnFieldNames.Remove("Provider");
}
}
#endregion
#region DisplayName
public override string DisplayName => "Philips *.xml loader";
#endregion
#region Load()
public override void Load()
{
bool fail = false;
try
{
this.doc = new XmlDocument();
this.content = File.ReadAllBytes(this.FileName);
this.textContent = Encoding.UTF8.GetString(this.content);
this.newline = this.textContent.Contains("\r\n") ? "\r\n" : "\n";
var settings = new XmlReaderSettings
{
CheckCharacters = false,
IgnoreProcessingInstructions = true,
ValidationFlags = XmlSchemaValidationFlags.None,
DtdProcessing = DtdProcessing.Ignore
};
using (var reader = XmlReader.Create(new StringReader(textContent), settings))
{
doc.Load(reader);
}
}
catch
{
fail = true;
}
var root = doc.FirstChild;
if (root is XmlDeclaration)
root = root.NextSibling;
if (fail || root == null || root.LocalName != "ChannelMap")
throw new FileLoadException("\"" + this.FileName + "\" is not a supported Philips XML file");
int rowId = 0;
foreach (XmlNode child in root.ChildNodes)
{
switch (child.LocalName)
{
case "Channel":
if (rowId == 0)
this.DetectFormatAndFeatures(child);
if (this.curList != null)
this.ReadChannel(child, rowId++);
break;
}
}
}
#endregion
#region DetectFormatAndFeatures()
private void DetectFormatAndFeatures(XmlNode node)
{
var setupNode = node["Setup"] ?? throw new FileLoadException("Missing Setup XML element");
var bcastNode = node["Broadcast"] ?? throw new FileLoadException("Missing Broadcast XML element");
var fname = Path.GetFileNameWithoutExtension(this.FileName).ToLower();
var medium = bcastNode.GetAttribute("medium");
if (medium == "" && fname.Length == 4 && fname.StartsWith("dvb"))
medium = fname;
bool hasEncrypt = false;
if (setupNode.HasAttribute("ChannelName"))
{
this.formatVersion = 1;
this.DataRoot.SupportedFavorites = Favorites.A;
this.DataRoot.SortedFavorites = true;
var dtype = bcastNode.GetAttribute("DecoderType");
if (dtype == "1")
medium = "dvbt";
else if (dtype == "2")
medium = "dvbc";
else if (dtype == "3")
medium = "dvbs";
hasEncrypt = setupNode.HasAttribute("Scrambled");
}
else if (setupNode.HasAttribute("name"))
{
this.formatVersion = 2;
this.DataRoot.SupportedFavorites = 0;
this.DataRoot.SortedFavorites = false;
foreach (var list in this.DataRoot.ChannelLists)
{
list.VisibleColumnFieldNames.Remove("Favorites");
list.VisibleColumnFieldNames.Remove("Lock");
list.VisibleColumnFieldNames.Remove("Hidden");
list.VisibleColumnFieldNames.Remove("ServiceType");
list.VisibleColumnFieldNames.Remove("ServiceTypeName");
list.VisibleColumnFieldNames.Remove("Encrypted");
}
}
else
throw new FileLoadException("Unknown data format");
switch (medium)
{
case "dvbt":
this.curList = this.terrChannels;
break;
case "dvbc":
this.curList = this.cableChannels;
break;
case "dvbs":
this.curList = this.satChannels;
break;
default:
this.curList = null;
break;
}
if (!hasEncrypt)
this.curList?.VisibleColumnFieldNames.Remove("Encrypted");
}
#endregion
#region ReadChannel()
private void ReadChannel(XmlNode node, int rowId)
{
var setupNode = node["Setup"] ?? throw new FileLoadException("Missing Setup XML element");
var bcastNode = node["Broadcast"] ?? throw new FileLoadException("Missing Broadcast XML element");
var data = new Dictionary<string,string>(StringComparer.InvariantCultureIgnoreCase);
foreach (var n in new[] {setupNode, bcastNode})
{
foreach(XmlAttribute attr in n.Attributes)
data.Add(attr.LocalName, attr.Value);
}
var chan = new Channel(this.curList.SignalSource, rowId, rowId, setupNode);
chan.OldProgramNr = -1;
chan.IsDeleted = false;
if (this.formatVersion == 1)
this.ParseChannelFormat1(data, chan);
else if (this.formatVersion == 2)
this.ParseChannelFormat2(data, chan);
if ((chan.SignalSource & SignalSource.MaskAdInput) == SignalSource.DvbT)
chan.ChannelOrTransponder = LookupData.Instance.GetDvbtTransponder(chan.FreqInMhz).ToString();
else if ((chan.SignalSource & SignalSource.MaskAdInput) == SignalSource.DvbC)
chan.ChannelOrTransponder = LookupData.Instance.GetDvbcChannelName(chan.FreqInMhz);
DataRoot.AddChannel(this.curList, chan);
}
#endregion
#region ParseChannelFormat1
private void ParseChannelFormat1(Dictionary<string,string> data, Channel chan)
{
chan.RawSatellite = data.TryGet("SatelliteName");
chan.Satellite = DecodeName(chan.RawSatellite);
chan.OldProgramNr = ParseInt(data.TryGet("ChannelNumber"));
chan.RawName = data.TryGet("ChannelName");
chan.Name = DecodeName(chan.RawName);
chan.Lock = data.TryGet("ChannelLock") == "1";
chan.Hidden = data.TryGet("UserHidden") == "1";
var fav = ParseInt(data.TryGet("FavoriteNumber"));
chan.FavIndex[0] = fav == 0 ? -1 : fav;
chan.OriginalNetworkId = ParseInt(data.TryGet("Onid"));
chan.TransportStreamId = ParseInt(data.TryGet("Tsid"));
chan.ServiceId = ParseInt(data.TryGet("Sid"));
var freq = ParseInt(data.TryGet("Frequency"));
chan.FreqInMhz = freq;
chan.ServiceType = ParseInt(data.TryGet("ServiceType"));
chan.SignalSource |= LookupData.Instance.IsRadioOrTv(chan.ServiceType);
chan.SymbolRate = ParseInt(data.TryGet("SymbolRate"));
if (data.TryGetValue("Polarization", out var pol))
chan.Polarity = pol == "0" ? 'H' : 'V';
chan.Hidden |= data.TryGet("SystemHidden") == "1";
chan.Encrypted = data.TryGet("Scrambled") == "1"; // doesn't exist in all format versions
}
#endregion
#region ParseChannelFormat2
private void ParseChannelFormat2(Dictionary<string, string> data, Channel chan)
{
chan.OldProgramNr = ParseInt(data.TryGet("presetnumber"));
chan.Name = data.TryGet("name");
chan.RawName = chan.Name;
chan.FreqInMhz = ParseInt(data.TryGet("frequency"));
if (chan.FreqInMhz > 100000)
chan.FreqInMhz /= 1000;
chan.ServiceId = ParseInt(data.TryGet("serviceID"));
chan.OriginalNetworkId = ParseInt(data.TryGet("ONID"));
chan.TransportStreamId = ParseInt(data.TryGet("TSID"));
chan.ServiceType = ParseInt(data.TryGet("serviceType"));
chan.SymbolRate = ParseInt(data.TryGet("symbolrate")) / 1000;
}
#endregion
#region ParseInt()
private int ParseInt(string input)
{
if (string.IsNullOrWhiteSpace(input))
return 0;
if (input.Length > 2 && input[0] == '0' && char.ToLower(input[1]) == 'x')
return int.Parse(input.Substring(2), NumberStyles.HexNumber);
if (int.TryParse(input, out var value))
return value;
return 0;
}
#endregion
#region DecodeName()
private string DecodeName(string input)
{
if (input == null || !input.StartsWith("0x")) // fallback for unknown input
return input;
var hexParts = input.Split(' ');
var buffer = new MemoryStream();
foreach (var part in hexParts)
{
if (part == "" || part == "0x00")
continue;
buffer.WriteByte((byte)ParseInt(part));
}
return this.DefaultEncoding.GetString(buffer.GetBuffer(), 0, (int)buffer.Length);
}
#endregion
#region DefaultEncoding
public override Encoding DefaultEncoding
{
get => base.DefaultEncoding;
set
{
if (value == this.DefaultEncoding)
return;
base.DefaultEncoding = value;
this.ChangeEncoding();
}
}
#endregion
#region ChangeEncoding
private void ChangeEncoding()
{
foreach (var list in this.DataRoot.ChannelLists)
{
foreach (var channel in list.Channels)
{
if (!(channel is Channel ch))
continue;
ch.Name = this.DecodeName(ch.RawName);
ch.Satellite = this.DecodeName(ch.RawSatellite);
}
}
}
#endregion
#region Save()
public override void Save(string tvOutputFile)
{
foreach (var list in this.DataRoot.ChannelLists)
this.UpdateChannelList(list);
// by default .NET reformats the whole XML. These settings produce almost same format as the TV xml files use
var xmlSettings = new XmlWriterSettings();
xmlSettings.Encoding = this.DefaultEncoding;
xmlSettings.CheckCharacters = false;
xmlSettings.Indent = true;
xmlSettings.IndentChars = "";
xmlSettings.NewLineHandling = NewLineHandling.None;
xmlSettings.NewLineChars = this.newline;
xmlSettings.OmitXmlDeclaration = false;
string xml;
using (var sw = new StringWriter())
using (var w = new CustomXmlWriter(sw, xmlSettings, false))
{
this.doc.WriteTo(w);
w.Flush();
xml = sw.ToString();
}
var enc = new UTF8Encoding(false, false);
File.WriteAllText(tvOutputFile, xml, enc);
}
#endregion
#region UpdateChannelList()
private void UpdateChannelList(ChannelList list)
{
foreach (var channel in list.Channels)
{
var ch = channel as Channel;
if (ch == null)
continue; // might be a proxy channel from a reference list
if (ch.IsDeleted || ch.NewProgramNr < 0)
{
ch.SetupNode.ParentNode.ParentNode.RemoveChild(ch.SetupNode.ParentNode);
continue;
}
if (this.formatVersion == 1)
this.UpdateChannelFormat1(ch);
else if (this.formatVersion == 2)
this.UpdateChannelFormat2(ch);
}
}
#endregion
#region UpdateChannelFormat1 and 2
private void UpdateChannelFormat1(Channel ch)
{
ch.SetupNode.Attributes["ChannelNumber"].Value = ch.NewProgramNr.ToString();
if (ch.IsNameModified)
ch.SetupNode.Attributes["ChannelName"].Value = EncodeName(ch.Name);
ch.SetupNode.Attributes["FavoriteNumber"].Value = Math.Max(ch.FavIndex[0], 0).ToString();
}
private void UpdateChannelFormat2(Channel ch)
{
ch.SetupNode.Attributes["presetnumber"].Value = ch.NewProgramNr.ToString();
if (ch.IsNameModified)
ch.SetupNode.Attributes["name"].Value = ch.Name;
}
#endregion
#region EncodeName
private string EncodeName(string name)
{
var bytes = this.DefaultEncoding.GetBytes(name);
var sb = new StringBuilder();
foreach (var b in bytes)
sb.Append($"0x{b:X2} 0x00 ");
return sb.ToString();
}
#endregion
}
}

View File

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