From 4f99e72bfee8264dbe31b40660c92b405ca9b684 Mon Sep 17 00:00:00 2001 From: CompoUK Date: Sun, 8 Mar 2015 14:15:39 +0000 Subject: [PATCH] initial code initial code --- src/Configuration/PluginConfiguration.cs | 48 + src/Configuration/VuPlusConfigurationPage.cs | 44 + src/Configuration/configPage.html | 209 +++ src/Helpers/EncryptionHelper.cs | 23 + src/Helpers/GeneralHelpers.cs | 71 + src/Helpers/XmlHelper.cs | 23 + src/Images/Vu+.png | Bin 0 -> 22848 bytes src/LiveTvService.cs | 1735 ++++++++++++++++++ src/MediaBrowser.Plugins.VuPlus.csproj | 88 + src/Plugin.cs | 46 + src/Properties/AssemblyInfo.cs | 32 + src/packages.config | 5 + 12 files changed, 2324 insertions(+) create mode 100644 src/Configuration/PluginConfiguration.cs create mode 100644 src/Configuration/VuPlusConfigurationPage.cs create mode 100644 src/Configuration/configPage.html create mode 100644 src/Helpers/EncryptionHelper.cs create mode 100644 src/Helpers/GeneralHelpers.cs create mode 100644 src/Helpers/XmlHelper.cs create mode 100644 src/Images/Vu+.png create mode 100644 src/LiveTvService.cs create mode 100644 src/MediaBrowser.Plugins.VuPlus.csproj create mode 100644 src/Plugin.cs create mode 100644 src/Properties/AssemblyInfo.cs create mode 100644 src/packages.config diff --git a/src/Configuration/PluginConfiguration.cs b/src/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000..f7fe0cc --- /dev/null +++ b/src/Configuration/PluginConfiguration.cs @@ -0,0 +1,48 @@ +using System; +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Plugins.VuPlus.Configuration +{ + /// + /// Class PluginConfiguration + /// + public class PluginConfiguration : BasePluginConfiguration + { + public string HostName { get; set; } + public string StreamingPort { get; set; } + public string WebInterfacePort { get; set; } + public string WebInterfaceUsername { get; set; } + public string WebInterfacePassword { get; set; } + public Boolean UseSecureHTTPS { get; set; } + public Boolean OnlyOneBouquet { get; set; } + public string TVBouquet { get; set; } + public Boolean ZapToChannel { get; set; } + public Boolean FetchPiconsFromWebInterface { get; set; } + public string PiconsPath { get; set; } + + public string RecordingPath { get; set; } + + public Boolean EnableDebugLogging { get; set; } + + + public PluginConfiguration() + { + HostName = "http://localhost"; + StreamingPort = "8001"; + WebInterfacePort = "8000"; + WebInterfaceUsername= ""; + WebInterfacePassword = ""; + UseSecureHTTPS = false; + OnlyOneBouquet = true; + TVBouquet = "Favourites (TV)"; + ZapToChannel = false; + FetchPiconsFromWebInterface = true; + PiconsPath = ""; + + RecordingPath = ""; + + EnableDebugLogging = false; + + } + } +} diff --git a/src/Configuration/VuPlusConfigurationPage.cs b/src/Configuration/VuPlusConfigurationPage.cs new file mode 100644 index 0000000..ad307db --- /dev/null +++ b/src/Configuration/VuPlusConfigurationPage.cs @@ -0,0 +1,44 @@ +using System.IO; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; + +namespace MediaBrowser.Plugins.VuPlus.Configuration +{ + /// + /// Class VuPlusConfigurationPage + /// + class VuPlusConfigurationPage : IPluginConfigurationPage + { + /// + /// Gets the type of the configuration page. + /// + /// The type of the configuration page. + public ConfigurationPageType ConfigurationPageType + { + get { return ConfigurationPageType.PluginConfiguration; } + } + + /// + /// Gets the HTML stream. + /// + /// Stream. + public Stream GetHtmlStream() + { + return GetType().Assembly.GetManifestResourceStream("MediaBrowser.Plugins.VuPlus.Configuration.configPage.html"); + } + + /// + /// Gets the name. + /// + /// The name. + public string Name + { + get { return "VuPlus"; } + } + + public IPlugin Plugin + { + get { return VuPlus.Plugin.Instance; } + } + } +} diff --git a/src/Configuration/configPage.html b/src/Configuration/configPage.html new file mode 100644 index 0000000..9ed4517 --- /dev/null +++ b/src/Configuration/configPage.html @@ -0,0 +1,209 @@ + + + + Vu+ + + +
+ +
+
+
+ +
    +
  • + + +
    + The host name (address) or ip address of your receiver +
    +
  • +
  • + + +
    + The Streaming port of your receiver eg. 8001 +
    +
  • +
  • + + +
    + The web Interface port of your receiver eg. 8000 +
    +
  • +
  • + + +
    + The web Interface username of your receiver (optional) +
    +
  • +
  • + + +
    + The web Interface password of your receiver (optional) +
    +
  • + +
  • + + +
    + Use HTTPS to connect to your receiver +
    +
  • + + +
  • + + +
    + Limit channels to only those contained within the specified TV Bouquet below (optional) +
    +
  • + + +
  • + + +
    + The TV Bouquet to load channels for (optional) +
    + +
  • + +
  • + + +
    + Set if only one tuner within receiver +
    +
  • + + + +
  • + + +
    + Set if you want to retrieve Picons from the web interface of the receiver +
    +
  • + + +
  • + + +
    + The local location of your Picons eg. C:\Picons\ (optional) +
    +
  • + +
  • + + +
    + The location to store your recordings on your receiver eg. /hdd/movie/ (optional) +
    +
  • + +
  • + + +
  • + + +
  • + + +
  • +
+ +
+
+
+ + + +
+ + diff --git a/src/Helpers/EncryptionHelper.cs b/src/Helpers/EncryptionHelper.cs new file mode 100644 index 0000000..ecca2cd --- /dev/null +++ b/src/Helpers/EncryptionHelper.cs @@ -0,0 +1,23 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace MediaBrowser.Plugins.VuPlus.Helpers +{ + public static class EncryptionHelper + { + public static string GetMd5Hash(string value) + { + HashAlgorithm hashAlgorithm = HashAlgorithm.Create("MD5"); + + if (hashAlgorithm != null) + { + byte[] hashValue = hashAlgorithm.ComputeHash(new UTF8Encoding().GetBytes(value)); + //Bit convertor return the byte to string as all caps hex values seperated by "-" + return BitConverter.ToString(hashValue).Replace("-", "").ToLowerInvariant(); + } + + return string.Empty; + } + } +} diff --git a/src/Helpers/GeneralHelpers.cs b/src/Helpers/GeneralHelpers.cs new file mode 100644 index 0000000..8d229a8 --- /dev/null +++ b/src/Helpers/GeneralHelpers.cs @@ -0,0 +1,71 @@ +using MediaBrowser.Model.LiveTv; +using System; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Plugins.VuPlus.Helpers +{ + public static class ChannelHelper + { + public static ChannelType GetChannelType(string channelType) + { + ChannelType type = new ChannelType(); + + if (channelType == "0x1") + { + type = ChannelType.TV; + } + else if (channelType == "0xa") + { + type = ChannelType.Radio; + } + + return type; + } + } + + public static class UtilsHelper + { + public static void DebugInformation(ILogger logger, string message) + { + var config = Plugin.Instance.Configuration; + bool enableDebugLogging = config.EnableDebugLogging; + + if (enableDebugLogging) + { + logger.Debug(message); + } + } + + } + + public static class RecordingHelper + { + + } + + public static class ApiHelper + { + private static readonly DateTime UnixEpoch = + new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public static long GetCurrentUnixTimestampMillis() + { + return (long)(DateTime.UtcNow - UnixEpoch).TotalMilliseconds; + } + + public static DateTime DateTimeFromUnixTimestampMillis(long millis) + { + return UnixEpoch.AddMilliseconds(millis); + } + + public static long GetCurrentUnixTimestampSeconds(DateTime date) + { + return (long)(date - UnixEpoch).TotalSeconds; + } + + public static DateTime DateTimeFromUnixTimestampSeconds(long seconds) + { + return UnixEpoch.AddSeconds(seconds); + } + } +} diff --git a/src/Helpers/XmlHelper.cs b/src/Helpers/XmlHelper.cs new file mode 100644 index 0000000..baab916 --- /dev/null +++ b/src/Helpers/XmlHelper.cs @@ -0,0 +1,23 @@ +using System.Xml; + +namespace MediaBrowser.Plugins.VuPlus.Helpers +{ + public static class XmlHelper + { + public static XmlNode GetSingleNode(string input, string xPath) + { + var xml = new XmlDocument(); + xml.LoadXml(input); + + return xml.SelectSingleNode(xPath); + } + + public static XmlNodeList GetMultipleNodes(string input, string xPath) + { + var xml = new XmlDocument(); + xml.LoadXml(input); + + return xml.SelectNodes(xPath); + } + } +} diff --git a/src/Images/Vu+.png b/src/Images/Vu+.png new file mode 100644 index 0000000000000000000000000000000000000000..27740652e4c93be6f95d1f1f5fd01dacebbaff0c GIT binary patch literal 22848 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 literal 0 HcmV?d00001 diff --git a/src/LiveTvService.cs b/src/LiveTvService.cs new file mode 100644 index 0000000..fc65cd4 --- /dev/null +++ b/src/LiveTvService.cs @@ -0,0 +1,1735 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; + +using MediaBrowser.Plugins.VuPlus.Helpers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using System.Xml; +using MediaBrowser.Model.LiveTv; +using System.Net; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.ApiClient; + +namespace MediaBrowser.Plugins.VuPlus +{ + /// + /// Class LiveTvService + /// + public class LiveTvService : ILiveTvService + { + private readonly IHttpClient _httpClient; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly ILogger _logger; + private int _liveStreams; + private readonly Dictionary _heartBeat = new Dictionary(); + + private String tvBouquetSRef; + List tvChannelInfos = new List(); + + + public LiveTvService(IHttpClient httpClient, IJsonSerializer jsonSerializer,ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + + /// + /// Ensure that we are connected to the VuPlus server + /// + /// + /// + private async Task EnsureConnectionAsync(CancellationToken cancellationToken) + { + _logger.Info("[VuPlus] Start EnsureConnectionAsync"); + + var config = Plugin.Instance.Configuration; + + // log settings + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync HostName: {0}", config.HostName)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync StreamingPort: {0}", config.StreamingPort)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync WebInterfacePort: {0}", config.WebInterfacePort)); + if (string.IsNullOrEmpty(config.WebInterfaceUsername)) + _logger.Info("[VuPlus] EnsureConnectionAsync WebInterfaceUsername: "); + else + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync WebInterfaceUsername: {0}", "********")); + if (string.IsNullOrEmpty(config.WebInterfacePassword)) + _logger.Info("[VuPlus] EnsureConnectionAsync WebInterfacePassword: "); + else + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync WebInterfaceUsername: {0}", "********")); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync UseSecureHTTPS: {0}", config.UseSecureHTTPS)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync OnlyOneBouquet: {0}", config.OnlyOneBouquet)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync TVBouquet: {0}", config.TVBouquet)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync ZapToChannel: {0}", config.ZapToChannel)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync FetchPiconsFromWebInterface: {0}", config.FetchPiconsFromWebInterface)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync PiconsPath: {0}", config.PiconsPath)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync RecordingPath: {0}", config.RecordingPath)); + _logger.Info(string.Format("[VuPlus] EnsureConnectionAsync EnableDebugLogging: {0}", config.EnableDebugLogging)); + + // validate settings + if (string.IsNullOrEmpty(config.HostName)) + { + _logger.Error("[VuPlus] HostName must be configured."); + throw new InvalidOperationException("VuPlus HostName must be configured."); + } + + if (string.IsNullOrEmpty(config.StreamingPort)) + { + _logger.Error("[VuPlus] Streaming Port must be configured."); + throw new InvalidOperationException("VuPlus Streaming Port must be configured."); + } + + if (string.IsNullOrEmpty(config.WebInterfacePort)) + { + _logger.Error("[VuPlus] Web Interface Port must be configured."); + throw new InvalidOperationException("VuPlus Web Interface Port must be configured."); + } + + if (config.OnlyOneBouquet) + { + if (string.IsNullOrEmpty(config.TVBouquet)) + { + _logger.Error("[VuPlus] TV Bouquet must be configured if Fetch only one TV bouquet selected."); + throw new InvalidOperationException("VuPlus TVBouquet must be configured if Fetch only one TV bouquet selected."); + } + } + + if (!config.FetchPiconsFromWebInterface) + { + if (string.IsNullOrEmpty(config.PiconsPath)) + { + _logger.Error("[VuPlus] Picons location must be configured if Fetch Picons from Web Service is disabled."); + throw new InvalidOperationException("VuPlus Picons location must be configured if Fetch Picons from Web Service is disabled."); + } + } + + _logger.Info("[VuPlus] EnsureConnectionAsync Validation of config parameters completed"); + + if (config.OnlyOneBouquet) + { + // connect to VuPlus box to test connectivity and at same time get sRef for TV Bouquet. + tvBouquetSRef = await InitiateSession(cancellationToken, config.TVBouquet).ConfigureAwait(false); + } else + { + // connect to VuPlus box to test connectivity. + String resultNotRequired = await InitiateSession(cancellationToken, null).ConfigureAwait(false); + tvBouquetSRef = null; + } + } + + + /// + /// Checks connection to VuPlus and retrieves service reference for channel if only one bouquet. + /// + /// The cancellation token. + /// The TV Bouquet. + /// Task{String>}. + public async Task InitiateSession(CancellationToken cancellationToken, String tvBouquet) + { + _logger.Info("[VuPlus] Start InitiateSession, validates connection and returns Bouquet reference if required"); + //await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/getservices", baseUrl); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] InitiateSession url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] InitiateSession response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + String tvBouquetReference = null; + + XmlNodeList e2services = xml.GetElementsByTagName("e2service"); + + // If TV Bouquet passed find associated service reference + if (!string.IsNullOrEmpty(tvBouquet)) + { + foreach (XmlNode xmlNode in e2services) + { + var channelInfo = new ChannelInfo(); + + var e2servicereference = "?"; + var e2servicename = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2servicereference") + { + e2servicereference = node.InnerText; + } + else if (node.Name == "e2servicename") + { + e2servicename = node.InnerText; + } + } + if (tvBouquet == e2servicename) + { + tvBouquetReference = e2servicereference; + return tvBouquetReference; + } + } + // make sure we have found the TV Bouquet + if (!string.IsNullOrEmpty(tvBouquet)) + { + _logger.Error("[VuPlus] Failed to find TV Bouquet specified in VuPlus configuration."); + throw new ApplicationException("Failed to find TV Bouquet specified in VuPlus configuration."); + } + } + return tvBouquetReference; + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse services information."); + _logger.Error(string.Format("[VuPlus] InitiateSession error: {0}", e.Message)); + throw new ApplicationException("Failed to connect to VuPlus."); + } + + } + } + } + + + /// + /// Gets the channels async. + /// + /// The cancellation token. + /// Task{IEnumerable{ChannelInfo}}. + public async Task> GetChannelsAsync(CancellationToken cancellationToken) + { + _logger.Info("[VuPlus] Start GetChannelsAsync, retrieve all channels"); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var baseUrlPicon = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = ""; + if (string.IsNullOrEmpty(tvBouquetSRef)) + { + url = string.Format("{0}/web/getservices", baseUrl); + } + else + { + url = string.Format("{0}/web/getservices?sRef={1}", baseUrl, tvBouquetSRef); + } + + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetChannelsAsync url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + UserAgent = "vuplus-pvraddon-agent/1.0" + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + + baseUrlPicon = protocol + "://" + Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword + "@" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetChannelsAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + List channelInfos = new List(); + + if (string.IsNullOrEmpty(tvBouquetSRef)) + { + // Load channels from all TV Bouquets + _logger.Info("[VuPlus] GetChannelsAsync for all TV Bouquets"); + + XmlNodeList e2services = xml.GetElementsByTagName("e2service"); + foreach (XmlNode xmlNode in e2services) + { + var channelInfo = new ChannelInfo(); + var e2servicereference = "?"; + var e2servicename = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2servicereference") + { + e2servicereference = node.InnerText; + } + else if (node.Name == "e2servicename") + { + e2servicename = node.InnerText; + } + } + + // get all channels for TV Bouquet + List channelInfosForBouquet = await GetChannelsForTVBouquetAsync(cancellationToken, e2servicereference).ConfigureAwait(false); + + // store all channels for TV Bouquet + channelInfos.AddRange(channelInfosForBouquet); + } + + return channelInfos; + } + else + { + // Load channels for specified TV Bouquet only + _logger.Info("[VuPlus] GetChannelsAsync for specified TV Bouquets"); + int count = 1; + + XmlNodeList e2services = xml.GetElementsByTagName("e2service"); + foreach (XmlNode xmlNode in e2services) + { + var channelInfo = new ChannelInfo(); + + var e2servicereference = "?"; + var e2servicename = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2servicereference") + { + e2servicereference = node.InnerText; + } + else if (node.Name == "e2servicename") + { + e2servicename = node.InnerText; + } + } + + // Check whether the current element is not just a label + if (!e2servicereference.StartsWith("1:64:")) + { + //check for radio channel + if (e2servicereference.ToUpper().Contains("RADIO")) + channelInfo.ChannelType = ChannelType.Radio; + else + channelInfo.ChannelType = ChannelType.TV; + + channelInfo.HasImage = true; + channelInfo.Id = e2servicereference; + + // image name is name is e2servicereference with last char removed, then replace all : with _, then add .png + var imageName = e2servicereference.Remove(e2servicereference.Length - 1); + imageName = imageName.Replace(":", "_"); + imageName = imageName + ".png"; + //var imageUrl = string.Format("{0}/picon/{1}", baseUrl, imageName); + var imageUrl = string.Format("{0}/picon/{1}", baseUrlPicon, imageName); + + if (Plugin.Instance.Configuration.FetchPiconsFromWebInterface) + { + channelInfo.ImagePath = null; + //channelInfo.ImageUrl = WebUtility.UrlEncode(imageUrl); + channelInfo.ImageUrl = imageUrl; + } + else + { + channelInfo.ImagePath = Plugin.Instance.Configuration.PiconsPath + imageName; + channelInfo.ImageUrl = null; + } + + channelInfo.Name = e2servicename; + channelInfo.Number = count.ToString(); + + channelInfos.Add(channelInfo); + count = count + 1; + } + else + { + _logger.Info("[VuPlus] ignoring channel label " + e2servicereference); + } + } + } + tvChannelInfos = channelInfos; + return channelInfos; + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse channel information."); + _logger.Error(string.Format("[VuPlus] GetChannelsAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse channel information."); + } + } + } + } + + + /// + /// Gets the channels async. + /// + /// The cancellation token. + /// Task{List}. + public async Task> GetChannelsForTVBouquetAsync(CancellationToken cancellationToken, String sRef) + { + _logger.Info("[VuPlus] Start GetChannelsForTVBouquetAsync, retrieve all channels for TV Bouquet " + sRef); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var baseUrlPicon = protocol + "://" + Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword + "@" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/getservices?sRef={1}", baseUrl, sRef); + + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetChannelsForTVBouquetAsync url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url, + UserAgent = "vuplus-pvraddon-agent/1.0" + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + + baseUrlPicon = protocol + "://" + Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword + "@" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetChannelsForTVBouquetAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + List channelInfos = new List(); + + // Load channels for specified TV Bouquet only + + int count = 1; + + XmlNodeList e2services = xml.GetElementsByTagName("e2service"); + foreach (XmlNode xmlNode in e2services) + { + var channelInfo = new ChannelInfo(); + + var e2servicereference = "?"; + var e2servicename = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2servicereference") + { + e2servicereference = node.InnerText; + } + else if (node.Name == "e2servicename") + { + e2servicename = node.InnerText; + } + } + + // Check whether the current element is not just a label + if (!e2servicereference.StartsWith("1:64:")) + { + //check for radio channel + if (e2servicereference.Contains("radio")) + channelInfo.ChannelType = ChannelType.Radio; + else + channelInfo.ChannelType = ChannelType.TV; + + channelInfo.HasImage = true; + channelInfo.Id = e2servicereference; + + // image name is name is e2servicereference with last char removed, then replace all : with _, then add .png + var imageName = e2servicereference.Remove(e2servicereference.Length - 1); + imageName = imageName.Replace(":", "_"); + imageName = imageName + ".png"; + //var imageUrl = string.Format("{0}/picon/{1}", baseUrl, imageName); + var imageUrl = string.Format("{0}/picon/{1}", baseUrlPicon, imageName); + + if (Plugin.Instance.Configuration.FetchPiconsFromWebInterface) + { + channelInfo.ImagePath = null; + //channelInfo.ImageUrl = WebUtility.UrlEncode(imageUrl); + channelInfo.ImageUrl = imageUrl; + } + else + { + channelInfo.ImagePath = Plugin.Instance.Configuration.PiconsPath + imageName; + channelInfo.ImageUrl = null; + } + + channelInfo.Name = e2servicename; + channelInfo.Number = count.ToString(); + + channelInfos.Add(channelInfo); + count = count + 1; + } + else + { + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] ignoring channel {0}", e2servicereference)); + } + } + return channelInfos; + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse channel information."); + _logger.Error(string.Format("[VuPlus] GetChannelsForTVBouquetAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse channel information."); + } + } + } + } + + + /// + /// Gets the Recordings async + /// + /// The cancellation token. + /// Task{IEnumerable{RecordingInfo}} + public async Task> GetRecordingsAsync(CancellationToken cancellationToken) + { + _logger.Info("[VuPlus] Start GetRecordingsAsync, retrieve all 'Inprogress' and 'Completed' recordings "); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/movielist", baseUrl); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetRecordingsAsync url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetRecordingsAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + List recordingInfos = new List(); + + int count = 1; + + XmlNodeList e2movie = xml.GetElementsByTagName("e2movie"); + + foreach (XmlNode xmlNode in e2movie) + { + var recordingInfo = new RecordingInfo(); + + var e2servicereference = "?"; + var e2title = "?"; + var e2description = "?"; + var e2servicename = "?"; + var e2time = "?"; + var e2length = "?"; + var e2filename = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2servicereference") + { + e2servicereference = node.InnerText; + } + else if (node.Name == "e2title") + { + e2title = node.InnerText; + } + else if (node.Name == "e2description") + { + e2description = node.InnerText; + } + else if (node.Name == "e2servicename") + { + e2servicename = node.InnerText; + } + else if (node.Name == "e2time") + { + e2time = node.InnerText; + } + else if (node.Name == "e2length") + { + e2length = node.InnerText; + } + else if (node.Name == "e2filename") + { + e2filename = node.InnerText; + } + } + + recordingInfo.Audio = null; + + recordingInfo.ChannelId = null; + //check for radio channel + if (e2servicereference.ToUpper().Contains("RADIO")) + recordingInfo.ChannelType = ChannelType.Radio; + else + recordingInfo.ChannelType = ChannelType.TV; + recordingInfo.HasImage = false; + recordingInfo.ImagePath = null; + recordingInfo.ImageUrl = null; + + foreach (ChannelInfo channelInfo in tvChannelInfos) + { + if (channelInfo.Name == e2servicename) + { + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetRecordingsAsync match on channel name : {0} for recording {1}", e2servicename, e2title)); + recordingInfo.ChannelId = channelInfo.Id; + recordingInfo.ChannelType = channelInfo.ChannelType; + recordingInfo.HasImage = true; + recordingInfo.ImagePath = channelInfo.ImagePath; + recordingInfo.ImageUrl = channelInfo.ImageUrl; + break; + } + } + + if (recordingInfo.ChannelId == null) + { + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetRecordingsAsync no match on channel name : {0} for recording {1}", e2servicename, e2title)); + } + + recordingInfo.CommunityRating = 0; + + Double sdated = Convert.ToDouble(e2time); + DateTime sdate = new DateTime(1970, 1, 1, 0, 0, 0, 0); //Set default date 1/1/1970 + sdate = sdate.AddSeconds(sdated); //add seconds + recordingInfo.StartDate = sdate.ToUniversalTime(); + + //length in format mm:ss + string[] words = e2length.Split(':'); + Double mins = Convert.ToDouble(words[0]); + Double seconds = Convert.ToDouble(words[1]); + + Double edated = Convert.ToDouble(e2time) + (mins * 60) + (seconds); + DateTime edate = new DateTime(1970, 1, 1, 0, 0, 0, 0); //Set default date 1/1/1970 + edate = edate.AddSeconds(edated); //add seconds + recordingInfo.EndDate = edate.ToUniversalTime(); + + recordingInfo.EpisodeTitle = null; + + recordingInfo.Overview = e2description; + + List genre = new List(); + genre.Add("Unknown"); + recordingInfo.Genres = genre; + + recordingInfo.Id = e2servicereference; + recordingInfo.IsHD = false; + recordingInfo.IsKids = false; + recordingInfo.IsLive = false; + recordingInfo.IsMovie = false; + recordingInfo.IsNews = false; + recordingInfo.IsPremiere = false; + recordingInfo.IsRepeat = false; + recordingInfo.IsSeries = false; + recordingInfo.IsSports = false; + recordingInfo.Name = e2title; + recordingInfo.OfficialRating = null; + recordingInfo.OriginalAirDate = null; + recordingInfo.Overview = e2description; + recordingInfo.Path = null; + recordingInfo.ProgramId = null; + recordingInfo.SeriesTimerId = null; + recordingInfo.Url = baseUrl + "/file?file=" + WebUtility.UrlEncode(e2filename); + + recordingInfos.Add(recordingInfo); + count = count + 1; + } + return recordingInfos; + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse timer information."); + _logger.Error(string.Format("[VuPlus] GetRecordingsAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse timer information."); + } + } + } + } + + + /// + /// Delete the Recording async from the disk + /// + /// The recordingId + /// The cancellationToken + /// + public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) + { + _logger.Info(string.Format("[VuPlus] Start Delete Recording Async for recordingId: {0}",recordingId)); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl =protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/moviedelete?sRef={1}", baseUrl, recordingId); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] DeleteRecordingAsync url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] DeleteRecordingAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + XmlNodeList e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); + foreach (XmlNode xmlNode in e2simplexmlresult) + { + var recordingInfo = new RecordingInfo(); + + var e2state = "?"; + var e2statetext = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2state") + { + e2state = node.InnerText; + } + else if (node.Name == "e2statetext") + { + e2statetext = node.InnerText; + } + } + + if (e2state != "True") + { + _logger.Error("[VuPlus] Failed to delete recording information."); + _logger.Error(string.Format("[VuPlus] DeleteRecordingAsync e2statetext: {0}", e2statetext)); + throw new ApplicationException("Failed to delete recording."); + } + } + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse delete recording information."); + _logger.Error(string.Format("[VuPlus] DeleteRecordingAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse delete recording information."); + } + } + } + } + + + /// + /// Cancel pending scheduled Recording + /// + /// The timerId + /// The cancellationToken + /// + public async Task CancelTimerAsync(string timerId, CancellationToken cancellationToken) + { + _logger.Info(string.Format("[VuPlus] Start CancelTimerAsync for recordingId: {0}", timerId)); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + // extract sRef, id, begin and end from passed timerId + string[] words = timerId.Split('~'); + var sRef = words[0]; + var id = words[1]; + var begin = words[2]; + var end = words[3]; + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/timerdelete?sRef={1}&begin={2}&end={3}", baseUrl, sRef, begin, end); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] CancelTimerAsync url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] CancelTimerAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + XmlNodeList e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); + foreach (XmlNode xmlNode in e2simplexmlresult) + { + var recordingInfo = new RecordingInfo(); + + var e2state = "?"; + var e2statetext = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2state") + { + e2state = node.InnerText; + } + else if (node.Name == "e2statetext") + { + e2statetext = node.InnerText; + } + } + + if (e2state != "True") + { + _logger.Error("[VuPlus] Failed to cancel timer."); + _logger.Error(string.Format("[VuPlus] CancelTimerAsync e2statetext: {0}", e2statetext)); + throw new ApplicationException("Failed to cancel timer."); + } + } + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse cancel timer information."); + _logger.Error(string.Format("[VuPlus] CancelTimerAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse cancel timer information."); + } + } + } + } + + + /// + /// Create a new recording + /// + /// The TimerInfo + /// The cancellationToken + /// + public async Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + { + _logger.Info(string.Format("[VuPlus] Start CreateTimerAsync for ChannelId: {0} & Name: {1}",info.ChannelId, info.Name)); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + // extract eventid from info.ProgramId + string[] words = info.ProgramId.Split('~'); + var eventid = words[1]; + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/timeraddbyeventid?sRef={1}&eventid={2}", baseUrl, info.ChannelId, eventid); + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.RecordingPath)) + { + url = url + string.Format("&dirname={0}", WebUtility.UrlEncode(Plugin.Instance.Configuration.RecordingPath)); + } + + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] CreateTimerAsync url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] CancelTimerAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + XmlNodeList e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); + foreach (XmlNode xmlNode in e2simplexmlresult) + { + var recordingInfo = new RecordingInfo(); + + var e2state = "?"; + var e2statetext = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2state") + { + e2state = node.InnerText; + } + else if (node.Name == "e2statetext") + { + e2statetext = node.InnerText; + } + } + + if (e2state != "True") + { + _logger.Error("[VuPlus] Failed to create timer."); + _logger.Error(string.Format("[VuPlus] CreateTimerAsync e2statetext: {0}", e2statetext)); + throw new ApplicationException("Failed to create timer."); + } + } + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse create timer information."); + _logger.Error(string.Format("[VuPlus] CreateTimerAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse create timer information."); + } + } + } + } + + + /// + /// Get the pending Recordings. + /// + /// The CancellationToken + /// IEnumerable + public async Task> GetTimersAsync(CancellationToken cancellationToken) + { + _logger.Info("[VuPlus] Start GetTimerAsync, retrieve the 'Pending' recordings"); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/timerlist", baseUrl); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetTimersAsync url: {0}", url)); + + var options = new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetTimersAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + List timerInfos = new List(); + + int count = 1; + + XmlNodeList e2timer = xml.GetElementsByTagName("e2timer"); + foreach (XmlNode xmlNode in e2timer) + { + var timerInfo = new TimerInfo(); + + var e2servicereference = "?"; + var e2name = "?"; + var e2description = "?"; + var e2eit = "?"; + var e2timebegin = "?"; + var e2timeend = "?"; + var e2state = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2servicereference") + { + e2servicereference = node.InnerText; + } + else if (node.Name == "e2name") + { + e2name = node.InnerText; + } + else if (node.Name == "e2description") + { + e2description = node.InnerText; + } + else if (node.Name == "e2eit") + { + e2eit = node.InnerText; + } + else if (node.Name == "e2timebegin") + { + e2timebegin = node.InnerText; + } + else if (node.Name == "e2timeend") + { + e2timeend = node.InnerText; + } + else if (node.Name == "e2state") + { + e2state = node.InnerText; + } + } + + // only interested in pending timers and ones recording now + if (e2state == "0" || e2state == "2") + { + + timerInfo.ChannelId = e2servicereference; + + Double edated = Convert.ToDouble(e2timeend); + DateTime edate = new DateTime(1970, 1, 1, 0, 0, 0, 0); //Set default date 1/1/1970 + edate = edate.AddSeconds(edated); //add seconds + timerInfo.EndDate = edate.ToUniversalTime(); + + timerInfo.Id = e2servicereference + "~" + e2eit + "~" + e2timebegin + "~" + e2timeend + "~" + count; + + timerInfo.IsPostPaddingRequired = false; + timerInfo.IsPrePaddingRequired = false; + timerInfo.Name = e2name; + timerInfo.Overview = e2description; + timerInfo.PostPaddingSeconds = 0; + timerInfo.PrePaddingSeconds = 0; + timerInfo.Priority = 0; + timerInfo.ProgramId = null; + timerInfo.SeriesTimerId = null; + + Double sdated = Convert.ToDouble(e2timebegin); + DateTime sdate = new DateTime(1970, 1, 1, 0, 0, 0, 0); //Set default date 1/1/1970 + sdate = sdate.AddSeconds(sdated); //add seconds + timerInfo.StartDate = sdate.ToUniversalTime(); + if (e2state == "0") + timerInfo.Status = RecordingStatus.Scheduled; + if (e2state == "2") + timerInfo.Status = RecordingStatus.InProgress; + + timerInfos.Add(timerInfo); + count = count + 1; + } + else + { + _logger.Info("[VuPlus] ignoring timer " + e2name); + } + } + return timerInfos; + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse timer information."); + _logger.Error(string.Format("[VuPlus] GetTimersAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse timer information."); + } + } + } + } + + + /// + /// Get the live channel stream, zap to channel if required. + /// + /// The CancellationToken + /// ChannelMediaInfo + public async Task GetChannelStream(string channelOid, CancellationToken cancellationToken) + { + _logger.Info("[VuPlus] Start GetChannelStream"); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.StreamingPort; + + //check if we need to zap to channel - single tuner + if (Plugin.Instance.Configuration.ZapToChannel) + { + await ZapToChannel(cancellationToken, channelOid).ConfigureAwait(false); + } + + _liveStreams++; + string streamUrl = string.Format("{0}/{1}", baseUrl, channelOid); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetChannelStream url: {0}", streamUrl)); + + return new ChannelMediaInfo + { + Id = _liveStreams.ToString(CultureInfo.InvariantCulture), + Path = streamUrl, + Protocol = MediaProtocol.Http + }; + throw new ResourceNotFoundException(string.Format("Could not stream channel {0}", channelOid)); + } + + + /// + /// zap to channel. + /// + /// The CancellationToken + /// The channel id + /// + public async Task ZapToChannel(CancellationToken cancellationToken, String channelOid) + { + _logger.Info("[VuPlus] Start ZapToChannel"); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/zap?sRef={1}", baseUrl, channelOid); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] ZapToChannel url: {0}", url)); + + var options = new HttpRequestOptions() + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] ZapToChannel response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + XmlNodeList e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); + foreach (XmlNode xmlNode in e2simplexmlresult) + { + var recordingInfo = new RecordingInfo(); + + var e2state = "?"; + var e2statetext = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2state") + { + e2state = node.InnerText; + } + else if (node.Name == "e2statetext") + { + e2statetext = node.InnerText; + } + } + + if (e2state != "True") + { + _logger.Error("[VuPlus] Failed to zap to channel."); + _logger.Error(string.Format("[VuPlus] ZapToChannel e2statetext: {0}", e2statetext)); + throw new ApplicationException("Failed to zap to channel."); + } + } + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse create timer information."); + _logger.Error(string.Format("[VuPlus] ZapToChannel error: {0}", e.Message)); + throw new ApplicationException("Failed to parse zap to channel information."); + } + } + } + + + } + + + /// + /// Get new timer defaults. + /// + /// The CancellationToken + /// null + /// SeriesTimerInfo + public async Task GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null) + { + _logger.Info("[VuPlus] Start GetNewTimerDefaultsAsync"); + + SeriesTimerInfo seriesTimerInfo = new SeriesTimerInfo(); + + return seriesTimerInfo; + } + + + /// + /// Get programs for specified channel within start and end date. + /// + /// channel id + /// start date/time + /// end date/time + /// The CancellationToken + /// IEnumerable + public async Task> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + { + _logger.Info(string.Format("[VuPlus] Start GetProgramsAsync for channelId: {0}",channelId)); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + Random rnd = new Random(); + + var imagePath = ""; + var imageUrl = ""; + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/epgservice?sRef={1}", baseUrl, channelId); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetProgramsAsync url: {0}", url)); + + var options = new HttpRequestOptions() + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetProgramsAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + List programInfos = new List(); + + int count = 1; + + XmlNodeList e2event = xml.GetElementsByTagName("e2event"); + foreach (XmlNode xmlNode in e2event) + { + var programInfo = new ProgramInfo(); + + var e2eventid = "?"; + var e2eventstart = "?"; + var e2eventduration = "?"; + var e2eventcurrenttime = "?"; + var e2eventtitle = "?"; + var e2eventdescription = "?"; + var e2eventdescriptionextended = "?"; + var e2eventservicereference = "?"; + var e2eventservicename = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2eventid") + { + e2eventid = node.InnerText; + } + else if (node.Name == "e2eventstart") + { + e2eventstart = node.InnerText; + } + else if (node.Name == "e2eventduration") + { + e2eventduration = node.InnerText; + } + else if (node.Name == "e2eventcurrenttime") + { + e2eventcurrenttime = node.InnerText; + } + else if (node.Name == "e2eventtitle") + { + e2eventtitle = node.InnerText; + } + else if (node.Name == "e2eventdescription") + { + e2eventdescription = node.InnerText; + } + else if (node.Name == "e2eventdescriptionextended") + { + e2eventdescriptionextended = node.InnerText; + } + else if (node.Name == "e2eventservicereference") + { + e2eventservicereference = node.InnerText; + } + else if (node.Name == "e2eventservicename") + { + e2eventservicename = node.InnerText; + } + } + + Double sdated = Convert.ToDouble(e2eventstart); + DateTime sdate = new DateTime(1970, 1, 1, 0, 0, 0, 0); //Set default date 1/1/1970 + sdate = sdate.AddSeconds(sdated); //add seconds + + // Check whether the current element is within the time range passed + if (sdate > endDateUtc) + { + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetProgramsAsync epc full ending without adding channel name : {0} program : {1}", e2eventservicename, e2eventtitle)); + return programInfos; + } + else + { + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetProgramsAsync adding program for channel name : {0} program : {1}", e2eventservicename, e2eventtitle)); + programInfo.HasImage = false; + if (count == 1) + { + foreach (ChannelInfo channelInfo in tvChannelInfos) + { + if (channelInfo.Name == e2eventservicename) + { + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetProgramsAsync match on channel name : {0}", e2eventservicename)); + //programInfo.HasImage = true; + //programInfo.ImagePath = channelInfo.ImagePath; + //programInfo.ImageUrl = channelInfo.ImageUrl; + imagePath = channelInfo.ImagePath; + imageUrl = channelInfo.ImageUrl; + break; + } + } + } + + programInfo.HasImage = true; + programInfo.ImagePath = imagePath; + programInfo.ImageUrl = imageUrl; + + programInfo.ChannelId = e2eventservicereference; + + // the Id appears to have to be unique so will try to make it so + programInfo.Id = e2eventservicereference + "~" + e2eventid + "~" + count + "~" + rnd.Next(); + + programInfo.Overview = e2eventdescriptionextended; + + Double edated = Convert.ToDouble(e2eventstart) + Convert.ToDouble(e2eventduration); + DateTime edate = new DateTime(1970, 1, 1, 0, 0, 0, 0); //Set default date 1/1/1970 + edate = edate.AddSeconds(edated); //add seconds + + programInfo.StartDate = sdate.ToUniversalTime(); + programInfo.EndDate = edate.ToUniversalTime(); + + List genre = new List(); + genre.Add("Unknown"); + programInfo.Genres = genre; + + programInfo.OriginalAirDate = null; + programInfo.Name = e2eventtitle; + programInfo.OfficialRating = null; + programInfo.CommunityRating = null; + programInfo.EpisodeTitle = null; + programInfo.Audio = null; + programInfo.IsHD = false; + programInfo.IsRepeat = false; + programInfo.IsSeries = false; + programInfo.IsNews = false; + programInfo.IsMovie = false; + programInfo.IsKids = false; + programInfo.IsSports = false; + + programInfos.Add(programInfo); + count = count + 1; + } + } + return programInfos; + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse program information."); + _logger.Error(string.Format("[VuPlus] GetProgramsAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse channel information."); + } + } + } + } + + + /// + /// Get server status info. + /// + /// The CancellationToken + /// LiveTvServiceStatusInfo + public async Task GetStatusInfoAsync(CancellationToken cancellationToken) + { + _logger.Info("[VuPlus] Start GetStatusInfoAsync Async, retrieve status details"); + await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); + + //TODO: Version check + + bool upgradeAvailable = false; + string serverVersion = "Unknown"; + + var protocol = "http"; + if (Plugin.Instance.Configuration.UseSecureHTTPS) + protocol = "https"; + + var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; + + var url = string.Format("{0}/web/deviceinfo", baseUrl); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetStatusInfoAsync url: {0}", url)); + + var options = new HttpRequestOptions() + { + CancellationToken = cancellationToken, + Url = url + }; + + if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) + { + string authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; + authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); + options.RequestHeaders["Authorization"] = "Basic " + authInfo; + } + + List liveTvTunerInfos = new List(); + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + string xmlResponse = reader.ReadToEnd(); + UtilsHelper.DebugInformation(_logger, string.Format("[VuPlus] GetStatusInfoAsync response: {0}", xmlResponse)); + + try + { + var xml = new XmlDocument(); + xml.LoadXml(xmlResponse); + + XmlNodeList e2frontend = xml.GetElementsByTagName("e2frontend"); + foreach (XmlNode xmlNode in e2frontend) + { + var liveTvTunerInfo = new LiveTvTunerInfo(); + + var e2name = "?"; + var e2model = "?"; + + foreach (XmlNode node in xmlNode.ChildNodes) + { + if (node.Name == "e2name") + { + e2name = node.InnerText; + } + else if (node.Name == "e2model") + { + e2model = node.InnerText; + } + } + + liveTvTunerInfo.Id = e2model; + liveTvTunerInfo.Name = e2name; + liveTvTunerInfo.SourceType = ""; + + liveTvTunerInfos.Add(liveTvTunerInfo); + } + + return new LiveTvServiceStatusInfo + { + HasUpdateAvailable = upgradeAvailable, + Version = serverVersion, + Tuners = liveTvTunerInfos + }; + + } + catch (Exception e) + { + _logger.Error("[VuPlus] Failed to parse tuner information."); + _logger.Error(string.Format("[VuPlus] GetStatusInfoAsync error: {0}", e.Message)); + throw new ApplicationException("Failed to parse tuner information."); + } + + } + } + } + + + /// + /// Gets the homepage url. + /// + /// The homepage url. + public string HomePageUrl + { + get { return "http://www.VuPlus.com/"; } + } + + + /// + /// Gets the name. + /// + /// The name. + public string Name + { + get { return "VuPlus"; } + } + + + public async Task GetRecordingStream(string recordingId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + public async Task CloseLiveStream(string id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + public async Task CopyFilesAsync(StreamReader source, StreamWriter destination) + { + _logger.Info("[VuPlus] Start CopyFiles Async"); + char[] buffer = new char[0x1000]; + int numRead; + while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0) + { + await destination.WriteAsync(buffer, 0, numRead); + } + } + + + public Task RecordLiveStream(string id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + /// + /// Get the recurrent recordings + /// + /// The CancellationToken + /// + public async Task> GetSeriesTimersAsync(CancellationToken cancellationToken) + { + _logger.Info("[VuPlus] Start GetSeriesTimersAsync"); + + List seriesTimerInfo = new List(); + return seriesTimerInfo; + } + + + /// + /// Create a recurrent recording + /// + /// The recurrend program info + /// The CancelationToken + /// + public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + /// + /// Update the series Timer + /// + /// The series program info + /// The CancellationToken + /// + public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + /// + /// Update a single Timer + /// + /// The program info + /// The CancellationToken + /// + public async Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + /// + /// Cancel the Series Timer + /// + /// The Timer Id + /// The CancellationToken + /// + public async Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + /// + /// Get the DefaultScheduleSettings + /// + /// The CancellationToken + /// + //private async Task GetDefaultScheduleSettings(CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + + public event EventHandler DataSourceChanged; + + + public event EventHandler RecordingStatusChanged; + + + public Task ResetTuner(string id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + + public Task GetChannelImageAsync(string channelId, CancellationToken cancellationToken) + { + // Leave as is. This is handled by supplying image url to ChannelInfo + throw new NotImplementedException(); + } + + + public Task GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken) + { + // Leave as is. This is handled by supplying image url to ProgramInfo + throw new NotImplementedException(); + } + + + public Task GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken) + { + // Leave as is. This is handled by supplying image url to RecordingInfo + throw new NotImplementedException(); + } + + public Task GetLiveTvChannelsAsync(string recordingId, CancellationToken cancellationToken) + { + // Leave as is. This is handled by supplying image url to RecordingInfo + throw new NotImplementedException(); + } + + + } +} diff --git a/src/MediaBrowser.Plugins.VuPlus.csproj b/src/MediaBrowser.Plugins.VuPlus.csproj new file mode 100644 index 0000000..e394e04 --- /dev/null +++ b/src/MediaBrowser.Plugins.VuPlus.csproj @@ -0,0 +1,88 @@ + + + + + Debug + AnyCPU + {815AB3F4-6B55-4FC4-9441-9297A3A116CB} + Library + Properties + MediaBrowser.Plugins.VuPlus + MediaBrowser.Plugins.VuPlus + v4.5 + 512 + ..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\MediaBrowser.Common.3.0.566\lib\net45\MediaBrowser.Common.dll + + + False + ..\packages\MediaBrowser.Server.Core.3.0.566\lib\net45\MediaBrowser.Controller.dll + + + False + ..\packages\MediaBrowser.Common.3.0.566\lib\net45\MediaBrowser.Model.dll + + + + + + + + + + + + Properties\SharedVersion.cs + + + + + + + + + + + + + + + + Designer + + + + + + xcopy "$(TargetPath)" "$(SolutionDir)\..\MediaBrowser\ProgramData-Server\Plugins\" /y + + + \ No newline at end of file diff --git a/src/Plugin.cs b/src/Plugin.cs new file mode 100644 index 0000000..ca7d8c6 --- /dev/null +++ b/src/Plugin.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Plugins.VuPlus.Configuration; + +namespace MediaBrowser.Plugins.VuPlus +{ + /// + /// Class Plugin + /// + public class Plugin : BasePlugin + { + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + /// + /// Gets the name of the plugin + /// + /// The name. + public override string Name + { + get { return "VuPlus"; } + } + + /// + /// Gets the description. + /// + /// The description. + public override string Description + { + get + { + return "Provides live tv using Vu+ / Enigma2 pvr as a back-end."; + } + } + + /// + /// Gets the instance. + /// + /// The instance. + public static Plugin Instance { get; private set; } + } +} diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a331800 --- /dev/null +++ b/src/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +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("MediaBrowser.Plugins.VuPlus")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MediaBrowser.Plugins.VuPlus")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("9564ac10-af23-39bc-449f-624f23cfa48f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// + diff --git a/src/packages.config b/src/packages.config new file mode 100644 index 0000000..4276370 --- /dev/null +++ b/src/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file