diff --git a/Ombi.Api.Interfaces/IDiscordApi.cs b/Ombi.Api.Interfaces/IDiscordApi.cs new file mode 100644 index 000000000..cb3ff9203 --- /dev/null +++ b/Ombi.Api.Interfaces/IDiscordApi.cs @@ -0,0 +1,38 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: IDiscordApi.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.Threading.Tasks; +using Ombi.Api.Models.Notifications; + +namespace Ombi.Api.Interfaces +{ + public interface IDiscordApi + { + void SendMessage(string message, string webhookId, string webhookToken, string username = null); + Task SendMessageAsync(string message, string webhookId, string webhookToken, string username = null); + } +} \ No newline at end of file diff --git a/Ombi.Api.Interfaces/INetflixApi.cs b/Ombi.Api.Interfaces/INetflixApi.cs index 9e1a02b6b..2c427f6f3 100644 --- a/Ombi.Api.Interfaces/INetflixApi.cs +++ b/Ombi.Api.Interfaces/INetflixApi.cs @@ -31,6 +31,6 @@ namespace Ombi.Api.Interfaces { public interface INetflixApi { - NetflixMovieResult GetMovies(string movieName, string year = null); + NetflixMovieResult CheckNetflix(string title, string year = null); } } \ No newline at end of file diff --git a/Ombi.Api.Interfaces/IRadarrApi.cs b/Ombi.Api.Interfaces/IRadarrApi.cs new file mode 100644 index 000000000..88e6d3028 --- /dev/null +++ b/Ombi.Api.Interfaces/IRadarrApi.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using Ombi.Api.Models.Radarr; +using Ombi.Api.Models.Sonarr; + +namespace Ombi.Api.Interfaces +{ + public interface IRadarrApi + { + RadarrAddMovie AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, Uri baseUrl, bool searchNow = false); + List GetMovies(string apiKey, Uri baseUrl); + List GetProfiles(string apiKey, Uri baseUrl); + SystemStatus SystemStatus(string apiKey, Uri baseUrl); + } +} \ No newline at end of file diff --git a/Ombi.Api.Interfaces/ISonarrApi.cs b/Ombi.Api.Interfaces/ISonarrApi.cs index 203020737..bce750901 100644 --- a/Ombi.Api.Interfaces/ISonarrApi.cs +++ b/Ombi.Api.Interfaces/ISonarrApi.cs @@ -40,10 +40,11 @@ SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFo bool searchForMissingEpisodes = false); SonarrAddSeries AddSeriesNew(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, - int[] seasons, string apiKey, Uri baseUrl, bool monitor = true, + int[] seasons, string apiKey, Uri baseUrl, bool monitor = true, bool searchForMissingEpisodes = false); SystemStatus SystemStatus(string apiKey, Uri baseUrl); + List GetRootFolders(string apiKey, Uri baseUrl); List GetSeries(string apiKey, Uri baseUrl); Series GetSeries(string seriesId, string apiKey, Uri baseUrl); diff --git a/Ombi.Api.Interfaces/ITraktApi.cs b/Ombi.Api.Interfaces/ITraktApi.cs new file mode 100644 index 000000000..ed20f039c --- /dev/null +++ b/Ombi.Api.Interfaces/ITraktApi.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TraktApiSharp.Enums; +using TraktApiSharp.Objects.Get.Shows; +using TraktApiSharp.Objects.Get.Shows.Common; + +namespace Ombi.Api.Interfaces +{ + public interface ITraktApi + { + Task> GetAnticipatedShows(int? page = default(int?), int? limitPerPage = default(int?)); + Task> GetMostWatchesShows(TraktTimePeriod period = null, int? page = default(int?), int? limitPerPage = default(int?)); + Task> GetPopularShows(int? page = default(int?), int? limitPerPage = default(int?)); + Task> GetTrendingShows(int? page = default(int?), int? limitPerPage = default(int?)); + } +} \ No newline at end of file diff --git a/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj b/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj index 201d97b71..c8c1ca938 100644 --- a/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj +++ b/Ombi.Api.Interfaces/Ombi.Api.Interfaces.csproj @@ -31,6 +31,10 @@ 4 + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + ..\packages\RestSharp.105.2.3\lib\net45\RestSharp.dll True @@ -43,19 +47,26 @@ + + ..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll + True + + + + diff --git a/Ombi.Api.Interfaces/packages.config b/Ombi.Api.Interfaces/packages.config index a63cb4deb..5ac87cab2 100644 --- a/Ombi.Api.Interfaces/packages.config +++ b/Ombi.Api.Interfaces/packages.config @@ -1,4 +1,6 @@  + + \ No newline at end of file diff --git a/Ombi.Api.Models/Movie/TmdbMovieDetails.cs b/Ombi.Api.Models/Movie/TmdbMovieDetails.cs new file mode 100644 index 000000000..4d987ac02 --- /dev/null +++ b/Ombi.Api.Models/Movie/TmdbMovieDetails.cs @@ -0,0 +1,104 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: TmdbMovieDetails.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.Collections.Generic; + +namespace Ombi.Api.Models.Movie +{ + + public class Genre + { + public int id { get; set; } + public string name { get; set; } + } + + public class ProductionCompany + { + public string name { get; set; } + public int id { get; set; } + } + + public class ProductionCountry + { + public string iso_3166_1 { get; set; } + public string name { get; set; } + } + + public class SpokenLanguage + { + public string iso_639_1 { get; set; } + public string name { get; set; } + } + + public class Result + { + public string id { get; set; } + public string iso_639_1 { get; set; } + public string iso_3166_1 { get; set; } + public string key { get; set; } + public string name { get; set; } + public string site { get; set; } + public int size { get; set; } + public string type { get; set; } + } + + public class Videos + { + public List results { get; set; } + } + + public class TmdbMovieDetails + { + public bool adult { get; set; } + public string backdrop_path { get; set; } + public object belongs_to_collection { get; set; } + public int budget { get; set; } + public List genres { get; set; } + public string homepage { get; set; } + public int id { get; set; } + public string imdb_id { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public string overview { get; set; } + public double popularity { get; set; } + public string poster_path { get; set; } + public List production_companies { get; set; } + public List production_countries { get; set; } + public string release_date { get; set; } + public int revenue { get; set; } + public int runtime { get; set; } + public List spoken_languages { get; set; } + public string status { get; set; } + public string tagline { get; set; } + public string title { get; set; } + public bool video { get; set; } + public double vote_average { get; set; } + public int vote_count { get; set; } + public Videos videos { get; set; } + } + +} \ No newline at end of file diff --git a/Ombi.Api.Models/Netflix/NetflixMovieResult.cs b/Ombi.Api.Models/Netflix/NetflixMovieResult.cs index 71007d34f..0ed213cf2 100644 --- a/Ombi.Api.Models/Netflix/NetflixMovieResult.cs +++ b/Ombi.Api.Models/Netflix/NetflixMovieResult.cs @@ -58,5 +58,12 @@ public class NetflixMovieResult public string Mediatype { get; set; } [JsonProperty(PropertyName = "runtime")] public string Runtime { get; set; } + + + // For errors + [JsonProperty(PropertyName = "errorcode")] + public int ErrorCode { get; set; } + [JsonProperty(PropertyName = "message")] + public string Message { get; set; } } } \ No newline at end of file diff --git a/Ombi.Api.Models/Notifications/DiscordWebhookRequest.cs b/Ombi.Api.Models/Notifications/DiscordWebhookRequest.cs new file mode 100644 index 000000000..653235c08 --- /dev/null +++ b/Ombi.Api.Models/Notifications/DiscordWebhookRequest.cs @@ -0,0 +1,34 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: DiscordWebhookRequest.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.Api.Models.Notifications +{ + public class DiscordWebhookRequest + { + public string content { get; set; } + public string username { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Api.Models/Notifications/DiscordWebhookResponse.cs b/Ombi.Api.Models/Notifications/DiscordWebhookResponse.cs new file mode 100644 index 000000000..ac7978c4c --- /dev/null +++ b/Ombi.Api.Models/Notifications/DiscordWebhookResponse.cs @@ -0,0 +1,47 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: DiscordWebhookResponse.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 Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Ombi.Api.Models.Notifications +{ + public class DiscordWebhookResponse + { + public string name { get; set; } + [JsonProperty(PropertyName = "channel_id")] + public string channelid { get; set; } + + public string token { get; set; } + public string avatar { get; set; } + [JsonProperty(PropertyName = "guild_id")] + public string guildid { get; set; } + + public string id { get; set; } + + } +} \ No newline at end of file diff --git a/Ombi.Api.Models/Ombi.Api.Models.csproj b/Ombi.Api.Models/Ombi.Api.Models.csproj index 32a55c507..39df12460 100644 --- a/Ombi.Api.Models/Ombi.Api.Models.csproj +++ b/Ombi.Api.Models/Ombi.Api.Models.csproj @@ -54,6 +54,7 @@ + @@ -62,6 +63,8 @@ + + @@ -80,6 +83,10 @@ + + + + @@ -95,6 +102,7 @@ + diff --git a/Ombi.Api.Models/Radarr/RadarrAddMovie.cs b/Ombi.Api.Models/Radarr/RadarrAddMovie.cs new file mode 100644 index 000000000..4b8aeb452 --- /dev/null +++ b/Ombi.Api.Models/Radarr/RadarrAddMovie.cs @@ -0,0 +1,56 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: RadarrAddMovie.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.Collections.Generic; +using Newtonsoft.Json; +using Ombi.Api.Models.Sonarr; + +namespace Ombi.Api.Models.Radarr +{ + public class RadarrAddMovie + { + + public RadarrAddMovie() + { + images = new List(); + } + public RadarrError Error { get; set; } + public RadarrAddOptions addOptions { get; set; } + public string title { get; set; } + public string rootFolderPath { get; set; } + public int qualityProfileId { get; set; } + public bool monitored { get; set; } + public int tmdbId { get; set; } + public List images { get; set; } + public string cleanTitle { get; set; } + public string imdbId { get; set; } + public string titleSlug { get; set; } + public int id { get; set; } + public int year { get; set; } + + } +} \ No newline at end of file diff --git a/Ombi.Api.Models/Radarr/RadarrAddOptions.cs b/Ombi.Api.Models/Radarr/RadarrAddOptions.cs new file mode 100644 index 000000000..dbe7ed729 --- /dev/null +++ b/Ombi.Api.Models/Radarr/RadarrAddOptions.cs @@ -0,0 +1,35 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: RadarrAddOptions.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.Api.Models.Radarr +{ + public class RadarrAddOptions + { + public bool ignoreEpisodesWithFiles { get; set; } + public bool ignoreEpisodesWithoutFiles { get; set; } + public bool searchForMovie { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Api.Models/Radarr/RadarrError.cs b/Ombi.Api.Models/Radarr/RadarrError.cs new file mode 100644 index 000000000..73ca43f1a --- /dev/null +++ b/Ombi.Api.Models/Radarr/RadarrError.cs @@ -0,0 +1,34 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: RadarrError.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.Api.Models.Radarr +{ + public class RadarrError + { + public string message { get; set; } + public string description { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Api.Models/Radarr/RadarrMovieResponse.cs b/Ombi.Api.Models/Radarr/RadarrMovieResponse.cs new file mode 100644 index 000000000..9b5bf0c2b --- /dev/null +++ b/Ombi.Api.Models/Radarr/RadarrMovieResponse.cs @@ -0,0 +1,80 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: RadarrMovieResponse.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.Collections.Generic; + +namespace Ombi.Api.Models.Radarr +{ + + + public class Image + { + public string coverType { get; set; } + public string url { get; set; } + } + + public class Ratings + { + public int votes { get; set; } + public double value { get; set; } + } + + public class RadarrMovieResponse + { + public string title { get; set; } + public string sortTitle { get; set; } + public double sizeOnDisk { get; set; } + public string status { get; set; } + public string overview { get; set; } + public string inCinemas { get; set; } + public string physicalRelease { get; set; } + public List images { get; set; } + public string website { get; set; } + public bool downloaded { get; set; } + public int year { get; set; } + public bool hasFile { get; set; } + public string youTubeTrailerId { get; set; } + public string studio { get; set; } + public string path { get; set; } + public int profileId { get; set; } + public bool monitored { get; set; } + public int runtime { get; set; } + public string lastInfoSync { get; set; } + public string cleanTitle { get; set; } + public string imdbId { get; set; } + public int tmdbId { get; set; } + public string titleSlug { get; set; } + public List genres { get; set; } + public List tags { get; set; } + public string added { get; set; } + public Ratings ratings { get; set; } + public List alternativeTitles { get; set; } + public int qualityProfileId { get; set; } + public int id { get; set; } + } + +} \ No newline at end of file diff --git a/Ombi.Api.Models/Sonarr/SonarrRootFolder.cs b/Ombi.Api.Models/Sonarr/SonarrRootFolder.cs new file mode 100644 index 000000000..d506feba6 --- /dev/null +++ b/Ombi.Api.Models/Sonarr/SonarrRootFolder.cs @@ -0,0 +1,35 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: SonarrRootFolder.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.Api.Models.Sonarr +{ + public class SonarrRootFolder + { + public int id { get; set; } + public string path { get; set; } + public long freespace { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.Api/ApiRequest.cs b/Ombi.Api/ApiRequest.cs index 67a5bbc9e..a27d4af28 100644 --- a/Ombi.Api/ApiRequest.cs +++ b/Ombi.Api/ApiRequest.cs @@ -70,7 +70,7 @@ public class ApiRequest : IApiRequest return response.Data; } - + public IRestResponse Execute(IRestRequest request, Uri baseUri) { var client = new RestClient { BaseUrl = baseUri }; diff --git a/Ombi.Api/DiscordApi.cs b/Ombi.Api/DiscordApi.cs new file mode 100644 index 000000000..d293d65a2 --- /dev/null +++ b/Ombi.Api/DiscordApi.cs @@ -0,0 +1,115 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: NetflixRouletteApi.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 Newtonsoft.Json; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.Netflix; +using Ombi.Api.Models.Notifications; +using RestSharp; + +namespace Ombi.Api +{ + public class DiscordApi : IDiscordApi + { + public DiscordApi(IApiRequest req) + { + Api = req; + } + + private IApiRequest Api { get; } + private Uri Endpoint => new Uri("https://discordapp.com/api/"); //webhooks/270828242636636161/lLysOMhJ96AFO1kvev0bSqP-WCZxKUh1UwfubhIcLkpS0DtM3cg4Pgeraw3waoTXbZii + + + public void SendMessage(string message, string webhookId, string webhookToken, string username = null) + { + var request = new RestRequest + { + Resource = "webhooks/{webhookId}/{webhookToken}", + Method = Method.POST + }; + + request.AddUrlSegment("webhookId", webhookId); + request.AddUrlSegment("webhookToken", webhookToken); + + var body = new DiscordWebhookRequest + { + content = message, + username = username + }; + request.AddJsonBody(body); + + request.AddHeader("Content-Type", "application/json"); + + Api.Execute(request, Endpoint); + } + + public async Task SendMessageAsync(string message, string webhookId, string webhookToken, string username = null) + { + var request = new RestRequest + { + Resource = "webhooks/{webhookId}/{webhookToken}", + Method = Method.POST + }; + + request.AddUrlSegment("webhookId", webhookId); + request.AddUrlSegment("webhookToken", webhookToken); + + var body = new DiscordWebhookRequest + { + content = message, + username = username + }; + request.AddJsonBody(body); + + request.AddHeader("Content-Type", "application/json"); + + await Task.Run( + () => + { + Api.Execute(request, Endpoint); + + }); + } + + + + public NetflixMovieResult CheckNetflix(string title, string year = null) + { + var request = new RestRequest(); + request.AddQueryParameter("title", title); + if (!string.IsNullOrEmpty(year)) + { + request.AddQueryParameter("year", year); + } + var result = Api.Execute(request, Endpoint); + + return JsonConvert.DeserializeObject(result.Content); + } + } +} \ No newline at end of file diff --git a/Ombi.Api/MovieBase.cs b/Ombi.Api/MovieBase.cs index c21bfcbd2..5d4c820b7 100644 --- a/Ombi.Api/MovieBase.cs +++ b/Ombi.Api/MovieBase.cs @@ -32,6 +32,8 @@ namespace Ombi.Api public abstract class MovieBase { private static readonly string Encrypted = "0T3QNSseexLO7n7UPiJvl70Y+KKnvbeTlsusl7Kwq0hPH0BHOuFNGwksNCjkwqWedyDdI/MJeUR4wtL4bIl0Z+//uHXEaYM/4H2pjeLbH5EWdUe5TTj1AhaIR5PQweamvcienRyFD/3YPCC/+qL5mHkKXBkPumMod3Zb/4yN0Ik="; - protected string ApiKey = StringCipher.Decrypt(Encrypted, "ApiKey"); + private string _apiKey; + + protected string ApiKey => _apiKey ?? (_apiKey = StringCipher.Decrypt(Encrypted, "ApiKey")); } } diff --git a/Ombi.Api/NetflixRouletteApi.cs b/Ombi.Api/NetflixRouletteApi.cs index ee556a410..dc6e6e220 100644 --- a/Ombi.Api/NetflixRouletteApi.cs +++ b/Ombi.Api/NetflixRouletteApi.cs @@ -26,6 +26,7 @@ #endregion using System; +using System.Threading.Tasks; using Newtonsoft.Json; using Ombi.Api.Interfaces; using Ombi.Api.Models.Netflix; @@ -43,10 +44,10 @@ public NetflixRouletteApi(IApiRequest req) private IApiRequest Api { get; } private Uri Endpoint => new Uri("http://netflixroulette.net/api/api.php"); - public NetflixMovieResult GetMovies(string movieName, string year = null) + public NetflixMovieResult CheckNetflix(string title, string year = null) { var request = new RestRequest(); - request.AddQueryParameter("title", movieName); + request.AddQueryParameter("title", title); if (!string.IsNullOrEmpty(year)) { request.AddQueryParameter("year", year); diff --git a/Ombi.Api/Ombi.Api.csproj b/Ombi.Api/Ombi.Api.csproj index b9151ca5d..b9e674cc2 100644 --- a/Ombi.Api/Ombi.Api.csproj +++ b/Ombi.Api/Ombi.Api.csproj @@ -66,10 +66,17 @@ ..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll True + + ..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll + True + + + + diff --git a/Ombi.Api/RadarrApi.cs b/Ombi.Api/RadarrApi.cs new file mode 100644 index 000000000..7eeb98d3f --- /dev/null +++ b/Ombi.Api/RadarrApi.cs @@ -0,0 +1,158 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: CouchPotatoApi.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.Linq; +using Newtonsoft.Json; +using NLog; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.Radarr; +using Ombi.Api.Models.Sonarr; +using Ombi.Helpers; +using RestSharp; + +namespace Ombi.Api +{ + public class RadarrApi : IRadarrApi + { + public RadarrApi() + { + Api = new ApiRequest(); + } + private ApiRequest Api { get; set; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public List GetProfiles(string apiKey, Uri baseUrl) + { + var request = new RestRequest { Resource = "/api/profile", Method = Method.GET }; + + request.AddHeader("X-Api-Key", apiKey); + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetProfiles for Sonarr, Retrying {0}", timespan), new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }); + + var obj = policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); + + return obj; + } + + public RadarrAddMovie AddMovie(int tmdbId, string title, int year, int qualityId, string rootPath, string apiKey, Uri baseUrl, bool searchNow = false) + { + var request = new RestRequest + { + Resource = "/api/movie", + Method = Method.POST + }; + + var options = new RadarrAddMovie + { + title = title, + tmdbId = tmdbId, + qualityProfileId = qualityId, + rootFolderPath = rootPath, + titleSlug = title, + monitored = true, + year = year + }; + + if (searchNow) + { + options.addOptions = new RadarrAddOptions + { + searchForMovie = true + }; + } + + + request.AddHeader("X-Api-Key", apiKey); + request.AddJsonBody(options); + + RadarrAddMovie result; + try + { + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling AddSeries for Sonarr, Retrying {0}", timespan), new TimeSpan[] { + TimeSpan.FromSeconds (2) + }); + + var response = policy.Execute(() => Api.Execute(request, baseUrl)); + if (response.Content.Contains("\"message\":")) + { + var error = JsonConvert.DeserializeObject < RadarrError>(response.Content); + return new RadarrAddMovie {Error = error}; + } + if (response.Content.Contains("\"errorMessage\":")) + { + var error = JsonConvert.DeserializeObject>(response.Content).FirstOrDefault(); + return new RadarrAddMovie {Error = new RadarrError {message = error?.errorMessage}}; + } + return JsonConvert.DeserializeObject < RadarrAddMovie>(response.Content); + } + catch (JsonSerializationException jse) + { + Log.Error(jse); + } + return null; + } + + + public SystemStatus SystemStatus(string apiKey, Uri baseUrl) + { + var request = new RestRequest { Resource = "/api/system/status", Method = Method.GET }; + request.AddHeader("X-Api-Key", apiKey); + + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling SystemStatus for Sonarr, Retrying {0}", timespan), new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }); + + var obj = policy.Execute(() => Api.ExecuteJson(request, baseUrl)); + + return obj; + } + + + public List GetMovies(string apiKey, Uri baseUrl) + { + var request = new RestRequest { Resource = "/api/movie", Method = Method.GET }; + request.AddHeader("X-Api-Key", apiKey); + + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling SystemStatus for Sonarr, Retrying {0}", timespan), new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }); + + var obj = policy.Execute(() => Api.Execute(request, baseUrl)); + + return JsonConvert.DeserializeObject>(obj.Content); + } + } +} \ No newline at end of file diff --git a/Ombi.Api/SonarrApi.cs b/Ombi.Api/SonarrApi.cs index 7c80883dc..2485150b8 100644 --- a/Ombi.Api/SonarrApi.cs +++ b/Ombi.Api/SonarrApi.cs @@ -62,6 +62,22 @@ public List GetProfiles(string apiKey, Uri baseUrl) return obj; } + public List GetRootFolders(string apiKey, Uri baseUrl) + { + var request = new RestRequest { Resource = "/api/rootfolder", Method = Method.GET }; + + request.AddHeader("X-Api-Key", apiKey); + var policy = RetryHandler.RetryAndWaitPolicy((exception, timespan) => Log.Error(exception, "Exception when calling GetRootFolders for Sonarr, Retrying {0}", timespan), new TimeSpan[] { + TimeSpan.FromSeconds (2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10) + }); + + var obj = policy.Execute(() => Api.ExecuteJson>(request, baseUrl)); + + return obj; + } + public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl, bool monitor = true, bool searchForMissingEpisodes = false) { Log.Debug("Adding series {0}", title); diff --git a/Ombi.Api/TheMovieDbApi.cs b/Ombi.Api/TheMovieDbApi.cs index 6ccd775dd..ad3f01251 100644 --- a/Ombi.Api/TheMovieDbApi.cs +++ b/Ombi.Api/TheMovieDbApi.cs @@ -25,12 +25,18 @@ // ************************************************************************/ #endregion +using System; using System.Collections.Generic; using System.Threading.Tasks; +using NLog; +using NLog.Fluent; +using Ombi.Api.Models.Movie; +using RestSharp; using TMDbLib.Client; using TMDbLib.Objects.General; using TMDbLib.Objects.Movies; using TMDbLib.Objects.Search; +using Movie = TMDbLib.Objects.Movies.Movie; namespace Ombi.Api { @@ -39,9 +45,13 @@ public class TheMovieDbApi : MovieBase public TheMovieDbApi() { Client = new TMDbClient(ApiKey); + Api = new ApiRequest(); } + private ApiRequest Api { get; } public TMDbClient Client { get; set; } + private const string BaseUrl = "https://api.themoviedb.org/3/"; + private static Logger Log = LogManager.GetCurrentClassLogger(); public async Task> SearchMovie(string searchTerm) { var results = await Client.SearchMovie(searchTerm); @@ -56,7 +66,27 @@ public async Task> GetCurrentPlayingMovies() public async Task> GetUpcomingMovies() { var movies = await Client.GetMovieList(MovieListType.Upcoming); - return movies?.Results ?? new List(); + return movies?.Results ?? new List(); + } + + public TmdbMovieDetails GetMovieInformationWithVideos(int tmdbId) + { + var request = new RestRequest { Resource = "movie/{movieId}", Method = Method.GET }; + request.AddUrlSegment("movieId", tmdbId.ToString()); + request.AddQueryParameter("api_key", ApiKey); + request.AddQueryParameter("append_to_response", "videos"); // Get the videos + + try + { + + var obj = Api.ExecuteJson(request, new Uri(BaseUrl)); + return obj; + } + catch (Exception e) + { + Log.Error(e); + return null; + } } public async Task GetMovieInformation(int tmdbId) diff --git a/Ombi.Api/TraktApi.cs b/Ombi.Api/TraktApi.cs new file mode 100644 index 000000000..ae2ff11b2 --- /dev/null +++ b/Ombi.Api/TraktApi.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Ombi.Api.Interfaces; +using Ombi.Helpers; +using TraktApiSharp; +using TraktApiSharp.Enums; +using TraktApiSharp.Objects.Get.Shows; +using TraktApiSharp.Objects.Get.Shows.Common; +using TraktApiSharp.Requests.Params; + +namespace Ombi.Api +{ + public class TraktApi : ITraktApi + { + private TraktClient Client { get; } + + private static readonly string Encrypted = "z/56wM/oEkkCWEvSIZCrzQyUvvqmafQ3njqf0UNK5xuKbNYh5Wz8ocoG2QDa5y1DBkozLaKsGxORmAB1XUvwbnom8DVNo9gE++9GTuwxmGlLDD318PXpRmYmpKqNwFSKRZgF6ewiY9qR4t3iG0pGQwPA08FK3+H7kpOKAGJNR9RMDP9wwB6Vl4DuOiZb9/DETjzZ+/zId0ZqimrbN+PLrg=="; + private readonly string _apiKey = StringCipher.Decrypt(Encrypted, "ApiKey"); + public TraktApi() + { + Client = new TraktClient(_apiKey); + } + + public async Task> GetPopularShows(int? page = null, int? limitPerPage = null) + { + var popular = await Client.Shows.GetPopularShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return popular.Items; + } + + public async Task> GetTrendingShows(int? page = null, int? limitPerPage = null) + { + var trendingShowsTop10 = await Client.Shows.GetTrendingShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return trendingShowsTop10.Items; + } + + public async Task> GetAnticipatedShows(int? page = null, int? limitPerPage = null) + { + var anticipatedShows = await Client.Shows.GetMostAnticipatedShowsAsync(new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return anticipatedShows.Items; + } + + public async Task> GetMostWatchesShows(TraktTimePeriod period = null, int? page = null, int? limitPerPage = null) + { + var anticipatedShows = await Client.Shows.GetMostWatchedShowsAsync(period ?? TraktTimePeriod.Monthly, new TraktExtendedInfo { Full = true }, null, page ?? 1, limitPerPage ?? 10); + return anticipatedShows.Items; + } + } +} diff --git a/Ombi.Api/packages.config b/Ombi.Api/packages.config index ce5ccf24e..a20220585 100644 --- a/Ombi.Api/packages.config +++ b/Ombi.Api/packages.config @@ -8,4 +8,5 @@ + \ No newline at end of file diff --git a/Ombi.Core.Migration/Migrations/Version2200.cs b/Ombi.Core.Migration/Migrations/Version2200.cs new file mode 100644 index 000000000..a28f852de --- /dev/null +++ b/Ombi.Core.Migration/Migrations/Version2200.cs @@ -0,0 +1,64 @@ +#region Copyright + +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Version1100.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.Data; +using NLog; +using Ombi.Core.SettingModels; + +namespace Ombi.Core.Migration.Migrations +{ + [Migration(22000, "v2.20.0.0")] + public class Version2200 : BaseMigration, IMigration + { + public Version2200(ISettingsService custom) + { + Customization = custom; + } + + public int Version => 22000; + private ISettingsService Customization { get; set; } + + + private static Logger Logger = LogManager.GetCurrentClassLogger(); + + public void Start(IDbConnection con) + { + //UpdateCustomSettings(); Turned off the migration for now until the search has been improved on. + //UpdateSchema(con, Version); + } + + private void UpdateCustomSettings() + { + var settings = Customization.GetSettings(); + settings.NewSearch = true; // Use the new search + + Customization.SaveSettings(settings); + } + } +} diff --git a/Ombi.Core.Migration/Ombi.Core.Migration.csproj b/Ombi.Core.Migration/Ombi.Core.Migration.csproj index 222f18fde..06dfda84d 100644 --- a/Ombi.Core.Migration/Ombi.Core.Migration.csproj +++ b/Ombi.Core.Migration/Ombi.Core.Migration.csproj @@ -69,6 +69,7 @@ + diff --git a/Ombi.Core/CacheKeys.cs b/Ombi.Core/CacheKeys.cs index 3638875eb..39ed7a71b 100644 --- a/Ombi.Core/CacheKeys.cs +++ b/Ombi.Core/CacheKeys.cs @@ -37,7 +37,9 @@ public struct TimeFrameMinutes public const string PlexEpisodes = nameof(PlexEpisodes); public const string TvDbToken = nameof(TvDbToken); public const string SonarrQualityProfiles = nameof(SonarrQualityProfiles); + public const string RadarrQualityProfiles = nameof(RadarrQualityProfiles); public const string SonarrQueued = nameof(SonarrQueued); + public const string RadarrMovies = nameof(RadarrMovies); public const string SickRageQualityProfiles = nameof(SickRageQualityProfiles); public const string SickRageQueued = nameof(SickRageQueued); public const string CouchPotatoQualityProfiles = nameof(CouchPotatoQualityProfiles); @@ -45,5 +47,6 @@ public struct TimeFrameMinutes public const string WatcherQueued = nameof(WatcherQueued); public const string GetPlexRequestSettings = nameof(GetPlexRequestSettings); public const string LastestProductVersion = nameof(LastestProductVersion); + public const string SonarrRootFolders = nameof(SonarrRootFolders); } } \ No newline at end of file diff --git a/Ombi.Core/ISecurityExtensions.cs b/Ombi.Core/ISecurityExtensions.cs index e3b52a613..0743b5a51 100644 --- a/Ombi.Core/ISecurityExtensions.cs +++ b/Ombi.Core/ISecurityExtensions.cs @@ -29,7 +29,10 @@ Response HasAnyPermissionsRedirect(NancyContext context, string routeName, HttpS /// Gets the username this could be the alias! We should always use this method when getting the username /// /// The username. - /// null if we cannot find a user + /// The session. + /// + /// null if we cannot find a user + /// string GetUsername(string username, ISession session); } } \ No newline at end of file diff --git a/Ombi.Core/MovieSender.cs b/Ombi.Core/MovieSender.cs index 39e2e0554..37eeee308 100644 --- a/Ombi.Core/MovieSender.cs +++ b/Ombi.Core/MovieSender.cs @@ -37,16 +37,20 @@ namespace Ombi.Core public class MovieSender : IMovieSender { public MovieSender(ISettingsService cp, ISettingsService watcher, - ICouchPotatoApi cpApi, IWatcherApi watcherApi) + ICouchPotatoApi cpApi, IWatcherApi watcherApi, IRadarrApi radarrApi, ISettingsService radarrSettings) { CouchPotatoSettings = cp; WatcherSettings = watcher; CpApi = cpApi; WatcherApi = watcherApi; + RadarrSettings = radarrSettings; + RadarrApi = radarrApi; } private ISettingsService CouchPotatoSettings { get; } private ISettingsService WatcherSettings { get; } + private ISettingsService RadarrSettings { get; } + private IRadarrApi RadarrApi { get; } private ICouchPotatoApi CpApi { get; } private IWatcherApi WatcherApi { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); @@ -55,6 +59,7 @@ public async Task Send(RequestedModel model, string qualityId { var cpSettings = await CouchPotatoSettings.GetSettingsAsync(); var watcherSettings = await WatcherSettings.GetSettingsAsync(); + var radarrSettings = await RadarrSettings.GetSettingsAsync(); if (cpSettings.Enabled) { @@ -66,6 +71,11 @@ public async Task Send(RequestedModel model, string qualityId return SendToWatcher(model, watcherSettings); } + if (radarrSettings.Enabled) + { + return SendToRadarr(model, radarrSettings); + } + return new MovieSenderResult { Result = false, MovieSendingEnabled = false }; } @@ -91,5 +101,23 @@ private MovieSenderResult SendToCp(RequestedModel model, CouchPotatoSettings set var result = CpApi.AddMovie(model.ImdbId, settings.ApiKey, model.Title, settings.FullUri, qualityId); return new MovieSenderResult { Result = result, MovieSendingEnabled = true }; } + + private MovieSenderResult SendToRadarr(RequestedModel model, RadarrSettings settings) + { + var qualityProfile = 0; + int.TryParse(settings.QualityProfile, out qualityProfile); + var result = RadarrApi.AddMovie(model.ProviderId, model.Title, model.ReleaseDate.Year, qualityProfile, settings.RootPath, settings.ApiKey, settings.FullUri, true); + + if (!string.IsNullOrEmpty(result.Error?.message)) + { + Log.Error(result.Error.message); + return new MovieSenderResult { Result = false, Error = true}; + } + if (!string.IsNullOrEmpty(result.title)) + { + return new MovieSenderResult { Result = true, MovieSendingEnabled = true }; + } + return new MovieSenderResult { Result = false, MovieSendingEnabled = true }; + } } } \ No newline at end of file diff --git a/Ombi.Core/MovieSenderResult.cs b/Ombi.Core/MovieSenderResult.cs index ccf96ebb9..6ec6ea556 100644 --- a/Ombi.Core/MovieSenderResult.cs +++ b/Ombi.Core/MovieSenderResult.cs @@ -36,5 +36,7 @@ public class MovieSenderResult /// true if [movie sending enabled]; otherwise, false. /// public bool MovieSendingEnabled { get; set; } + + public bool Error { get; set; } } } \ No newline at end of file diff --git a/Ombi.Core/Ombi.Core.csproj b/Ombi.Core/Ombi.Core.csproj index 828ed3b4e..f3f05b9da 100644 --- a/Ombi.Core/Ombi.Core.csproj +++ b/Ombi.Core/Ombi.Core.csproj @@ -122,6 +122,8 @@ + + diff --git a/Ombi.Core/Queue/TransientFaultQueue.cs b/Ombi.Core/Queue/TransientFaultQueue.cs index a3be57dc8..11eb1bd5a 100644 --- a/Ombi.Core/Queue/TransientFaultQueue.cs +++ b/Ombi.Core/Queue/TransientFaultQueue.cs @@ -97,7 +97,7 @@ public async Task QueueItemAsync(RequestedModel request, string id, RequestType Content = ByteConverterHelper.ReturnBytes(request), PrimaryIdentifier = id, FaultType = faultType, - Message = description ?? string.Empty + Description = description ?? string.Empty }; await RequestQueue.InsertAsync(queue); } diff --git a/Ombi.Core/SecurityExtensions.cs b/Ombi.Core/SecurityExtensions.cs index 8ec7f3689..7a244554a 100644 --- a/Ombi.Core/SecurityExtensions.cs +++ b/Ombi.Core/SecurityExtensions.cs @@ -94,40 +94,33 @@ public bool IsNormalUser(IUserIdentity user) /// Gets the username this could be the alias! We should always use this method when getting the username /// /// The username. - /// null if we cannot find a user + /// + /// + /// null if we cannot find a user + /// public string GetUsername(string username, ISession session) { var plexUser = PlexUsers.GetUserByUsername(username); if (plexUser != null) { - if (!string.IsNullOrEmpty(plexUser.UserAlias)) - { - return plexUser.UserAlias; - } - else - { - return plexUser.Username; - } + return !string.IsNullOrEmpty(plexUser.UserAlias) ? plexUser.UserAlias : plexUser.Username; } var dbUser = UserRepository.GetUserByUsername(username); if (dbUser != null) { var userProps = ByteConverterHelper.ReturnObject(dbUser.UserProperties); - if (!string.IsNullOrEmpty(userProps.UserAlias)) - { - return userProps.UserAlias; - } - else - { - return dbUser.UserName; - } + return !string.IsNullOrEmpty(userProps.UserAlias) ? userProps.UserAlias : dbUser.UserName; } // could be a local user - var localName = session[SessionKeys.UsernameKey]; + var hasSessionKey = session[SessionKeys.UsernameKey] != null; + if (hasSessionKey) + { + return (string)session[SessionKeys.UsernameKey]; + } - return localName as string; + return string.Empty; } diff --git a/Ombi.Core/SettingModels/CustomizationSettings.cs b/Ombi.Core/SettingModels/CustomizationSettings.cs index 2fa664f35..d7aff1e51 100644 --- a/Ombi.Core/SettingModels/CustomizationSettings.cs +++ b/Ombi.Core/SettingModels/CustomizationSettings.cs @@ -53,5 +53,7 @@ public class CustomizationSettings : Settings public int DefaultLang { get; set; } + public bool NewSearch { get; set; } + } } \ No newline at end of file diff --git a/Ombi.Core/SettingModels/DiscordNotificationSettings.cs b/Ombi.Core/SettingModels/DiscordNotificationSettings.cs new file mode 100644 index 000000000..899b7f16e --- /dev/null +++ b/Ombi.Core/SettingModels/DiscordNotificationSettings.cs @@ -0,0 +1,31 @@ +using System; +using Newtonsoft.Json; + +namespace Ombi.Core.SettingModels +{ + public sealed class DiscordNotificationSettings : NotificationSettings + { + public string WebhookUrl { get; set; } + public string Username { get; set; } + + [JsonIgnore] + public string WebookId => SplitWebUrl(4); + + [JsonIgnore] + public string Token => SplitWebUrl(5); + + private string SplitWebUrl(int index) + { + if (!WebhookUrl.StartsWith("http", StringComparison.InvariantCulture)) + { + WebhookUrl = "https://" + WebhookUrl; + } + var split = WebhookUrl.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + return split.Length < index + ? string.Empty + : split[index]; + } + + } +} \ No newline at end of file diff --git a/Ombi.Core/SettingModels/EmailNotificationSettings.cs b/Ombi.Core/SettingModels/EmailNotificationSettings.cs index 65f55a61a..672588922 100644 --- a/Ombi.Core/SettingModels/EmailNotificationSettings.cs +++ b/Ombi.Core/SettingModels/EmailNotificationSettings.cs @@ -34,7 +34,6 @@ public sealed class EmailNotificationSettings : NotificationSettings public string EmailSender { get; set; } public string EmailUsername { get; set; } public bool Authentication { get; set; } - public bool EnableUserEmailNotifications { get; set; } public string RecipientEmail { get; set; } } } \ No newline at end of file diff --git a/Ombi.Core/SettingModels/RadarrSettings.cs b/Ombi.Core/SettingModels/RadarrSettings.cs new file mode 100644 index 000000000..b8a6287f7 --- /dev/null +++ b/Ombi.Core/SettingModels/RadarrSettings.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SonarrSettings.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 RadarrSettings : ExternalSettings + { + public bool Enabled { get; set; } + public string ApiKey { get; set; } + public string QualityProfile { get; set; } + public string RootPath { get; set; } + + } +} \ No newline at end of file diff --git a/Ombi.Core/SettingModels/ScheduledJobsSettings.cs b/Ombi.Core/SettingModels/ScheduledJobsSettings.cs index 76921a679..a2609206b 100644 --- a/Ombi.Core/SettingModels/ScheduledJobsSettings.cs +++ b/Ombi.Core/SettingModels/ScheduledJobsSettings.cs @@ -46,5 +46,6 @@ public class ScheduledJobsSettings : Settings public int FaultQueueHandler { get; set; } public int PlexContentCacher { get; set; } public int PlexUserChecker { get; set; } + public int RadarrCacher { get; set; } } } \ No newline at end of file diff --git a/Ombi.Core/SettingModels/SonarrSettings.cs b/Ombi.Core/SettingModels/SonarrSettings.cs index 31f96b3e6..d8ab8f991 100644 --- a/Ombi.Core/SettingModels/SonarrSettings.cs +++ b/Ombi.Core/SettingModels/SonarrSettings.cs @@ -32,7 +32,13 @@ public sealed class SonarrSettings : ExternalSettings public string ApiKey { get; set; } public string QualityProfile { get; set; } public bool SeasonFolders { get; set; } + /// + /// This is the root path ID + /// + /// + /// The root path. + /// public string RootPath { get; set; } - + public string FullRootPath { get; set; } } } \ No newline at end of file diff --git a/Ombi.Core/TvSender.cs b/Ombi.Core/TvSender.cs index 1e97e3550..3f6d5f86e 100644 --- a/Ombi.Core/TvSender.cs +++ b/Ombi.Core/TvSender.cs @@ -34,19 +34,22 @@ using Ombi.Api.Models.SickRage; using Ombi.Api.Models.Sonarr; using Ombi.Core.SettingModels; +using Ombi.Helpers; using Ombi.Store; namespace Ombi.Core { public class TvSender { - public TvSender(ISonarrApi sonarrApi, ISickRageApi srApi) + public TvSender(ISonarrApi sonarrApi, ISickRageApi srApi, ICacheProvider cache) { SonarrApi = sonarrApi; SickrageApi = srApi; + Cache = cache; } private ISonarrApi SonarrApi { get; } private ISickRageApi SickrageApi { get; } + private ICacheProvider Cache { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); public async Task SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model) @@ -82,6 +85,8 @@ public async Task SendToSonarr(SonarrSettings sonarrSettings, R var latest = model.SeasonsRequested?.Equals("Latest", StringComparison.CurrentCultureIgnoreCase); var specificSeasonRequest = model.SeasonList?.Any(); + var rootFolderPath = model.RootFolderSelected <= 0 ? sonarrSettings.FullRootPath : await GetRootPath(model.RootFolderSelected, sonarrSettings); + if (episodeRequest) { // Does series exist? @@ -96,7 +101,7 @@ public async Task SendToSonarr(SonarrSettings sonarrSettings, R // Series doesn't exist, need to add it as unmonitored. var addResult = await Task.Run(() => SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, - sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, new int[0], sonarrSettings.ApiKey, + sonarrSettings.SeasonFolders, rootFolderPath, 0, new int[0], sonarrSettings.ApiKey, sonarrSettings.FullUri, false)); @@ -125,7 +130,7 @@ public async Task SendToSonarr(SonarrSettings sonarrSettings, R { // Set the series as monitored with a season count as 0 so it doesn't search for anything SonarrApi.AddSeriesNew(model.ProviderId, model.Title, qualityProfile, - sonarrSettings.SeasonFolders, sonarrSettings.RootPath, new int[] {1,2,3,4,5,6,7,8,9,10,11,12,13}, sonarrSettings.ApiKey, + sonarrSettings.SeasonFolders, rootFolderPath, new int[] {1,2,3,4,5,6,7,8,9,10,11,12,13}, sonarrSettings.ApiKey, sonarrSettings.FullUri); await Task.Delay(TimeSpan.FromSeconds(1)); @@ -372,5 +377,20 @@ private async Task GetSonarrSeries(SonarrSettings sonarrSettings, int sh return selectedSeries; } + + private async Task GetRootPath(int pathId, SonarrSettings sonarrSettings) + { + var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () => + { + return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri)); + }); + + foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) + { + return r.path; + } + return string.Empty; + } + } } \ No newline at end of file diff --git a/Ombi.Core/TvSenderOld.cs b/Ombi.Core/TvSenderOld.cs index 6594fe5b8..d3423e291 100644 --- a/Ombi.Core/TvSenderOld.cs +++ b/Ombi.Core/TvSenderOld.cs @@ -34,19 +34,22 @@ using Ombi.Api.Models.SickRage; using Ombi.Api.Models.Sonarr; using Ombi.Core.SettingModels; +using Ombi.Helpers; using Ombi.Store; namespace Ombi.Core { public class TvSenderOld { - public TvSenderOld(ISonarrApi sonarrApi, ISickRageApi srApi) + public TvSenderOld(ISonarrApi sonarrApi, ISickRageApi srApi, ICacheProvider cache) { SonarrApi = sonarrApi; SickrageApi = srApi; + Cache = cache; } private ISonarrApi SonarrApi { get; } private ISickRageApi SickrageApi { get; } + private ICacheProvider Cache { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); public async Task SendToSonarr(SonarrSettings sonarrSettings, RequestedModel model) @@ -67,6 +70,8 @@ public async Task SendToSonarr(SonarrSettings sonarrSettings, R { int.TryParse(sonarrSettings.QualityProfile, out qualityProfile); } + var rootFolderPath = model.RootFolderSelected <= 0 ? sonarrSettings.FullRootPath : await GetRootPath(model.RootFolderSelected, sonarrSettings); + var series = await GetSonarrSeries(sonarrSettings, model.ProviderId); @@ -84,7 +89,7 @@ public async Task SendToSonarr(SonarrSettings sonarrSettings, R // Series doesn't exist, need to add it as unmonitored. var addResult = await Task.Run(() => SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, - sonarrSettings.SeasonFolders, sonarrSettings.RootPath, 0, new int[0], sonarrSettings.ApiKey, + sonarrSettings.SeasonFolders, rootFolderPath, 0, new int[0], sonarrSettings.ApiKey, sonarrSettings.FullUri, false)); @@ -156,7 +161,7 @@ public async Task SendToSonarr(SonarrSettings sonarrSettings, R var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, - sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey, + sonarrSettings.SeasonFolders, rootFolderPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey, sonarrSettings.FullUri, true, true); return result; @@ -298,5 +303,20 @@ private async Task GetSonarrSeries(SonarrSettings sonarrSettings, int sh return selectedSeries; } + + + private async Task GetRootPath(int pathId, SonarrSettings sonarrSettings) + { + var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () => + { + return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri)); + }); + + foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) + { + return r.path; + } + return string.Empty; + } } } \ No newline at end of file diff --git a/Ombi.Core/Users/UserHelperModel.cs b/Ombi.Core/Users/UserHelperModel.cs index 1b08d9fb1..ace42d4f9 100644 --- a/Ombi.Core/Users/UserHelperModel.cs +++ b/Ombi.Core/Users/UserHelperModel.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion +using Newtonsoft.Json; using Ombi.Helpers; using Ombi.Helpers.Permissions; @@ -38,5 +39,8 @@ public class UserHelperModel public Features Features { get; set; } public string EmailAddress { get; set; } public UserType Type { get; set; } + + [JsonIgnore] + public string UsernameOrAlias => string.IsNullOrEmpty(UserAlias) ? Username : UserAlias; } } \ No newline at end of file diff --git a/Ombi.Helpers/DateTimeHelper.cs b/Ombi.Helpers/DateTimeHelper.cs index d8dbc681a..b25972ddf 100644 --- a/Ombi.Helpers/DateTimeHelper.cs +++ b/Ombi.Helpers/DateTimeHelper.cs @@ -46,5 +46,12 @@ public static DateTime UnixTimeStampToDateTime(this int unixTimeStamp) dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime(); return dtDateTime; } + + public static long ToJavascriptTimestamp(this DateTime input) + { + var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var time = input.Subtract(new TimeSpan(epoch.Ticks)); + return (long)(time.Ticks / 10000); + } } } diff --git a/Ombi.Services/Interfaces/IAvailabilityChecker.cs b/Ombi.Services/Interfaces/IAvailabilityChecker.cs index 1e4f66af4..f2915faa0 100644 --- a/Ombi.Services/Interfaces/IAvailabilityChecker.cs +++ b/Ombi.Services/Interfaces/IAvailabilityChecker.cs @@ -34,6 +34,7 @@ namespace Ombi.Services.Interfaces { public interface IAvailabilityChecker { + void Start(); void CheckAndUpdateAll(); IEnumerable GetPlexMovies(IEnumerable content); bool IsMovieAvailable(PlexContent[] plexMovies, string title, string year, string providerId = null); diff --git a/Ombi.Services/Jobs/IPlexContentCacher.cs b/Ombi.Services/Interfaces/IPlexContentCacher.cs similarity index 100% rename from Ombi.Services/Jobs/IPlexContentCacher.cs rename to Ombi.Services/Interfaces/IPlexContentCacher.cs diff --git a/Ombi.Services/Interfaces/IRadarrCacher.cs b/Ombi.Services/Interfaces/IRadarrCacher.cs new file mode 100644 index 000000000..85b2fae38 --- /dev/null +++ b/Ombi.Services/Interfaces/IRadarrCacher.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Ombi.Services.Models; + +namespace Ombi.Services.Interfaces +{ + public interface IRadarrCacher + { + void Queued(); + int[] QueuedIds(); + } +} diff --git a/Ombi.Services/Jobs/IRecentlyAdded.cs b/Ombi.Services/Interfaces/IRecentlyAdded.cs similarity index 88% rename from Ombi.Services/Jobs/IRecentlyAdded.cs rename to Ombi.Services/Interfaces/IRecentlyAdded.cs index 4aa279b9a..09a7220f5 100644 --- a/Ombi.Services/Jobs/IRecentlyAdded.cs +++ b/Ombi.Services/Interfaces/IRecentlyAdded.cs @@ -6,5 +6,6 @@ public interface IRecentlyAdded { void Execute(IJobExecutionContext context); void Test(); + void Start(); } } \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IStoreBackup.cs b/Ombi.Services/Interfaces/IStoreBackup.cs new file mode 100644 index 000000000..0673dc0ab --- /dev/null +++ b/Ombi.Services/Interfaces/IStoreBackup.cs @@ -0,0 +1,10 @@ +using Quartz; + +namespace Ombi.Services.Jobs +{ + public interface IStoreBackup + { + void Start(); + void Execute(IJobExecutionContext context); + } +} \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IStoreCleanup.cs b/Ombi.Services/Interfaces/IStoreCleanup.cs new file mode 100644 index 000000000..7313bf8ce --- /dev/null +++ b/Ombi.Services/Interfaces/IStoreCleanup.cs @@ -0,0 +1,10 @@ +using Quartz; + +namespace Ombi.Services.Jobs +{ + public interface IStoreCleanup + { + void Execute(IJobExecutionContext context); + void Start(); + } +} \ No newline at end of file diff --git a/Ombi.Services/Interfaces/IUserRequestLimitResetter.cs b/Ombi.Services/Interfaces/IUserRequestLimitResetter.cs new file mode 100644 index 000000000..9fd72f401 --- /dev/null +++ b/Ombi.Services/Interfaces/IUserRequestLimitResetter.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Ombi.Core.SettingModels; +using Ombi.Store.Models; +using Quartz; + +namespace Ombi.Services.Jobs +{ + public interface IUserRequestLimitResetter + { + void AlbumLimit(PlexRequestSettings s, IEnumerable allUsers); + void Execute(IJobExecutionContext context); + void MovieLimit(PlexRequestSettings s, IEnumerable allUsers); + void Start(); + void TvLimit(PlexRequestSettings s, IEnumerable allUsers); + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/FaultQueueHandler.cs b/Ombi.Services/Jobs/FaultQueueHandler.cs index 61d3a4674..6d18efa2a 100644 --- a/Ombi.Services/Jobs/FaultQueueHandler.cs +++ b/Ombi.Services/Jobs/FaultQueueHandler.cs @@ -45,7 +45,7 @@ namespace Ombi.Services.Jobs { - public class FaultQueueHandler : IJob + public class FaultQueueHandler : IJob, IFaultQueueHandler { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -53,7 +53,7 @@ public FaultQueueHandler(IJobRecord record, IRepository repo, ISon ISickRageApi srApi, ISettingsService sonarrSettings, ISettingsService srSettings, ICouchPotatoApi cpApi, ISettingsService cpsettings, IRequestService requestService, ISettingsService hpSettings, IHeadphonesApi headphonesApi, ISettingsService prSettings, - ISecurityExtensions security, IMovieSender movieSender) + ISecurityExtensions security, IMovieSender movieSender, ICacheProvider cache) { Record = record; Repo = repo; @@ -71,6 +71,8 @@ public FaultQueueHandler(IJobRecord record, IRepository repo, ISon Security = security; PrSettings = prSettings.GetSettings(); MovieSender = movieSender; + + Cache = cache; } private IMovieSender MovieSender { get; } @@ -78,6 +80,7 @@ public FaultQueueHandler(IJobRecord record, IRepository repo, ISon private IJobRecord Record { get; } private ISonarrApi SonarrApi { get; } private ISickRageApi SrApi { get; } + private ICacheProvider Cache { get; } private ICouchPotatoApi CpApi { get; } private IHeadphonesApi HpApi { get; } private IRequestService RequestService { get; } @@ -88,9 +91,8 @@ public FaultQueueHandler(IJobRecord record, IRepository repo, ISon private ISettingsService HeadphoneSettings { get; } private ISecurityExtensions Security { get; } - public void Execute(IJobExecutionContext context) + public void Start() { - Record.SetRunning(true, JobNames.CpCacher); try { @@ -113,6 +115,11 @@ public void Execute(IJobExecutionContext context) Record.SetRunning(false, JobNames.CpCacher); } } + public void Execute(IJobExecutionContext context) + { + + Start(); + } private void ProcessMissingInformation(List requests) @@ -163,7 +170,7 @@ private bool ProcessTvShow(RequestedModel tvModel, SonarrSettings sonarr, SickRa try { - var sender = new TvSenderOld(SonarrApi, SrApi); + var sender = new TvSenderOld(SonarrApi, SrApi, Cache); if (sonarr.Enabled) { var task = sender.SendToSonarr(sonarr, tvModel, sonarr.QualityProfile); diff --git a/Ombi.Services/Jobs/IFaultQueueHandler.cs b/Ombi.Services/Jobs/IFaultQueueHandler.cs new file mode 100644 index 000000000..18f51a47f --- /dev/null +++ b/Ombi.Services/Jobs/IFaultQueueHandler.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Ombi.Core.SettingModels; +using Ombi.Store; +using Quartz; + +namespace Ombi.Services.Jobs +{ + public interface IFaultQueueHandler + { + void Execute(IJobExecutionContext context); + bool ShouldAutoApprove(RequestType requestType, PlexRequestSettings prSettings, List username); + void Start(); + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/IPlexEpisodeCacher.cs b/Ombi.Services/Jobs/IPlexEpisodeCacher.cs new file mode 100644 index 000000000..fc13d285b --- /dev/null +++ b/Ombi.Services/Jobs/IPlexEpisodeCacher.cs @@ -0,0 +1,12 @@ +using Ombi.Core.SettingModels; +using Quartz; + +namespace Ombi.Services.Jobs +{ + public interface IPlexEpisodeCacher + { + void CacheEpisodes(PlexSettings settings); + void Execute(IJobExecutionContext context); + void Start(); + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/IPlexUserChecker.cs b/Ombi.Services/Jobs/IPlexUserChecker.cs new file mode 100644 index 000000000..a19bce971 --- /dev/null +++ b/Ombi.Services/Jobs/IPlexUserChecker.cs @@ -0,0 +1,10 @@ +using Quartz; + +namespace Ombi.Services.Jobs +{ + public interface IPlexUserChecker + { + void Execute(IJobExecutionContext context); + void Start(); + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/JobNames.cs b/Ombi.Services/Jobs/JobNames.cs index 06b5b32ac..8b663a8ae 100644 --- a/Ombi.Services/Jobs/JobNames.cs +++ b/Ombi.Services/Jobs/JobNames.cs @@ -32,6 +32,7 @@ public static class JobNames public const string CpCacher = "CouchPotato Cacher"; public const string WatcherCacher = "Watcher Cacher"; public const string SonarrCacher = "Sonarr Cacher"; + public const string RadarrCacher = "Radarr Cacher"; public const string SrCacher = "SickRage Cacher"; public const string PlexChecker = "Plex Availability Cacher"; public const string PlexCacher = "Plex Cacher"; diff --git a/Ombi.Services/Jobs/PlexAvailabilityChecker.cs b/Ombi.Services/Jobs/PlexAvailabilityChecker.cs index a58b5841c..e205b5b1b 100644 --- a/Ombi.Services/Jobs/PlexAvailabilityChecker.cs +++ b/Ombi.Services/Jobs/PlexAvailabilityChecker.cs @@ -79,6 +79,7 @@ public PlexAvailabilityChecker(ISettingsService plexSettings, IReq public void CheckAndUpdateAll() { + var plexSettings = Plex.GetSettings(); if (!ValidateSettings(plexSettings)) @@ -472,5 +473,23 @@ public void Execute(IJobExecutionContext context) Job.SetRunning(false, JobNames.PlexChecker); } } + + public void Start() + { + Job.SetRunning(true, JobNames.PlexChecker); + try + { + CheckAndUpdateAll(); + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + Job.Record(JobNames.PlexChecker); + Job.SetRunning(false, JobNames.PlexChecker); + } + } } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/PlexEpisodeCacher.cs b/Ombi.Services/Jobs/PlexEpisodeCacher.cs index 1dae188d6..b7af87022 100644 --- a/Ombi.Services/Jobs/PlexEpisodeCacher.cs +++ b/Ombi.Services/Jobs/PlexEpisodeCacher.cs @@ -43,7 +43,7 @@ namespace Ombi.Services.Jobs { - public class PlexEpisodeCacher : IJob + public class PlexEpisodeCacher : IJob, IPlexEpisodeCacher { public PlexEpisodeCacher(ISettingsService plexSettings, IPlexApi plex, ICacheProvider cache, IJobRecord rec, IRepository repo, ISettingsService jobs) @@ -140,6 +140,38 @@ public void CacheEpisodes(PlexSettings settings) } } + public void Start() + { + try + { + var s = Plex.GetSettings(); + if (!s.EnableTvEpisodeSearching) + { + return; + } + + var jobs = Job.GetJobs(); + var job = jobs.FirstOrDefault(x => x.Name.Equals(JobNames.EpisodeCacher, StringComparison.CurrentCultureIgnoreCase)); + if (job != null) + { + if (job.LastRun > DateTime.Now.AddHours(-11)) // If it's been run in the last 11 hours + { + return; + } + } + Job.SetRunning(true, JobNames.EpisodeCacher); + CacheEpisodes(s); + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + Job.Record(JobNames.EpisodeCacher); + Job.SetRunning(false, JobNames.EpisodeCacher); + } + } public void Execute(IJobExecutionContext context) { diff --git a/Ombi.Services/Jobs/PlexUserChecker.cs b/Ombi.Services/Jobs/PlexUserChecker.cs index 5ea9c74a7..3303d1dcf 100644 --- a/Ombi.Services/Jobs/PlexUserChecker.cs +++ b/Ombi.Services/Jobs/PlexUserChecker.cs @@ -42,7 +42,7 @@ namespace Ombi.Services.Jobs { - public class PlexUserChecker : IJob + public class PlexUserChecker : IJob, IPlexUserChecker { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -68,7 +68,7 @@ public PlexUserChecker(IPlexUserRepository plexUsers, IPlexApi plexAPi, IJobReco private IRequestService RequestService { get; } private IUserRepository LocalUserRepository { get; } - public void Execute(IJobExecutionContext context) + public void Start() { JobRecord.SetRunning(true, JobNames.PlexUserChecker); @@ -153,7 +153,7 @@ public void Execute(IJobExecutionContext context) } // Looks like it's a new user! - var m = new PlexUsers + var m = new PlexUsers { PlexUserId = user.Id, Permissions = UserManagementHelper.GetPermissions(userManagementSettings), @@ -170,7 +170,7 @@ public void Execute(IJobExecutionContext context) // Main Plex user var dbMainAcc = dbUsers.FirstOrDefault(x => x.Username.Equals(mainPlexAccount.Username, StringComparison.CurrentCulture)); var localMainAcc = localUsers.FirstOrDefault(x => x.UserName.Equals(mainPlexAccount.Username, StringComparison.CurrentCulture)); - + // TODO if admin acc does exist, check if we need to update it @@ -188,7 +188,7 @@ public void Execute(IJobExecutionContext context) LoginId = Guid.NewGuid().ToString() }; - a.Permissions += (int) Permissions.Administrator; // Make admin + a.Permissions += (int)Permissions.Administrator; // Make admin Repo.Insert(a); } @@ -205,5 +205,9 @@ public void Execute(IJobExecutionContext context) JobRecord.Record(JobNames.PlexUserChecker); } } + public void Execute(IJobExecutionContext context) + { + Start(); + } } } \ No newline at end of file diff --git a/Ombi.Services/Jobs/RadarrCacher.cs b/Ombi.Services/Jobs/RadarrCacher.cs new file mode 100644 index 000000000..fc7338ecf --- /dev/null +++ b/Ombi.Services/Jobs/RadarrCacher.cs @@ -0,0 +1,110 @@ +#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.Collections.Generic; +using System.Linq; +using NLog; +using Ombi.Api.Interfaces; +using Ombi.Api.Models.Radarr; +using Ombi.Core; +using Ombi.Core.SettingModels; +using Ombi.Helpers; +using Ombi.Services.Interfaces; +using Quartz; + +namespace Ombi.Services.Jobs +{ + public class RadarrCacher : IJob, IRadarrCacher + { + public RadarrCacher(ISettingsService radarrService, IRadarrApi radarrApi, ICacheProvider cache, IJobRecord rec) + { + RadarrSettings = radarrService; + RadarrApi = radarrApi; + Job = rec; + Cache = cache; + } + + private ISettingsService RadarrSettings { get; } + private ICacheProvider Cache { get; } + private IRadarrApi RadarrApi { get; } + private IJobRecord Job { get; } + + private static Logger Log = LogManager.GetCurrentClassLogger(); + + public void Queued() + { + var settings = RadarrSettings.GetSettings(); + if (settings.Enabled) + { + Job.SetRunning(true, JobNames.RadarrCacher); + try + { + var movies = RadarrApi.GetMovies(settings.ApiKey, settings.FullUri); + if (movies != null) + { + var movieIds = new List(); + foreach (var m in movies) + { + if (m.tmdbId > 0) + { + movieIds.Add(m.tmdbId); + } + } + //var movieIds = movies.Select(x => x.tmdbId).ToList(); + Cache.Set(CacheKeys.RadarrMovies, movieIds, CacheKeys.TimeFrameMinutes.SchedulerCaching); + } + } + catch (System.Exception ex) + { + Log.Error(ex, "Failed caching queued items from Radarr"); + } + finally + { + Job.Record(JobNames.RadarrCacher); + Job.SetRunning(false, JobNames.RadarrCacher); + } + } + } + + // we do not want to set here... + public int[] QueuedIds() + { + var retVal = new List(); + var movies = Cache.Get>(CacheKeys.RadarrMovies); + if (movies != null) + { + retVal.AddRange(movies); + } + return retVal.ToArray(); + } + + public void Execute(IJobExecutionContext context) + { + Queued(); + } + } +} \ No newline at end of file diff --git a/Ombi.Services/Jobs/RecentlyAdded.cs b/Ombi.Services/Jobs/RecentlyAdded.cs index 7e7293578..8ac7b6743 100644 --- a/Ombi.Services/Jobs/RecentlyAdded.cs +++ b/Ombi.Services/Jobs/RecentlyAdded.cs @@ -78,7 +78,7 @@ public RecentlyAdded(IPlexApi api, ISettingsService plexSettings, private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - public void Execute(IJobExecutionContext context) + public void Start() { try { @@ -100,6 +100,10 @@ public void Execute(IJobExecutionContext context) JobRecord.SetRunning(false, JobNames.RecentlyAddedEmail); } } + public void Execute(IJobExecutionContext context) + { + Start(); + } public void Test() { @@ -455,7 +459,7 @@ private void Send(NewletterSettings newletterSettings, string html, PlexSettings if (!testEmail) { - var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification); + var users = UserHelper.GetUsersWithFeature(Features.Newsletter); if (users != null) { foreach (var user in users) diff --git a/Ombi.Services/Jobs/StoreBackup.cs b/Ombi.Services/Jobs/StoreBackup.cs index 6a9f4ac6c..d90aadd76 100644 --- a/Ombi.Services/Jobs/StoreBackup.cs +++ b/Ombi.Services/Jobs/StoreBackup.cs @@ -35,7 +35,7 @@ namespace Ombi.Services.Jobs { - public class StoreBackup : IJob + public class StoreBackup : IJob, IStoreBackup { public StoreBackup(ISqliteConfiguration sql, IJobRecord rec) { @@ -48,6 +48,13 @@ public StoreBackup(ISqliteConfiguration sql, IJobRecord rec) private static Logger Log = LogManager.GetCurrentClassLogger(); + public void Start() + { + JobRecord.SetRunning(true, JobNames.CpCacher); + TakeBackup(); + Cleanup(); + } + public void Execute(IJobExecutionContext context) { JobRecord.SetRunning(true, JobNames.CpCacher); diff --git a/Ombi.Services/Jobs/StoreCleanup.cs b/Ombi.Services/Jobs/StoreCleanup.cs index 75bdd2f34..fd9df3aa9 100644 --- a/Ombi.Services/Jobs/StoreCleanup.cs +++ b/Ombi.Services/Jobs/StoreCleanup.cs @@ -36,7 +36,7 @@ namespace Ombi.Services.Jobs { - public class StoreCleanup : IJob + public class StoreCleanup : IJob, IStoreCleanup { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -81,6 +81,11 @@ private void Cleanup() } + public void Start() + { + JobRecord.SetRunning(true, JobNames.CpCacher); + Cleanup(); + } public void Execute(IJobExecutionContext context) { JobRecord.SetRunning(true, JobNames.CpCacher); diff --git a/Ombi.Services/Jobs/UserRequestLimitResetter.cs b/Ombi.Services/Jobs/UserRequestLimitResetter.cs index 446b2d7ba..0a97a912b 100644 --- a/Ombi.Services/Jobs/UserRequestLimitResetter.cs +++ b/Ombi.Services/Jobs/UserRequestLimitResetter.cs @@ -39,7 +39,7 @@ namespace Ombi.Services.Jobs { - public class UserRequestLimitResetter : IJob + public class UserRequestLimitResetter : IJob, IUserRequestLimitResetter { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -94,6 +94,31 @@ private void CheckAndDelete(IEnumerable allUsers, RequestType type } } + + public void Start() + { + Record.SetRunning(true, JobNames.CpCacher); + try + { + var settings = Settings.GetSettings(); + var users = Repo.GetAll(); + var requestLimits = users as RequestLimit[] ?? users.ToArray(); + + MovieLimit(settings, requestLimits); + TvLimit(settings, requestLimits); + AlbumLimit(settings, requestLimits); + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + Record.Record(JobNames.RequestLimitReset); + Record.SetRunning(false, JobNames.CpCacher); + } + } + public void Execute(IJobExecutionContext context) { Record.SetRunning(true, JobNames.CpCacher); diff --git a/Ombi.Services/Notification/DiscordNotification.cs b/Ombi.Services/Notification/DiscordNotification.cs new file mode 100644 index 000000000..4cfdfdbb5 --- /dev/null +++ b/Ombi.Services/Notification/DiscordNotification.cs @@ -0,0 +1,165 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SlackNotification.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.Api.Models.Notifications; +using Ombi.Core; +using Ombi.Core.Models; +using Ombi.Core.SettingModels; +using Ombi.Services.Interfaces; + +namespace Ombi.Services.Notification +{ + public class DiscordNotification : INotification + { + public DiscordNotification(IDiscordApi api, ISettingsService sn) + { + Api = api; + Settings = sn; + } + + public string NotificationName => "DiscordNotification"; + + private IDiscordApi Api { get; } + private ISettingsService Settings { get; } + private static Logger Log = LogManager.GetCurrentClassLogger(); + + + public async Task NotifyAsync(NotificationModel model) + { + var settings = Settings.GetSettings(); + + await NotifyAsync(model, settings); + } + + public async Task NotifyAsync(NotificationModel model, Settings settings) + { + if (settings == null) await NotifyAsync(model); + + var pushSettings = (DiscordNotificationSettings)settings; + if (!ValidateConfiguration(pushSettings)) + { + Log.Error("Settings for Slack was not correct, we cannot push a notification"); + return; + } + + switch (model.NotificationType) + { + case NotificationType.NewRequest: + await PushNewRequestAsync(model, pushSettings); + break; + case NotificationType.Issue: + await PushIssueAsync(model, pushSettings); + break; + case NotificationType.RequestAvailable: + break; + case NotificationType.RequestApproved: + break; + case NotificationType.AdminNote: + break; + case NotificationType.Test: + await PushTest(pushSettings); + break; + case NotificationType.RequestDeclined: + await PushRequestDeclinedAsync(model, pushSettings); + break; + case NotificationType.ItemAddedToFaultQueue: + await PushFaultQueue(model, pushSettings); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private async Task PushNewRequestAsync(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"{model.Title} has been requested by user: {model.User}"; + await Push(settings, message); + } + + private async Task PushRequestDeclinedAsync(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"Hello! Your request for {model.Title} has been declined, Sorry!"; + await Push(settings, message); + } + + private async Task PushIssueAsync(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; + await Push(settings, message); + } + + private async Task PushTest(DiscordNotificationSettings settings) + { + var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!"; + await Push(settings, message); + } + + private async Task PushFaultQueue(NotificationModel model, DiscordNotificationSettings settings) + { + var message = $"Hello! The user '{model.User}' has requested {model.Title} but it could not be added. This has been added into the requests queue and will keep retrying"; + await Push(settings, message); + } + + private async Task Push(DiscordNotificationSettings config, string message) + { + try + { + await Api.SendMessageAsync(message, config.WebookId, config.Token, config.Username); + } + catch (Exception e) + { + Log.Error(e); + } + } + + private bool ValidateConfiguration(DiscordNotificationSettings settings) + { + if (!settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(settings.WebhookUrl)) + { + return false; + } + try + { + var a = settings.Token; + var b = settings.WebookId; + } + catch (IndexOutOfRangeException) + { + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/Ombi.Services/Notification/EmailMessageNotification.cs b/Ombi.Services/Notification/EmailMessageNotification.cs index f073dcf02..729726eef 100644 --- a/Ombi.Services/Notification/EmailMessageNotification.cs +++ b/Ombi.Services/Notification/EmailMessageNotification.cs @@ -119,14 +119,6 @@ private bool ValidateConfiguration(EmailNotificationSettings settings) return false; } - if (!settings.EnableUserEmailNotifications) - { - if (!settings.Enabled) - { - return false; - } - } - return true; } @@ -237,16 +229,12 @@ private async Task EmailRequestApproved(NotificationModel model, EmailNotificati private async Task EmailAvailableRequest(NotificationModel model, EmailNotificationSettings settings) { - if (!settings.EnableUserEmailNotifications) - { - await Task.FromResult(false); - } var email = new EmailBasicTemplate(); var html = email.LoadTemplate( $"Ombi: {model.Title} is now available!", - $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)", + $"Hello! You requested {model.Title} on Ombi! This is now available on Plex! :)", model.ImgSrc); - var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! You requested {model.Title} on PlexRequests! This is now available on Plex! :)" }; + var body = new BodyBuilder { HtmlBody = html, TextBody = $"Hello! You requested {model.Title} on Ombi! This is now available on Plex! :)" }; var message = new MimeMessage { diff --git a/Ombi.Services/Notification/NotificationEngine.cs b/Ombi.Services/Notification/NotificationEngine.cs index 7a119cca1..2f51d7109 100644 --- a/Ombi.Services/Notification/NotificationEngine.cs +++ b/Ombi.Services/Notification/NotificationEngine.cs @@ -98,10 +98,31 @@ public async Task NotifyUsers(IEnumerable modelChanged, string a return; } - var localUser = - users.FirstOrDefault( x => - x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase) || - x.UserAlias.Equals(user, StringComparison.CurrentCultureIgnoreCase)); + UserHelperModel localUser = null; + //users.FirstOrDefault( x => + // x.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase) || + // x.UserAlias.Equals(user, StringComparison.CurrentCultureIgnoreCase)); + + foreach (var userHelperModel in users) + { + if (!string.IsNullOrEmpty(userHelperModel.Username)) + { + if (userHelperModel.Username.Equals(user, StringComparison.CurrentCultureIgnoreCase)) + { + localUser = userHelperModel; + break; + } + } + if (!string.IsNullOrEmpty(userHelperModel.UserAlias)) + { + if (userHelperModel.UserAlias.Equals(user, StringComparison.CurrentCultureIgnoreCase)) + { + localUser = userHelperModel; + break; + } + } + } + // So if the request was from an alias, then we need to use the local user (since that contains the alias). // If we do not have a local user, then we should be using the Plex user if that user exists. @@ -152,8 +173,36 @@ public async Task NotifyUsers(RequestedModel model, string apiKey, NotificationT var users = UserHelper.GetUsersWithFeature(Features.RequestAddedNotification).ToList(); Log.Debug("Notifying Users Count {0}", users.Count); - var selectedUsers = users.Select(x => x.Username).Intersect(model.RequestedUsers, StringComparer.CurrentCultureIgnoreCase); - foreach (var user in selectedUsers) + // Get the usernames or alias depending if they have an alias + var userNamesWithFeature = users.Select(x => x.UsernameOrAlias).ToList(); + Log.Debug("Users with the feature count {0}", userNamesWithFeature.Count); + Log.Debug("Usernames: "); + foreach (var u in userNamesWithFeature) + { + Log.Debug(u); + } + + Log.Debug("Users in the requested model count: {0}", model.AllUsers.Count); + Log.Debug("usernames from model: "); + foreach (var modelAllUser in model.AllUsers) + { + Log.Debug(modelAllUser); + } + + if (model.AllUsers == null || !model.AllUsers.Any()) + { + Log.Debug("There are no users in the model.AllUsers, no users to notify"); + return; + } + var usersToNotify = userNamesWithFeature.Intersect(model.AllUsers, StringComparer.CurrentCultureIgnoreCase).ToList(); + + if (!usersToNotify.Any()) + { + Log.Debug("Could not find any users after the .Intersect()"); + } + + Log.Debug("Users being notified for this request count {0}", users.Count); + foreach (var user in usersToNotify) { Log.Info("Notifying user {0}", user); if (user.Equals(adminUsername, StringComparison.CurrentCultureIgnoreCase)) diff --git a/Ombi.Services/Ombi.Services.csproj b/Ombi.Services/Ombi.Services.csproj index fabb8e69f..6705cf939 100644 --- a/Ombi.Services/Ombi.Services.csproj +++ b/Ombi.Services/Ombi.Services.csproj @@ -86,13 +86,21 @@ + + + + + + + + - - + + @@ -126,6 +134,7 @@ + diff --git a/Ombi.Store/Models/PlexUsers.cs b/Ombi.Store/Models/PlexUsers.cs index b61b2a99e..0a3b735d1 100644 --- a/Ombi.Store/Models/PlexUsers.cs +++ b/Ombi.Store/Models/PlexUsers.cs @@ -26,6 +26,7 @@ #endregion using Dapper.Contrib.Extensions; +using Newtonsoft.Json; namespace Ombi.Store.Models { diff --git a/Ombi.Store/Models/RequestQueue.cs b/Ombi.Store/Models/RequestQueue.cs index e8067a20f..b28807120 100644 --- a/Ombi.Store/Models/RequestQueue.cs +++ b/Ombi.Store/Models/RequestQueue.cs @@ -41,7 +41,7 @@ public class RequestQueue : Entity public FaultType FaultType { get; set; } public DateTime? LastRetry { get; set; } - public string Message { get; set; } + public string Description { get; set; } } public enum FaultType diff --git a/Ombi.Store/RequestedModel.cs b/Ombi.Store/RequestedModel.cs index ffd416966..c54d68e5c 100644 --- a/Ombi.Store/RequestedModel.cs +++ b/Ombi.Store/RequestedModel.cs @@ -27,9 +27,6 @@ public RequestedModel() public string Status { get; set; } public bool Approved { get; set; } - [Obsolete("Use RequestedUsers")] //TODO remove this obsolete property - public string RequestedBy { get; set; } - public DateTime RequestedDate { get; set; } public bool Available { get; set; } public IssueState Issues { get; set; } @@ -46,6 +43,13 @@ public RequestedModel() public List Episodes { get; set; } public bool Denied { get; set; } public string DeniedReason { get; set; } + /// + /// For TV Shows with a custom root folder + /// + /// + /// The root folder selected. + /// + public int RootFolderSelected { get; set; } [JsonIgnore] public List AllUsers @@ -53,14 +57,9 @@ public List AllUsers get { var u = new List(); - if (!string.IsNullOrEmpty(RequestedBy)) - { - u.Add(RequestedBy); - } - - if (RequestedUsers.Any()) + if (RequestedUsers != null && RequestedUsers.Any()) { - u.AddRange(RequestedUsers.Where(requestedUser => requestedUser != RequestedBy)); + u.AddRange(RequestedUsers); } return u; } diff --git a/Ombi.UI.Tests/TvSenderTests.cs b/Ombi.UI.Tests/TvSenderTests.cs index 1f6d337da..1a766ad67 100644 --- a/Ombi.UI.Tests/TvSenderTests.cs +++ b/Ombi.UI.Tests/TvSenderTests.cs @@ -35,6 +35,7 @@ using Ombi.Api.Models.Sonarr; using Ombi.Core; using Ombi.Core.SettingModels; +using Ombi.Helpers; using Ombi.Store; using Ploeh.AutoFixture; @@ -49,6 +50,7 @@ public class TvSenderTests private TvSender Sender { get; set; } private Fixture F { get; set; } + private Mock Cache { get; set; } [SetUp] public void Setup() @@ -56,7 +58,8 @@ public void Setup() F = new Fixture(); SonarrMock = new Mock(); SickrageMock = new Mock(); - Sender = new TvSender(SonarrMock.Object, SickrageMock.Object); + Cache = new Mock(); + Sender = new TvSender(SonarrMock.Object, SickrageMock.Object, Cache.Object); } [Test] @@ -66,7 +69,7 @@ public async Task HappyPathSendSeriesToSonarrAllSeason() var seriesResult = new SonarrAddSeries() { title = "ABC"}; SonarrMock.Setup(x => x.GetSeries(It.IsAny(), It.IsAny())).Returns(F.Build().With(x => x.tvdbId, 1).With(x => x.title, "ABC").CreateMany().ToList()); - Sender = new TvSender(SonarrMock.Object, SickrageMock.Object); + Sender = new TvSender(SonarrMock.Object, SickrageMock.Object, Cache.Object); var request = new RequestedModel {SeasonsRequested = "All", ProviderId = 1, Title = "ABC"}; @@ -116,7 +119,7 @@ public async Task HappyPathSendEpisodeWithExistingSeriesToSonarr() SonarrMock.Setup(x => x.GetEpisodes(It.IsAny(), It.IsAny(), It.IsAny())).Returns(F.CreateMany()); - Sender = new TvSender(SonarrMock.Object, SickrageMock.Object); + Sender = new TvSender(SonarrMock.Object, SickrageMock.Object, Cache.Object); var episodes = new List { new EpisodesModel diff --git a/Ombi.UI/Content/base.css b/Ombi.UI/Content/base.css index deae5b1f4..987834924 100644 --- a/Ombi.UI/Content/base.css +++ b/Ombi.UI/Content/base.css @@ -509,3 +509,11 @@ label { background-position: center; position: absolute; } +.list-group-item-dropdown { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #3e3e3e; + border: 1px solid transparent; } + diff --git a/Ombi.UI/Content/base.min.css b/Ombi.UI/Content/base.min.css index 9bd570797..4ac0fc57e 100644 --- a/Ombi.UI/Content/base.min.css +++ b/Ombi.UI/Content/base.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.small-label{display:inline-block !important;margin-bottom:.5rem !important;font-size:11px !important;}.small-checkbox{min-height:0 !important;}.round-checkbox{border-radius:8px;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}#cacherRunning{background-color:#4e5d6c;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.small-checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:8px;min-height:0 !important;}.small-checkbox input[type=checkbox]{display:none;}.small-checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{min-height:0 !important;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}}#lightbox{background-color:#808080;filter:alpha(opacity=50);opacity:.5;-moz-opacity:.5;top:0;left:0;z-index:20;height:100%;width:100%;background-repeat:no-repeat;background-position:center;position:absolute;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.small-label{display:inline-block !important;margin-bottom:.5rem !important;font-size:11px !important;}.small-checkbox{min-height:0 !important;}.round-checkbox{border-radius:8px;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}#cacherRunning{background-color:#4e5d6c;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.small-checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:8px;min-height:0 !important;}.small-checkbox input[type=checkbox]{display:none;}.small-checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.small-checkbox label{min-height:0 !important;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}.shadow{-moz-box-shadow:3px 3px 5px 6px #191919;-webkit-box-shadow:3px 3px 5px 6px #191919;box-shadow:3px 3px 5px 6px #191919;}.img-circle{border-radius:50%;}#wrapper{padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled{padding-right:250px;}#sidebar-wrapper{z-index:1000;position:fixed;right:250px;width:0;height:100%;margin-right:-250px;overflow-y:auto;background:#4e5d6c;padding-left:0;-webkit-transition:all .5s ease;-moz-transition:all .5s ease;-o-transition:all .5s ease;transition:all .5s ease;}#wrapper.toggled #sidebar-wrapper{width:500px;}#page-content-wrapper{width:100%;position:absolute;padding:15px;}#wrapper.toggled #page-content-wrapper{position:absolute;margin-left:-250px;}.sidebar-nav{position:absolute;top:0;width:500px;margin:0;padding-left:0;list-style:none;}.sidebar-nav li{text-indent:20px;line-height:40px;}.sidebar-nav li a{display:block;text-decoration:none;color:#999;}.sidebar-nav li a:hover{text-decoration:none;color:#fff;background:rgba(255,255,255,.2);}.sidebar-nav li a:active,.sidebar-nav li a:focus{text-decoration:none;}.sidebar-nav>.sidebar-brand{height:65px;font-size:18px;line-height:60px;}.sidebar-nav>.sidebar-brand a{color:#999;}.sidebar-nav>.sidebar-brand a:hover{color:#fff;background:none;}@media(min-width:768px){#wrapper{padding-right:250px;}#wrapper.toggled{padding-right:0;}#sidebar-wrapper{width:500px;}#wrapper.toggled #sidebar-wrapper{width:0;}#page-content-wrapper{padding:20px;position:relative;}#wrapper.toggled #page-content-wrapper{position:relative;margin-right:0;}}#lightbox{background-color:#808080;filter:alpha(opacity=50);opacity:.5;-moz-opacity:.5;top:0;left:0;z-index:20;height:100%;width:100%;background-repeat:no-repeat;background-position:center;position:absolute;}.list-group-item-dropdown{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#3e3e3e;border:1px solid transparent;} \ No newline at end of file diff --git a/Ombi.UI/Content/base.scss b/Ombi.UI/Content/base.scss index 6d58a2d8c..aa0417b06 100644 --- a/Ombi.UI/Content/base.scss +++ b/Ombi.UI/Content/base.scss @@ -632,3 +632,13 @@ $border-radius: 10px; background-position:center; position:absolute; } + + +.list-group-item-dropdown { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #3e3e3e; + border: 1px solid transparent; +} \ No newline at end of file diff --git a/Ombi.UI/Content/bootstrap.css b/Ombi.UI/Content/bootstrap.css index 0d1a92e78..0e555d972 100644 --- a/Ombi.UI/Content/bootstrap.css +++ b/Ombi.UI/Content/bootstrap.css @@ -5259,6 +5259,7 @@ a.thumbnail.active { background-color: #4e5d6c; border: 1px solid transparent; } + .list-group-item:first-child { border-top-right-radius: 0; border-top-left-radius: 0; diff --git a/Ombi.UI/Content/requests.js b/Ombi.UI/Content/requests.js index 7928f39aa..1d2ad987d 100644 --- a/Ombi.UI/Content/requests.js +++ b/Ombi.UI/Content/requests.js @@ -559,6 +559,25 @@ $(document).on("click", ".approve-with-quality", function (e) { }); +// Change root folder +$(document).on("click", ".change-root-folder", function (e) { + e.preventDefault(); + var $this = $(this); + var $button = $this.parents('.btn-split').children('.change').first(); + var rootFolderId = e.target.id + var $form = $this.parents('form').first(); + + if ($button.text() === " Loading...") { + return; + } + + loadingButton($button.attr('id'), "success"); + + changeRootFolder($form, rootFolderId, function () { + }); + +}); + // Change Availability $(document).on("click", ".change", function (e) { @@ -638,6 +657,37 @@ function approveRequest($form, qualityId, successCallback) { }); } +function changeRootFolder($form, rootFolderId, successCallback) { + + var formData = $form.serialize(); + if (rootFolderId) formData += ("&rootFolderId=" + rootFolderId); + + $.ajax({ + type: $form.prop('method'), + url: $form.prop('action'), + data: formData, + dataType: "json", + success: function (response) { + + if (checkJsonResponse(response)) { + if (response.message) { + generateNotify(response.message, "success"); + } else { + generateNotify("Success! Changed Root Path.", "success"); + } + + if (successCallback) { + successCallback(); + } + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); +} + function denyRequest($form, successCallback) { var formData = $form.serialize(); @@ -808,6 +858,9 @@ function buildRequestContext(result, type) { musicBrainzId: result.musicBrainzId, denied: result.denied, deniedReason: result.deniedReason, + hasRootFolders: result.hasRootFolders, + rootFolders: result.rootFolders, + currentRootPath : result.currentRootPath }; return context; diff --git a/Ombi.UI/Content/search.js b/Ombi.UI/Content/search.js index 674660b11..fde3077e4 100644 --- a/Ombi.UI/Content/search.js +++ b/Ombi.UI/Content/search.js @@ -24,7 +24,8 @@ Function.prototype.bind = function (parent) { $(function () { - var searchSource = $("#search-template").html(); + var useNewSearch = $('#useNewSearch').text() == 'True'; + var searchSource = useNewSearch ? $("#search-templateNew").html() : $("#search-template").html(); var seasonsSource = $("#seasons-template").html(); var musicSource = $("#music-template").html(); var seasonsNumberSource = $("#seasonNumber-template").html(); @@ -72,6 +73,25 @@ $(function () { moviesInTheaters(); }); + // TV DropDown + $('#popularShows').on('click', function (e) { + e.preventDefault(); + popularShows(); + }); + + $('#trendingShows').on('click', function (e) { + e.preventDefault(); + trendingTv(); + }); + $('#mostWatchedShows').on('click', function (e) { + e.preventDefault(); + mostwatchedTv(); + }); + $('#anticipatedShows').on('click', function (e) { + e.preventDefault(); + anticipatedTv(); + }); + // Type in TV search $("#tvSearchContent").on("input", function () { if (searchTimer) { @@ -293,6 +313,23 @@ $(function () { getMovies(url); } + function popularShows() { + var url = createBaseUrl(base, '/search/tv/popular'); + getTvShows(url, true); + } + function anticipatedTv() { + var url = createBaseUrl(base, '/search/tv/anticipated'); + getTvShows(url, true); + } + function trendingTv() { + var url = createBaseUrl(base, '/search/tv/trending'); + getTvShows(url, true); + } + function mostwatchedTv() { + var url = createBaseUrl(base, '/search/tv/mostwatched'); + getTvShows(url, true); + } + function getMovies(url) { resetMovies(); @@ -304,6 +341,8 @@ $(function () { var html = searchTemplate(context); $("#movieList").append(html); + + checkNetflix(context.title, context.id); }); } else { @@ -321,10 +360,10 @@ $(function () { var query = $("#tvSearchContent").val(); var url = createBaseUrl(base, '/search/tv/'); - query ? getTvShows(url + query) : resetTvShows(); + query ? getTvShows(url + query, false) : resetTvShows(); } - function getTvShows(url) { + function getTvShows(url, loadImage) { resetTvShows(); $('#tvSearchButton').attr("class", "fa fa-spinner fa-spin"); @@ -334,6 +373,11 @@ $(function () { var context = buildTvShowContext(result); var html = searchTemplate(context); $("#tvList").append(html); + + checkNetflix(context.title, context.id); + if (loadImage) { + getTvPoster(result.id); + } }); } else { @@ -343,6 +387,19 @@ $(function () { }); }; + function checkNetflix(title, id) { + var url = createBaseUrl(base, '/searchextension/netflix/' + title); + $.ajax(url).success(function (results) { + + if (results.result) { + // It's on Netflix + $('#' + id + 'netflixTab') + .html("Avaialble on Netflix"); + } + + }); + } + function resetTvShows() { $("#tvList").html(""); } @@ -388,6 +445,16 @@ $(function () { }); }; + function getTvPoster(theTvDbId) { + + var url = createBaseUrl(base, '/search/tv/poster/'); + $.ajax(url + theTvDbId).success(function (result) { + if (result) { + $('#' + theTvDbId + "imgDiv").html(" poster"); + } + }); + }; + function buildMovieContext(result) { var date = new Date(result.releaseDate); var year = date.getFullYear(); @@ -404,7 +471,11 @@ $(function () { requested: result.requested, approved: result.approved, available: result.available, - url: result.plexUrl + url: result.plexUrl, + trailer: result.trailer, + homepage: result.homepage, + releaseDate: Humanize(result.releaseDate), + status: result.status }; return context; @@ -414,6 +485,7 @@ $(function () { var date = new Date(result.firstAired); var year = date.getFullYear(); var context = { + status: result.status, posterPath: result.banner, id: result.id, title: result.seriesName, @@ -430,8 +502,11 @@ $(function () { tvPartialAvailable: result.tvPartialAvailable, disableTvRequestsByEpisode: result.disableTvRequestsByEpisode, disableTvRequestsBySeason: result.disableTvRequestsBySeason, - enableTvRequestsForOnlySeries: result.enableTvRequestsForOnlySeries - }; + enableTvRequestsForOnlySeries: result.enableTvRequestsForOnlySeries, + trailer: result.trailer, + homepage: result.homepage, + firstAired: Humanize(result.firstAired) + }; return context; } diff --git a/Ombi.UI/Content/site.js b/Ombi.UI/Content/site.js index 85cae41f8..29b0eff76 100644 --- a/Ombi.UI/Content/site.js +++ b/Ombi.UI/Content/site.js @@ -8,7 +8,7 @@ return s; } -$(function() { +$(function () { $('[data-toggle="tooltip"]').tooltip(); }); @@ -93,6 +93,19 @@ function createBaseUrl(base, url) { return url; } + +function createLocalUrl(url) { + var base = $('#baseUrl').text(); + if (base) { + if (url.charAt(0) === "/") { + url = "/" + base + url; + } else { + url = "/" + base + "/" + url; + } + } + return url; +} + var noResultsHtml = "
" + "
Sorry, we didn't find any results!
"; var noResultsMusic = "
" + diff --git a/Ombi.UI/Content/systemjs.config.js b/Ombi.UI/Content/systemjs.config.js new file mode 100644 index 000000000..7fe21249b --- /dev/null +++ b/Ombi.UI/Content/systemjs.config.js @@ -0,0 +1,41 @@ +/** + * System configuration for Angular 2 samples + * Adjust as necessary for your application needs. + */ +(function (global) { + System.config({ + paths: { + // paths serve as alias + 'npm:': '../node_modules/', + 'app' : '../app/' + }, + // map tells the System loader where to look for things + map: { + // our app is within the app folder + app: 'app', + + // angular bundles + '@angular/core': 'npm:@angular/core/bundles/core.umd.js', + '@angular/common': 'npm:@angular/common/bundles/common.umd.js', + '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', + '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', + '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', + '@angular/http': 'npm:@angular/http/bundles/http.umd.js', + '@angular/router': 'npm:@angular/router/bundles/router.umd.js', + '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', + + // other libraries + 'rxjs': 'npm:rxjs' + }, + // packages tells the System loader how to load when no filename and/or no extension + packages: { + app: { + main: './main.js', + defaultExtension: 'js' + }, + rxjs: { + defaultExtension: 'js' + } + } + }); +})(this); \ No newline at end of file diff --git a/Ombi.UI/Helpers/BaseUrlHelper.cs b/Ombi.UI/Helpers/BaseUrlHelper.cs index 0d5581838..99c37459e 100644 --- a/Ombi.UI/Helpers/BaseUrlHelper.cs +++ b/Ombi.UI/Helpers/BaseUrlHelper.cs @@ -199,7 +199,7 @@ public static IHtmlString LoadWizardAssets(this HtmlHelpers helper) var assetLocation = GetBaseUrl(); var content = GetContentUrl(assetLocation); - + sb.AppendLine($""); return helper.Raw(sb.ToString()); @@ -226,9 +226,9 @@ public static IHtmlString LoadUserManagementAssets(this HtmlHelpers helper) sb.Append($""); sb.Append($""); sb.Append($""); - sb.Append($""); - sb.Append($""); - sb.Append($""); + sb.Append($""); + sb.Append($""); + sb.Append($""); return helper.Raw(sb.ToString()); } @@ -290,16 +290,19 @@ public static IHtmlString LoadFavIcon(this HtmlHelpers helper) return helper.Raw(asset); } - public static IHtmlString GetSidebarUrl(this HtmlHelpers helper, NancyContext context, string url, string title) + public static IHtmlString GetSidebarUrl(this HtmlHelpers helper, NancyContext context, string url, string title, string icon = null) { var content = GetLinkUrl(GetBaseUrl()); if (!string.IsNullOrEmpty(content)) { url = $"/{content}{url}"; } - var returnString = context.Request.Path == url - ? $"{title}" - : $"{title}"; + + + var iconHtml = string.IsNullOrEmpty(icon) ? "" : $""; + var returnString = context.Request.Path == url + ? $"{title} {iconHtml}" + : $"{title} {iconHtml}"; return helper.Raw(returnString); } @@ -311,8 +314,8 @@ public static IHtmlString GetNavbarUrl(this HtmlHelpers helper, NancyContext con { url = $"/{content}{url}"; } - var returnString = context.Request.Path == url ? - $"
  • {title}
  • " + var returnString = context.Request.Path == url ? + $"
  • {title}
  • " : $"
  • {title}
  • "; return helper.Raw(returnString); @@ -326,8 +329,8 @@ public static IHtmlString GetNavbarUrl(this HtmlHelpers helper, NancyContext con url = $"/{content}{url}"; } - var returnString = context.Request.Path == url - ? $"
  • {title} {extraHtml}
  • " + var returnString = context.Request.Path == url + ? $"
  • {title} {extraHtml}
  • " : $"
  • {title} {extraHtml}
  • "; return helper.Raw(returnString); diff --git a/Ombi.UI/Jobs/Scheduler.cs b/Ombi.UI/Jobs/Scheduler.cs index 7450e1c86..b2c32188a 100644 --- a/Ombi.UI/Jobs/Scheduler.cs +++ b/Ombi.UI/Jobs/Scheduler.cs @@ -75,6 +75,7 @@ private IEnumerable CreateJobs() JobBuilder.Create().WithIdentity("UserRequestLimiter", "Request").Build(), JobBuilder.Create().WithIdentity("RecentlyAddedModel", "Email").Build(), JobBuilder.Create().WithIdentity("FaultQueueHandler", "Fault").Build(), + JobBuilder.Create().WithIdentity("RadarrCacher", "Cache").Build(), }; jobs.AddRange(jobList); @@ -170,6 +171,10 @@ private IEnumerable CreateTriggers() { s.PlexUserChecker = 24; } + if (s.RadarrCacher == 0) + { + s.RadarrCacher = 60; + } var triggers = new List(); @@ -222,6 +227,14 @@ private IEnumerable CreateTriggers() .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.WatcherCacher).RepeatForever()) .Build(); + var radarrCacher = + TriggerBuilder.Create() + .WithIdentity("RadarrCacher", "Cache") + .StartNow() + //.StartAt(DateBuilder.FutureDate(2, IntervalUnit.Minute)) + .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.RadarrCacher).RepeatForever()) + .Build(); + var storeBackup = TriggerBuilder.Create() .WithIdentity("StoreBackup", "Database") @@ -280,6 +293,7 @@ private IEnumerable CreateTriggers() triggers.Add(fault); triggers.Add(plexCacher); triggers.Add(plexUserChecker); + triggers.Add(radarrCacher); return triggers; } diff --git a/Ombi.UI/ModelDataProviders/SonarrSettingsDataProvider.cs b/Ombi.UI/ModelDataProviders/SonarrSettingsDataProvider.cs index 694973e6a..7f3474ff8 100644 --- a/Ombi.UI/ModelDataProviders/SonarrSettingsDataProvider.cs +++ b/Ombi.UI/ModelDataProviders/SonarrSettingsDataProvider.cs @@ -52,7 +52,7 @@ public SwaggerModelData GetModelData() with.Property(x => x.QualityProfile).Description("Sonarr's quality profile").Required(true); with.Property(x => x.SeasonFolders).Description("Sonarr's season folders").Required(false); - + with.Property(x => x.RootPath).Description("Sonarr's root path").Required(false); }); } diff --git a/Ombi.UI/Models/RequestViewModel.cs b/Ombi.UI/Models/RequestViewModel.cs index b6db27bca..b31237bdc 100644 --- a/Ombi.UI/Models/RequestViewModel.cs +++ b/Ombi.UI/Models/RequestViewModel.cs @@ -58,5 +58,8 @@ public class RequestViewModel public Store.EpisodesModel[] Episodes { get; set; } public bool Denied { get; set; } public string DeniedReason { get; set; } + public RootFolderModel[] RootFolders { get; set; } + public bool HasRootFolders { get; set; } + public string CurrentRootPath { get; set; } } } diff --git a/Ombi.UI/Models/RootFolderModel.cs b/Ombi.UI/Models/RootFolderModel.cs new file mode 100644 index 000000000..9d15024fe --- /dev/null +++ b/Ombi.UI/Models/RootFolderModel.cs @@ -0,0 +1,36 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: RootFolderModel.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.UI.Models +{ + + public class RootFolderModel + { + public string Id { get; set; } + public string Path { get; set; } + public long FreeSpace { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.UI/Models/SearchLoadViewModel.cs b/Ombi.UI/Models/SearchLoadViewModel.cs new file mode 100644 index 000000000..2bdf5078c --- /dev/null +++ b/Ombi.UI/Models/SearchLoadViewModel.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: SearchLoadViewModel.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 Ombi.Core.SettingModels; + +namespace Ombi.UI.Models +{ + public class SearchLoadViewModel + { + public PlexRequestSettings Settings { get; set; } + public CustomizationSettings CustomizationSettings { get; set; } + } +} \ No newline at end of file diff --git a/Ombi.UI/Models/SearchMovieViewModel.cs b/Ombi.UI/Models/SearchMovieViewModel.cs index d0d71bfd9..445f8b3b4 100644 --- a/Ombi.UI/Models/SearchMovieViewModel.cs +++ b/Ombi.UI/Models/SearchMovieViewModel.cs @@ -47,5 +47,8 @@ public class SearchMovieViewModel : SearchViewModel public double VoteAverage { get; set; } public int VoteCount { get; set; } public bool AlreadyInCp { get; set; } + public string Trailer { get; set; } + public string Homepage { get; set; } + public string ImdbId { get; set; } } } \ No newline at end of file diff --git a/Ombi.UI/Models/SearchTvShowViewModel.cs b/Ombi.UI/Models/SearchTvShowViewModel.cs index faa6333ba..29380a835 100644 --- a/Ombi.UI/Models/SearchTvShowViewModel.cs +++ b/Ombi.UI/Models/SearchTvShowViewModel.cs @@ -58,5 +58,20 @@ public SearchTvShowViewModel() public bool DisableTvRequestsByEpisode { get; set; } public bool DisableTvRequestsBySeason { get; set; } public bool EnableTvRequestsForOnlySeries { get; set; } + + /// + /// This is used from the Trakt API + /// + /// + /// The trailer. + /// + public string Trailer { get; set; } + /// + /// This is used from the Trakt API + /// + /// + /// The trailer. + /// + public string Homepage { get; set; } } } \ No newline at end of file diff --git a/Ombi.UI/Modules/Admin/AdminModule.cs b/Ombi.UI/Modules/Admin/AdminModule.cs index ea320ff14..97a610441 100644 --- a/Ombi.UI/Modules/Admin/AdminModule.cs +++ b/Ombi.UI/Modules/Admin/AdminModule.cs @@ -93,6 +93,10 @@ public class AdminModule : BaseModule private IAnalytics Analytics { get; } private IRecentlyAdded RecentlyAdded { get; } private ISettingsService NotifySettings { get; } + private ISettingsService DiscordSettings { get; } + private IDiscordApi DiscordApi { get; } + private ISettingsService RadarrSettings { get; } + private IRadarrApi RadarrApi { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); public AdminModule(ISettingsService prService, @@ -118,7 +122,9 @@ public AdminModule(ISettingsService prService, ISlackApi slackApi, ISettingsService lp, ISettingsService scheduler, IJobRecord rec, IAnalytics analytics, ISettingsService notifyService, IRecentlyAdded recentlyAdded, - ISettingsService watcherSettings + ISettingsService watcherSettings , + ISettingsService discord, + IDiscordApi discordapi, ISettingsService settings, IRadarrApi radarrApi , ISecurityExtensions security) : base("admin", prService, security) { PrService = prService; @@ -150,6 +156,10 @@ ISettingsService watcherSettings NotifySettings = notifyService; RecentlyAdded = recentlyAdded; WatcherSettings = watcherSettings; + DiscordSettings = discord; + DiscordApi = discordapi; + RadarrSettings = settings; + RadarrApi = radarrApi; Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); @@ -165,18 +175,19 @@ ISettingsService watcherSettings Get["/getusers"] = _ => GetUsers(); Get["/couchpotato"] = _ => CouchPotato(); - Post["/couchpotato"] = _ => SaveCouchPotato(); + Post["/couchpotato", true] = async (x, ct) => await SaveCouchPotato(); Get["/plex"] = _ => Plex(); Post["/plex", true] = async (x, ct) => await SavePlex(); Get["/sonarr"] = _ => Sonarr(); Post["/sonarr"] = _ => SaveSonarr(); + Post["/sonarrprofiles"] = _ => GetSonarrQualityProfiles(); + Get["/sickrage"] = _ => Sickrage(); Post["/sickrage"] = _ => SaveSickrage(); - Post["/sonarrprofiles"] = _ => GetSonarrQualityProfiles(); Post["/cpprofiles", true] = async (x, ct) => await GetCpProfiles(); Post["/cpapikey"] = x => GetCpApiKey(); @@ -200,18 +211,21 @@ ISettingsService watcherSettings Get["/headphones"] = _ => Headphones(); Post["/headphones"] = _ => SaveHeadphones(); - Get["/newsletter"] = _ => Newsletter(); - Post["/newsletter"] = _ => SaveNewsletter(); + Get["/newsletter", true] = async (x, ct) => await Newsletter(); + Post["/newsletter", true] = async (x, ct) => await SaveNewsletter(); Post["/createapikey"] = x => CreateApiKey(); Post["/testslacknotification", true] = async (x, ct) => await TestSlackNotification(); - Get["/slacknotification"] = _ => SlackNotifications(); Post["/slacknotification"] = _ => SaveSlackNotifications(); + Post["/testdiscordnotification", true] = async (x, ct) => await TestDiscordNotification(); + Get["/discordnotification", true] = async (x, ct) => await DiscordNotification(); + Post["/discordnotification", true] = async (x, ct) => await SaveDiscordNotifications(); + Get["/landingpage", true] = async (x, ct) => await LandingPage(); Post["/landingpage", true] = async (x, ct) => await SaveLandingPage(); @@ -368,7 +382,7 @@ private Negotiator CouchPotato() return View["CouchPotato", settings]; } - private Response SaveCouchPotato() + private async Task SaveCouchPotato() { var couchPotatoSettings = this.Bind(); var valid = this.Validate(couchPotatoSettings); @@ -377,7 +391,7 @@ private Response SaveCouchPotato() return Response.AsJson(valid.SendJsonError()); } - var watcherSettings = WatcherSettings.GetSettings(); + var watcherSettings = await WatcherSettings.GetSettingsAsync(); if (watcherSettings.Enabled) { @@ -389,8 +403,20 @@ private Response SaveCouchPotato() }); } + var radarrSettings = await RadarrSettings.GetSettingsAsync(); + + if (radarrSettings.Enabled) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Cannot have Radarr and CouchPotato both enabled." + }); + } + couchPotatoSettings.ApiKey = couchPotatoSettings.ApiKey.Trim(); - var result = CpService.SaveSettings(couchPotatoSettings); + var result = await CpService.SaveSettingsAsync(couchPotatoSettings); return Response.AsJson(result ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for CouchPotato!" } : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); @@ -448,6 +474,7 @@ private Response SaveSonarr() { return Response.AsJson(new JsonResponseModel { Result = false, Message = "SickRage is enabled, we cannot enable Sonarr and SickRage" }); } + sonarrSettings.ApiKey = sonarrSettings.ApiKey.Trim(); var result = SonarrService.SaveSettings(sonarrSettings); @@ -456,6 +483,10 @@ private Response SaveSonarr() : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } + + + + private Negotiator Sickrage() { var settings = SickRageService.GetSettings(); @@ -814,13 +845,13 @@ private Response SaveHeadphones() : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } - private Negotiator Newsletter() + private async Task Newsletter() { - var settings = NewsLetterService.GetSettings(); + var settings = await NewsLetterService.GetSettingsAsync(); return View["NewsletterSettings", settings]; } - private Response SaveNewsletter() + private async Task SaveNewsletter() { var settings = this.Bind(); @@ -828,9 +859,17 @@ private Response SaveNewsletter() if (!valid.IsValid) { var error = valid.SendJsonError(); - Log.Info("Error validating Headphones settings, message: {0}", error.Message); + Log.Info("Error validating Newsletter settings, message: {0}", error.Message); return Response.AsJson(error); } + + // Make sure emails are setup + var emailSettings = await EmailService.GetSettingsAsync(); + if (!emailSettings.Enabled) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Please enable your email notifications" }); + } + settings.SendRecentlyAddedEmail = settings.SendRecentlyAddedEmail; var result = NewsLetterService.SaveSettings(settings); @@ -918,6 +957,71 @@ private Response SaveSlackNotifications() : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } + private async Task DiscordNotification() + { + var settings = await DiscordSettings.GetSettingsAsync(); + return View["DiscordNotification", settings]; + } + + private async Task TestDiscordNotification() + { + var settings = this.BindAndValidate(); + if (!ModelValidationResult.IsValid) + { + return Response.AsJson(ModelValidationResult.SendJsonError()); + } + var notificationModel = new NotificationModel + { + NotificationType = NotificationType.Test, + DateTime = DateTime.Now + }; + + var currentDicordSettings = await DiscordSettings.GetSettingsAsync(); + try + { + NotificationService.Subscribe(new DiscordNotification(DiscordApi, DiscordSettings)); + settings.Enabled = true; + await NotificationService.Publish(notificationModel, settings); + Log.Info("Sent Discord notification test"); + } + catch (Exception e) + { + Log.Error(e, "Failed to subscribe and publish test Discord Notification"); + } + finally + { + if (!currentDicordSettings.Enabled) + { + NotificationService.UnSubscribe(new DiscordNotification(DiscordApi, DiscordSettings)); + } + } + return Response.AsJson(new JsonResponseModel { Result = true, Message = "Successfully sent a test Discord Notification! If you do not receive it please check the logs." }); + } + + private async Task SaveDiscordNotifications() + { + var settings = this.BindAndValidate(); + if (!ModelValidationResult.IsValid) + { + return Response.AsJson(ModelValidationResult.SendJsonError()); + } + + var result = await DiscordSettings.SaveSettingsAsync(settings); + if (settings.Enabled) + { + NotificationService.Subscribe(new DiscordNotification(DiscordApi, DiscordSettings)); + } + else + { + NotificationService.UnSubscribe(new DiscordNotification(DiscordApi, DiscordSettings)); + } + + Log.Info("Saved discord settings, result: {0}", result); + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Discord Notifications!" } + : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); + } + private async Task LandingPage() { var settings = await LandingSettings.GetSettingsAsync(); diff --git a/Ombi.UI/Modules/Admin/FaultQueueModule.cs b/Ombi.UI/Modules/Admin/FaultQueueModule.cs index ab8ddc927..aacd5b992 100644 --- a/Ombi.UI/Modules/Admin/FaultQueueModule.cs +++ b/Ombi.UI/Modules/Admin/FaultQueueModule.cs @@ -64,7 +64,7 @@ private Negotiator Index() Id = r.Id, PrimaryIdentifier = r.PrimaryIdentifier, LastRetry = r.LastRetry, - Message = r.Message + Message = r.Description }).ToList(); return View["RequestFaultQueue", model]; diff --git a/Ombi.UI/Modules/Admin/IntegrationModule.cs b/Ombi.UI/Modules/Admin/IntegrationModule.cs index 9cc718e40..36ec5fcc9 100644 --- a/Ombi.UI/Modules/Admin/IntegrationModule.cs +++ b/Ombi.UI/Modules/Admin/IntegrationModule.cs @@ -36,6 +36,7 @@ using Nancy.ModelBinding; using Nancy.Responses.Negotiation; using Nancy.Validation; +using Ombi.Api.Interfaces; using Ombi.Core; using Ombi.Core.SettingModels; using Ombi.Core.StatusChecker; @@ -52,22 +53,40 @@ 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) + ISettingsService cp,ISecurityExtensions security, IAnalytics a, ISettingsService radarrSettings, + ICacheProvider cache, IRadarrApi radarrApi, ISonarrApi sonarrApi) : base("admin", settingsService, security) { WatcherSettings = watcher; Analytics = a; CpSettings = cp; + Cache = cache; + RadarrApi = radarrApi; + RadarrSettings = radarrSettings; + SonarrApi = sonarrApi; Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); + + Post["/sonarrrootfolders"] = _ => GetSonarrRootFolders(); + Get["/watcher", true] = async (x, ct) => await Watcher(); Post["/watcher", true] = async (x, ct) => await SaveWatcher(); + + Get["/radarr", true] = async (x, ct) => await Radarr(); + Post["/radarr", true] = async (x, ct) => await SaveRadarr(); + + + Post["/radarrprofiles"] = _ => GetRadarrQualityProfiles(); } private ISettingsService WatcherSettings { get; } private ISettingsService CpSettings { get; } + private ISettingsService RadarrSettings { get; } + private IRadarrApi RadarrApi { get; } + private ICacheProvider Cache { get; } private IAnalytics Analytics { get; } + private ISonarrApi SonarrApi { get; } private async Task Watcher() { @@ -97,6 +116,18 @@ private async Task SaveWatcher() }); } + var radarrSettings = await RadarrSettings.GetSettingsAsync(); + + if (radarrSettings.Enabled) + { + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = "Cannot have Radarr and CouchPotato both enabled." + }); + } + settings.ApiKey = settings.ApiKey.Trim(); var result = await WatcherSettings.SaveSettingsAsync(settings); return Response.AsJson(result @@ -104,5 +135,72 @@ private async Task SaveWatcher() : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } + private async Task Radarr() + { + var settings = await RadarrSettings.GetSettingsAsync(); + + return View["Radarr", settings]; + } + + private async Task SaveRadarr() + { + var radarrSettings = this.Bind(); + + //Check Watcher and CP make sure they are not enabled + var watcher = await WatcherSettings.GetSettingsAsync(); + if (watcher.Enabled) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "Watcher is enabled, we cannot enable Watcher and Radarr" }); + } + + var cp = await CpSettings.GetSettingsAsync(); + if (cp.Enabled) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "CouchPotato is enabled, we cannot enable Watcher and CouchPotato" }); + } + + var valid = this.Validate(radarrSettings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + + radarrSettings.ApiKey = radarrSettings.ApiKey.Trim(); + var result = await RadarrSettings.SaveSettingsAsync(radarrSettings); + + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Radarr!" } + : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); + } + + private Response GetRadarrQualityProfiles() + { + var settings = this.Bind(); + var profiles = RadarrApi.GetProfiles(settings.ApiKey, settings.FullUri); + + // set the cache + if (profiles != null) + { + Cache.Set(CacheKeys.RadarrQualityProfiles, profiles); + } + + return Response.AsJson(profiles); + } + + private Response GetSonarrRootFolders() + { + var settings = this.Bind(); + + var rootFolders = SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); + + // set the cache + if (rootFolders != null) + { + Cache.Set(CacheKeys.SonarrRootFolders, rootFolders); + } + + return Response.AsJson(rootFolders); + } + } } \ No newline at end of file diff --git a/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs b/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs new file mode 100644 index 000000000..a21ebd16a --- /dev/null +++ b/Ombi.UI/Modules/Admin/ScheduledJobsRunnerModule.cs @@ -0,0 +1,150 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: AboutModule.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 Nancy; +using Ombi.Core; +using Ombi.Core.SettingModels; +using Ombi.Helpers.Permissions; +using Ombi.Services.Interfaces; +using Ombi.Services.Jobs; +using Ombi.UI.Models; +using ISecurityExtensions = Ombi.Core.ISecurityExtensions; + +namespace Ombi.UI.Modules.Admin +{ + public class ScheduledJobsRunnerModule : BaseModule + { + public ScheduledJobsRunnerModule(ISettingsService settingsService, + ISecurityExtensions security, IPlexContentCacher contentCacher, ISonarrCacher sonarrCacher, IWatcherCacher watcherCacher, + IRadarrCacher radarrCacher, ICouchPotatoCacher cpCacher, IStoreBackup store, ISickRageCacher srCacher, IAvailabilityChecker plexChceker, + IStoreCleanup cleanup, IUserRequestLimitResetter requestLimit, IPlexEpisodeCacher episodeCacher, IRecentlyAdded recentlyAdded, + IFaultQueueHandler faultQueueHandler, IPlexUserChecker plexUserChecker) : base("admin", settingsService, security) + { + Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); + + PlexContentCacher = contentCacher; + SonarrCacher = sonarrCacher; + RadarrCacher = radarrCacher; + WatcherCacher = watcherCacher; + CpCacher = cpCacher; + StoreBackup = store; + SrCacher = srCacher; + AvailabilityChecker = plexChceker; + StoreCleanup = cleanup; + RequestLimit = requestLimit; + EpisodeCacher = episodeCacher; + RecentlyAdded = recentlyAdded; + FaultQueueHandler = faultQueueHandler; + PlexUserChecker = plexUserChecker; + + Post["/schedulerun", true] = async (x, ct) => await ScheduleRun((string)Request.Form.key); + } + + private IPlexContentCacher PlexContentCacher { get; } + private IRadarrCacher RadarrCacher { get; } + private ISonarrCacher SonarrCacher { get; } + private IWatcherCacher WatcherCacher { get; } + private ICouchPotatoCacher CpCacher { get; } + private IStoreBackup StoreBackup { get; } + private ISickRageCacher SrCacher { get; } + private IAvailabilityChecker AvailabilityChecker { get; } + private IStoreCleanup StoreCleanup { get; } + private IUserRequestLimitResetter RequestLimit { get; } + private IPlexEpisodeCacher EpisodeCacher { get; } + private IRecentlyAdded RecentlyAdded { get; } + private IFaultQueueHandler FaultQueueHandler { get; } + private IPlexUserChecker PlexUserChecker { get; } + + + private async Task ScheduleRun(string key) + { + if (key.Equals(JobNames.PlexCacher, StringComparison.CurrentCultureIgnoreCase)) + { + PlexContentCacher.CacheContent(); + } + + if (key.Equals(JobNames.WatcherCacher, StringComparison.CurrentCultureIgnoreCase)) + { + WatcherCacher.Queued(); + } + + if (key.Equals(JobNames.SonarrCacher, StringComparison.CurrentCultureIgnoreCase)) + { + SonarrCacher.Queued(); + } + if (key.Equals(JobNames.RadarrCacher, StringComparison.CurrentCultureIgnoreCase)) + { + RadarrCacher.Queued(); + } + if (key.Equals(JobNames.CpCacher, StringComparison.CurrentCultureIgnoreCase)) + { + CpCacher.Queued(); + } + if (key.Equals(JobNames.StoreBackup, StringComparison.CurrentCultureIgnoreCase)) + { + StoreBackup.Start(); + } + if (key.Equals(JobNames.SrCacher, StringComparison.CurrentCultureIgnoreCase)) + { + SrCacher.Queued(); + } + if (key.Equals(JobNames.PlexChecker, StringComparison.CurrentCultureIgnoreCase)) + { + AvailabilityChecker.Start(); + } + if (key.Equals(JobNames.StoreCleanup, StringComparison.CurrentCultureIgnoreCase)) + { + StoreCleanup.Start(); + } + if (key.Equals(JobNames.RequestLimitReset, StringComparison.CurrentCultureIgnoreCase)) + { + RequestLimit.Start(); + } + if (key.Equals(JobNames.EpisodeCacher, StringComparison.CurrentCultureIgnoreCase)) + { + EpisodeCacher.Start(); + } + if (key.Equals(JobNames.RecentlyAddedEmail, StringComparison.CurrentCultureIgnoreCase)) + { + RecentlyAdded.Start(); + } + if (key.Equals(JobNames.FaultQueueHandler, StringComparison.CurrentCultureIgnoreCase)) + { + FaultQueueHandler.Start(); + } + if (key.Equals(JobNames.PlexUserChecker, StringComparison.CurrentCultureIgnoreCase)) + { + RequestLimit.Start(); + } + + + return Response.AsJson(new JsonResponseModel { Result = true }); + } + } +} \ No newline at end of file diff --git a/Ombi.UI/Modules/ApplicationTesterModule.cs b/Ombi.UI/Modules/ApplicationTesterModule.cs index b0d4f2d17..a7608f547 100644 --- a/Ombi.UI/Modules/ApplicationTesterModule.cs +++ b/Ombi.UI/Modules/ApplicationTesterModule.cs @@ -46,7 +46,7 @@ public class ApplicationTesterModule : BaseAuthModule public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi, ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService pr, ISecurityExtensions security, - IWatcherApi watcherApi) : base("test", pr, security) + IWatcherApi watcherApi, IRadarrApi radarrApi) : base("test", pr, security) { this.RequiresAuthentication(); @@ -56,9 +56,11 @@ public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPle SickRageApi = srApi; HeadphonesApi = hpApi; WatcherApi = watcherApi; + RadarrApi = radarrApi; Post["/cp"] = _ => CouchPotatoTest(); Post["/sonarr"] = _ => SonarrTest(); + Post["/radarr"] = _ => RadarrTest(); Post["/plex"] = _ => PlexTest(); Post["/sickrage"] = _ => SickRageTest(); Post["/headphones"] = _ => HeadphonesTest(); @@ -73,6 +75,7 @@ public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPle private ISickRageApi SickRageApi { get; } private IHeadphonesApi HeadphonesApi { get; } private IWatcherApi WatcherApi { get; } + private IRadarrApi RadarrApi { get; } private Response CouchPotatoTest() { @@ -148,7 +151,7 @@ private Response SonarrTest() : Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Sonarr, 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 Sonarr's status: "); Log.Warn(e); @@ -161,6 +164,35 @@ private Response SonarrTest() } } + private Response RadarrTest() + { + var radarrSettings = this.Bind(); + var valid = this.Validate(radarrSettings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + try + { + var status = RadarrApi.SystemStatus(radarrSettings.ApiKey, radarrSettings.FullUri); + return status?.version != null + ? Response.AsJson(new JsonResponseModel { Result = true, Message = "Connected to Radarr successfully!" }) + : Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not connect to Radarr, please check your settings." }); + + } + 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 Radarr's status: "); + Log.Warn(e); + var message = $"Could not connect to Radarr, please check your settings. Exception Message: {e.Message}"; + if (e.InnerException != null) + { + message = $"Could not connect to Radarr, please check your settings. Exception Message: {e.InnerException.Message}"; + } + return Response.AsJson(new JsonResponseModel { Result = false, Message = message }); + } + } + private Response PlexTest() { var plexSettings = this.Bind(); diff --git a/Ombi.UI/Modules/ApprovalModule.cs b/Ombi.UI/Modules/ApprovalModule.cs index 9781b79a9..667b3e843 100644 --- a/Ombi.UI/Modules/ApprovalModule.cs +++ b/Ombi.UI/Modules/ApprovalModule.cs @@ -50,7 +50,7 @@ public class ApprovalModule : BaseAuthModule public ApprovalModule(IRequestService service, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr, ITransientFaultQueue faultQueue - , ISecurityExtensions security, IMovieSender movieSender) : base("approval", pr, security) + , ISecurityExtensions security, IMovieSender movieSender, ICacheProvider cache) : base("approval", pr, security) { Before += (ctx) => Security.AdminLoginRedirect(ctx, Permissions.Administrator,Permissions.ManageRequests); @@ -64,6 +64,7 @@ public ApprovalModule(IRequestService service, ISonarrApi sonarrApi, HeadphoneApi = hpApi; FaultQueue = faultQueue; MovieSender = movieSender; + Cache = cache; 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); @@ -86,6 +87,7 @@ public ApprovalModule(IRequestService service, ISonarrApi sonarrApi, private ISickRageApi SickRageApi { get; } private IHeadphonesApi HeadphoneApi { get; } private ITransientFaultQueue FaultQueue { get; } + private ICacheProvider Cache { get; } /// /// Approves the specified request identifier. @@ -120,7 +122,7 @@ private async Task Approve(int requestId, string qualityId) private async Task RequestTvAndUpdateStatus(RequestedModel request, string qualityId) { - var sender = new TvSenderOld(SonarrApi, SickRageApi); // TODO put back + var sender = new TvSenderOld(SonarrApi, SickRageApi, Cache); // TODO put back var sonarrSettings = await SonarrSettings.GetSettingsAsync(); if (sonarrSettings.Enabled) @@ -435,7 +437,7 @@ private async Task UpdateRequestsAsync(RequestedModel[] requestedModel } if (r.Type == RequestType.TvShow) { - var sender = new TvSenderOld(SonarrApi, SickRageApi); // TODO put back + var sender = new TvSenderOld(SonarrApi, SickRageApi, Cache); // TODO put back var sr = await SickRageSettings.GetSettingsAsync(); var sonarr = await SonarrSettings.GetSettingsAsync(); if (sr.Enabled) diff --git a/Ombi.UI/Modules/BaseModule.cs b/Ombi.UI/Modules/BaseModule.cs index 59904dcb6..f5f8010be 100644 --- a/Ombi.UI/Modules/BaseModule.cs +++ b/Ombi.UI/Modules/BaseModule.cs @@ -31,6 +31,7 @@ using System.Threading; using Nancy; using Nancy.Security; +using NLog; using Ombi.Core; using Ombi.Core.SettingModels; using Ombi.Helpers; @@ -100,6 +101,7 @@ protected int DateTimeOffset } + private static Logger Log = LogManager.GetCurrentClassLogger(); private string _username; /// /// Returns the Username or UserAlias @@ -112,15 +114,16 @@ protected string Username { try { - var username = Security.GetUsername(User.UserName, Session); + var username = Security.GetUsername(User?.UserName, Session); if (string.IsNullOrEmpty(username)) { - return Session[SessionKeys.UsernameKey].ToString(); + return string.Empty; } _username = username; } - catch (Exception) + catch (Exception e) { + Log.Info(e); return string.Empty; } } @@ -149,9 +152,10 @@ protected bool IsAdmin protected bool LoggedIn => Context?.CurrentUser != null; - protected string Culture { get; set; } + private string Culture { get; set; } protected const string CultureCookieName = "_culture"; - protected Response SetCookie() + + private Response SetCookie() { try { diff --git a/Ombi.UI/Modules/RequestsModule.cs b/Ombi.UI/Modules/RequestsModule.cs index f0c0934ac..699b19b67 100644 --- a/Ombi.UI/Modules/RequestsModule.cs +++ b/Ombi.UI/Modules/RequestsModule.cs @@ -96,6 +96,8 @@ public RequestsModule( Post["/changeavailability", true] = async (x, ct) => await ChangeRequestAvailability((int)Request.Form.Id, (bool)Request.Form.Available); + Post["/changeRootFolder", true] = async (x, ct) => await ChangeRootFolder((int) Request.Form.requestId, (int) Request.Form.rootFolderId); + Get["/UpdateFilters", true] = async (x, ct) => await GetFilterAndSortSettings(); } @@ -160,7 +162,7 @@ private async Task GetMovies() } } - + var canManageRequest = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests); var viewModel = dbMovies.Select(movie => new RequestViewModel { @@ -185,7 +187,7 @@ private async Task GetMovies() IssueId = movie.IssueId, Denied = movie.Denied, DeniedReason = movie.DeniedReason, - Qualities = qualities.ToArray() + Qualities = qualities.ToArray(), }).ToList(); return Response.AsJson(viewModel); @@ -193,32 +195,39 @@ private async Task GetMovies() private async Task GetTvShows() { - var settingsTask = PrSettings.GetSettingsAsync(); - var requests = await Service.GetAllAsync(); requests = requests.Where(x => x.Type == RequestType.TvShow); var dbTv = requests; - var settings = await settingsTask; if (Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests) && !IsAdmin) { dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList(); } IEnumerable qualities = new List(); + IEnumerable rootFolders = new List(); + + var sonarrSettings = await SonarrSettings.GetSettingsAsync(); if (IsAdmin) { try { - var sonarrSettings = await SonarrSettings.GetSettingsAsync(); if (sonarrSettings.Enabled) { - var result = Cache.GetOrSetAsync(CacheKeys.SonarrQualityProfiles, async () => + var result = await Cache.GetOrSetAsync(CacheKeys.SonarrQualityProfiles, async () => { return await Task.Run(() => SonarrApi.GetProfiles(sonarrSettings.ApiKey, sonarrSettings.FullUri)); }); - qualities = result.Result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList(); - } + qualities = result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList(); + + + var rootFoldersResult =await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () => + { + return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri)); + }); + + rootFolders = rootFoldersResult.Select(x => new RootFolderModel { Id = x.id.ToString(), Path = x.path, FreeSpace = x.freespace}).ToList(); + } else { var sickRageSettings = await SickRageSettings.GetSettingsAsync(); @@ -235,6 +244,8 @@ private async Task GetTvShows() } + + var canManageRequest = Security.HasAnyPermissions(User, Permissions.Administrator, Permissions.ManageRequests); var viewModel = dbTv.Select(tv => new RequestViewModel { @@ -243,7 +254,7 @@ private async Task GetTvShows() Status = tv.Status, ImdbId = tv.ImdbId, Id = tv.Id, - PosterPath = tv.PosterPath, + PosterPath = tv.PosterPath.Contains("http:") ? tv.PosterPath.Replace("http:", "https:") : tv.PosterPath, // We make the poster path https on request, but this is just incase ReleaseDate = tv.ReleaseDate, ReleaseDateTicks = tv.ReleaseDate.Ticks, RequestedDate = tv.RequestedDate, @@ -262,11 +273,30 @@ private async Task GetTvShows() TvSeriesRequestType = tv.SeasonsRequested, Qualities = qualities.ToArray(), Episodes = tv.Episodes.ToArray(), + RootFolders = rootFolders.ToArray(), + HasRootFolders = rootFolders.Any(), + CurrentRootPath = sonarrSettings.Enabled ? GetRootPath(tv.RootFolderSelected, sonarrSettings).Result : null }).ToList(); return Response.AsJson(viewModel); } + private async Task GetRootPath(int pathId, SonarrSettings sonarrSettings) + { + var rootFoldersResult = await Cache.GetOrSetAsync(CacheKeys.SonarrRootFolders, async () => + { + return await Task.Run(() => SonarrApi.GetRootFolders(sonarrSettings.ApiKey, sonarrSettings.FullUri)); + }); + + foreach (var r in rootFoldersResult.Where(r => r.id == pathId)) + { + return r.path; + } + + // Return default path + return rootFoldersResult.FirstOrDefault(x => x.id.Equals(int.Parse(sonarrSettings.RootPath)))?.path ?? string.Empty; + } + private async Task GetAlbumRequests() { var settings = PrSettings.GetSettings(); @@ -430,5 +460,34 @@ private async Task GetFilterAndSortSettings() return Response.AsJson(vm); } - } + + private async Task ChangeRootFolder(int id, int rootFolderId) + { + // Get all root folders + var settings = await SonarrSettings.GetSettingsAsync(); + var rootFolders = SonarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); + + // Get Request + var allRequests = await Service.GetAllAsync(); + var request = allRequests.FirstOrDefault(x => x.Id == id); + + if (request == null) + { + return Response.AsJson(new JsonResponseModel {Result = false}); + } + + foreach (var folder in rootFolders) + { + if (folder.id.Equals(rootFolderId)) + { + request.RootFolderSelected = folder.id; + break; + } + } + + await Service.UpdateRequestAsync(request); + + return Response.AsJson(new JsonResponseModel {Result = true}); + } + } } diff --git a/Ombi.UI/Modules/SearchExtensionModule.cs b/Ombi.UI/Modules/SearchExtensionModule.cs new file mode 100644 index 000000000..4c0ee4a6f --- /dev/null +++ b/Ombi.UI/Modules/SearchExtensionModule.cs @@ -0,0 +1,62 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: SearchExtensionModule.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.Threading.Tasks; +using Nancy; +using Ombi.Api.Interfaces; +using Ombi.Core; +using Ombi.Core.SettingModels; + +namespace Ombi.UI.Modules +{ + public class SearchExtensionModule : BaseAuthModule + { + public SearchExtensionModule(ISettingsService pr, ISecurityExtensions security, INetflixApi netflix) : base("searchextension",pr, security) + { + NetflixApi = netflix; + + Get["/netflix/{searchTerm}", true] = async (x, ctx) => await Netflix(x.searchTerm); + } + + private INetflixApi NetflixApi { get; } + + + public async Task Netflix(string title) + { + var result = NetflixApi.CheckNetflix(title); + + if (!string.IsNullOrEmpty(result.Message)) + { + return Response.AsJson(new { Result = false }); + } + + return Response.AsJson(new { Result = true, NetflixId = result.ShowId }); + } + + + } +} \ No newline at end of file diff --git a/Ombi.UI/Modules/SearchModule.cs b/Ombi.UI/Modules/SearchModule.cs index 8759d0325..4aa29c316 100644 --- a/Ombi.UI/Modules/SearchModule.cs +++ b/Ombi.UI/Modules/SearchModule.cs @@ -58,6 +58,7 @@ using Ombi.UI.Helpers; using Ombi.UI.Models; using TMDbLib.Objects.General; +using TraktApiSharp.Objects.Get.Shows; using Action = Ombi.Helpers.Analytics.Action; using EpisodesModel = Ombi.Store.EpisodesModel; using ISecurityExtensions = Ombi.Core.ISecurityExtensions; @@ -76,7 +77,7 @@ public SearchModule(ICacheProvider cache, ISettingsService plexService, ISettingsService auth, IRepository u, ISettingsService email, IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, - ISecurityExtensions security, IMovieSender movieSender) + ISecurityExtensions security, IMovieSender movieSender, IRadarrCacher radarrCacher, ITraktApi traktApi, ISettingsService cus) : base("search", prSettings, security) { Auth = auth; @@ -108,6 +109,9 @@ public SearchModule(ICacheProvider cache, PlexContentRepository = content; MovieSender = movieSender; WatcherCacher = watcherCacher; + RadarrCacher = radarrCacher; + TraktApi = traktApi; + CustomizationSettings = cus; Get["SearchIndex", "/", true] = async (x, ct) => await RequestLoad(); @@ -119,6 +123,13 @@ public SearchModule(ICacheProvider cache, Get["movie/upcoming", true] = async (x, ct) => await UpcomingMovies(); Get["movie/playing", true] = async (x, ct) => await CurrentlyPlayingMovies(); + Get["tv/popular", true] = async (x, ct) => await ProcessShows(ShowSearchType.Popular); + Get["tv/trending", true] = async (x, ct) => await ProcessShows(ShowSearchType.Trending); + Get["tv/mostwatched", true] = async (x, ct) => await ProcessShows(ShowSearchType.MostWatched); + Get["tv/anticipated", true] = async (x, ct) => await ProcessShows(ShowSearchType.Anticipated); + + Get["tv/poster/{id}"] = p => GetTvPoster((int)p.id); + Post["request/movie", true] = async (x, ct) => await RequestMovie((int)Request.Form.movieId); Post["request/tv", true] = async (x, ct) => await RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); @@ -128,6 +139,7 @@ public SearchModule(ICacheProvider cache, Get["/seasons"] = x => GetSeasons(); Get["/episodes", true] = async (x, ct) => await GetEpisodes(); } + private ITraktApi TraktApi { get; } private IWatcherCacher WatcherCacher { get; } private IMovieSender MovieSender { get; } private IRepository PlexContentRepository { get; } @@ -157,14 +169,23 @@ public SearchModule(ICacheProvider cache, private IAnalytics Analytics { get; } private ITransientFaultQueue FaultQueue { get; } private IRepository RequestLimitRepo { get; } + private IRadarrCacher RadarrCacher { get; } + private ISettingsService CustomizationSettings { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); private async Task RequestLoad() { var settings = await PrService.GetSettingsAsync(); + var custom = await CustomizationSettings.GetSettingsAsync(); + var searchViewModel = new SearchLoadViewModel + { + Settings = settings, + CustomizationSettings = custom + }; + - return View["Search/Index", settings]; + return View["Search/Index", searchViewModel]; } private async Task UpcomingMovies() @@ -188,6 +209,17 @@ private async Task SearchMovie(string searchTerm) return await ProcessMovies(MovieSearchType.Search, searchTerm); } + private Response GetTvPoster(int theTvDbId) + { + var result = TvApi.ShowLookupByTheTvDbId(theTvDbId); + + var banner = result.image?.medium; + if (!string.IsNullOrEmpty(banner)) + { + banner = banner.Replace("http", "https"); // Always use the Https banners + } + return banner; + } private async Task ProcessMovies(MovieSearchType searchType, string searchTerm) { List apiMovies; @@ -236,22 +268,13 @@ private async Task ProcessMovies(MovieSearchType searchType, string se var cpCached = CpCacher.QueuedIds(); var watcherCached = WatcherCacher.QueuedIds(); + var radarrCached = RadarrCacher.QueuedIds(); var content = PlexContentRepository.GetAll(); var plexMovies = Checker.GetPlexMovies(content); var viewMovies = new List(); var counter = 0; foreach (var movie in apiMovies) { - var imdbId = string.Empty; - if (counter <= 5) // Let's only do it for the first 5 items - { - var movieInfoTask = await MovieApi.GetMovieInformation(movie.Id).ConfigureAwait(false); - // TODO needs to be careful about this, it's adding extra time to search... - // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 - imdbId = movieInfoTask?.ImdbId; - counter++; - } - var viewMovie = new SearchMovieViewModel { Adult = movie.Adult, @@ -269,6 +292,28 @@ private async Task ProcessMovies(MovieSearchType searchType, string se VoteAverage = movie.VoteAverage, VoteCount = movie.VoteCount }; + + var imdbId = string.Empty; + if (counter <= 5) // Let's only do it for the first 5 items + { + var movieInfo = MovieApi.GetMovieInformationWithVideos(movie.Id); + + // TODO needs to be careful about this, it's adding extra time to search... + // https://www.themoviedb.org/talk/5807f4cdc3a36812160041f2 + viewMovie.ImdbId = movieInfo?.imdb_id; + viewMovie.Homepage = movieInfo?.homepage; + var videoId = movieInfo?.video ?? false + ? movieInfo?.videos?.results?.FirstOrDefault()?.key + : string.Empty; + + viewMovie.Trailer = string.IsNullOrEmpty(videoId) + ? string.Empty + : $"https://www.youtube.com/watch?v={videoId}"; + + counter++; + } + + var canSee = CanUserSeeThisRequest(viewMovie.Id, Security.HasPermissions(User, Permissions.UsersCanViewOnlyOwnRequests), dbMovies); var plexMovie = Checker.GetMovie(plexMovies.ToArray(), movie.Title, movie.ReleaseDate?.Year.ToString(), imdbId); @@ -287,13 +332,19 @@ private async Task ProcessMovies(MovieSearchType searchType, string se } else if (cpCached.Contains(movie.Id) && canSee) // compare to the couchpotato db { + viewMovie.Approved = true; viewMovie.Requested = true; } else if(watcherCached.Contains(imdbId) && canSee) // compare to the watcher db { + viewMovie.Approved = true; + viewMovie.Requested = true; + } + else if (radarrCached.Contains(movie.Id) && canSee) + { + viewMovie.Approved = true; viewMovie.Requested = true; } - viewMovies.Add(viewMovie); } @@ -312,6 +363,186 @@ private bool CanUserSeeThisRequest(int movieId, bool usersCanViewOnlyOwnRequests return true; } + private async Task ProcessShows(ShowSearchType type) + { + var shows = new List(); + var prSettings = await PrService.GetSettingsAsync(); + switch (type) + { + case ShowSearchType.Popular: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Popular", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var popularShows = await TraktApi.GetPopularShows(); + + foreach (var popularShow in popularShows) + { + var theTvDbId = int.Parse(popularShow.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = popularShow.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = popularShow.Ids.Imdb, + Network = popularShow.Network, + Overview = popularShow.Overview.RemoveHtml(), + Rating = popularShow.Rating.ToString(), + Runtime = popularShow.Runtime.ToString(), + SeriesName = popularShow.Title, + Status = popularShow.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = popularShow.Trailer, + Homepage = popularShow.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Anticipated: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Anticipated", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var anticipated = await TraktApi.GetAnticipatedShows(); + foreach (var anticipatedShow in anticipated) + { + var show = anticipatedShow.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network ?? string.Empty, + Overview = show.Overview?.RemoveHtml() ?? string.Empty, + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status?.DisplayName ?? string.Empty, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.MostWatched: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "MostWatched", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var mostWatched = await TraktApi.GetMostWatchesShows(); + foreach (var watched in mostWatched) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + case ShowSearchType.Trending: + Analytics.TrackEventAsync(Category.Search, Action.TvShow, "Trending", Username, CookieHelper.GetAnalyticClientId(Cookies)); + var trending = await TraktApi.GetTrendingShows(); + foreach (var watched in trending) + { + var show = watched.Show; + var theTvDbId = int.Parse(show.Ids.Tvdb.ToString()); + var model = new SearchTvShowViewModel + { + FirstAired = show.FirstAired?.ToString("yyyy-MM-ddTHH:mm:ss"), + Id = theTvDbId, + ImdbId = show.Ids.Imdb, + Network = show.Network, + Overview = show.Overview.RemoveHtml(), + Rating = show.Rating.ToString(), + Runtime = show.Runtime.ToString(), + SeriesName = show.Title, + Status = show.Status.DisplayName, + DisableTvRequestsByEpisode = prSettings.DisableTvRequestsByEpisode, + DisableTvRequestsBySeason = prSettings.DisableTvRequestsBySeason, + EnableTvRequestsForOnlySeries = (prSettings.DisableTvRequestsByEpisode && prSettings.DisableTvRequestsBySeason), + Trailer = show.Trailer, + Homepage = show.Homepage + }; + shows.Add(model); + } + shows = await MapToTvModel(shows, prSettings); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + + + return Response.AsJson(shows); + } + + private async Task> MapToTvModel(List shows, PlexRequestSettings prSettings) + { + + var plexSettings = await PlexService.GetSettingsAsync(); + + var providerId = string.Empty; + // Get the requests + var allResults = await RequestService.GetAllAsync(); + allResults = allResults.Where(x => x.Type == RequestType.TvShow); + var distinctResults = allResults.DistinctBy(x => x.ProviderId); + var dbTv = distinctResults.ToDictionary(x => x.ProviderId); + + // Check the external applications + var sonarrCached = SonarrCacher.QueuedIds().ToList(); + var sickRageCache = SickRageCacher.QueuedIds(); // consider just merging sonarr/sickrage arrays + var content = PlexContentRepository.GetAll(); + var plexTvShows = Checker.GetPlexTvShows(content).ToList(); + + foreach (var show in shows) + { + if (plexSettings.AdvancedSearch) + { + providerId = show.Id.ToString(); + } + + var plexShow = Checker.GetTvShow(plexTvShows.ToArray(), show.SeriesName, show.FirstAired?.Substring(0, 4), + providerId); + if (plexShow != null) + { + show.Available = true; + show.PlexUrl = plexShow.Url; + } + else + { + if (dbTv.ContainsKey(show.Id)) + { + var dbt = dbTv[show.Id]; + + show.Requested = true; + show.Episodes = dbt.Episodes.ToList(); + show.Approved = dbt.Approved; + } + if (sonarrCached.Select(x => x.TvdbId).Contains(show.Id) || sickRageCache.Contains(show.Id)) + // compare to the sonarr/sickrage db + { + show.Requested = true; + } + } + } + return shows; + } + private async Task SearchTvShow(string searchTerm) { @@ -345,6 +576,10 @@ private async Task SearchTvShow(string searchTerm) var viewTv = new List(); foreach (var t in apiTv) { + if (!(t.show.externals?.thetvdb.HasValue) ?? false) + { + continue; + } var banner = t.show.image?.medium; if (!string.IsNullOrEmpty(banner)) { @@ -574,7 +809,17 @@ private async Task RequestMovie(int movieId) if (result.Result) { return await AddRequest(model, settings, - $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + $"{fullMovieName} {Resources.UI.Search_SuccessfullyAdded}"); + } + if (result.Error) + + { + return + Response.AsJson(new JsonResponseModel + { + Message = "Could not add movie, please contract your administrator", + Result = false + }); } if (!result.MovieSendingEnabled) { @@ -679,11 +924,13 @@ private async Task RequestTvShow(int showId, string seasons) DateTime.TryParse(showInfo.premiered, out firstAir); string fullShowName = $"{showInfo.name} ({firstAir.Year})"; + // For some reason the poster path is always http + var posterPath = showInfo.image?.medium.Replace("http:", "https:"); var model = new RequestedModel { Type = RequestType.TvShow, Overview = showInfo.summary.RemoveHtml(), - PosterPath = showInfo.image?.medium, + PosterPath = posterPath, Title = showInfo.name, ReleaseDate = firstAir, Status = showInfo.status, @@ -762,6 +1009,11 @@ private async Task RequestTvShow(int showId, string seasons) existingRequest.Episodes.AddRange(newEpisodes ?? Enumerable.Empty()); // It's technically a new request now, so set the status to not approved. + var autoApprove = ShouldAutoApprove(RequestType.TvShow); + if (autoApprove) + { + return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); + } existingRequest.Approved = false; return await AddUserToRequest(existingRequest, settings, fullShowName, true); @@ -888,54 +1140,7 @@ await NotificationService.Publish(new NotificationModel { if (ShouldAutoApprove(RequestType.TvShow)) { - model.Approved = true; - var s = await sonarrSettings; - var sender = new TvSenderOld(SonarrApi, SickrageApi); // TODO put back - if (s.Enabled) - { - var result = await sender.SendToSonarr(s, model); - if (!string.IsNullOrEmpty(result?.title)) - { - if (existingRequest != null) - { - return await UpdateRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - return - await - AddRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - Log.Debug("Error with sending to sonarr."); - return - Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); - } - - var srSettings = SickRageService.GetSettings(); - if (srSettings.Enabled) - { - var result = sender.SendToSickRage(srSettings, model); - if (result?.result == "success") - { - return await AddRequest(model, settings, - $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - return - Response.AsJson(new JsonResponseModel - { - Result = false, - Message = result?.message ?? Resources.UI.Search_SickrageError - }); - } - - if (!srSettings.Enabled && !s.Enabled) - { - return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); - } - - return - Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); - + return await SendTv(model, sonarrSettings, existingRequest, fullShowName, settings); } return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); } @@ -1385,5 +1590,64 @@ public bool ShouldAutoApprove(RequestType requestType) return false; } } + + private enum ShowSearchType + { + Popular, + Anticipated, + MostWatched, + Trending + } + + private async Task SendTv(RequestedModel model, Task sonarrSettings, RequestedModel existingRequest, string fullShowName, PlexRequestSettings settings) + { + model.Approved = true; + var s = await sonarrSettings; + var sender = new TvSenderOld(SonarrApi, SickrageApi, Cache); // TODO put back + if (s.Enabled) + { + var result = await sender.SendToSonarr(s, model); + if (!string.IsNullOrEmpty(result?.title)) + { + if (existingRequest != null) + { + return await UpdateRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + await + AddRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + Log.Debug("Error with sending to sonarr."); + return + Response.AsJson(ValidationHelper.SendSonarrError(result?.ErrorMessages ?? new List())); + } + + var srSettings = SickRageService.GetSettings(); + if (srSettings.Enabled) + { + var result = sender.SendToSickRage(srSettings, model); + if (result?.result == "success") + { + return await AddRequest(model, settings, + $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + return + Response.AsJson(new JsonResponseModel + { + Result = false, + Message = result?.message ?? Resources.UI.Search_SickrageError + }); + } + + if (!srSettings.Enabled && !s.Enabled) + { + return await AddRequest(model, settings, $"{fullShowName} {Resources.UI.Search_SuccessfullyAdded}"); + } + + return + Response.AsJson(new JsonResponseModel { Result = false, Message = Resources.UI.Search_TvNotSetUp }); + } } } diff --git a/Ombi.UI/Modules/UserLoginModule.cs b/Ombi.UI/Modules/UserLoginModule.cs index 434892496..40fea430f 100644 --- a/Ombi.UI/Modules/UserLoginModule.cs +++ b/Ombi.UI/Modules/UserLoginModule.cs @@ -233,13 +233,28 @@ private async Task UsernameLogin() var result = await AuthenticationSetup(userId, username, dateTimeOffset, loginGuid, isOwner); + var landingSettings = await LandingPageSettings.GetSettingsAsync(); + + if (landingSettings.Enabled) + { + if (!landingSettings.BeforeLogin) // After Login + { + var uri = Linker.BuildRelativeUri(Context, "LandingPageIndex"); + if (loginGuid != Guid.Empty) + { + return CustomModuleExtensions.LoginAndRedirect(this, result.LoginGuid, null, uri.ToString()); + } + return Response.AsRedirect(uri.ToString()); + } + } + + var retVal = Linker.BuildRelativeUri(Context, "SearchIndex"); if (result.LoginGuid != Guid.Empty) { return CustomModuleExtensions.LoginAndRedirect(this, result.LoginGuid, null, retVal.ToString()); } return Response.AsJson(new { result = true, url = retVal.ToString() }); - } private async Task IsPlexUser(string username) @@ -318,6 +333,21 @@ private async Task PasswordLogin() var m = await AuthenticationSetup(userId, username, dateTimeOffset, loginGuid, isOwner); + var landingSettings = await LandingPageSettings.GetSettingsAsync(); + + if (landingSettings.Enabled) + { + if (!landingSettings.BeforeLogin) // After Login + { + var uri = Linker.BuildRelativeUri(Context, "LandingPageIndex"); + if (m.LoginGuid != Guid.Empty) + { + return CustomModuleExtensions.LoginAndRedirect(this, m.LoginGuid, null, uri.ToString()); + } + return Response.AsRedirect(uri.ToString()); + } + } + var retVal = Linker.BuildRelativeUri(Context, "SearchIndex"); if (m.LoginGuid != Guid.Empty) { @@ -534,7 +564,7 @@ private async Task AuthenticationSetup(string userId, string usernam UserLogins.Insert(new UserLogins { UserId = userId, Type = UserType.PlexUser, LastLoggedIn = DateTime.UtcNow }); Log.Debug("We are authenticated! Setting session."); // Add to the session (Used in the BaseModules) - Session[SessionKeys.UsernameKey] = (string)username; + Session[SessionKeys.UsernameKey] = username; Session[SessionKeys.ClientDateTimeOffsetKey] = dateTimeOffset; var plexLocal = plexLocalUsers.FirstOrDefault(x => x.Username == username); diff --git a/Ombi.UI/Modules/UserManagementModule.cs b/Ombi.UI/Modules/UserManagementModule.cs index 92d848606..af62f880e 100644 --- a/Ombi.UI/Modules/UserManagementModule.cs +++ b/Ombi.UI/Modules/UserManagementModule.cs @@ -420,7 +420,7 @@ private UserManagementUsersViewModel MapPlexUser(UserFriends plexInfo, PlexUsers FeaturesFormattedString = newUser ? "Processing..." : features.ToString(), Username = plexInfo.Title, Type = UserType.PlexUser, - EmailAddress = plexInfo.Email, + EmailAddress = string.IsNullOrEmpty(plexInfo.Email) ? dbUser.EmailAddress : plexInfo.Email, Alias = dbUser?.UserAlias ?? string.Empty, LastLoggedIn = lastLoggedIn, PlexInfo = new UserManagementPlexInformation diff --git a/Ombi.UI/NinjectModules/ApiModule.cs b/Ombi.UI/NinjectModules/ApiModule.cs index 4dd37a8db..1a45764c7 100644 --- a/Ombi.UI/NinjectModules/ApiModule.cs +++ b/Ombi.UI/NinjectModules/ApiModule.cs @@ -46,6 +46,10 @@ public override void Load() Bind().To(); Bind().To(); Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); + Bind().To(); } } } \ No newline at end of file diff --git a/Ombi.UI/NinjectModules/ServicesModule.cs b/Ombi.UI/NinjectModules/ServicesModule.cs index 7c5f9095a..edafddb03 100644 --- a/Ombi.UI/NinjectModules/ServicesModule.cs +++ b/Ombi.UI/NinjectModules/ServicesModule.cs @@ -48,9 +48,16 @@ public override void Load() Bind().To(); Bind().To(); Bind().To(); + Bind().To(); Bind().To(); Bind().To(); Bind().To(); + Bind().To(); + Bind().To(); + 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 38a6bdf40..24fa57f52 100644 --- a/Ombi.UI/Ombi.UI.csproj +++ b/Ombi.UI/Ombi.UI.csproj @@ -206,6 +206,10 @@ ..\packages\TMDbLib.0.9.0.0-alpha\lib\net45\TMDbLib.dll True + + ..\packages\TraktApiSharp.0.8.0\lib\portable-net45+netcore45+wpa81\TraktApiSharp.dll + True + @@ -246,13 +250,16 @@ + + + @@ -273,6 +280,7 @@ + @@ -287,6 +295,7 @@ + @@ -788,6 +797,12 @@ Always + + Always + + + Always + web.config diff --git a/Ombi.UI/Startup.cs b/Ombi.UI/Startup.cs index e366901c1..ff8590da4 100644 --- a/Ombi.UI/Startup.cs +++ b/Ombi.UI/Startup.cs @@ -124,6 +124,10 @@ private void SubscribeAllObservers(IResolutionRoot container) var slackService = container.Get>(); var slackSettings = slackService.GetSettings(); SubScribeOvserver(slackSettings, notificationService, new SlackNotification(container.Get(), slackService)); + + var discordSettings = container.Get>(); + var discordService = discordSettings.GetSettings(); + SubScribeOvserver(discordService, notificationService, new DiscordNotification(container.Get(), discordSettings)); } private void SubScribeOvserver(T settings, INotificationService notificationService, INotification notification) diff --git a/Ombi.UI/Validators/RadarrValidator.cs b/Ombi.UI/Validators/RadarrValidator.cs new file mode 100644 index 000000000..75550c787 --- /dev/null +++ b/Ombi.UI/Validators/RadarrValidator.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 RadarrValidator : AbstractValidator + { + public RadarrValidator() + { + RuleFor(request => request.ApiKey).NotEmpty().WithMessage("You must specify a Api Key."); + 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.QualityProfile).NotEmpty().WithMessage("You must specify a Quality Profile."); + } + } +} \ No newline at end of file diff --git a/Ombi.UI/Views/Admin/DiscordNotification.cshtml b/Ombi.UI/Views/Admin/DiscordNotification.cshtml new file mode 100644 index 000000000..008b96e01 --- /dev/null +++ b/Ombi.UI/Views/Admin/DiscordNotification.cshtml @@ -0,0 +1,99 @@ +@using Ombi.UI.Helpers +@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase +@Html.Partial("Shared/Partial/_Sidebar") + +
    +
    +
    + Discord Notifications + + @Html.Checkbox(Model.Enabled, "Enabled", "Enabled") + +
    + + This is the full webhook url. + Discord > Edit Channel > Webhooks > Create Webook > Copy the Webhook Url > Press Save +
    + +
    +
    + +
    + + You can override the default username +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/Ombi.UI/Views/Admin/EmailNotifications.cshtml b/Ombi.UI/Views/Admin/EmailNotifications.cshtml index 3dfee8f8b..f2d7aab82 100644 --- a/Ombi.UI/Views/Admin/EmailNotifications.cshtml +++ b/Ombi.UI/Views/Admin/EmailNotifications.cshtml @@ -31,20 +31,7 @@
    -
    -
    - @if (Model.EnableUserEmailNotifications) - { - - } - else - { - - } - -
    -
    @@ -59,7 +46,7 @@
    - Please note that if user notifications is enabled, the email will get sent with the SMTP set-up below. +
    @@ -82,12 +69,15 @@
    + + The sender is who the email will be sent from, this can be for any email including user notification emails (if that is enabled).
    + The recipient email is used for emails going to the administrator. diff --git a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml index 37ed9d27c..8b0d462ff 100644 --- a/Ombi.UI/Views/Admin/NewsletterSettings.cshtml +++ b/Ombi.UI/Views/Admin/NewsletterSettings.cshtml @@ -16,35 +16,20 @@
    @if (Model.SendRecentlyAddedEmail) { - + } else { - + } - -
    -
    - @if (Model.SendToPlexUsers) - { - - } - else - { - - } - -
    -

    -
    - + You can add multiple email address by using the ; delimiter
    diff --git a/Ombi.UI/Views/Admin/SchedulerSettings.cshtml b/Ombi.UI/Views/Admin/SchedulerSettings.cshtml index 0c0b0200a..d0ec15837 100644 --- a/Ombi.UI/Views/Admin/SchedulerSettings.cshtml +++ b/Ombi.UI/Views/Admin/SchedulerSettings.cshtml @@ -3,22 +3,32 @@
    + + + + + + + + + + + @foreach (var record in Model.JobRecorder) + { + + + + + + + } + +
    Job NameLast Run
    + @record.Key + + @record.Value.ToString("R") +
    -
    -
    Job Name -
    -
    Last Run -
    -
    -
    - @foreach (var record in Model.JobRecorder) - { -
    -
    @record.Key
    -
    @record.Value.ToString("R")
    -
    -
    - }

    @@ -45,6 +55,14 @@
    +
    + + +
    +
    + + +
    Please note, the minimum time for this to run is 11 hours, if set below 11 then we will ignore that value. This is a very resource intensive job, the less we run it the better.
    @@ -114,6 +132,34 @@ $obj.text(newDate); }); + + $('.refresh').click(function(e) { + var id = e.currentTarget.id; + + var ev = $(e.currentTarget.children[0]); + ev.addClass("fa-spin"); + + var url = createLocalUrl("/admin/schedulerun"); + $.ajax({ + type: 'POST', + data: {key:id}, + url: url, + dataType: "json", + success: function (response) { + if (response.result === true) { + generateNotify("Success!", "success"); + } else { + generateNotify(response.message, "warning"); + } + }, + error: function (e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + + }); + $('#save') .click(function (e) { e.preventDefault(); diff --git a/Ombi.UI/Views/Admin/Sonarr.cshtml b/Ombi.UI/Views/Admin/Sonarr.cshtml index f2b4ca49a..28e2a9140 100644 --- a/Ombi.UI/Views/Admin/Sonarr.cshtml +++ b/Ombi.UI/Views/Admin/Sonarr.cshtml @@ -15,16 +15,18 @@
    Sonarr Settings + +
    - @if (Model.Enabled) - { - - } - else - { - - } + @if (Model.Enabled) + { + + } + else + { + + }
    @@ -51,14 +53,14 @@
    - @if (Model.Ssl) - { - - } - else - { - - } + @if (Model.Ssl) + { + + } + else + { + + }
    @@ -69,7 +71,7 @@
    - +
    @@ -80,32 +82,47 @@
    -
    - - + + +
    + +
    +
    + +
    + +
    + @*
    + +
    + + +
    +
    *@ +
    - @if (Model.SeasonFolders) - { - - } - else - { - - } - + @if (Model.SeasonFolders) + { + + } + else + { + + } +
    - +
    @@ -161,6 +178,40 @@ } + @if (!string.IsNullOrEmpty(Model.RootPath)) + { + + + console.log('Hit root folders..'); + + var rootFolderSelected = '@Model.RootPath'; + if (!rootFolderSelected) { + return; + } + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: "sonarrrootfolders", + dataType: "json", + success: function(response) { + response.forEach(function(result) { + $('#selectedRootFolder').html(""); + if (result.id == rootFolderSelected) { + $("#selectRootFolder").append(""); + } else { + $("#selectRootFolder").append(""); + } + }); + }, + error: function(e) { + console.log(e); + generateNotify("Something went wrong!", "danger"); + } + }); + + } + $('#save').click(function(e) { e.preventDefault(); @@ -170,11 +221,14 @@ return; } var qualityProfile = $("#profiles option:selected").val(); + var rootFolder = $("#rootFolders option:selected").val(); + var rootFolderPath = $('#rootFolders option:selected').text(); + $('#fullRootPath').val(rootFolderPath); var $form = $("#mainForm"); var data = $form.serialize(); - data = data + "&qualityProfile=" + qualityProfile; + data = data + "&qualityProfile=" + qualityProfile + "&rootPath=" + rootFolder; $.ajax({ type: $form.prop("method"), @@ -201,17 +255,17 @@ e.preventDefault(); if (!$('#Ip').val()) { generateNotify("Please enter a valid IP/Hostname.", "warning"); - $('#getSpinner').attr("class", "fa fa-times"); + $('#getSpinner').attr("class", "fa fa-times"); return; } if (!$('#portNumber').val()) { generateNotify("Please enter a valid Port Number.", "warning"); - $('#getSpinner').attr("class", "fa fa-times"); + $('#getSpinner').attr("class", "fa fa-times"); return; } if (!$('#ApiKey').val()) { generateNotify("Please enter a valid ApiKey.", "warning"); - $('#getSpinner').attr("class", "fa fa-times"); + $('#getSpinner').attr("class", "fa fa-times"); return; } var $form = $("#mainForm"); @@ -222,18 +276,58 @@ dataType: "json", success: function (response) { response.forEach(function (result) { - $('#getSpinner').attr("class", "fa fa-check"); + $('#getSpinner').attr("class", "fa fa-check"); $("#select").append(""); }); }, error: function (e) { console.log(e); - $('#getSpinner').attr("class", "fa fa-times"); + $('#getSpinner').attr("class", "fa fa-times"); generateNotify("Something went wrong!", "danger"); } }); }); + $('#getRootFolders').click(function (e) { + + $('#getRootFolderSpinner').attr("class", "fa fa-spinner fa-spin"); + e.preventDefault(); + if (!$('#Ip').val()) { + generateNotify("Please enter a valid IP/Hostname.", "warning"); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + return; + } + if (!$('#portNumber').val()) { + generateNotify("Please enter a valid Port Number.", "warning"); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + return; + } + if (!$('#ApiKey').val()) { + generateNotify("Please enter a valid ApiKey.", "warning"); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + return; + } + var $form = $("#mainForm"); + $.ajax({ + type: $form.prop("method"), + data: $form.serialize(), + url: "sonarrrootfolders", + dataType: "json", + success: function (response) { + response.forEach(function (result) { + $('#getRootFolderSpinner').attr("class", "fa fa-check"); + $("#selectRootFolder").append(""); + }); + }, + error: function (e) { + console.log(e); + $('#getRootFolderSpinner').attr("class", "fa fa-times"); + generateNotify("Something went wrong!", "danger"); + } + }); + }); + + var base = '@Html.GetBaseUrl()'; $('#testSonarr').click(function (e) { @@ -245,7 +339,7 @@ var data = $form.serialize(); data = data + "&qualityProfile=" + qualityProfile; - + var url = createBaseUrl(base, '/test/sonarr'); $.ajax({ type: $form.prop("method"), @@ -256,16 +350,16 @@ console.log(response); if (response.result === true) { generateNotify(response.message, "success"); - $('#spinner').attr("class", "fa fa-check"); + $('#spinner').attr("class", "fa fa-check"); } else { generateNotify(response.message, "warning"); - $('#spinner').attr("class", "fa fa-times"); + $('#spinner').attr("class", "fa fa-times"); } }, error: function (e) { console.log(e); generateNotify("Something went wrong!", "danger"); - $('#spinner').attr("class", "fa fa-times"); + $('#spinner').attr("class", "fa fa-times"); } }); }); diff --git a/Ombi.UI/Views/Customization/Customization.cshtml b/Ombi.UI/Views/Customization/Customization.cshtml index ff2e7a296..583dbabae 100644 --- a/Ombi.UI/Views/Customization/Customization.cshtml +++ b/Ombi.UI/Views/Customization/Customization.cshtml @@ -40,6 +40,7 @@
    +
    @@ -103,6 +104,7 @@
    + @Html.Checkbox(Model.Settings.NewSearch, "NewSearch", "Use New Search")
    diff --git a/Ombi.UI/Views/FaultQueue/RequestFaultQueue.cshtml b/Ombi.UI/Views/FaultQueue/RequestFaultQueue.cshtml index 59f2dde6b..f5df31e65 100644 --- a/Ombi.UI/Views/FaultQueue/RequestFaultQueue.cshtml +++ b/Ombi.UI/Views/FaultQueue/RequestFaultQueue.cshtml @@ -2,7 +2,7 @@ @Html.Partial("Shared/Partial/_Sidebar")
    - Release Fault Queue + Request Fault Queue diff --git a/Ombi.UI/Views/Integration/Radarr.cshtml b/Ombi.UI/Views/Integration/Radarr.cshtml new file mode 100644 index 000000000..3d4520e68 --- /dev/null +++ b/Ombi.UI/Views/Integration/Radarr.cshtml @@ -0,0 +1,244 @@ +@using Ombi.UI.Helpers +@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase +@Html.Partial("Shared/Partial/_Sidebar") +@{ + int port; + if (Model.Port == 0) + { + port = 7878; + } + else + { + port = Model.Port; + } +} +
    + +
    + Radarr 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/Requests/Index.cshtml b/Ombi.UI/Views/Requests/Index.cshtml index 0447e25fa..7199253c7 100644 --- a/Ombi.UI/Views/Requests/Index.cshtml +++ b/Ombi.UI/Views/Requests/Index.cshtml @@ -244,6 +244,11 @@
    @UI.Requests_RequestedBy: {{requestedUsers}}
    {{/if}}
    @UI.Requests_RequestedDate: {{requestedDate}}
    + {{#if admin}} + {{#if currentRootPath}} +
    Root Path: {{currentRootPath}}
    + {{/if}} + {{/if}}
    {{#if_eq issueId 0}} @*Nothing*@ @@ -275,6 +280,28 @@ {{/if_eq}} + + +
    + + {{#if_eq hasRootFolders true}} +
    + + + +
    + {{/if_eq}} + + + + {{#unless denied}}
    diff --git a/Ombi.UI/Views/Search/Index.cshtml b/Ombi.UI/Views/Search/Index.cshtml index 1ec29775f..db5b25dd9 100644 --- a/Ombi.UI/Views/Search/Index.cshtml +++ b/Ombi.UI/Views/Search/Index.cshtml @@ -1,5 +1,6 @@ @using Ombi.UI.Helpers @using Ombi.UI.Resources +@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase @{ var baseUrl = Html.GetBaseUrl(); var url = string.Empty; @@ -9,6 +10,8 @@ } }
    + +

    @UI.Search_Title

    @UI.Search_Paragraph


    @@ -16,21 +19,21 @@