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 0000000..2774065 Binary files /dev/null and b/src/Images/Vu+.png differ 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