Notifications wired up server sided

This commit is contained in:
Mark McDowall 2013-05-19 16:17:32 -07:00
commit e9bf78a97d
57 changed files with 977 additions and 951 deletions

View file

@ -0,0 +1,64 @@
using System;
using NzbDrone.Common;
using NzbDrone.Core.Model.Xbmc;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class EventClientProvider
{
private readonly UdpProvider _udpProvider;
public EventClientProvider(UdpProvider udpProvider)
{
_udpProvider = udpProvider;
}
public EventClientProvider()
{
}
public virtual bool SendNotification(string caption, string message, IconType iconType, string iconFile, string address)
{
byte[] icon = new byte[0];
if (iconType != IconType.None)
{
icon = ResourceManager.GetRawLogo(iconFile);
}
byte[] payload = new byte[caption.Length + message.Length + 7 + icon.Length];
int offset = 0;
for (int i = 0; i < caption.Length; i++)
payload[offset++] = (byte)caption[i];
payload[offset++] = (byte)'\0';
for (int i = 0; i < message.Length; i++)
payload[offset++] = (byte)message[i];
payload[offset++] = (byte)'\0';
payload[offset++] = (byte)iconType;
for (int i = 0; i < 4; i++)
payload[offset++] = 0;
Array.Copy(icon, 0, payload, caption.Length + message.Length + 7, icon.Length);
return _udpProvider.Send(address, UdpProvider.PacketType.Notification, payload);
}
public virtual bool SendAction(string address, ActionType action, string messages)
{
var payload = new byte[messages.Length + 2];
int offset = 0;
payload[offset++] = (byte)action;
for (int i = 0; i < messages.Length; i++)
payload[offset++] = (byte)messages[i];
payload[offset++] = (byte)'\0';
return _udpProvider.Send(address, UdpProvider.PacketType.Action, payload);
}
}
}

View file

@ -0,0 +1,58 @@
using System.Drawing;
using System.IO;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class ResourceManager
{
public static Icon GetIcon(string Name)
{
Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("NzbDrone.Core.{0}.ico", Name));
if (stm == null) return null;
return new Icon(stm);
}
public static byte[] GetRawData(string Name)
{
byte[] data;
using (Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("NzbDrone.Core.{0}.ico", Name)))
{
if (stm == null) return null;
data = new byte[stm.Length];
stm.Read(data, 0, data.Length);
}
return data;
}
public static byte[] GetRawLogo(string Name)
{
byte[] data;
using (Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("NzbDrone.Core.{0}", Name)))
{
if (stm == null) return null;
data = new byte[stm.Length];
stm.Read(data, 0, data.Length);
}
return data;
}
public static Bitmap GetIconAsImage(string Name)
{
Stream stm = typeof(ResourceManager).Assembly.GetManifestResourceStream(string.Format("NzbDrone.Core.{0}.ico", Name));
if (stm == null) return null;
Bitmap bmp;
using (Icon ico = new Icon(stm))
{
bmp = new Bitmap(ico.Width, ico.Height);
using (Graphics g = Graphics.FromImage(bmp))
{
g.DrawIcon(ico, 0, 0);
}
}
return bmp;
}
}
}

View file

@ -0,0 +1,53 @@
using NLog;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class Xbmc : NotificationWithSetting<XbmcSettings>
{
private readonly XbmcProvider _xbmcProvider;
public Xbmc(XbmcProvider xbmcProvider, Logger logger)
{
_xbmcProvider = xbmcProvider;
}
public override string Name
{
get { return "XBMC"; }
}
public override void OnGrab(string message)
{
const string header = "NzbDrone [TV] - Grabbed";
_xbmcProvider.Notify(Settings, header, message);
}
public override void OnDownload(string message, Series series)
{
const string header = "NzbDrone [TV] - Downloaded";
_xbmcProvider.Notify(Settings, header, message);
UpdateAndClean(series);
}
public override void AfterRename(Series series)
{
UpdateAndClean(series);
}
private void UpdateAndClean(Series series)
{
if (Settings.UpdateLibrary)
{
_xbmcProvider.Update(Settings, series);
}
if (Settings.CleanLibrary)
{
_xbmcProvider.Clean(Settings);
}
}
}
}

View file

