diff --git a/Ombi.Api.Interfaces/IWatcherApi.cs b/Ombi.Api.Interfaces/IWatcherApi.cs index 2aa90b4d5..c747769ed 100644 --- a/Ombi.Api.Interfaces/IWatcherApi.cs +++ b/Ombi.Api.Interfaces/IWatcherApi.cs @@ -34,7 +34,7 @@ namespace Ombi.Api.Interfaces public interface IWatcherApi { WatcherAddMovieResult AddMovie(string imdbId, string apiKey, Uri baseUrl); - List ListMovies(string apiKey, Uri baseUrl); - List ListMovies(string apiKey, Uri baseUrl, string imdbId); + WatcherListStatusResultContainer ListMovies(string apiKey, Uri baseUrl); + WatcherListStatusResultContainer ListMovies(string apiKey, Uri baseUrl, string imdbId); } } \ No newline at end of file diff --git a/Ombi.Api.Models/Watcher/WatcherAddMovieResult.cs b/Ombi.Api.Models/Watcher/WatcherAddMovieResult.cs index dd546ac51..28fbef946 100644 --- a/Ombi.Api.Models/Watcher/WatcherAddMovieResult.cs +++ b/Ombi.Api.Models/Watcher/WatcherAddMovieResult.cs @@ -24,11 +24,19 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion + +using Newtonsoft.Json; + namespace Ombi.Api.Models.Watcher { public class WatcherAddMovieResult { public string status { get; set; } public string message { get; set; } + + [JsonIgnore] + public string ErrorMessage { get; set; } + [JsonIgnore] + public bool Error { get; set; } } } \ No newline at end of file diff --git a/Ombi.Api.Models/Watcher/WatcherListStatusResult.cs b/Ombi.Api.Models/Watcher/WatcherListStatusResult.cs index 4d4d043a4..6fe46f864 100644 --- a/Ombi.Api.Models/Watcher/WatcherListStatusResult.cs +++ b/Ombi.Api.Models/Watcher/WatcherListStatusResult.cs @@ -33,11 +33,11 @@ namespace Ombi.Api.Models.Watcher { public class Quality2 { - [DeserializeAs(Name = "720P")] + [JsonProperty("720P")] public List Q720P { get; set; } - [DeserializeAs(Name = "1080P")] + [JsonProperty("1080P")] public List Q1080P { get; set; } - [DeserializeAs( Name = "4K")] + [JsonProperty("4K")] public List Q4K { get; set; } public List SD { get; set; } } @@ -51,7 +51,7 @@ namespace Ombi.Api.Models.Watcher public class Quality { - [DeserializeAs(Name = "Quality")] + [JsonProperty("Quality")] public Quality2 quality { get; set; } public Filters Filters { get; set; } } @@ -73,5 +73,15 @@ namespace Ombi.Api.Models.Watcher public string tomatorating { get; set; } public string imdbid { get; set; } public Quality quality { get; set; } + + } + + public class WatcherListStatusResultContainer + { + public List Results { get; set; } + [JsonIgnore] + public string ErrorMessage { get; set; } + [JsonIgnore] + public bool Error { get; set; } } } \ No newline at end of file diff --git a/Ombi.Api/WatcherApi.cs b/Ombi.Api/WatcherApi.cs index 769ff58eb..5d1da833a 100644 --- a/Ombi.Api/WatcherApi.cs +++ b/Ombi.Api/WatcherApi.cs @@ -1,7 +1,8 @@ #region Copyright + // /************************************************************************ // Copyright (c) 2016 Jamie Rees -// File: CouchPotatoApi.cs +// File: WatcherApi.cs // Created By: Jamie Rees // // Permission is hereby granted, free of charge, to any person obtaining @@ -23,16 +24,14 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ + #endregion using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json; using NLog; using Ombi.Api.Interfaces; -using Ombi.Api.Models.Movie; using Ombi.Api.Models.Watcher; -using Ombi.Helpers; using RestSharp; namespace Ombi.Api @@ -49,21 +48,68 @@ namespace Ombi.Api public WatcherAddMovieResult AddMovie(string imdbId, string apiKey, Uri baseUrl) { - return Send("addmovie", apiKey, baseUrl, imdbId); + var response = Send("addmovie", apiKey, baseUrl, imdbId); + try + { + return JsonConvert.DeserializeObject(response.Content); + } + catch (Exception e) + { + Log.Error(e); + return new WatcherAddMovieResult + { + Error = true, + ErrorMessage = e.Message + }; + } } - public List ListMovies(string apiKey, Uri baseUrl) + public WatcherListStatusResultContainer ListMovies(string apiKey, Uri baseUrl) { - return Send>("liststatus", apiKey, baseUrl); + var response = Send("liststatus", apiKey, baseUrl); + try + { + if (response.Content.Contains("No movies found")) + { + return new WatcherListStatusResultContainer(); + } + return JsonConvert.DeserializeObject(response.Content); + } + catch (Exception e) + { + Log.Error(e); + return new WatcherListStatusResultContainer + { + Error = true, + ErrorMessage = e.Message + }; + } } - public List ListMovies(string apiKey, Uri baseUrl, string imdbId) + public WatcherListStatusResultContainer ListMovies(string apiKey, Uri baseUrl, string imdbId) { - return Send>("liststatus", apiKey, baseUrl, imdbId); + var response = Send("liststatus", apiKey, baseUrl, imdbId); + try + { + if (response.Content.Contains("No movies found")) + { + return new WatcherListStatusResultContainer(); + } + return JsonConvert.DeserializeObject(response.Content); + } + catch (Exception e) + { + Log.Error(e); + return new WatcherListStatusResultContainer + { + Error = true, + ErrorMessage = e.Message + }; + } } - private T Send(string mode, string apiKey, Uri baseUrl, string imdbid = "") where T : new() + private IRestResponse Send(string mode, string apiKey, Uri baseUrl, string imdbid = "") { RestRequest request; request = new RestRequest @@ -72,11 +118,13 @@ namespace Ombi.Api }; request.AddUrlSegment("apikey", apiKey); - if(!string.IsNullOrEmpty(imdbid)) - { request.AddUrlSegment("imdbid", imdbid);} + if (!string.IsNullOrEmpty(imdbid)) + { + request.AddUrlSegment("imdbid", imdbid); + } request.AddUrlSegment("mode", mode); - return Api.Execute(request, baseUrl); + return Api.Execute(request, baseUrl); } } diff --git a/Ombi.Core/CacheKeys.cs b/Ombi.Core/CacheKeys.cs index ba94819c8..3638875eb 100644 --- a/Ombi.Core/CacheKeys.cs +++ b/Ombi.Core/CacheKeys.cs @@ -42,6 +42,7 @@ namespace Ombi.Core public const string SickRageQueued = nameof(SickRageQueued); public const string CouchPotatoQualityProfiles = nameof(CouchPotatoQualityProfiles); public const string CouchPotatoQueued = nameof(CouchPotatoQueued); + public const string WatcherQueued = nameof(WatcherQueued); public const string GetPlexRequestSettings = nameof(GetPlexRequestSettings); public const string LastestProductVersion = nameof(LastestProductVersion); } diff --git a/Ombi.Core/IMovieSender.cs b/Ombi.Core/IMovieSender.cs new file mode 100644 index 000000000..ea6a7ac5c --- /dev/null +++ b/Ombi.Core/IMovieSender.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ombi.Store; + +namespace Ombi.Core +{ + public interface IMovieSender + { + Task Send(RequestedModel model, string qualityId = ""); + } +} \ No newline at end of file diff --git a/Ombi.Core/MovieSender.cs b/Ombi.Core/MovieSender.cs new file mode 100644 index 000000000..faf2e138e --- /dev/null +++ b/Ombi.Core/MovieSender.cs @@ -0,0 +1,95 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: MovieSender.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Threading.Tasks; +using NLog; +using Ombi.Api.Interfaces; +using Ombi.Core.SettingModels; +using Ombi.Store; + +namespace Ombi.Core +{ + public class MovieSender : IMovieSender + { + public MovieSender(ISettingsService cp, ISettingsService watcher, + ICouchPotatoApi cpApi, IWatcherApi watcherApi) + { + CouchPotatoSettings = cp; + WatcherSettings = watcher; + CpApi = cpApi; + WatcherApi = watcherApi; + } + + private ISettingsService CouchPotatoSettings { get; } + private ISettingsService WatcherSettings { get; } + private ICouchPotatoApi CpApi { get; } + private IWatcherApi WatcherApi { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public async Task Send(RequestedModel model, string qualityId = "") + { + var cpSettings = await CouchPotatoSettings.GetSettingsAsync(); + var watcherSettings = await WatcherSettings.GetSettingsAsync(); + + if (cpSettings.Enabled) + { + return SendToCp(model, cpSettings, string.IsNullOrEmpty(qualityId) ? cpSettings.ProfileId : qualityId); + } + + if (watcherSettings.Enabled) + { + return SendToWatcher(model, watcherSettings); + } + + return new MovieSenderResult { Result = false, MovieSendingEnabled = false }; + } + + private MovieSenderResult SendToWatcher(RequestedModel model, WatcherSettings settings) + { + var result = WatcherApi.AddMovie(model.ImdbId, settings.ApiKey, settings.FullUri); + + if (result.Error) + { + Log.Error(result.ErrorMessage); + return new MovieSenderResult { Result = false }; + } + if (result.status.Equals("success", StringComparison.CurrentCultureIgnoreCase)) + { + return new MovieSenderResult { Result = true, MovieSendingEnabled = true }; + } + Log.Error(result.message); + return new MovieSenderResult { Result = false, MovieSendingEnabled = true }; + } + + private MovieSenderResult SendToCp(RequestedModel model, CouchPotatoSettings settings, string qualityId) + { + var result = CpApi.AddMovie(model.ImdbId, settings.ApiKey, model.Title, settings.FullUri, qualityId); + return new MovieSenderResult { Result = result, MovieSendingEnabled = true }; + } + } +} \ No newline at end of file diff --git a/Ombi.Core/MovieSenderResult.cs b/Ombi.Core/MovieSenderResult.cs new file mode 100644 index 000000000..ccf96ebb9 --- /dev/null +++ b/Ombi.Core/MovieSenderResult.cs @@ -0,0 +1,40 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: MovieSenderResult.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Core +{ + public class MovieSenderResult + { + public bool Result { get; set; } + /// + /// Gets or sets a value indicating whether we can send to either CP or Watcher. + /// + /// + /// true if [movie sending enabled]; otherwise, false. + /// + public bool MovieSendingEnabled { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Core/Ombi.Core.csproj b/Ombi.Core/Ombi.Core.csproj index 927d331ae..828ed3b4e 100644 --- a/Ombi.Core/Ombi.Core.csproj +++ b/Ombi.Core/Ombi.Core.csproj @@ -95,9 +95,12 @@ + + + @@ -119,6 +122,7 @@ + diff --git a/Ombi.Core/SettingModels/ScheduledJobsSettings.cs b/Ombi.Core/SettingModels/ScheduledJobsSettings.cs index 8b9504307..76921a679 100644 --- a/Ombi.Core/SettingModels/ScheduledJobsSettings.cs +++ b/Ombi.Core/SettingModels/ScheduledJobsSettings.cs @@ -35,6 +35,7 @@ namespace Ombi.Core.SettingModels public int SickRageCacher { get; set; } public int SonarrCacher { get; set; } public int CouchPotatoCacher { get; set; } + public int WatcherCacher { get; set; } public int StoreBackup { get; set; } public int StoreCleanup { get; set; } public int UserRequestLimitResetter { get; set; } diff --git a/Ombi.Core/SettingModels/WatcherSettings.cs b/Ombi.Core/SettingModels/WatcherSettings.cs new file mode 100644 index 000000000..7f22f7bbe --- /dev/null +++ b/Ombi.Core/SettingModels/WatcherSettings.cs @@ -0,0 +1,34 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: WatcherSettings.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace Ombi.Core.SettingModels +{ + public sealed class WatcherSettings : ExternalSettings + { + public bool Enabled { get; set; } + public string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IWatcherCacher.cs b/Ombi.Services/Interfaces/IWatcherCacher.cs new file mode 100644 index 000000000..6bdc935eb --- /dev/null +++ b/Ombi.Services/Interfaces/IWatcherCacher.cs @@ -0,0 +1,8 @@ +namespace Ombi.Services.Interfaces +{ + public interface IWatcherCacher + { + void Queued(); + string[] QueuedIds(); + } +} diff --git a/Ombi.Services/Jobs/JobNames.cs b/Ombi.Services/Jobs/JobNames.cs index c5b2da663..06b5b32ac 100644 --- a/Ombi.Services/Jobs/JobNames.cs +++ b/Ombi.Services/Jobs/JobNames.cs @@ -30,6 +30,7 @@ namespace Ombi.Services.Jobs { public const string StoreBackup = "Database Backup"; public const string CpCacher = "CouchPotato Cacher"; + public const string WatcherCacher = "Watcher Cacher"; public const string SonarrCacher = "Sonarr Cacher"; public const string SrCacher = "SickRage Cacher"; public const string PlexChecker = "Plex Availability Cacher"; diff --git a/Ombi.Services/Jobs/WatcherCacher.cs b/Ombi.Services/Jobs/WatcherCacher.cs new file mode 100644 index 000000000..e25784040 --- /dev/null +++ b/Ombi.Services/Jobs/WatcherCacher.cs @@ -0,0 +1,132 @@ +#region Copyright + +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexAvailabilityChecker.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ + +#endregion + +using System; +using System.Linq; +using NLog; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.Movie; +using Ombi.Core; +using Ombi.Core.SettingModels; +using Ombi.Helpers; +using Ombi.Services.Interfaces; +using Quartz; + +namespace Ombi.Services.Jobs +{ + public class WatcherCacher : IJob, IWatcherCacher + { + public WatcherCacher( + ISettingsService watcher, + IWatcherApi watcherApi, ICacheProvider cache, IJobRecord rec) + { + WatcherSettings = watcher; + WatcherApi = WatcherApi; + Cache = cache; + Job = rec; + } + + private ISettingsService WatcherSettings { get; } + private ICacheProvider Cache { get; } + private IWatcherApi WatcherApi { get; } + private IJobRecord Job { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public void Queued() + { + Log.Trace("Getting the settings"); + + var watcherSettings = WatcherSettings.GetSettings(); + + Job.SetRunning(true, JobNames.WatcherCacher); + try + { + if (watcherSettings.Enabled) + { + var movies = WatcherApi.ListMovies(watcherSettings.ApiKey, watcherSettings.FullUri); + if (movies.Error) + { + Log.Error("Error when trying to get Watchers movies"); + Log.Error(movies.ErrorMessage); + } + var wantedMovies = + movies?.Results?.Where(x => x.status.Equals("Wanted", StringComparison.CurrentCultureIgnoreCase)); + if (wantedMovies != null && wantedMovies.Any()) + { + Cache.Set(CacheKeys.WatcherQueued, movies.Results.Select(x => x.imdbid), CacheKeys.TimeFrameMinutes.SchedulerCaching); + } + + } + + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + Job.Record(JobNames.WatcherCacher); + Job.SetRunning(false, JobNames.WatcherCacher); + } + } + + // we do not want to set here... + public string[] QueuedIds() + { + try + { + + var watcherSettings = WatcherSettings.GetSettings(); + + if (watcherSettings.Enabled) + { + var movies = Cache.Get(CacheKeys.WatcherQueued); + + if (movies != null) + { + return movies; + } + return new string[] {}; + } + } + catch (Exception e) + { + Log.Error(e); + return new string[] { }; + } + return new string[] {}; + } + + public void Execute(IJobExecutionContext context) + { + Queued(); + } + } +} diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj index d27712399..fabb8e69f 100644 --- a/Ombi.Services/Ombi.Services.csproj +++ b/Ombi.Services/Ombi.Services.csproj @@ -86,8 +86,10 @@ + + diff --git a/Ombi.UI/Jobs/Scheduler.cs b/Ombi.UI/Jobs/Scheduler.cs index 9f01ac625..641d32a15 100644 --- a/Ombi.UI/Jobs/Scheduler.cs +++ b/Ombi.UI/Jobs/Scheduler.cs @@ -33,6 +33,7 @@ using System.Linq; using NLog; using Ombi.Core; using Ombi.Core.SettingModels; +using Ombi.Services.Interfaces; using Ombi.Services.Jobs; using Ombi.UI.Helpers; using Quartz; @@ -68,6 +69,7 @@ namespace Ombi.UI.Jobs JobBuilder.Create().WithIdentity("SickRageCacher", "Cache").Build(), JobBuilder.Create().WithIdentity("SonarrCacher", "Cache").Build(), JobBuilder.Create().WithIdentity("CouchPotatoCacher", "Cache").Build(), + JobBuilder.Create().WithIdentity("WatcherCacher", "Cache").Build(), JobBuilder.Create().WithIdentity("StoreBackup", "Database").Build(), JobBuilder.Create().WithIdentity("StoreCleanup", "Database").Build(), JobBuilder.Create().WithIdentity("UserRequestLimiter", "Request").Build(), @@ -117,6 +119,10 @@ namespace Ombi.UI.Jobs { s.CouchPotatoCacher = 60; } + if (s.WatcherCacher == 0) + { + s.WatcherCacher = 60; + } if (s.PlexAvailabilityChecker == 0) { s.PlexAvailabilityChecker = 60; @@ -208,6 +214,13 @@ namespace Ombi.UI.Jobs .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.CouchPotatoCacher).RepeatForever()) .Build(); + var watcherCacher = + TriggerBuilder.Create() + .WithIdentity("WatcherCacher", "Cache") + .StartAt(DateBuilder.FutureDate(4, IntervalUnit.Minute)) + .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.WatcherCacher).RepeatForever()) + .Build(); + var storeBackup = TriggerBuilder.Create() .WithIdentity("StoreBackup", "Database") @@ -258,6 +271,7 @@ namespace Ombi.UI.Jobs triggers.Add(srCacher); triggers.Add(sonarrCacher); triggers.Add(cpCacher); + triggers.Add(watcherCacher); triggers.Add(storeBackup); triggers.Add(storeCleanup); triggers.Add(userRequestLimiter); diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index 2b3601924..ea320ff14 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -75,6 +75,7 @@ namespace Ombi.UI.Modules.Admin private ISettingsService PushoverService { get; } private ISettingsService HeadphonesService { get; } private ISettingsService NewsLetterService { get; } + private ISettingsService WatcherSettings { get; } private ISettingsService LogService { get; } private IPlexApi PlexApi { get; } private ISonarrApi SonarrApi { get; } @@ -116,7 +117,8 @@ namespace Ombi.UI.Modules.Admin ICacheProvider cache, ISettingsService slackSettings, ISlackApi slackApi, ISettingsService lp, ISettingsService scheduler, IJobRecord rec, IAnalytics analytics, - ISettingsService notifyService, IRecentlyAdded recentlyAdded + ISettingsService notifyService, IRecentlyAdded recentlyAdded, + ISettingsService watcherSettings , ISecurityExtensions security) : base("admin", prService, security) { PrService = prService; @@ -147,6 +149,7 @@ namespace Ombi.UI.Modules.Admin Analytics = analytics; NotifySettings = notifyService; RecentlyAdded = recentlyAdded; + WatcherSettings = watcherSettings; Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); @@ -374,6 +377,18 @@ namespace Ombi.UI.Modules.Admin return Response.AsJson(valid.SendJsonError()); } + var watcherSettings = WatcherSettings.GetSettings(); + + if (watcherSettings.Enabled) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Cannot have Watcher and CouchPotato both enabled." + }); + } + couchPotatoSettings.ApiKey = couchPotatoSettings.ApiKey.Trim(); var result = CpService.SaveSettings(couchPotatoSettings); return Response.AsJson(result diff --git a/Ombi.UI/Modules/Admin/IntegrationModule.cs b/Ombi.UI/Modules/Admin/IntegrationModule.cs new file mode 100644 index 000000000..75c1f48c9 --- /dev/null +++ b/Ombi.UI/Modules/Admin/IntegrationModule.cs @@ -0,0 +1,107 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SystemStatusModule.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using MarkdownSharp; +using Nancy; +using Nancy.ModelBinding; +using Nancy.Responses.Negotiation; +using Nancy.Validation; +using Ombi.Core; +using Ombi.Core.SettingModels; +using Ombi.Core.StatusChecker; +using Ombi.Helpers; +using Ombi.Helpers.Analytics; +using Ombi.Helpers.Permissions; +using Ombi.UI.Helpers; +using Ombi.UI.Models; +using Action = Ombi.Helpers.Analytics.Action; +using ISecurityExtensions = Ombi.Core.ISecurityExtensions; + +namespace Ombi.UI.Modules.Admin +{ + public class IntegrationModule : BaseModule + { + public IntegrationModule(ISettingsService settingsService, ISettingsService watcher, + ISettingsService cp,ISecurityExtensions security, IAnalytics a) : base("admin", settingsService, security) + { + + WatcherSettings = watcher; + Analytics = a; + + Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); + + Get["/watcher", true] = async (x, ct) => await Watcher(); + Post["/watcher", true] = async (x, ct) => await SaveWatcher(); + } + + private ISettingsService WatcherSettings { get; } + private ISettingsService CpSettings { get; } + private IAnalytics Analytics { get; } + + private async Task Watcher() + { + var settings = await WatcherSettings.GetSettingsAsync(); + + return View["Watcher", settings]; + } + + private async Task SaveWatcher() + { + var settings = this.Bind(); + var valid = this.Validate(settings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + + var cpSettings = await CpSettings.GetSettingsAsync().ConfigureAwait(false); + + if (cpSettings.Enabled) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Cannot have Watcher and CouchPotato both enabled." + }); + } + + settings.ApiKey = settings.ApiKey.Trim(); + var result = await WatcherSettings.SaveSettingsAsync(settings); + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Watcher!" } + : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); + } + + } +} \ No newline at end of file diff --git a/Ombi.UI/Modules/ApplicationTesterModule.cs b/Ombi.UI/Modules/ApplicationTesterModule.cs index 9a3349600..1fabfef12 100644 --- a/Ombi.UI/Modules/ApplicationTesterModule.cs +++ b/Ombi.UI/Modules/ApplicationTesterModule.cs @@ -45,7 +45,8 @@ namespace Ombi.UI.Modules { public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi, - ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService pr, ISecurityExtensions security) : base("test", pr, security) + ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService pr, ISecurityExtensions security, + IWatcherApi watcherApi) : base("test", pr, security) { this.RequiresAuthentication(); @@ -54,6 +55,7 @@ namespace Ombi.UI.Modules PlexApi = plexApi; SickRageApi = srApi; HeadphonesApi = hpApi; + WatcherApi = watcherApi; Post["/cp"] = _ => CouchPotatoTest(); Post["/sonarr"] = _ => SonarrTest(); @@ -61,6 +63,7 @@ namespace Ombi.UI.Modules Post["/sickrage"] = _ => SickRageTest(); Post["/headphones"] = _ => HeadphonesTest(); Post["/plexdb"] = _ => TestPlexDb(); + Post["/watcher"] = _ => WatcherTest(); } private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -69,6 +72,7 @@ namespace Ombi.UI.Modules private IPlexApi PlexApi { get; } private ISickRageApi SickRageApi { get; } private IHeadphonesApi HeadphonesApi { get; } + private IWatcherApi WatcherApi { get; } private Response CouchPotatoTest() { @@ -86,7 +90,7 @@ namespace Ombi.UI.Modules : Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to CouchPotato, please check your settings." }); } - catch (Exception e) // Exceptions are expected if we cannot connect so we will just log and swallow them. + catch (Exception e) // Exceptions are expected if we cannot connect so we will just log and swallow them. { Log.Warn("Exception thrown when attempting to get CP's status: "); Log.Warn(e); @@ -99,6 +103,35 @@ namespace Ombi.UI.Modules } } + private Response WatcherTest() + { + var settings = this.Bind(); + var valid = this.Validate(settings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + try + { + var status = WatcherApi.ListMovies(settings.ApiKey, settings.FullUri); + return !status.Error + ? Response.AsJson(new JsonResponseModel { Result = true, Message = "Connected to Watcher successfully!" }) + : Response.AsJson(new JsonResponseModel { Result = false, Message = $"Could not connect to Watcher, Error: {status.ErrorMessage}" }); + + } + catch (Exception e) // Exceptions are expected if we cannot connect so we will just log and swallow them. + { + Log.Warn("Exception thrown when attempting to test Watcher "); + Log.Warn(e); + var message = $"Could not connect to Watcher, please check your settings. Exception Message: {e.Message}"; + if (e.InnerException != null) + { + message = $"Could not connect to Watcher, please check your settings. Exception Message: {e.InnerException.Message}"; + } + return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); + } + } + private Response SonarrTest() { var sonarrSettings = this.Bind(); diff --git a/Ombi.UI/Modules/ApprovalModule.cs b/Ombi.UI/Modules/ApprovalModule.cs index 3e47c3714..9781b79a9 100644 --- a/Ombi.UI/Modules/ApprovalModule.cs +++ b/Ombi.UI/Modules/ApprovalModule.cs @@ -47,17 +47,15 @@ namespace Ombi.UI.Modules public class ApprovalModule : BaseAuthModule { - public ApprovalModule(IRequestService service, ISettingsService cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, + public ApprovalModule(IRequestService service, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr, ITransientFaultQueue faultQueue - , ISecurityExtensions security) : base("approval", pr, security) + , ISecurityExtensions security, IMovieSender movieSender) : base("approval", pr, security) { Before += (ctx) => Security.AdminLoginRedirect(ctx, Permissions.Administrator,Permissions.ManageRequests); Service = service; - CpService = cpService; - CpApi = cpApi; SonarrApi = sonarrApi; SonarrSettings = sonarrSettings; SickRageApi = srApi; @@ -65,6 +63,7 @@ namespace Ombi.UI.Modules HeadphonesSettings = hpSettings; HeadphoneApi = hpApi; FaultQueue = faultQueue; + MovieSender = movieSender; Post["/approve", true] = async (x, ct) => await Approve((int)Request.Form.requestid, (string)Request.Form.qualityId); Post["/deny", true] = async (x, ct) => await DenyRequest((int)Request.Form.requestid, (string)Request.Form.reason); @@ -77,15 +76,14 @@ namespace Ombi.UI.Modules } private IRequestService Service { get; } + private IMovieSender MovieSender { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); private ISettingsService SonarrSettings { get; } private ISettingsService SickRageSettings { get; } - private ISettingsService CpService { get; } private ISettingsService HeadphonesSettings { get; } private ISonarrApi SonarrApi { get; } private ISickRageApi SickRageApi { get; } - private ICouchPotatoApi CpApi { get; } private IHeadphonesApi HeadphoneApi { get; } private ITransientFaultQueue FaultQueue { get; } @@ -186,19 +184,19 @@ namespace Ombi.UI.Modules private async Task RequestMovieAndUpdateStatus(RequestedModel request, string qualityId) { - var cpSettings = await CpService.GetSettingsAsync(); - Log.Info("Adding movie to CouchPotato : {0}", request.Title); - if (!cpSettings.Enabled) + var result = await MovieSender.Send(request, qualityId); + + if (!result.MovieSendingEnabled) { // Approve it request.Approved = true; - Log.Warn("We approved movie: {0} but could not add it to CouchPotato because it has not been setup", request.Title); + Log.Warn("We approved movie: {0} but could not add it to CouchPotato/Watcher because it has not been setup", request.Title); // Update the record var inserted = await Service.UpdateRequestAsync(request); return Response.AsJson(inserted - ? new JsonResponseModel { Result = true, Message = "This has been approved, but It has not been sent to CouchPotato because it has not been configured." } + ? new JsonResponseModel { Result = true, Message = "This has been approved, but It has not been sent to CouchPotato/Watcher because it has not been configured." } : new JsonResponseModel { Result = false, @@ -206,9 +204,7 @@ namespace Ombi.UI.Modules }); } - var result = CpApi.AddMovie(request.ImdbId, cpSettings.ApiKey, request.Title, cpSettings.FullUri, string.IsNullOrEmpty(qualityId) ? cpSettings.ProfileId : qualityId); - Log.Trace("Adding movie to CP result {0}", result); - if (result) + if (result.Result) { // Approve it request.Approved = true; @@ -230,7 +226,7 @@ namespace Ombi.UI.Modules { Result = false, Message = - "Something went wrong adding the movie to CouchPotato! Please check your settings." + "Something went wrong adding the movie! Please check your settings." }); } @@ -415,26 +411,23 @@ namespace Ombi.UI.Modules private async Task UpdateRequestsAsync(RequestedModel[] requestedModels) { - var cpSettings = await CpService.GetSettingsAsync(); var updatedRequests = new List(); foreach (var r in requestedModels) { if (r.Type == RequestType.Movie) { - if (cpSettings.Enabled) + var movieResult = await MovieSender.Send(r); + if (movieResult.Result) { - var res = SendMovie(cpSettings, r, CpApi); - if (res) - { - r.Approved = true; - updatedRequests.Add(r); - } - else - { - Log.Error("Could not approve and send the movie {0} to couch potato!", r.Title); - } + r.Approved = true; + updatedRequests.Add(r); + } else + { + Log.Error("Could not approve and send the movie {0} to couch potato!", r.Title); + } + if(!movieResult.MovieSendingEnabled) { r.Approved = true; updatedRequests.Add(r); @@ -512,13 +505,5 @@ namespace Ombi.UI.Modules : Response.AsJson(new JsonResponseModel { Result = false, Message = "An error happened, could not update the DB" }); } - - private bool SendMovie(CouchPotatoSettings settings, RequestedModel r, ICouchPotatoApi cp) - { - Log.Info("Adding movie to CP : {0}", r.Title); - var result = cp.AddMovie(r.ImdbId, settings.ApiKey, r.Title, settings.FullUri, settings.ProfileId); - Log.Trace("Adding movie to CP result {0}", result); - return result; - } } } \ No newline at end of file diff --git a/Ombi.UI/Modules/SearchModule.cs b/Ombi.UI/Modules/SearchModule.cs index 5722fc53f..51f687cb9 100644 --- a/Ombi.UI/Modules/SearchModule.cs +++ b/Ombi.UI/Modules/SearchModule.cs @@ -66,22 +66,22 @@ namespace Ombi.UI.Modules { public class SearchModule : BaseAuthModule { - public SearchModule(ICacheProvider cache, ISettingsService cpSettings, + public SearchModule(ICacheProvider cache, ISettingsService prSettings, IAvailabilityChecker checker, IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, - ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, + ISettingsService sickRageService, ISickRageApi srApi, INotificationService notify, IMusicBrainzApi mbApi, IHeadphonesApi hpApi, ISettingsService hpService, - ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, + ICouchPotatoCacher cpCacher, IWatcherCacher watcherCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, ISettingsService plexService, ISettingsService auth, IRepository u, ISettingsService email, - IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, ISecurityExtensions security) + IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, + ISecurityExtensions security, IMovieSender movieSender) : base("search", prSettings, security) { Auth = auth; PlexService = plexService; PlexApi = plexApi; - CpService = cpSettings; PrService = prSettings; MovieApi = new TheMovieDbApi(); Cache = cache; @@ -92,7 +92,6 @@ namespace Ombi.UI.Modules RequestService = request; SonarrApi = sonarrApi; SonarrService = sonarrSettings; - CouchPotatoApi = cpApi; SickRageService = sickRageService; SickrageApi = srApi; NotificationService = notify; @@ -107,7 +106,8 @@ namespace Ombi.UI.Modules FaultQueue = tfQueue; TvApi = new TvMazeApi(); PlexContentRepository = content; - + MovieSender = movieSender; + WatcherCacher = watcherCacher; Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); @@ -128,20 +128,19 @@ namespace Ombi.UI.Modules Get["/seasons"] = x => GetSeasons(); Get["/episodes", true] = async (x, ct) => await GetEpisodes(); } - + private IWatcherCacher WatcherCacher { get; } + private IMovieSender MovieSender { get; } private IRepository PlexContentRepository { get; } private TvMazeApi TvApi { get; } private IPlexApi PlexApi { get; } private TheMovieDbApi MovieApi { get; } private INotificationService NotificationService { get; } - private ICouchPotatoApi CouchPotatoApi { get; } private ISonarrApi SonarrApi { get; } private ISickRageApi SickrageApi { get; } private IRequestService RequestService { get; } private ICacheProvider Cache { get; } private ISettingsService Auth { get; } private ISettingsService PlexService { get; } - private ISettingsService CpService { get; } private ISettingsService PrService { get; } private ISettingsService SonarrService { get; } private ISettingsService SickRageService { get; } @@ -236,9 +235,9 @@ namespace Ombi.UI.Modules var cpCached = CpCacher.QueuedIds(); + var watcherCached = WatcherCacher.QueuedIds(); var content = PlexContentRepository.GetAll(); var plexMovies = Checker.GetPlexMovies(content); - var settings = await PrService.GetSettingsAsync(); var viewMovies = new List(); var counter = 0; foreach (var movie in apiMovies) @@ -290,6 +289,10 @@ namespace Ombi.UI.Modules { viewMovie.Requested = true; } + else if(watcherCached.Contains(imdbId) && canSee) // compare to the watcher db + { + viewMovie.Requested = true; + } viewMovies.Add(viewMovie); } @@ -564,30 +567,25 @@ namespace Ombi.UI.Modules { if (ShouldAutoApprove(RequestType.Movie, settings, Username)) { - var cpSettings = await CpService.GetSettingsAsync(); - model.Approved = true; - if (cpSettings.Enabled) - { - Log.Info("Adding movie to CP (No approval required)"); - var result = CouchPotatoApi.AddMovie(model.ImdbId, cpSettings.ApiKey, model.Title, - cpSettings.FullUri, cpSettings.ProfileId); - Log.Debug("Adding movie to CP result {0}", result); - if (result) - { - return - await - AddRequest(model, settings, - $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); - } - return Response.AsJson(new JsonResponseModel - { - Result = false, - Message = Resources.UI.Search_CouchPotatoError - }); + var result = await MovieSender.Send(model); + if (result.Result) + { + return await AddRequest(model, settings, + $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); } - model.Approved = true; - return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + if (!result.MovieSendingEnabled) + { + + model.Approved = true; + return await AddRequest(model, settings, $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = Resources.UI.Search_CouchPotatoError + }); } diff --git a/Ombi.UI/NinjectModules/ServicesModule.cs b/Ombi.UI/NinjectModules/ServicesModule.cs index 49590be0c..63d5ade86 100644 --- a/Ombi.UI/NinjectModules/ServicesModule.cs +++ b/Ombi.UI/NinjectModules/ServicesModule.cs @@ -43,6 +43,7 @@ namespace Ombi.UI.NinjectModules { Bind().To(); Bind().To(); + Bind().To(); Bind().To(); Bind().To(); Bind().To(); diff --git a/Ombi.UI/Ombi.UI.csproj b/Ombi.UI/Ombi.UI.csproj index dc4b3c2ff..766485f1c 100644 --- a/Ombi.UI/Ombi.UI.csproj +++ b/Ombi.UI/Ombi.UI.csproj @@ -251,6 +251,7 @@ + @@ -282,6 +283,7 @@ + @@ -761,6 +763,9 @@ Always + + Always + PreserveNewest diff --git a/Ombi.UI/Validators/WatcherValidator.cs b/Ombi.UI/Validators/WatcherValidator.cs new file mode 100644 index 000000000..4f78fbdba --- /dev/null +++ b/Ombi.UI/Validators/WatcherValidator.cs @@ -0,0 +1,43 @@ + +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SonarrValidator.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using FluentValidation; +using Ombi.Core.SettingModels; + +namespace Ombi.UI.Validators +{ + public class WatcherValidator : AbstractValidator + { + public WatcherValidator() + { + RuleFor(request => request.Ip).NotEmpty().WithMessage("You must specify a IP/Host name."); + RuleFor(request => request.Port).NotEmpty().WithMessage("You must specify a Port."); + RuleFor(request => request.ApiKey).NotEmpty().WithMessage("You must specify a Api Key."); + } + } +} \ No newline at end of file diff --git a/Ombi.UI/Views/Integration/Watcher.cshtml b/Ombi.UI/Views/Integration/Watcher.cshtml new file mode 100644 index 000000000..6dcda811c --- /dev/null +++ b/Ombi.UI/Views/Integration/Watcher.cshtml @@ -0,0 +1,133 @@ +@using Ombi.UI.Helpers +@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase +@Html.Partial("Shared/Partial/_Sidebar") +@{ + int port; + if (Model.Port == 0) + { + port = 9090; + } + else + { + port = Model.Port; + } +} +
+
+
+ Watcher Settings + + @Html.Checkbox(Model.Enabled, "Enabled", "Enabled") + +
+ +
+ +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ +
+
+ + @Html.Checkbox(Model.Ssl, "Ssl", "SSL") + +
+ +
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml b/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml index 82a78674a..0a2a64e86 100644 --- a/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml +++ b/Ombi.UI/Views/Shared/Partial/_Sidebar.cshtml @@ -9,6 +9,7 @@ @Html.GetSidebarUrl(Context, "/admin/usermanagementsettings", "User Management Settings") @Html.GetSidebarUrl(Context, "/admin/plex", "Plex") @Html.GetSidebarUrl(Context, "/admin/couchpotato", "CouchPotato") + @Html.GetSidebarUrl(Context, "/admin/watcher", "Watcher") @Html.GetSidebarUrl(Context, "/admin/sonarr", "Sonarr") @Html.GetSidebarUrl(Context, "/admin/sickrage", "SickRage") @Html.GetSidebarUrl(Context, "/admin/headphones", "Headphones (Beta)")