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.Configuration; using NzbDrone.Core.Tv; using NzbDrone.Core.Model.Xbmc; using NzbDrone.Core.Providers.Xbmc; namespace NzbDrone.Core.Providers { public class XbmcProvider { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly IConfigService _configService; private readonly HttpProvider _httpProvider; private readonly EventClientProvider _eventClientProvider; public XbmcProvider(IConfigService configService, HttpProvider httpProvider, EventClientProvider eventClientProvider) { _configService = configService; _httpProvider = httpProvider; _eventClientProvider = eventClientProvider; } public XbmcProvider() { } public virtual void Notify(string header, string message) { //Always use EventServer, until Json has real support for it foreach (var host in _configService.XbmcHosts.Split(',')) { Logger.Trace("Sending Notifcation to XBMC Host: {0}", host); _eventClientProvider.SendNotification(header, message, IconType.Jpeg, "NzbDrone.jpg", GetHostWithoutPort(host)); } } public virtual void Update(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 = _configService.XbmcUsername; var password = _configService.XbmcPassword; foreach (var host in _configService.XbmcHosts.Split(',')) { 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 (!_configService.XbmcUpdateWhenPlaying) { 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"); continue; } } 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 (!_configService.XbmcUpdateWhenPlaying) { 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"); continue; } } UpdateWithJsonExecBuiltIn(series, host, username, password); } else if (version >= new XbmcVersion(5)) { //Check for active player only when we should skip updates when playing if (!_configService.XbmcUpdateWhenPlaying) { 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"); continue; } } 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>(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() { //Use EventServer, once Dharma is extinct use Json? foreach (var host in _configService.XbmcHosts.Split(',')) { Logger.Trace("Sending DB Clean Request to XBMC Host: {0}", host); var command = "ExecBuiltIn(CleanLibrary(video))"; _eventClientProvider.SendAction(GetHostWithoutPort(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;;footer;;opentag;;closetag;;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("&", "&"))); 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>(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(versionObject.Value.ToString()); throw new InvalidCastException("Unknown Version structure!: " + versionObject); } catch (Exception ex) { Logger.DebugException(ex.Message, ex); } return new XbmcVersion(); } public virtual Dictionary 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(response); return result.Result; } catch (Exception ex) { Logger.DebugException(ex.Message, ex); } return null; } public virtual List 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(response); return result.Result; } catch (Exception ex) { Logger.DebugException(ex.Message, ex); } return null; } public virtual List 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(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(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", GetHostWithoutPort(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"); } } private string GetHostWithoutPort(string address) { return address.Split(':')[0]; } } }