@ -0,0 +1,454 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Model.Xbmc;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class XbmcProvider
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IHttpProvider _httpProvider;
private readonly EventClientProvider _eventClientProvider;
public XbmcProvider(IHttpProvider httpProvider, EventClientProvider eventClientProvider)
{
_httpProvider = httpProvider;
_eventClientProvider = eventClientProvider;
}
public virtual void Notify(XbmcSettings settings, string header, string message)
{
//Always use EventServer, until Json has real support for it
var host = settings.Host;
Logger.Trace("Sending Notifcation to XBMC Host: {0}", host);
_eventClientProvider.SendNotification(header, message, IconType.Jpeg, "NzbDrone.jpg", host);
}
public virtual void Update(XbmcSettings settings, Series series)
{
//Use Json for Eden/Nightly or depricated HTTP for 10.x (Dharma) to get the proper path
//Perform update with EventServer (Json currently doesn't support updating a specific path only - July 2011)
var username = settings.Username;
var password = settings.Password;
var host = settings.Host;
Logger.Trace("Determining version of XBMC Host: {0}", host);
var version = GetJsonVersion(host, username, password);
//If Dharma
if (version == new XbmcVersion(2))
{
//Check for active player only when we should skip updates when playing
if (!settings.AlwaysUpdate)
{
Logger.Trace("Determining if there are any active players on XBMC host: {0}", host);
var activePlayers = GetActivePlayersDharma(host, username, password);
//If video is currently playing, then skip update
if (activePlayers["video"])
{
Logger.Debug("Video is currently playing, skipping library update");
return;
}
}
UpdateWithHttp(series, host, username, password);
}
//If Eden or newer (attempting to make it future compatible)
else if (version == new XbmcVersion(3) || version == new XbmcVersion(4))
{
//Check for active player only when we should skip updates when playing
if (!settings.AlwaysUpdate)
{
Logger.Trace("Determining if there are any active players on XBMC host: {0}", host);
var activePlayers = GetActivePlayersEden(host, username, password);
//If video is currently playing, then skip update
if (activePlayers.Any(a => a.Type.Equals("video")))
{
Logger.Debug("Video is currently playing, skipping library update");
return;
}
}
UpdateWithJsonExecBuiltIn(series, host, username, password);
}
else if (version >= new XbmcVersion(5))
{
//Check for active player only when we should skip updates when playing
if (!settings.AlwaysUpdate)
{
Logger.Trace("Determining if there are any active players on XBMC host: {0}", host);
var activePlayers = GetActivePlayersEden(host, username, password);
//If video is currently playing, then skip update
if (activePlayers.Any(a => a.Type.Equals("video")))
{
Logger.Debug("Video is currently playing, skipping library update");
return;
}
}
UpdateWithJsonVideoLibraryScan(series, host, username, password);
}
//Log Version zero if check failed
else
Logger.Trace("Unknown version: [{0}], skipping.", version);
}
public virtual bool UpdateWithJsonExecBuiltIn(Series series, string host, string username, string password)
{
try
{
//Use Json!
var xbmcShows = GetTvShowsJson(host, username, password);
TvShow path = null;
//Log if response is null, otherwise try to find XBMC's path for series
if (xbmcShows == null)
Logger.Trace("Failed to get TV Shows from XBMC");
else
path = xbmcShows.FirstOrDefault(s => s.ImdbNumber == series.Id || s.Label == series.Title);
//var hostOnly = GetHostWithoutPort(host);
if (path != null)
{
Logger.Trace("Updating series [{0}] (Path: {1}) on XBMC host: {2}", series.Title, path.File, host);
var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", path.File);
SendCommand(host, command, username, password);
}
else
{
Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host);
SendCommand(host, "ExecBuiltIn(UpdateLibrary(video))", username, password);
}
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
return false;
}
return true;
}
public virtual bool UpdateWithJsonVideoLibraryScan(Series series, string host, string username, string password)
{
try
{
//Use Json!
var xbmcShows = GetTvShowsJson(host, username, password);
TvShow path = null;
//Log if response is null, otherwise try to find XBMC's path for series
if (xbmcShows == null)
Logger.Trace("Failed to get TV Shows from XBMC");
else
path = xbmcShows.FirstOrDefault(s => s.ImdbNumber == series.Id || s.Label == series.Title);
var postJson = new JObject();
postJson.Add(new JProperty("jsonrpc", "2.0"));
postJson.Add(new JProperty("method", "VideoLibrary.Scan"));
postJson.Add(new JProperty("id", 55));
if (path != null)
{
Logger.Trace("Updating series [{0}] (Path: {1}) on XBMC host: {2}", series.Title, path.File, host);
postJson.Add(new JProperty("params", new JObject(new JObject(new JProperty("directory", path.File)))));
}
else
Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host);
var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());
if (CheckForJsonError(response))
return false;
Logger.Trace(" from response");
var result = JsonConvert.DeserializeObject<XbmcJsonResult<String>>(response);
if (!result.Result.Equals("OK", StringComparison.InvariantCultureIgnoreCase))
return false;
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
return false;
}
return true;
}
public virtual bool UpdateWithHttp(Series series, string host, string username, string password)
{
try
{
Logger.Trace("Sending Update DB Request to XBMC Host: {0}", host);
var xbmcSeriesPath = GetXbmcSeriesPath(host, series.Id, username, password);
//If the path is found update it, else update the whole library
if (!String.IsNullOrEmpty(xbmcSeriesPath))
{
Logger.Trace("Updating series [{0}] on XBMC host: {1}", series.Title, host);
var command = String.Format("ExecBuiltIn(UpdateLibrary(video,{0}))", xbmcSeriesPath);
SendCommand(host, command, username, password);
}
else
{
//Update the entire library
Logger.Trace("Series [{0}] doesn't exist on XBMC host: {1}, Updating Entire Library", series.Title, host);
SendCommand(host, "ExecBuiltIn(UpdateLibrary(video))", username, password);
}
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
return false;
}
return true;
}
public virtual void Clean(XbmcSettings settings)
{
//Use EventServer, once Dharma is extinct use Json?
var host = settings.Host;
Logger.Trace("Sending DB Clean Request to XBMC Host: {0}", host);
var command = "ExecBuiltIn(CleanLibrary(video))";
_eventClientProvider.SendAction(host, ActionType.ExecBuiltin, command);
}
public virtual string SendCommand(string host, string command, string username, string password)
{
var url = String.Format("http://{0}/xbmcCmds/xbmcHttp?command={1}", host, command);
if (!String.IsNullOrEmpty(username))
{
return _httpProvider.DownloadString(url, username, password);
}
return _httpProvider.DownloadString(url);
}
public virtual string GetXbmcSeriesPath(string host, int seriesId, string username, string password)
{
var query =
String.Format(
"select path.strPath from path, tvshow, tvshowlinkpath where tvshow.c12 = {0} and tvshowlinkpath.idShow = tvshow.idShow and tvshowlinkpath.idPath = path.idPath",
seriesId);
var command = String.Format("QueryVideoDatabase({0})", query);
const string setResponseCommand =
"SetResponseFormat(webheader;false;webfooter;false;header;<xml>;footer;</xml>;opentag;<tag>;closetag;</tag>;closefinaltag;false)";
const string resetResponseCommand = "SetResponseFormat()";
SendCommand(host, setResponseCommand, username, password);
var response = SendCommand(host, command, username, password);
SendCommand(host, resetResponseCommand, username, password);
if (String.IsNullOrEmpty(response))
return String.Empty;
var xDoc = XDocument.Load(new StringReader(response.Replace("&", "&amp;")));
var xml = (from x in xDoc.Descendants("xml") select x).FirstOrDefault();
if (xml == null)
return String.Empty;
var field = xml.Descendants("field").FirstOrDefault();
if (field == null)
return String.Empty;
return field.Value;
}
public virtual XbmcVersion GetJsonVersion(string host, string username, string password)
{
//2 = Dharma
//3 & 4 = Eden
//5 & 6 = Frodo
try
{
var postJson = new JObject();
postJson.Add(new JProperty("jsonrpc", "2.0"));
postJson.Add(new JProperty("method", "JSONRPC.Version"));
postJson.Add(new JProperty("id", 10));
var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());
if (CheckForJsonError(response))
return new XbmcVersion();
Logger.Trace("Getting version from response");
var result = JsonConvert.DeserializeObject<XbmcJsonResult<JObject>>(response);
var versionObject = result.Result.Property("version");
if (versionObject.Value.Type == JTokenType.Integer)
return new XbmcVersion((int)versionObject.Value);
if (versionObject.Value.Type == JTokenType.Object)
return JsonConvert.DeserializeObject<XbmcVersion>(versionObject.Value.ToString());
throw new InvalidCastException("Unknown Version structure!: " + versionObject);
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
}
return new XbmcVersion();
}
public virtual Dictionary<string, bool> GetActivePlayersDharma(string host, string username, string password)
{
try
{
var postJson = new JObject();
postJson.Add(new JProperty("jsonrpc", "2.0"));
postJson.Add(new JProperty("method", "Player.GetActivePlayers"));
postJson.Add(new JProperty("id", 10));
var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());
if (CheckForJsonError(response))
return null;
var result = JsonConvert.DeserializeObject<ActivePlayersDharmaResult>(response);
return result.Result;
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
}
return null;
}
public virtual List<ActivePlayer> GetActivePlayersEden(string host, string username, string password)
{
try
{
var postJson = new JObject();
postJson.Add(new JProperty("jsonrpc", "2.0"));
postJson.Add(new JProperty("method", "Player.GetActivePlayers"));
postJson.Add(new JProperty("id", 10));
var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());
if (CheckForJsonError(response))
return null;
var result = JsonConvert.DeserializeObject<ActivePlayersEdenResult>(response);
return result.Result;
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
}
return null;
}
public virtual List<TvShow> GetTvShowsJson(string host, string username, string password)
{
try
{
var postJson = new JObject();
postJson.Add(new JProperty("jsonrpc", "2.0"));
postJson.Add(new JProperty("method", "VideoLibrary.GetTvShows"));
postJson.Add(new JProperty("params", new JObject { new JProperty("properties", new string[] { "file", "imdbnumber" }) }));
postJson.Add(new JProperty("id", 10));
var response = _httpProvider.PostCommand(host, username, password, postJson.ToString());
if (CheckForJsonError(response))
return null;
var result = JsonConvert.DeserializeObject<TvShowResponse>(response);
var shows = result.Result.TvShows;
return shows;
}
catch (Exception ex)
{
Logger.DebugException(ex.Message, ex);
}
return null;
}
public virtual bool CheckForJsonError(string response)
{
Logger.Trace("Looking for error in response: {0}", response);
if (response.StartsWith("{\"error\""))
{
var error = JsonConvert.DeserializeObject<ErrorResult>(response);
var code = error.Error["code"];
var message = error.Error["message"];
Logger.Debug("XBMC Json Error. Code = {0}, Message: {1}", code, message);
return true;
}
if (String.IsNullOrWhiteSpace(response))
{
Logger.Debug("Invalid response from XBMC, the response is not valid JSON");
return true;
}
return false;
}
public virtual void TestNotification(string hosts)
{
foreach (var host in hosts.Split(','))
{
Logger.Trace("Sending Test Notifcation to XBMC Host: {0}", host);
_eventClientProvider.SendNotification("Test Notification", "Success! Notifications are setup correctly", IconType.Jpeg, "NzbDrone.jpg", host);
}
}
public virtual void TestJsonApi(string hosts, string username, string password)
{
foreach (var host in hosts.Split(','))
{
Logger.Trace("Sending Test Notifcation to XBMC Host: {0}", host);
var version = GetJsonVersion(host, username, password);
if (version == new XbmcVersion())
throw new Exception("Failed to get JSON version in test");
}
}
}
}

View file

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.Notifications.Xbmc
{
public class XbmcSettings : INotifcationSettings
{
[FieldDefinition(0, Label = "Host", HelpText = "XBMC Hostnname or IP")]
public String Host { get; set; }
[FieldDefinition(1, Label = "Port", HelpText = "Webserver port")]
public Int32 Port { get; set; }
[FieldDefinition(2, Label = "Username", HelpText = "Webserver Username")]
public String Username { get; set; }
[FieldDefinition(3, Label = "Password", HelpText = "Webserver Password ")]
public String Password { get; set; }
[FieldDefinition(4, Label = "Update Library", HelpText = "Update Library on Download & Rename?")]
public Boolean UpdateLibrary { get; set; }
[FieldDefinition(5, Label = "Update Library", HelpText = "Clean Library after update?")]
public Boolean CleanLibrary { get; set; }
[FieldDefinition(6, Label = "Always Update", HelpText = "Update Library even when a video is playing?")]
public Boolean AlwaysUpdate { get; set; }
public bool IsValid
{
get
{
return !string.IsNullOrWhiteSpace(Host) && Port > 0;
}
}
}
}