diff --git a/src/Ombi.Api.Mattermost/IMattermostApi.cs b/src/Ombi.Api.Mattermost/IMattermostApi.cs new file mode 100644 index 000000000..b07802b25 --- /dev/null +++ b/src/Ombi.Api.Mattermost/IMattermostApi.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ombi.Api.Mattermost.Models; + +namespace Ombi.Api.Mattermost +{ + public interface IMattermostApi + { + Task PushAsync(string webhook, MattermostBody message); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Mattermost/MattermostApi.cs b/src/Ombi.Api.Mattermost/MattermostApi.cs new file mode 100644 index 000000000..c20641aca --- /dev/null +++ b/src/Ombi.Api.Mattermost/MattermostApi.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Ombi.Api.Mattermost.Models; + +namespace Ombi.Api.Mattermost +{ + public class MattermostApi : IMattermostApi + { + public MattermostApi(IApi api) + { + _api = api; + } + + private readonly IApi _api; + + public async Task PushAsync(string webhook, MattermostBody message) + { + var request = new Request(string.Empty, webhook, HttpMethod.Post); + + request.AddJsonBody(message); + + var result = await _api.RequestContent(request); + return result; + } + } +} diff --git a/src/Ombi.Api.Mattermost/Models/MattermostBody.cs b/src/Ombi.Api.Mattermost/Models/MattermostBody.cs new file mode 100644 index 000000000..383554c26 --- /dev/null +++ b/src/Ombi.Api.Mattermost/Models/MattermostBody.cs @@ -0,0 +1,45 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2017 Jamie Rees +// File: MattermostBody.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; + +namespace Ombi.Api.Mattermost.Models +{ + public class MattermostBody + { + [JsonConstructor] + public MattermostBody() + { + username = "Ombi"; + } + + public string username { get; set; } = "Ombi"; + public string channel { get; set; } + public string text { get; set; } + public string icon_url { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Mattermost/Ombi.Api.Mattermost.csproj b/src/Ombi.Api.Mattermost/Ombi.Api.Mattermost.csproj new file mode 100644 index 000000000..123fcdcf4 --- /dev/null +++ b/src/Ombi.Api.Mattermost/Ombi.Api.Mattermost.csproj @@ -0,0 +1,15 @@ + + + + netstandard1.6 + + + + + + + + + + + diff --git a/src/Ombi.Api.Plex/IPlexApi.cs b/src/Ombi.Api.Plex/IPlexApi.cs index fdfec7760..0c7bbe11f 100644 --- a/src/Ombi.Api.Plex/IPlexApi.cs +++ b/src/Ombi.Api.Plex/IPlexApi.cs @@ -10,10 +10,11 @@ namespace Ombi.Api.Plex Task GetStatus(string authToken, string uri); Task SignIn(UserRequest user); Task GetServer(string authToken); - Task GetLibrarySections(string authToken, string plexFullHost); - Task GetLibrary(string authToken, string plexFullHost, string libraryId); - Task GetEpisodeMetaData(string authToken, string host, string ratingKey); - Task GetMetadata(string authToken, string plexFullHost, string itemId); - Task GetSeasons(string authToken, string plexFullHost, string ratingKey); + Task GetLibrarySections(string authToken, string plexFullHost); + Task GetLibrary(string authToken, string plexFullHost, string libraryId); + Task GetEpisodeMetaData(string authToken, string host, int ratingKey); + Task GetMetadata(string authToken, string plexFullHost, int itemId); + Task GetSeasons(string authToken, string plexFullHost, int ratingKey); + Task GetAllEpisodes(string authToken, string host, string section, int start, int retCount); } } \ No newline at end of file diff --git a/src/Ombi.Api.Plex/Models/Mediacontainer.cs b/src/Ombi.Api.Plex/Models/Mediacontainer.cs index b409c2d76..c9f417a9c 100644 --- a/src/Ombi.Api.Plex/Models/Mediacontainer.cs +++ b/src/Ombi.Api.Plex/Models/Mediacontainer.cs @@ -5,6 +5,7 @@ namespace Ombi.Api.Plex.Models public class Mediacontainer { public int size { get; set; } + public int totalSize { get; set; } public bool allowSync { get; set; } public string identifier { get; set; } public string mediaTagPrefix { get; set; } diff --git a/src/Ombi.Api.Plex/Models/Metadata.cs b/src/Ombi.Api.Plex/Models/Metadata.cs index 0d803901f..e7fda9962 100644 --- a/src/Ombi.Api.Plex/Models/Metadata.cs +++ b/src/Ombi.Api.Plex/Models/Metadata.cs @@ -2,7 +2,7 @@ namespace Ombi.Api.Plex.Models { public class Metadata { - public string ratingKey { get; set; } + public int ratingKey { get; set; } public string key { get; set; } public string studio { get; set; } public string type { get; set; } @@ -28,8 +28,8 @@ namespace Ombi.Api.Plex.Models public Genre[] Genre { get; set; } public Role[] Role { get; set; } public string primaryExtraKey { get; set; } - public string parentRatingKey { get; set; } - public string grandparentRatingKey { get; set; } + public int parentRatingKey { get; set; } + public int grandparentRatingKey { get; set; } public string guid { get; set; } public int librarySectionID { get; set; } public string librarySectionKey { get; set; } diff --git a/src/Ombi.Api.Plex/Models/PlexLibraries.cs b/src/Ombi.Api.Plex/Models/PlexContainer.cs similarity index 97% rename from src/Ombi.Api.Plex/Models/PlexLibraries.cs rename to src/Ombi.Api.Plex/Models/PlexContainer.cs index 996a69742..7a0b10efc 100644 --- a/src/Ombi.Api.Plex/Models/PlexLibraries.cs +++ b/src/Ombi.Api.Plex/Models/PlexContainer.cs @@ -26,7 +26,7 @@ #endregion namespace Ombi.Api.Plex.Models { - public class PlexLibraries + public class PlexContainer { public Mediacontainer MediaContainer { get; set; } } diff --git a/src/Ombi.Api.Plex/PlexApi.cs b/src/Ombi.Api.Plex/PlexApi.cs index 43ee1a31b..2609a51f7 100644 --- a/src/Ombi.Api.Plex/PlexApi.cs +++ b/src/Ombi.Api.Plex/PlexApi.cs @@ -60,18 +60,18 @@ namespace Ombi.Api.Plex return await Api.Request(request); } - public async Task GetLibrarySections(string authToken, string plexFullHost) + public async Task GetLibrarySections(string authToken, string plexFullHost) { var request = new Request("library/sections", plexFullHost, HttpMethod.Get); AddHeaders(request, authToken); - return await Api.Request(request); + return await Api.Request(request); } - public async Task GetLibrary(string authToken, string plexFullHost, string libraryId) + public async Task GetLibrary(string authToken, string plexFullHost, string libraryId) { var request = new Request($"library/sections/{libraryId}/all", plexFullHost, HttpMethod.Get); AddHeaders(request, authToken); - return await Api.Request(request); + return await Api.Request(request); } /// @@ -85,27 +85,47 @@ namespace Ombi.Api.Plex /// /// /// - public async Task GetEpisodeMetaData(string authToken, string plexFullHost, string ratingKey) + public async Task GetEpisodeMetaData(string authToken, string plexFullHost, int ratingKey) { var request = new Request($"/library/metadata/{ratingKey}", plexFullHost, HttpMethod.Get); AddHeaders(request, authToken); return await Api.Request(request); } - public async Task GetMetadata(string authToken, string plexFullHost, string itemId) + public async Task GetMetadata(string authToken, string plexFullHost, int itemId) { var request = new Request($"library/metadata/{itemId}", plexFullHost, HttpMethod.Get); AddHeaders(request, authToken); return await Api.Request(request); } - public async Task GetSeasons(string authToken, string plexFullHost, string ratingKey) + public async Task GetSeasons(string authToken, string plexFullHost, int ratingKey) { var request = new Request($"library/metadata/{ratingKey}/children", plexFullHost, HttpMethod.Get); AddHeaders(request, authToken); return await Api.Request(request); } + /// + /// Gets all episodes. + /// + /// The authentication token. + /// The host. + /// The section. + /// The start count. + /// The return count, how many items you want returned. + /// + public async Task GetAllEpisodes(string authToken, string host, string section, int start, int retCount) + { + var request = new Request($"/library/sections/{section}/all", host, HttpMethod.Get); + + request.AddQueryString("type", "4"); + AddLimitHeaders(request, start, retCount); + AddHeaders(request, authToken); + + return await Api.Request(request); + } + /// /// Adds the required headers and also the authorization header /// @@ -129,5 +149,11 @@ namespace Ombi.Api.Plex request.AddContentHeader("Content-Type", request.ContentType == ContentType.Json ? "application/json" : "application/xml"); request.AddHeader("Accept", "application/json"); } + + private void AddLimitHeaders(Request request, int from, int to) + { + request.AddHeader("X-Plex-Container-Start", from.ToString()); + request.AddHeader("X-Plex-Container-Size", to.ToString()); + } } } diff --git a/src/Ombi.Api.Pushover/IPushoverApi.cs b/src/Ombi.Api.Pushover/IPushoverApi.cs new file mode 100644 index 000000000..42e8e9060 --- /dev/null +++ b/src/Ombi.Api.Pushover/IPushoverApi.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ombi.Api.Pushover.Models; + +namespace Ombi.Api.Pushover +{ + public interface IPushoverApi + { + Task PushAsync(string accessToken, string message, string userToken); + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Pushover/Models/PushoverResponse.cs b/src/Ombi.Api.Pushover/Models/PushoverResponse.cs new file mode 100644 index 000000000..417c728d0 --- /dev/null +++ b/src/Ombi.Api.Pushover/Models/PushoverResponse.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ombi.Api.Pushover.Models +{ + public class PushoverResponse + { + public int status { get; set; } + public string request { get; set; } + } +} diff --git a/src/Ombi.Api.Pushover/Ombi.Api.Pushover.csproj b/src/Ombi.Api.Pushover/Ombi.Api.Pushover.csproj new file mode 100644 index 000000000..105bb5244 --- /dev/null +++ b/src/Ombi.Api.Pushover/Ombi.Api.Pushover.csproj @@ -0,0 +1,11 @@ + + + + netstandard1.6 + + + + + + + diff --git a/src/Ombi.Api.Pushover/PushoverApi.cs b/src/Ombi.Api.Pushover/PushoverApi.cs new file mode 100644 index 000000000..96f4d2e95 --- /dev/null +++ b/src/Ombi.Api.Pushover/PushoverApi.cs @@ -0,0 +1,26 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Ombi.Api.Pushover.Models; + +namespace Ombi.Api.Pushover +{ + public class PushoverApi : IPushoverApi + { + public PushoverApi(IApi api) + { + _api = api; + } + + private readonly IApi _api; + private const string PushoverEndpoint = "https://api.pushover.net/1"; + + public async Task PushAsync(string accessToken, string message, string userToken) + { + var request = new Request($"messages.json?token={accessToken}&user={userToken}&message={message}", PushoverEndpoint, HttpMethod.Post); + + var result = await _api.Request(request); + return result; + } + } +} diff --git a/src/Ombi.Api.Radarr/RadarrApi.cs b/src/Ombi.Api.Radarr/RadarrApi.cs index 716ba4bf5..9ec66eb70 100644 --- a/src/Ombi.Api.Radarr/RadarrApi.cs +++ b/src/Ombi.Api.Radarr/RadarrApi.cs @@ -98,7 +98,7 @@ namespace Ombi.Api.Radarr } catch (JsonSerializationException jse) { - Logger.LogError(LoggingEvents.RadarrApiException, jse, "Error When adding movie to Radarr"); + Logger.LogError(LoggingEvents.RadarrApi, jse, "Error When adding movie to Radarr"); } return null; } diff --git a/src/Ombi.Api.TvMaze/Ombi.Api.TvMaze.csproj b/src/Ombi.Api.TvMaze/Ombi.Api.TvMaze.csproj index 761bb6ee1..c173d1dd7 100644 --- a/src/Ombi.Api.TvMaze/Ombi.Api.TvMaze.csproj +++ b/src/Ombi.Api.TvMaze/Ombi.Api.TvMaze.csproj @@ -9,10 +9,4 @@ - - - ..\..\..\..\..\.nuget\packages\microsoft.extensions.logging.abstractions\1.1.1\lib\netstandard1.1\Microsoft.Extensions.Logging.Abstractions.dll - - - \ No newline at end of file diff --git a/src/Ombi.Api.TvMaze/TvMazeApi.cs b/src/Ombi.Api.TvMaze/TvMazeApi.cs index 3324a5b63..9aa547483 100644 --- a/src/Ombi.Api.TvMaze/TvMazeApi.cs +++ b/src/Ombi.Api.TvMaze/TvMazeApi.cs @@ -61,7 +61,7 @@ namespace Ombi.Api.TvMaze } catch (Exception e) { - Logger.LogError(LoggingEvents.ApiException, e, "Exception when calling ShowLookupByTheTvDbId with id:{0}",theTvDbId); + Logger.LogError(LoggingEvents.Api, e, "Exception when calling ShowLookupByTheTvDbId with id:{0}",theTvDbId); return null; } } diff --git a/src/Ombi.Api/Api.cs b/src/Ombi.Api/Api.cs index 6e168d63f..de1ec8caf 100644 --- a/src/Ombi.Api/Api.cs +++ b/src/Ombi.Api/Api.cs @@ -45,7 +45,7 @@ namespace Ombi.Api { if (!httpResponseMessage.IsSuccessStatusCode) { - Logger.LogError(LoggingEvents.ApiException, $"StatusCode: {httpResponseMessage.StatusCode}, Reason: {httpResponseMessage.ReasonPhrase}"); + Logger.LogError(LoggingEvents.Api, $"StatusCode: {httpResponseMessage.StatusCode}, Reason: {httpResponseMessage.ReasonPhrase}"); } // do something with the response var data = httpResponseMessage.Content; @@ -89,7 +89,7 @@ namespace Ombi.Api { if (!httpResponseMessage.IsSuccessStatusCode) { - Logger.LogError(LoggingEvents.ApiException, $"StatusCode: {httpResponseMessage.StatusCode}, Reason: {httpResponseMessage.ReasonPhrase}"); + Logger.LogError(LoggingEvents.Api, $"StatusCode: {httpResponseMessage.StatusCode}, Reason: {httpResponseMessage.ReasonPhrase}"); } // do something with the response var data = httpResponseMessage.Content; @@ -123,7 +123,7 @@ namespace Ombi.Api { if (!httpResponseMessage.IsSuccessStatusCode) { - Logger.LogError(LoggingEvents.ApiException, $"StatusCode: {httpResponseMessage.StatusCode}, Reason: {httpResponseMessage.ReasonPhrase}"); + Logger.LogError(LoggingEvents.Api, $"StatusCode: {httpResponseMessage.StatusCode}, Reason: {httpResponseMessage.ReasonPhrase}"); } } } diff --git a/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj b/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj index 204b0d1b0..20321dae1 100644 --- a/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj +++ b/src/Ombi.Core.Tests/Ombi.Core.Tests.csproj @@ -5,10 +5,10 @@ - - - + + + diff --git a/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs index 558a7c850..50bf6f0c3 100644 --- a/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Request/AutoApproveRuleTests.cs @@ -26,8 +26,8 @@ namespace Ombi.Core.Tests.Rule.Request var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); - Assert.Equal(result.Success, true); - Assert.Equal(request.Approved, true); + Assert.True(result.Success); + Assert.True(request.Approved); } [Fact] @@ -37,8 +37,8 @@ namespace Ombi.Core.Tests.Rule.Request var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); - Assert.Equal(result.Success, true); - Assert.Equal(request.Approved, true); + Assert.True(result.Success); + Assert.True(request.Approved); } [Fact] @@ -48,8 +48,8 @@ namespace Ombi.Core.Tests.Rule.Request var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); - Assert.Equal(result.Success, true); - Assert.Equal(request.Approved, true); + Assert.True(result.Success); + Assert.True(request.Approved); } [Fact] @@ -59,8 +59,8 @@ namespace Ombi.Core.Tests.Rule.Request var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); - Assert.Equal(result.Success, true); - Assert.Equal(request.Approved, true); + Assert.True(result.Success); + Assert.True(request.Approved); } [Fact] @@ -69,8 +69,8 @@ namespace Ombi.Core.Tests.Rule.Request var request = new BaseRequest() { RequestType = Store.Entities.RequestType.Movie }; var result = await Rule.Execute(request); - Assert.Equal(result.Success, true); - Assert.Equal(request.Approved, false); + Assert.True(result.Success); + Assert.False(request.Approved); } [Fact] @@ -79,8 +79,8 @@ namespace Ombi.Core.Tests.Rule.Request var request = new BaseRequest() { RequestType = Store.Entities.RequestType.TvShow }; var result = await Rule.Execute(request); - Assert.Equal(result.Success, true); - Assert.Equal(request.Approved, false); + Assert.True(result.Success); + Assert.False(request.Approved); } } } diff --git a/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs index 2516325a5..ab28afa1a 100644 --- a/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/ExistingRequestRuleTests.cs @@ -33,7 +33,7 @@ namespace Ombi.Core.Tests.Rule.Search Approved = true }; - MovieMock.Setup(x => x.GetRequest(123)).ReturnsAsync(list); + MovieMock.Setup(x => x.GetRequest(123)).Returns(list); var search = new SearchMovieViewModel { Id = 123, @@ -42,7 +42,7 @@ namespace Ombi.Core.Tests.Rule.Search var result = await Rule.Execute(search); Assert.True(result.Success); - Assert.Equal(search.Approved, true); + Assert.False(search.Approved); } [Fact] @@ -54,7 +54,7 @@ namespace Ombi.Core.Tests.Rule.Search Approved = true }; - MovieMock.Setup(x => x.GetRequest(123)).ReturnsAsync(list); + MovieMock.Setup(x => x.GetRequest(123)).Returns(list); var search = new SearchMovieViewModel { Id = 999, @@ -63,7 +63,7 @@ namespace Ombi.Core.Tests.Rule.Search var result = await Rule.Execute(search); Assert.True(result.Success); - Assert.Equal(search.Approved, false); + Assert.False(search.Approved); } [Fact] @@ -82,7 +82,7 @@ namespace Ombi.Core.Tests.Rule.Search } }; - TvMock.Setup(x => x.GetRequest(123)).ReturnsAsync(list); + TvMock.Setup(x => x.GetRequest(123)).Returns(list); var search = new SearchTvShowViewModel { Id = 123, @@ -91,7 +91,7 @@ namespace Ombi.Core.Tests.Rule.Search var result = await Rule.Execute(search); Assert.True(result.Success); - Assert.Equal(search.Approved, true); + Assert.True(search.Approved); } [Fact] @@ -111,7 +111,7 @@ namespace Ombi.Core.Tests.Rule.Search }; - TvMock.Setup(x => x.GetRequest(123)).ReturnsAsync(list); + TvMock.Setup(x => x.GetRequest(123)).Returns(list); var search = new SearchTvShowViewModel() { Id = 999, @@ -120,7 +120,7 @@ namespace Ombi.Core.Tests.Rule.Search var result = await Rule.Execute(search); Assert.True(result.Success); - Assert.Equal(search.Approved, false); + Assert.False(search.Approved); } diff --git a/src/Ombi.Core/Claims/OmbiRoles.cs b/src/Ombi.Core/Claims/OmbiRoles.cs index 15ce52f9a..5d809e405 100644 --- a/src/Ombi.Core/Claims/OmbiRoles.cs +++ b/src/Ombi.Core/Claims/OmbiRoles.cs @@ -8,5 +8,6 @@ public const string PowerUser = nameof(PowerUser); public const string RequestTv = nameof(RequestTv); public const string RequestMovie = nameof(RequestMovie); + public const string Disabled = nameof(Disabled); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieSearchEngine.cs b/src/Ombi.Core/Engine/MovieSearchEngine.cs index 90622cbd6..f4d5230fd 100644 --- a/src/Ombi.Core/Engine/MovieSearchEngine.cs +++ b/src/Ombi.Core/Engine/MovieSearchEngine.cs @@ -148,10 +148,18 @@ namespace Ombi.Core.Engine { var showInfo = await MovieApi.GetMovieInformation(viewMovie.Id); viewMovie.Id = showInfo.Id; // TheMovieDbId + viewMovie.ImdbId = showInfo.ImdbId; } + // So when we run the rule to check if it's available in Plex we need the ImdbId + // But we only pass down the SearchViewModel that doesn't contain this + // So set the ImdbId to viewMovie.Id and then set it back afterwards + var oldId = viewMovie.Id; + viewMovie.CustomId = viewMovie.ImdbId ?? string.Empty; + await RunSearchRules(viewMovie); + viewMovie.Id = oldId; return viewMovie; } diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index d36ef78bb..4390b0674 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -141,12 +141,9 @@ namespace Ombi.Core.Engine public async Task UpdateChildRequest(ChildRequests request) { await Audit.Record(AuditType.Updated, AuditArea.TvRequest, $"Updated Request {request.Title}", Username); - var allRequests = TvRepository.GetChild(); - var results = await allRequests.FirstOrDefaultAsync(x => x.Id == request.Id); - - // TODO need to check if we need to approve any child requests since they may have updated - await TvRepository.UpdateChild(results); - return results; + + await TvRepository.UpdateChild(request); + return request; } public async Task RemoveTvChild(int requestId) diff --git a/src/Ombi.Core/Models/Search/SearchViewModel.cs b/src/Ombi.Core/Models/Search/SearchViewModel.cs index 7890707f1..85ff4786e 100644 --- a/src/Ombi.Core/Models/Search/SearchViewModel.cs +++ b/src/Ombi.Core/Models/Search/SearchViewModel.cs @@ -1,4 +1,6 @@ -namespace Ombi.Core.Models.Search +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ombi.Core.Models.Search { public class SearchViewModel { @@ -7,5 +9,16 @@ public bool Requested { get; set; } public bool Available { get; set; } public string PlexUrl { get; set; } + public string Quality { get; set; } + + + /// + /// This is used for the PlexAvailabilityCheck rule + /// + /// + /// The custom identifier. + /// + [NotMapped] + public string CustomId { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Models/UI/MattermostNotificationsViewModel.cs b/src/Ombi.Core/Models/UI/MattermostNotificationsViewModel.cs new file mode 100644 index 000000000..d4a03a4b0 --- /dev/null +++ b/src/Ombi.Core/Models/UI/MattermostNotificationsViewModel.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + /// + /// The view model for the notification settings page + /// + /// + public class MattermostNotificationsViewModel : MattermostNotificationSettings + { + /// + /// Gets or sets the notification templates. + /// + /// + /// The notification templates. + /// + public List NotificationTemplates { get; set; } + } +} diff --git a/src/Ombi.Core/Models/UI/PushoverNotificationViewModel.cs b/src/Ombi.Core/Models/UI/PushoverNotificationViewModel.cs new file mode 100644 index 000000000..21fe39ab3 --- /dev/null +++ b/src/Ombi.Core/Models/UI/PushoverNotificationViewModel.cs @@ -0,0 +1,23 @@ + +using System.Collections.Generic; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; + +namespace Ombi.Core.Models.UI +{ + /// + /// The view model for the notification settings page + /// + /// + public class PushoverNotificationViewModel : PushoverSettings + { + /// + /// Gets or sets the notification templates. + /// + /// + /// The notification templates. + /// + public List NotificationTemplates { get; set; } + + } +} diff --git a/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs b/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs index abbafd8f9..b84f9c652 100644 --- a/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/ExistingRule.cs @@ -19,9 +19,9 @@ namespace Ombi.Core.Rule.Rules.Search private IMovieRequestRepository Movie { get; } private ITvRequestRepository Tv { get; } - public async Task Execute(SearchViewModel obj) + public Task Execute(SearchViewModel obj) { - var movieRequests = await Movie.GetRequest(obj.Id); + var movieRequests = Movie.GetRequest(obj.Id); if (movieRequests != null) // Do we already have a request for this? { @@ -29,10 +29,10 @@ namespace Ombi.Core.Rule.Rules.Search obj.Approved = movieRequests.Approved; obj.Available = movieRequests.Available; - return Success(); + return Task.FromResult(Success()); } - var tvRequests = await Tv.GetRequest(obj.Id); + var tvRequests = Tv.GetRequest(obj.Id); if (tvRequests != null) // Do we already have a request for this? { @@ -40,9 +40,9 @@ namespace Ombi.Core.Rule.Rules.Search obj.Approved = tvRequests.ChildRequests.Any(x => x.Approved); obj.Available = tvRequests.ChildRequests.Any(x => x.Available); - return Success(); + return Task.FromResult(Success()); } - return Success(); + return Task.FromResult(Success()); } } } \ No newline at end of file diff --git a/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs index 9c2a0bbb1..c293e00be 100644 --- a/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs @@ -16,11 +16,12 @@ namespace Ombi.Core.Rule.Rules.Search public async Task Execute(SearchViewModel obj) { - var item = await PlexContentRepository.Get(obj.Id.ToString()); + var item = await PlexContentRepository.Get(obj.CustomId); if (item != null) { obj.Available = true; obj.PlexUrl = item.Url; + obj.Quality = item.Quality; } return Success(); } diff --git a/src/Ombi.Core/Rule/Rules/Search/RadarrCacheRule.cs b/src/Ombi.Core/Rule/Rules/Search/RadarrCacheRule.cs index 033d279a9..b2187e206 100644 --- a/src/Ombi.Core/Rule/Rules/Search/RadarrCacheRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/RadarrCacheRule.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Ombi.Core.Models.Search; using Ombi.Core.Rule.Interfaces; using Ombi.Store.Context; @@ -15,16 +16,16 @@ namespace Ombi.Core.Rule.Rules.Search private readonly IOmbiContext _ctx; - public Task Execute(SearchViewModel obj) + public async Task Execute(SearchViewModel obj) { // Check if it's in Radarr - var result = _ctx.RadarrCache.FirstOrDefault(x => x.TheMovieDbId == obj.Id); + var result = await _ctx.RadarrCache.FirstOrDefaultAsync(x => x.TheMovieDbId == obj.Id); if (result != null) { obj.Approved = true; // It's in radarr so it's approved... Maybe have a new property called "Processing" or something? } - return Task.FromResult(Success()); + return Success(); } } } \ No newline at end of file diff --git a/src/Ombi.Core/Senders/MovieSender.cs b/src/Ombi.Core/Senders/MovieSender.cs index 49d5d982a..051081df2 100644 --- a/src/Ombi.Core/Senders/MovieSender.cs +++ b/src/Ombi.Core/Senders/MovieSender.cs @@ -68,7 +68,7 @@ namespace Ombi.Core if (!string.IsNullOrEmpty(result.Error?.message)) { - Log.LogError(LoggingEvents.RadarrCacherException,result.Error.message); + Log.LogError(LoggingEvents.RadarrCacher,result.Error.message); return new MovieSenderResult { Success = false, Message = result.Error.message, MovieSent = false }; } if (!string.IsNullOrEmpty(result.title)) diff --git a/src/Ombi.Core/Senders/TvSender.cs b/src/Ombi.Core/Senders/TvSender.cs index e20e8c54a..d2fc19683 100644 --- a/src/Ombi.Core/Senders/TvSender.cs +++ b/src/Ombi.Core/Senders/TvSender.cs @@ -185,7 +185,7 @@ namespace Ombi.Core.Senders { var sonarrSeason = sonarrEpisodes.Where(x => x.seasonNumber == season.SeasonNumber); var sonarrEpCount = sonarrSeason.Count(); - var ourRequestCount = season.Episodes.Count(); + var ourRequestCount = season.Episodes.Count; if (sonarrEpCount == ourRequestCount) { diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 44edaf696..495afd347 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Ombi.Api.Discord; @@ -28,11 +29,14 @@ using Ombi.Notifications.Agents; using Ombi.Schedule.Jobs.Radarr; using Ombi.Api; using Ombi.Api.FanartTv; +using Ombi.Api.Mattermost; using Ombi.Api.Pushbullet; +using Ombi.Api.Pushover; using Ombi.Api.Service; using Ombi.Api.Slack; using Ombi.Core.Rule.Interfaces; using Ombi.Core.Senders; +using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Ombi; using Ombi.Store.Repository.Requests; using PlexContentCacher = Ombi.Schedule.Jobs.Plex.PlexContentCacher; @@ -78,13 +82,15 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static void RegisterStore(this IServiceCollection services) { services.AddEntityFrameworkSqlite().AddDbContext(); - services.AddScoped(); + services.AddScoped(); // https://docs.microsoft.com/en-us/aspnet/core/data/entity-framework-6 services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -109,11 +115,16 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } public static void RegisterJobs(this IServiceCollection services) { services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index 414a854e9..86267112b 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -14,8 +14,10 @@ + + diff --git a/src/Ombi.Helpers/LoggingEvents.cs b/src/Ombi.Helpers/LoggingEvents.cs index 0381b098d..9c45e0831 100644 --- a/src/Ombi.Helpers/LoggingEvents.cs +++ b/src/Ombi.Helpers/LoggingEvents.cs @@ -6,11 +6,12 @@ namespace Ombi.Helpers { public static EventId Authentication => new EventId(500); - public static EventId ApiException => new EventId(1000); - public static EventId RadarrApiException => new EventId(1001); + public static EventId Api => new EventId(1000); + public static EventId RadarrApi => new EventId(1001); - public static EventId CacherException => new EventId(2000); - public static EventId RadarrCacherException => new EventId(2001); + public static EventId Cacher => new EventId(2000); + public static EventId RadarrCacher => new EventId(2001); + public static EventId PlexEpisodeCacher => new EventId(2001); public static EventId MovieSender => new EventId(3000); @@ -18,6 +19,8 @@ namespace Ombi.Helpers public static EventId DiscordNotification => new EventId(4001); public static EventId PushbulletNotification => new EventId(4002); public static EventId SlackNotification => new EventId(4003); + public static EventId MattermostNotification => new EventId(4004); + public static EventId PushoverNotification => new EventId(4005); public static EventId TvSender => new EventId(5000); public static EventId SonarrSender => new EventId(5001); diff --git a/src/Ombi.Helpers/NotificationAgent.cs b/src/Ombi.Helpers/NotificationAgent.cs index c1b3515dc..515277f9e 100644 --- a/src/Ombi.Helpers/NotificationAgent.cs +++ b/src/Ombi.Helpers/NotificationAgent.cs @@ -7,6 +7,7 @@ Pushbullet, Pushover, Telegram, - Slack + Slack, + Mattermost, } } \ No newline at end of file diff --git a/src/Ombi.Helpers/PlexHelper.cs b/src/Ombi.Helpers/PlexHelper.cs index 5149ec8f6..9eb938c70 100644 --- a/src/Ombi.Helpers/PlexHelper.cs +++ b/src/Ombi.Helpers/PlexHelper.cs @@ -91,7 +91,7 @@ namespace Ombi.Helpers return 0; } - public static string GetPlexMediaUrl(string machineId, string mediaId) + public static string GetPlexMediaUrl(string machineId, int mediaId) { var url = $"https://app.plex.tv/web/app#!/server/{machineId}/details?key=library%2Fmetadata%2F{mediaId}"; diff --git a/src/Ombi.Mapping/Profiles/SettingsProfile.cs b/src/Ombi.Mapping/Profiles/SettingsProfile.cs index cdb38a35b..339e0eb68 100644 --- a/src/Ombi.Mapping/Profiles/SettingsProfile.cs +++ b/src/Ombi.Mapping/Profiles/SettingsProfile.cs @@ -12,6 +12,8 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } } \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/IMattermostNotification.cs b/src/Ombi.Notifications/Agents/IMattermostNotification.cs new file mode 100644 index 000000000..224514770 --- /dev/null +++ b/src/Ombi.Notifications/Agents/IMattermostNotification.cs @@ -0,0 +1,6 @@ +namespace Ombi.Notifications.Agents +{ + public interface IMattermostNotification : INotification + { + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/IPushoverNotification.cs b/src/Ombi.Notifications/Agents/IPushoverNotification.cs new file mode 100644 index 000000000..513bdddf7 --- /dev/null +++ b/src/Ombi.Notifications/Agents/IPushoverNotification.cs @@ -0,0 +1,6 @@ +namespace Ombi.Notifications.Agents +{ + public interface IPushoverNotification : INotification + { + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/MattermostNotification.cs b/src/Ombi.Notifications/Agents/MattermostNotification.cs new file mode 100644 index 000000000..683fd4084 --- /dev/null +++ b/src/Ombi.Notifications/Agents/MattermostNotification.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.Discord; +using Ombi.Api.Discord.Models; +using Ombi.Api.Mattermost; +using Ombi.Api.Mattermost.Models; +using Ombi.Core.Settings; +using Ombi.Helpers; +using Ombi.Notifications.Interfaces; +using Ombi.Notifications.Models; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Notifications.Agents +{ + public class MattermostNotification : BaseNotification, IMattermostNotification + { + public MattermostNotification(IMattermostApi api, ISettingsService sn, ILogger log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t) : base(sn, r, m, t) + { + Api = api; + Logger = log; + } + + public override string NotificationName => "MattermostNotification"; + + private IMattermostApi Api { get; } + private ILogger Logger { get; } + + protected override bool ValidateConfiguration(MattermostNotificationSettings settings) + { + if (!settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(settings.WebhookUrl)) + { + return false; + } + + return true; + } + + protected override async Task NewRequest(NotificationOptions model, MattermostNotificationSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.NewRequest, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + + notification.Other.Add("image", parsed.Image); + await Send(notification, settings); + } + + protected override async Task Issue(NotificationOptions model, MattermostNotificationSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.Issue, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + notification.Other.Add("image", parsed.Image); + await Send(notification, settings); + } + + protected override async Task AddedToRequestQueue(NotificationOptions model, MattermostNotificationSettings settings) + { + var user = string.Empty; + var title = string.Empty; + var image = string.Empty; + if (model.RequestType == RequestType.Movie) + { + user = MovieRequest.RequestedUser.UserAlias; + title = MovieRequest.Title; + image = MovieRequest.PosterPath; + } + else + { + user = TvRequest.RequestedUser.UserAlias; + title = TvRequest.ParentRequest.Title; + image = TvRequest.ParentRequest.PosterPath; + } + var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying"; + var notification = new NotificationMessage + { + Message = message + }; + notification.Other.Add("image", image); + await Send(notification, settings); + } + + protected override async Task RequestDeclined(NotificationOptions model, MattermostNotificationSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.RequestDeclined, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + notification.Other.Add("image", parsed.Image); + await Send(notification, settings); + } + + protected override async Task RequestApproved(NotificationOptions model, MattermostNotificationSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.RequestApproved, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + + notification.Other.Add("image", parsed.Image); + await Send(notification, settings); + } + + protected override async Task AvailableRequest(NotificationOptions model, MattermostNotificationSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.RequestAvailable, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + notification.Other.Add("image", parsed.Image); + await Send(notification, settings); + } + + protected override async Task Send(NotificationMessage model, MattermostNotificationSettings settings) + { + try + { + var body = new MattermostBody + { + username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username, + channel = settings.Channel, + text = model.Message, + icon_url = settings.IconUrl + }; + await Api.PushAsync(settings.WebhookUrl, body); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.MattermostNotification, e, "Failed to send Mattermost Notification"); + } + } + + protected override async Task Test(NotificationOptions model, MattermostNotificationSettings settings) + { + var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + } +} diff --git a/src/Ombi.Notifications/Agents/PushoverNotification.cs b/src/Ombi.Notifications/Agents/PushoverNotification.cs new file mode 100644 index 000000000..3e3ae20e2 --- /dev/null +++ b/src/Ombi.Notifications/Agents/PushoverNotification.cs @@ -0,0 +1,145 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.Pushbullet; +using Ombi.Api.Pushover; +using Ombi.Core.Settings; +using Ombi.Helpers; +using Ombi.Notifications.Interfaces; +using Ombi.Notifications.Models; +using Ombi.Settings.Settings.Models.Notifications; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Notifications.Agents +{ + public class PushoverNotification : BaseNotification, IPushoverNotification + { + public PushoverNotification(IPushoverApi api, ISettingsService sn, ILogger log, INotificationTemplatesRepository r, IMovieRequestRepository m, ITvRequestRepository t) : base(sn, r, m, t) + { + Api = api; + Logger = log; + } + + public override string NotificationName => "PushoverNotification"; + + private IPushoverApi Api { get; } + private ILogger Logger { get; } + + protected override bool ValidateConfiguration(PushoverSettings settings) + { + if (!settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(settings.AccessToken)) + { + return false; + } + + return true; + } + + protected override async Task NewRequest(NotificationOptions model, PushoverSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.NewRequest, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + + await Send(notification, settings); + } + + protected override async Task Issue(NotificationOptions model, PushoverSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.Issue, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + await Send(notification, settings); + } + + protected override async Task AddedToRequestQueue(NotificationOptions model, PushoverSettings settings) + { + string user; + string title; + if (model.RequestType == RequestType.Movie) + { + user = MovieRequest.RequestedUser.UserAlias; + title = MovieRequest.Title; + } + else + { + user = TvRequest.RequestedUser.UserAlias; + title = TvRequest.ParentRequest.Title; + } + var message = $"Hello! The user '{user}' has requested {title} but it could not be added. This has been added into the requests queue and will keep retrying"; + var notification = new NotificationMessage + { + Message = message + }; + await Send(notification, settings); + } + + protected override async Task RequestDeclined(NotificationOptions model, PushoverSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.RequestDeclined, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + await Send(notification, settings); + } + + protected override async Task RequestApproved(NotificationOptions model, PushoverSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.RequestApproved, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + + await Send(notification, settings); + } + + protected override async Task AvailableRequest(NotificationOptions model, PushoverSettings settings) + { + var parsed = await LoadTemplate(NotificationAgent.Pushover, NotificationType.RequestAvailable, model); + + var notification = new NotificationMessage + { + Message = parsed.Message, + }; + await Send(notification, settings); + } + + protected override async Task Send(NotificationMessage model, PushoverSettings settings) + { + try + { + await Api.PushAsync(settings.AccessToken, model.Message, settings.UserToken); + } + catch (Exception e) + { + Logger.LogError(LoggingEvents.PushoverNotification, e, "Failed to send Pushover Notification"); + } + } + + protected override async Task Test(NotificationOptions model, PushoverSettings settings) + { + var message = $"This is a test from Ombi, if you can see this then we have successfully pushed a notification!"; + var notification = new NotificationMessage + { + Message = message, + }; + await Send(notification, settings); + } + } +} diff --git a/src/Ombi.Notifications/Ombi.Notifications.csproj b/src/Ombi.Notifications/Ombi.Notifications.csproj index 293861219..e8ce7ca42 100644 --- a/src/Ombi.Notifications/Ombi.Notifications.csproj +++ b/src/Ombi.Notifications/Ombi.Notifications.csproj @@ -10,7 +10,9 @@ + + diff --git a/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj new file mode 100644 index 000000000..dd9930445 --- /dev/null +++ b/src/Ombi.Schedule.Tests/Ombi.Schedule.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp1.1 + + + + + + + + + + + + + + + + + diff --git a/src/Ombi.Schedule.Tests/PlexAvailabilityCheckerTests.cs b/src/Ombi.Schedule.Tests/PlexAvailabilityCheckerTests.cs new file mode 100644 index 000000000..6cb2863a4 --- /dev/null +++ b/src/Ombi.Schedule.Tests/PlexAvailabilityCheckerTests.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Castle.Components.DictionaryAdapter; +using Moq; +using NUnit.Framework; +using Ombi.Schedule.Jobs.Plex; +using Ombi.Store.Entities; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Schedule.Tests +{ + [TestFixture] + public class PlexAvailabilityCheckerTests + { + public PlexAvailabilityCheckerTests() + { + _repo = new Mock(); + _tv = new Mock(); + _movie = new Mock(); + Checker = new PlexAvailabilityChecker(_repo.Object, _tv.Object, _movie.Object); + } + + private readonly Mock _repo; + private readonly Mock _tv; + private readonly Mock _movie; + private PlexAvailabilityChecker Checker { get; } + + [Test] + public async Task ProcessMovies_ShouldMarkAvailable_WhenInPlex() + { + var request = new MovieRequests + { + ImdbId = "test" + }; + _movie.Setup(x => x.Get()).Returns(new List { request }.AsQueryable()); + _repo.Setup(x => x.Get("test")).ReturnsAsync(new PlexContent()); + + await Checker.Start(); + + _movie.Verify(x => x.Save(), Times.Once); + + Assert.True(request.Available); + } + + [Test] + public async Task ProcessMovies_ShouldNotBeAvailable_WhenInNotPlex() + { + var request = new MovieRequests + { + ImdbId = "test" + }; + _movie.Setup(x => x.Get()).Returns(new List { request }.AsQueryable()); + + await Checker.Start(); + + _movie.Verify(x => x.Save(), Times.Once); + Assert.False(request.Available); + } + + [Test] + [Ignore("EF IAsyncQueryProvider")] + public async Task ProcessTv_ShouldMark_Episode_Available_WhenInPlex() + { + var request = new ChildRequests + { + ParentRequest = new TvRequests {TvDbId = 1}, + SeasonRequests = new EditableList + { + new SeasonRequests + { + Episodes = new EditableList + { + new EpisodeRequests + { + EpisodeNumber = 1, + Season = new SeasonRequests + { + SeasonNumber = 2 + } + } + } + } + } + }; + _tv.Setup(x => x.GetChild()).Returns(new List { request }.AsQueryable()); + _repo.Setup(x => x.GetAllEpisodes()).Returns(new List + { + new PlexEpisode + { + Series = new PlexContent + { + ProviderId = 1.ToString(), + }, + EpisodeNumber = 1, + SeasonNumber = 2 + } + }.AsQueryable); + + await Checker.Start(); + + _tv.Verify(x => x.Save(), Times.Once); + + Assert.True(request.SeasonRequests[0].Episodes[0].Available); + + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule.Tests/Properties/launchSettings.json b/src/Ombi.Schedule.Tests/Properties/launchSettings.json new file mode 100644 index 000000000..57de33708 --- /dev/null +++ b/src/Ombi.Schedule.Tests/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:62604/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Ombi.Schedule.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:62605/" + } + } +} diff --git a/src/Ombi.Schedule/IPlexAvailabilityChecker.cs b/src/Ombi.Schedule/IPlexAvailabilityChecker.cs new file mode 100644 index 000000000..0f65ddeb8 --- /dev/null +++ b/src/Ombi.Schedule/IPlexAvailabilityChecker.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Plex +{ + public interface IPlexAvailabilityChecker + { + Task Start(); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Plex/IPlexContentCacher.cs b/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexContentCacher.cs similarity index 100% rename from src/Ombi.Schedule/Jobs/Plex/IPlexContentCacher.cs rename to src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexContentCacher.cs diff --git a/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexEpisodeCacher.cs b/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexEpisodeCacher.cs new file mode 100644 index 000000000..b6fdc9b41 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexEpisodeCacher.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Ombi.Schedule.Jobs.Plex +{ + public interface IPlexEpisodeCacher + { + Task Start(); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs b/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs new file mode 100644 index 000000000..fb6082eab --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Plex/PlexAvailabilityChecker.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; + +namespace Ombi.Schedule.Jobs.Plex +{ + public class PlexAvailabilityChecker : IPlexAvailabilityChecker + { + public PlexAvailabilityChecker(IPlexContentRepository repo, ITvRequestRepository tvRequest, IMovieRequestRepository movies) + { + _tvRepo = tvRequest; + _repo = repo; + _movieRepo = movies; + } + + private readonly ITvRequestRepository _tvRepo; + private readonly IMovieRequestRepository _movieRepo; + private readonly IPlexContentRepository _repo; + + public async Task Start() + { + await ProcessMovies(); + await ProcessTv(); + } + + private async Task ProcessTv() + { + var tv = _tvRepo.GetChild().Where(x => !x.Available); + var plexEpisodes = _repo.GetAllEpisodes().Include(x => x.Series); + + foreach (var child in tv) + { + var tvDbId = child.ParentRequest.TvDbId; + var seriesEpisodes = plexEpisodes.Where(x => x.Series.ProviderId == tvDbId.ToString()); + foreach (var season in child.SeasonRequests) + { + foreach (var episode in season.Episodes) + { + var foundEp = await seriesEpisodes.FirstOrDefaultAsync( + x => x.EpisodeNumber == episode.EpisodeNumber && + x.SeasonNumber == episode.Season.SeasonNumber); + + if (foundEp != null) + { + episode.Available = true; + } + } + } + + // Check to see if all of the episodes in all seasons are available for this request + var allAvailable = child.SeasonRequests.All(x => x.Episodes.All(c => c.Available)); + if (allAvailable) + { + // We have fulfulled this request! + child.Available = true; + } + } + + await _tvRepo.Save(); + } + + private async Task ProcessMovies() + { + // Get all non available + var movies = _movieRepo.Get().Where(x => !x.Available); + + foreach (var movie in movies) + { + var plexContent = await _repo.Get(movie.ImdbId); + if (plexContent == null) + { + // We don't yet have this + continue; + } + + movie.Available = true; + } + + await _movieRepo.Save(); + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexContentCacher.cs b/src/Ombi.Schedule/Jobs/Plex/PlexContentCacher.cs index d90d419b4..22b3fe529 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexContentCacher.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexContentCacher.cs @@ -29,6 +29,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Hangfire; using Microsoft.Extensions.Logging; using Ombi.Api.Plex; using Ombi.Api.Plex.Models; @@ -42,22 +43,25 @@ namespace Ombi.Schedule.Jobs.Plex { public class PlexContentCacher : IPlexContentCacher { - public PlexContentCacher(ISettingsService plex, IPlexApi plexApi, ILogger logger, IPlexContentRepository repo) + public PlexContentCacher(ISettingsService plex, IPlexApi plexApi, ILogger logger, IPlexContentRepository repo, + IPlexEpisodeCacher epsiodeCacher) { Plex = plex; PlexApi = plexApi; Logger = logger; Repo = repo; + EpisodeCacher = epsiodeCacher; } private ISettingsService Plex { get; } private IPlexApi PlexApi { get; } private ILogger Logger { get; } private IPlexContentRepository Repo { get; } + private IPlexEpisodeCacher EpisodeCacher { get; } public async Task CacheContent() { - var plexSettings = Plex.GetSettings(); + var plexSettings = await Plex.GetSettingsAsync(); if (!plexSettings.Enable) { return; @@ -71,10 +75,12 @@ namespace Ombi.Schedule.Jobs.Plex try { await StartTheCache(plexSettings); + + BackgroundJob.Enqueue(() => EpisodeCacher.Start()); } catch (Exception e) { - Logger.LogWarning(LoggingEvents.CacherException, e, "Exception thrown when attempting to cache the Plex Content"); + Logger.LogWarning(LoggingEvents.Cacher, e, "Exception thrown when attempting to cache the Plex Content"); } } @@ -101,10 +107,10 @@ namespace Ombi.Schedule.Jobs.Plex { seasonsContent.Add(new PlexSeasonsContent { - ParentKey = int.Parse(season.parentRatingKey), - SeasonKey = int.Parse(season.ratingKey), + ParentKey = season.parentRatingKey, + SeasonKey = season.ratingKey, SeasonNumber = season.index, - PlexContentId = int.Parse(show.ratingKey) + PlexContentId = show.ratingKey }); } @@ -181,7 +187,8 @@ namespace Ombi.Schedule.Jobs.Plex Type = PlexMediaTypeEntity.Movie, Title = movie.title, Url = PlexHelper.GetPlexMediaUrl(servers.MachineIdentifier, movie.ratingKey), - Seasons = new List() + Seasons = new List(), + Quality = movie.Media?.FirstOrDefault()?.videoResolution ?? string.Empty }; contentToAdd.Add(item); @@ -191,8 +198,7 @@ namespace Ombi.Schedule.Jobs.Plex if (contentToAdd.Any()) { - - contentToAdd.ForEach(async x => await Repo.Add(x)); + await Repo.AddRange(contentToAdd); } } @@ -216,12 +222,16 @@ namespace Ombi.Schedule.Jobs.Plex { if (plexSettings.PlexSelectedLibraries.Any()) { - // Only get the enabled libs - var keys = plexSettings.PlexSelectedLibraries.Where(x => x.Enabled).Select(x => x.Key.ToString()).ToList(); - if (!keys.Contains(dir.key)) + if (plexSettings.PlexSelectedLibraries.Any(x => x.Enabled)) { - // We are not monitoring this lib - continue; + // Only get the enabled libs + var keys = plexSettings.PlexSelectedLibraries.Where(x => x.Enabled) + .Select(x => x.Key.ToString()).ToList(); + if (!keys.Contains(dir.key)) + { + // We are not monitoring this lib + continue; + } } } var lib = PlexApi.GetLibrary(plexSettings.PlexAuthToken, plexSettings.FullUri, dir.key).Result; diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeCacher.cs b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeCacher.cs new file mode 100644 index 000000000..0d777f428 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeCacher.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Hangfire; +using Hangfire.Common; +using Microsoft.Extensions.Logging; +using Ombi.Api.Plex; +using Ombi.Api.Plex.Models; +using Ombi.Api.Plex.Models.Server; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Serilog; + +namespace Ombi.Schedule.Jobs.Plex +{ + public class PlexEpisodeCacher : IPlexEpisodeCacher + { + public PlexEpisodeCacher(ISettingsService s, ILogger log, IPlexApi plexApi, + IPlexContentRepository repo, IPlexAvailabilityChecker a) + { + _settings = s; + _log = log; + _api = plexApi; + _repo = repo; + _availabilityChecker = a; + } + + private readonly ISettingsService _settings; + private readonly ILogger _log; + private readonly IPlexApi _api; + private readonly IPlexContentRepository _repo; + private readonly IPlexAvailabilityChecker _availabilityChecker; + + public async Task Start() + { + try + { + var s = await _settings.GetSettingsAsync(); + if (!s.Enable) + { + return; + } + + foreach (var server in s.Servers) + { + + await Cache(server); + BackgroundJob.Enqueue(() => _availabilityChecker.Start()); + } + } + catch (Exception e) + { + _log.LogError(LoggingEvents.Cacher, e, "Caching Episodes Failed"); + } + } + + private async Task Cache(PlexServers settings) + { + if (!Validate(settings)) + { + return; + } + + // Get the librarys and then get the tv section + var sections = await _api.GetLibrarySections(settings.PlexAuthToken, settings.FullUri); + + // Filter the libSections + var tvSections = sections.MediaContainer.Directory.Where(x => x.type.Equals(Jobs.PlexContentCacher.PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)); + + foreach (var section in tvSections) + { + if (settings.PlexSelectedLibraries.Any()) + { + // Are any enabled? + if (settings.PlexSelectedLibraries.Any(x => x.Enabled)) + { + // Make sure we have enabled this + var keys = settings.PlexSelectedLibraries.Where(x => x.Enabled).Select(x => x.Key.ToString()) + .ToList(); + if (!keys.Contains(section.key)) + { + // We are not monitoring this lib + continue; + } + } + } + + // Get the episodes + await GetEpisodes(settings, section); + } + + } + + private async Task GetEpisodes(PlexServers settings, Directory section) + { + + // Get the first 50 + var currentPosition = 0; + var ResultCount = 50; + var episodes = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, ResultCount); + var currentData = _repo.GetAllEpisodes(); + _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}"); + + await ProcessEpsiodes(episodes, currentData); + currentPosition += ResultCount; + + while (currentPosition < episodes.MediaContainer.totalSize) + { + var ep = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, + ResultCount); + await ProcessEpsiodes(ep, currentData); + _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Processed {ResultCount} more episodes. Total Remaining {currentPosition - episodes.MediaContainer.totalSize}"); + currentPosition += ResultCount; + } + + } + + private async Task ProcessEpsiodes(PlexContainer episodes, IQueryable currentEpisodes) + { + var ep = new HashSet(); + foreach (var episode in episodes.MediaContainer.Metadata) + { + // I don't think we need to get the metadata, we only need to get the metadata if we need the provider id (TheTvDbid). Why do we need it for episodes? + // We have the parent and grandparent rating keys to link up to the season and series + //var metadata = _api.GetEpisodeMetaData(server.PlexAuthToken, server.FullUri, episode.ratingKey); + + var epExists = currentEpisodes.Any(x => episode.ratingKey == x.Key && + episode.grandparentRatingKey == x.GrandparentKey); + if (epExists) + { + continue; + } + + ep.Add(new PlexEpisode + { + EpisodeNumber = episode.index, + SeasonNumber = episode.parentIndex, + GrandparentKey = episode.grandparentRatingKey, + ParentKey = episode.parentRatingKey, + Key = episode.ratingKey, + Title = episode.title + }); + } + + await _repo.AddRange(ep); + } + + private bool Validate(PlexServers settings) + { + if (string.IsNullOrEmpty(settings.PlexAuthToken)) + { + return false ; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Radarr/RadarrCacher.cs b/src/Ombi.Schedule/Jobs/Radarr/RadarrCacher.cs index fdacd6307..021d612b8 100644 --- a/src/Ombi.Schedule/Jobs/Radarr/RadarrCacher.cs +++ b/src/Ombi.Schedule/Jobs/Radarr/RadarrCacher.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -29,38 +30,45 @@ namespace Ombi.Schedule.Jobs.Radarr public async Task CacheContent() { - var settings = RadarrSettings.GetSettings(); - if (settings.Enabled) + try { - try + var settings = RadarrSettings.GetSettings(); + if (settings.Enabled) { - var movies = await RadarrApi.GetMovies(settings.ApiKey, settings.FullUri); - if (movies != null) + try { - // Let's remove the old cached data - await _ctx.Database.ExecuteSqlCommandAsync("TRUNCATE TABLE RadarrCache"); - - var movieIds = new List(); - foreach (var m in movies) + var movies = await RadarrApi.GetMovies(settings.ApiKey, settings.FullUri); + if (movies != null) { - if (m.tmdbId > 0) - { - movieIds.Add(new RadarrCache{TheMovieDbId = m.tmdbId}); - } - else + // Let's remove the old cached data + await _ctx.Database.ExecuteSqlCommandAsync("TRUNCATE TABLE RadarrCache"); + + var movieIds = new List(); + foreach (var m in movies) { - Log.Error("TMDBId is not > 0 for movie {0}", m.title); + if (m.tmdbId > 0) + { + movieIds.Add(new RadarrCache { TheMovieDbId = m.tmdbId }); + } + else + { + Log.Error("TMDBId is not > 0 for movie {0}", m.title); + } } - } - await _ctx.RadarrCache.AddRangeAsync(movieIds); + await _ctx.RadarrCache.AddRangeAsync(movieIds); - await _ctx.SaveChangesAsync(); + await _ctx.SaveChangesAsync(); + } + } + catch (System.Exception ex) + { + Logger.LogError(LoggingEvents.Cacher, ex, "Failed caching queued items from Radarr"); } } - catch (System.Exception ex) - { - Logger.LogError(LoggingEvents.CacherException, ex, "Failed caching queued items from Radarr"); - } + } + catch (Exception e) + { + Logger.LogInformation(LoggingEvents.RadarrCacher, "Radarr is not setup, cannot cache episodes"); } } diff --git a/src/Ombi.Settings/Settings/Models/AuthenticationSettings.cs b/src/Ombi.Settings/Settings/Models/AuthenticationSettings.cs new file mode 100644 index 000000000..7c2725ec5 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/AuthenticationSettings.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace Ombi.Settings.Settings.Models +{ + public class AuthenticationSettings + { + /// + /// This determins if Plex and/or Emby users can log into Ombi + /// + /// + /// true if [allow external users to authenticate]; otherwise, false. + /// + public bool AllowExternalUsersToAuthenticate { get; set; } + + // Password Options + public bool RequireDigit { get; set; } + public int RequiredLength { get; set; } + public bool RequireLowercase { get; set; } + public bool RequireNonAlphanumeric { get; set; } + public bool RequireUppercase { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs b/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs index 7363e8fb1..94963a03d 100644 --- a/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/PlexSettings.cs @@ -17,7 +17,7 @@ namespace Ombi.Core.Settings.Models.External public string PlexAuthToken { get; set; } public string MachineIdentifier { get; set; } - public List PlexSelectedLibraries { get; set; } + public List PlexSelectedLibraries { get; set; } = new List(); } public class PlexSelectedLibraries { diff --git a/src/Ombi.Settings/Settings/Models/Notifications/MattermostNotificationSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/MattermostNotificationSettings.cs new file mode 100644 index 000000000..4e3f4727d --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/MattermostNotificationSettings.cs @@ -0,0 +1,11 @@ +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class MattermostNotificationSettings : Settings + { + public string WebhookUrl { get; set; } + public string Channel { get; set; } + public string Username { get; set; } + public string IconUrl { get; set; } + public bool Enabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/Notifications/PushoverSettings.cs b/src/Ombi.Settings/Settings/Models/Notifications/PushoverSettings.cs new file mode 100644 index 000000000..d845e8695 --- /dev/null +++ b/src/Ombi.Settings/Settings/Models/Notifications/PushoverSettings.cs @@ -0,0 +1,12 @@ +using System; +using Newtonsoft.Json; + +namespace Ombi.Settings.Settings.Models.Notifications +{ + public class PushoverSettings : Settings + { + public bool Enabled { get; set; } + public string AccessToken { get; set; } + public string UserToken { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/Models/OmbiSettings.cs b/src/Ombi.Settings/Settings/Models/OmbiSettings.cs index 3c989dc2e..da13109b0 100644 --- a/src/Ombi.Settings/Settings/Models/OmbiSettings.cs +++ b/src/Ombi.Settings/Settings/Models/OmbiSettings.cs @@ -7,6 +7,5 @@ public bool Wizard { get; set; } public string ApiKey { get; set; } - public bool AllowExternalUsersToAuthenticate { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Settings/Settings/SettingsService.cs b/src/Ombi.Settings/Settings/SettingsService.cs index a6777fdfc..5bcf1a835 100644 --- a/src/Ombi.Settings/Settings/SettingsService.cs +++ b/src/Ombi.Settings/Settings/SettingsService.cs @@ -40,13 +40,17 @@ namespace Ombi.Settings.Settings public async Task GetSettingsAsync() { - var result = await Repo.GetAsync(EntityName).ConfigureAwait(false); + var result = await Repo.GetAsync(EntityName); if (result == null) { return new T(); } result.Content = DecryptSettings(result); - return string.IsNullOrEmpty(result.Content) ? null : JsonConvert.DeserializeObject(result.Content, SerializerSettings.Settings); + var obj = string.IsNullOrEmpty(result.Content) ? null : JsonConvert.DeserializeObject(result.Content, SerializerSettings.Settings); + + var model = obj; + + return model; } public bool SaveSettings(T model) @@ -67,10 +71,10 @@ namespace Ombi.Settings.Settings var modified = model; modified.Id = entity.Id; + entity.Content = JsonConvert.SerializeObject(modified, SerializerSettings.Settings); - var globalSettings = new GlobalSettings { SettingsName = EntityName, Content = JsonConvert.SerializeObject(modified, SerializerSettings.Settings), Id = entity.Id }; - globalSettings.Content = EncryptSettings(globalSettings); - Repo.Update(globalSettings); + entity.Content = EncryptSettings(entity); + Repo.Update(entity); return true; } @@ -85,7 +89,7 @@ namespace Ombi.Settings.Settings var settings = new GlobalSettings { SettingsName = EntityName, Content = JsonConvert.SerializeObject(newEntity, SerializerSettings.Settings) }; settings.Content = EncryptSettings(settings); - var insertResult = await Repo.InsertAsync(settings).ConfigureAwait(false); + var insertResult = await Repo.InsertAsync(settings); return insertResult != null; } @@ -93,9 +97,10 @@ namespace Ombi.Settings.Settings var modified = model; modified.Id = entity.Id; - var globalSettings = new GlobalSettings { SettingsName = EntityName, Content = JsonConvert.SerializeObject(modified, SerializerSettings.Settings), Id = entity.Id }; - entity.Content = EncryptSettings(globalSettings); - await Repo.UpdateAsync(entity).ConfigureAwait(false); + entity.Content = JsonConvert.SerializeObject(modified, SerializerSettings.Settings); + + entity.Content = EncryptSettings(entity); + await Repo.UpdateAsync(entity); return true; } @@ -117,7 +122,7 @@ namespace Ombi.Settings.Settings { await Repo.DeleteAsync(entity); } - + } private string EncryptSettings(GlobalSettings settings) diff --git a/src/Ombi.Store/Context/IOmbiContext.cs b/src/Ombi.Store/Context/IOmbiContext.cs index c7e613a7c..bb00bd8a1 100644 --- a/src/Ombi.Store/Context/IOmbiContext.cs +++ b/src/Ombi.Store/Context/IOmbiContext.cs @@ -15,6 +15,7 @@ namespace Ombi.Store.Context Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); DbSet Settings { get; set; } DbSet PlexContent { get; set; } + DbSet PlexEpisode { get; set; } DbSet RadarrCache { get; set; } DatabaseFacade Database { get; } EntityEntry Entry(T entry) where T : class; @@ -30,5 +31,7 @@ namespace Ombi.Store.Context DbSet MovieIssues { get; set; } DbSet TvIssues { get; set; } DbSet Tokens { get; set; } + EntityEntry Update(object entity); + EntityEntry Update(TEntity entity) where TEntity : class; } } \ No newline at end of file diff --git a/src/Ombi.Store/Context/OmbiContext.cs b/src/Ombi.Store/Context/OmbiContext.cs index c1364c87b..7c1fea71f 100644 --- a/src/Ombi.Store/Context/OmbiContext.cs +++ b/src/Ombi.Store/Context/OmbiContext.cs @@ -19,13 +19,14 @@ namespace Ombi.Store.Context Database.Migrate(); // Add the notifcation templates - + } public DbSet NotificationTemplates { get; set; } public DbSet Settings { get; set; } public DbSet PlexContent { get; set; } + public DbSet PlexEpisode { get; set; } public DbSet RadarrCache { get; set; } public DbSet MovieRequests { get; set; } @@ -44,6 +45,16 @@ namespace Ombi.Store.Context optionsBuilder.UseSqlite("Data Source=Ombi.db"); } + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(p => p.Series) + .WithMany(b => b.Episodes) + .HasPrincipalKey(x => x.Key) + .HasForeignKey(p => p.GrandparentKey); + base.OnModelCreating(builder); + } + public void Seed() { @@ -70,19 +81,23 @@ namespace Ombi.Store.Context SaveChanges(); } - - // Check if templates exist + //Check if templates exist var templates = NotificationTemplates.ToList(); - if (templates.Any()) - { - return; - } + //if (templates.Any()) + //{ + // return; + //} var allAgents = Enum.GetValues(typeof(NotificationAgent)).Cast().ToList(); var allTypes = Enum.GetValues(typeof(NotificationType)).Cast().ToList(); foreach (var agent in allAgents) { + if (templates.Any(x => x.Agent == agent)) + { + // We have all the templates for this notification agent + continue; + } foreach (var notificationType in allTypes) { NotificationTemplates notificationToAdd; diff --git a/src/Ombi.Store/Entities/PlexContent.cs b/src/Ombi.Store/Entities/PlexContent.cs index 211b2e611..0e8295cbc 100644 --- a/src/Ombi.Store/Entities/PlexContent.cs +++ b/src/Ombi.Store/Entities/PlexContent.cs @@ -45,12 +45,15 @@ namespace Ombi.Store.Entities /// Only used for TV Shows /// public virtual ICollection Seasons { get; set; } + + public ICollection Episodes { get; set; } /// /// Plex's internal ID for this item /// - public string Key { get; set; } + public int Key { get; set; } public DateTime AddedAt { get; set; } + public string Quality { get; set; } } [Table("PlexSeasonsContent")] diff --git a/src/Ombi.Store/Entities/PlexEpisode.cs b/src/Ombi.Store/Entities/PlexEpisode.cs new file mode 100644 index 000000000..ecd03742c --- /dev/null +++ b/src/Ombi.Store/Entities/PlexEpisode.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Ombi.Store.Entities +{ + [Table("PlexEpisode")] + public class PlexEpisode : Entity + { + public int EpisodeNumber { get; set; } + public int SeasonNumber { get; set; } + public int Key { get; set; } // RatingKey + public string Title { get; set; } + /// + /// The Season key + /// + /// + /// The parent key. + /// + public int ParentKey { get; set; } + /// + /// The Series key + /// + /// + /// The grandparent key. + /// + public int GrandparentKey { get; set; } + + + public PlexContent Series { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Migrations/20170811145836_Inital.Designer.cs b/src/Ombi.Store/Migrations/20170825114646_Inital.Designer.cs similarity index 94% rename from src/Ombi.Store/Migrations/20170811145836_Inital.Designer.cs rename to src/Ombi.Store/Migrations/20170825114646_Inital.Designer.cs index 80c66cbfd..4f5573b00 100644 --- a/src/Ombi.Store/Migrations/20170811145836_Inital.Designer.cs +++ b/src/Ombi.Store/Migrations/20170825114646_Inital.Designer.cs @@ -10,7 +10,7 @@ using Ombi.Helpers; namespace Ombi.Store.Migrations { [DbContext(typeof(OmbiContext))] - [Migration("20170811145836_Inital")] + [Migration("20170825114646_Inital")] partial class Inital { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -254,10 +254,12 @@ namespace Ombi.Store.Migrations b.Property("AddedAt"); - b.Property("Key"); + b.Property("Key"); b.Property("ProviderId"); + b.Property("Quality"); + b.Property("ReleaseYear"); b.Property("Title"); @@ -271,6 +273,30 @@ namespace Ombi.Store.Migrations b.ToTable("PlexContent"); }); + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => { b.Property("Id") @@ -546,6 +572,15 @@ namespace Ombi.Store.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => { b.HasOne("Ombi.Store.Entities.PlexContent") diff --git a/src/Ombi.Store/Migrations/20170811145836_Inital.cs b/src/Ombi.Store/Migrations/20170825114646_Inital.cs similarity index 94% rename from src/Ombi.Store/Migrations/20170811145836_Inital.cs rename to src/Ombi.Store/Migrations/20170825114646_Inital.cs index d366494c9..3229f6fbf 100644 --- a/src/Ombi.Store/Migrations/20170811145836_Inital.cs +++ b/src/Ombi.Store/Migrations/20170825114646_Inital.cs @@ -132,8 +132,9 @@ namespace Ombi.Store.Migrations Id = table.Column(nullable: false) .Annotation("Sqlite:Autoincrement", true), AddedAt = table.Column(nullable: false), - Key = table.Column(nullable: true), + Key = table.Column(nullable: false), ProviderId = table.Column(nullable: true), + Quality = table.Column(nullable: true), ReleaseYear = table.Column(nullable: true), Title = table.Column(nullable: true), Type = table.Column(nullable: false), @@ -142,6 +143,7 @@ namespace Ombi.Store.Migrations constraints: table => { table.PrimaryKey("PK_PlexContent", x => x.Id); + table.UniqueConstraint("AK_PlexContent_Key", x => x.Key); }); migrationBuilder.CreateTable( @@ -316,6 +318,30 @@ namespace Ombi.Store.Migrations onDelete: ReferentialAction.Restrict); }); + migrationBuilder.CreateTable( + name: "PlexEpisode", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EpisodeNumber = table.Column(nullable: false), + GrandparentKey = table.Column(nullable: false), + Key = table.Column(nullable: false), + ParentKey = table.Column(nullable: false), + SeasonNumber = table.Column(nullable: false), + Title = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PlexEpisode", x => x.Id); + table.ForeignKey( + name: "FK_PlexEpisode_PlexContent_GrandparentKey", + column: x => x.GrandparentKey, + principalTable: "PlexContent", + principalColumn: "Key", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "PlexSeasonsContent", columns: table => new @@ -511,6 +537,11 @@ namespace Ombi.Store.Migrations column: "NormalizedUserName", unique: true); + migrationBuilder.CreateIndex( + name: "IX_PlexEpisode_GrandparentKey", + table: "PlexEpisode", + column: "GrandparentKey"); + migrationBuilder.CreateIndex( name: "IX_PlexSeasonsContent_PlexContentId", table: "PlexSeasonsContent", @@ -596,6 +627,9 @@ namespace Ombi.Store.Migrations migrationBuilder.DropTable( name: "NotificationTemplates"); + migrationBuilder.DropTable( + name: "PlexEpisode"); + migrationBuilder.DropTable( name: "PlexSeasonsContent"); diff --git a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs index ed875997e..585745fea 100644 --- a/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/OmbiContextModelSnapshot.cs @@ -253,10 +253,12 @@ namespace Ombi.Store.Migrations b.Property("AddedAt"); - b.Property("Key"); + b.Property("Key"); b.Property("ProviderId"); + b.Property("Quality"); + b.Property("ReleaseYear"); b.Property("Title"); @@ -270,6 +272,30 @@ namespace Ombi.Store.Migrations b.ToTable("PlexContent"); }); + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("EpisodeNumber"); + + b.Property("GrandparentKey"); + + b.Property("Key"); + + b.Property("ParentKey"); + + b.Property("SeasonNumber"); + + b.Property("Title"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => { b.Property("Id") @@ -545,6 +571,15 @@ namespace Ombi.Store.Migrations .OnDelete(DeleteBehavior.Cascade); }); + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => { b.HasOne("Ombi.Store.Entities.PlexContent") diff --git a/src/Ombi.Store/Repository/IPlexContentRepository.cs b/src/Ombi.Store/Repository/IPlexContentRepository.cs index 41e9456ac..85e61d38e 100644 --- a/src/Ombi.Store/Repository/IPlexContentRepository.cs +++ b/src/Ombi.Store/Repository/IPlexContentRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Ombi.Store.Entities; @@ -11,7 +12,12 @@ namespace Ombi.Store.Repository Task ContentExists(string providerId); Task> GetAll(); Task Get(string providerId); - Task GetByKey(string key); + Task GetByKey(int key); Task Update(PlexContent existingContent); + IQueryable GetAllEpisodes(); + Task Add(PlexEpisode content); + Task GetEpisodeByKey(int key); + Task AddRange(IEnumerable content); + IQueryable Get(); } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/PlexContentRepository.cs b/src/Ombi.Store/Repository/PlexContentRepository.cs index 48ee88e4e..196d47021 100644 --- a/src/Ombi.Store/Repository/PlexContentRepository.cs +++ b/src/Ombi.Store/Repository/PlexContentRepository.cs @@ -26,6 +26,7 @@ #endregion using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Store.Context; @@ -71,7 +72,12 @@ namespace Ombi.Store.Repository return await Db.PlexContent.FirstOrDefaultAsync(x => x.ProviderId == providerId); } - public async Task GetByKey(string key) + public IQueryable Get() + { + return Db.PlexContent.AsQueryable(); + } + + public async Task GetByKey(int key) { return await Db.PlexContent.Include(x => x.Seasons).FirstOrDefaultAsync(x => x.Key == key); } @@ -81,5 +87,26 @@ namespace Ombi.Store.Repository Db.PlexContent.Update(existingContent); await Db.SaveChangesAsync(); } + + public IQueryable GetAllEpisodes() + { + return Db.PlexEpisode.AsQueryable(); + } + + public async Task Add(PlexEpisode content) + { + await Db.PlexEpisode.AddAsync(content); + await Db.SaveChangesAsync(); + return content; + } + public async Task GetEpisodeByKey(int key) + { + return await Db.PlexEpisode.FirstOrDefaultAsync(x => x.Key == key); + } + public async Task AddRange(IEnumerable content) + { + Db.PlexEpisode.AddRange(content); + await Db.SaveChangesAsync(); + } } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs b/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs index b73aff999..f4e46e272 100644 --- a/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/IMovieRequestRepository.cs @@ -9,7 +9,9 @@ namespace Ombi.Store.Repository Task Add(MovieRequests request); Task Delete(MovieRequests request); IQueryable Get(); - Task GetRequest(int theMovieDbId); + Task GetRequestAsync(int theMovieDbId); + MovieRequests GetRequest(int theMovieDbId); Task Update(MovieRequests request); + Task Save(); } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs b/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs index cd524e3b6..906f30e44 100644 --- a/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/ITvRequestRepository.cs @@ -11,9 +11,11 @@ namespace Ombi.Store.Repository.Requests Task Delete(TvRequests request); Task DeleteChild(ChildRequests request); IQueryable Get(); - Task GetRequest(int tvDbId); + Task GetRequestAsync(int tvDbId); + TvRequests GetRequest(int tvDbId); Task Update(TvRequests request); Task UpdateChild(ChildRequests request); IQueryable GetChild(); + Task Save(); } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs index 9b18dbad9..4a280d366 100644 --- a/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/MovieRequestRepository.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Store.Context; @@ -15,11 +17,27 @@ namespace Ombi.Store.Repository.Requests private IOmbiContext Db { get; } - public async Task GetRequest(int theMovieDbId) + public async Task GetRequestAsync(int theMovieDbId) { - return await Db.MovieRequests.Where(x => x.TheMovieDbId == theMovieDbId) + try + { + return await Db.MovieRequests.Where(x => x.TheMovieDbId == theMovieDbId) + .Include(x => x.RequestedUser) + .FirstOrDefaultAsync(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + } + + public MovieRequests GetRequest(int theMovieDbId) + { + return Db.MovieRequests.Where(x => x.TheMovieDbId == theMovieDbId) .Include(x => x.RequestedUser) - .FirstOrDefaultAsync(); + .FirstOrDefault(); } public IQueryable Get() @@ -46,5 +64,10 @@ namespace Ombi.Store.Repository.Requests { await Db.SaveChangesAsync(); } - } + + public async Task Save() + { + await Db.SaveChangesAsync(); + } + } } \ No newline at end of file diff --git a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs index bead9e4cd..8ad975db3 100644 --- a/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs +++ b/src/Ombi.Store/Repository/Requests/TvRequestRepository.cs @@ -15,17 +15,28 @@ namespace Ombi.Store.Repository.Requests private IOmbiContext Db { get; } - public async Task GetRequest(int tvDbId) + public async Task GetRequestAsync(int tvDbId) { return await Db.TvRequests.Where(x => x.TvDbId == tvDbId) .Include(x => x.ChildRequests) - .ThenInclude(x => x.RequestedUser) + .ThenInclude(x => x.RequestedUser) .Include(x => x.ChildRequests) .ThenInclude(x => x.SeasonRequests) .ThenInclude(x => x.Episodes) .FirstOrDefaultAsync(); } + public TvRequests GetRequest(int tvDbId) + { + return Db.TvRequests.Where(x => x.TvDbId == tvDbId) + .Include(x => x.ChildRequests) + .ThenInclude(x => x.RequestedUser) + .Include(x => x.ChildRequests) + .ThenInclude(x => x.SeasonRequests) + .ThenInclude(x => x.Episodes) + .FirstOrDefault(); + } + public IQueryable Get() { return Db.TvRequests @@ -40,11 +51,17 @@ namespace Ombi.Store.Repository.Requests { return Db.ChildRequests .Include(x => x.RequestedUser) + .Include(x => x.ParentRequest) .Include(x => x.SeasonRequests) .ThenInclude(x => x.Episodes) .AsQueryable(); } + public async Task Save() + { + await Db.SaveChangesAsync(); + } + public async Task Add(TvRequests request) { await Db.TvRequests.AddAsync(request); @@ -74,11 +91,15 @@ namespace Ombi.Store.Repository.Requests public async Task Update(TvRequests request) { + Db.Attach(request).State = EntityState.Modified; + await Db.SaveChangesAsync(); } public async Task UpdateChild(ChildRequests request) { + Db.Attach(request).State = EntityState.Modified; + await Db.SaveChangesAsync(); } } diff --git a/src/Ombi.Store/Repository/SettingsJsonRepository.cs b/src/Ombi.Store/Repository/SettingsJsonRepository.cs index 89958665f..a5aa94474 100644 --- a/src/Ombi.Store/Repository/SettingsJsonRepository.cs +++ b/src/Ombi.Store/Repository/SettingsJsonRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -36,27 +37,28 @@ namespace Ombi.Store.Repository public IEnumerable GetAll() { - var page = Db.Settings.ToList(); + var page = Db.Settings.AsNoTracking().ToList(); return page; } public async Task> GetAllAsync() { - var page = await Db.Settings.ToListAsync(); + var page = await Db.Settings.AsNoTracking().ToListAsync(); return page; } public GlobalSettings Get(string pageName) { - var entity = Db.Settings.FirstOrDefault(x => x.SettingsName == pageName); - Db.Entry(entity).Reload(); + var entity = Db.Settings.AsNoTracking().FirstOrDefault(x => x.SettingsName == pageName); return entity; } public async Task GetAsync(string settingsName) { - return await Db.Settings.FirstOrDefaultAsync(x => x.SettingsName == settingsName); + + var obj = await Db.Settings.AsNoTracking().FirstOrDefaultAsync(x => x.SettingsName == settingsName); + return obj; } public async Task DeleteAsync(GlobalSettings entity) @@ -67,6 +69,7 @@ namespace Ombi.Store.Repository public async Task UpdateAsync(GlobalSettings entity) { + Db.Update(entity); await Db.SaveChangesAsync(); } diff --git a/src/Ombi.sln b/src/Ombi.sln index 151454a4f..1a77eafe9 100644 --- a/src/Ombi.sln +++ b/src/Ombi.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi", "Ombi\Ombi.csproj", "{C987AA67-AFE1-468F-ACD3-EAD5A48E1F6A}" EndProject @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\build.cake = ..\build.cake ..\BuildTask.ps1 = ..\BuildTask.ps1 ..\CHANGELOG.md = ..\CHANGELOG.md + ..\global.json = ..\global.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Core", "Ombi.Core\Ombi.Core.csproj", "{F56E79C7-791D-4668-A0EC-29E3BBC8D24B}" @@ -73,7 +74,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.FanartTv", "Ombi.A EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Pushbullet", "Ombi.Api.Pushbullet\Ombi.Api.Pushbullet.csproj", "{E237CDF6-D044-437D-B157-E9A3CC0BCF53}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Api.Slack", "Ombi.Api.Slack\Ombi.Api.Slack.csproj", "{71708256-9152-4E81-9FCA-E3181A185806}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Slack", "Ombi.Api.Slack\Ombi.Api.Slack.csproj", "{71708256-9152-4E81-9FCA-E3181A185806}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Mattermost", "Ombi.Api.Mattermost\Ombi.Api.Mattermost.csproj", "{737B2620-FE5A-4135-A017-79C269A7D36C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ombi.Api.Pushover", "Ombi.Api.Pushover\Ombi.Api.Pushover.csproj", "{CA55DD4F-4EFF-4906-A848-35FCC7BD5654}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ombi.Schedule.Tests", "Ombi.Schedule.Tests\Ombi.Schedule.Tests.csproj", "{BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -185,6 +192,18 @@ Global {71708256-9152-4E81-9FCA-E3181A185806}.Debug|Any CPU.Build.0 = Debug|Any CPU {71708256-9152-4E81-9FCA-E3181A185806}.Release|Any CPU.ActiveCfg = Release|Any CPU {71708256-9152-4E81-9FCA-E3181A185806}.Release|Any CPU.Build.0 = Release|Any CPU + {737B2620-FE5A-4135-A017-79C269A7D36C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {737B2620-FE5A-4135-A017-79C269A7D36C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {737B2620-FE5A-4135-A017-79C269A7D36C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {737B2620-FE5A-4135-A017-79C269A7D36C}.Release|Any CPU.Build.0 = Release|Any CPU + {CA55DD4F-4EFF-4906-A848-35FCC7BD5654}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA55DD4F-4EFF-4906-A848-35FCC7BD5654}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA55DD4F-4EFF-4906-A848-35FCC7BD5654}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA55DD4F-4EFF-4906-A848-35FCC7BD5654}.Release|Any CPU.Build.0 = Release|Any CPU + {BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -210,5 +229,11 @@ Global {FD947E63-A0D2-4878-8378-2005D5E9AB8A} = {9293CA11-360A-4C20-A674-B9E794431BF5} {E237CDF6-D044-437D-B157-E9A3CC0BCF53} = {9293CA11-360A-4C20-A674-B9E794431BF5} {71708256-9152-4E81-9FCA-E3181A185806} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {737B2620-FE5A-4135-A017-79C269A7D36C} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {CA55DD4F-4EFF-4906-A848-35FCC7BD5654} = {9293CA11-360A-4C20-A674-B9E794431BF5} + {BDD8B924-016E-4CDA-9FFA-50B0A34BCD3C} = {6F42AB98-9196-44C4-B888-D5E409F415A1} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {192E9BF8-00B4-45E4-BCCC-4C215725C869} EndGlobalSection EndGlobal diff --git a/src/Ombi/ClientApp/app/app.component.ts b/src/Ombi/ClientApp/app/app.component.ts index 71e6ee246..5d701e4c5 100644 --- a/src/Ombi/ClientApp/app/app.component.ts +++ b/src/Ombi/ClientApp/app/app.component.ts @@ -15,30 +15,24 @@ import { ICustomizationSettings } from './interfaces/ISettings'; }) export class AppComponent implements OnInit { - constructor(public notificationService: NotificationService, public authService: AuthService, private router: Router, private settingsService: SettingsService) - { - } + constructor(public notificationService: NotificationService, public authService: AuthService, private router: Router, private settingsService: SettingsService) { } customizationSettings: ICustomizationSettings; user: ILocalUser; ngOnInit(): void { - - - this.user = this.authService.claims(); this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); this.router.events.subscribe(() => { - this.user = this.authService.claims(); this.showNav = this.authService.loggedIn(); }); } hasRole(role: string): boolean { - return this.user.roles.some(r => r === role) + return this.user.roles.some(r => r === role); } logOut() { diff --git a/src/Ombi/ClientApp/app/auth/auth.service.ts b/src/Ombi/ClientApp/app/auth/auth.service.ts index 1b6defc19..b6ec60ab8 100644 --- a/src/Ombi/ClientApp/app/auth/auth.service.ts +++ b/src/Ombi/ClientApp/app/auth/auth.service.ts @@ -1,14 +1,11 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Rx'; +import { tokenNotExpired, JwtHelper } from 'angular2-jwt'; +import { Http, Headers } from '@angular/http'; import { ServiceHelpers } from '../services/service.helpers'; - import { IUserLogin, ILocalUser } from './IUserLogin'; -import { tokenNotExpired, JwtHelper } from 'angular2-jwt'; - -import { Http, Headers } from '@angular/http'; - @Injectable() export class AuthService extends ServiceHelpers { constructor(http: Http) { @@ -23,7 +20,6 @@ export class AuthService extends ServiceHelpers { return this.http.post(`${this.url}/`, JSON.stringify(login), { headers: this.headers }) .map(this.extractData); - } loggedIn() { @@ -48,13 +44,16 @@ export class AuthService extends ServiceHelpers { } else { u.roles.push(roles); } - return u; - } return {}; } + + hasRole(role: string): boolean { + return this.claims().roles.some(r => r === role); + } + logout() { localStorage.removeItem('id_token'); } diff --git a/src/Ombi/ClientApp/app/interfaces/INotifcationSettings.ts b/src/Ombi/ClientApp/app/interfaces/INotifcationSettings.ts index a7e53c53a..513d65735 100644 --- a/src/Ombi/ClientApp/app/interfaces/INotifcationSettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/INotifcationSettings.ts @@ -64,4 +64,18 @@ export interface IPushbulletNotificationSettings extends INotificationSettings { accessToken: string, notificationTemplates: INotificationTemplates[], channelTag: string; -} \ No newline at end of file +} + +export interface IPushoverNotificationSettings extends INotificationSettings { + accessToken: string, + notificationTemplates: INotificationTemplates[], + userToken: string; +} + +export interface IMattermostNotifcationSettings extends INotificationSettings { + webhookUrl: string, + username: string, + channel: string, + iconUrl:string, + notificationTemplates: INotificationTemplates[], +} diff --git a/src/Ombi/ClientApp/app/interfaces/IRequestModel.ts b/src/Ombi/ClientApp/app/interfaces/IRequestModel.ts index df5765ae8..c34c24b8c 100644 --- a/src/Ombi/ClientApp/app/interfaces/IRequestModel.ts +++ b/src/Ombi/ClientApp/app/interfaces/IRequestModel.ts @@ -105,7 +105,8 @@ export interface IBaseRequest { denied: boolean, deniedReason: string, requestType: RequestType, - requestedUser: IUser + requestedUser: IUser, + canApprove:boolean, } export interface ITvRequests { diff --git a/src/Ombi/ClientApp/app/interfaces/ISearchMovieResult.ts b/src/Ombi/ClientApp/app/interfaces/ISearchMovieResult.ts index 99af3b533..e4488fd4b 100644 --- a/src/Ombi/ClientApp/app/interfaces/ISearchMovieResult.ts +++ b/src/Ombi/ClientApp/app/interfaces/ISearchMovieResult.ts @@ -20,5 +20,6 @@ approved: boolean, requested: boolean, available: boolean, - plexUrl: string + plexUrl: string, + quality:string } \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/app/interfaces/ISettings.ts index d140a8991..89399e1c8 100644 --- a/src/Ombi/ClientApp/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/ISettings.ts @@ -86,4 +86,16 @@ export interface ILandingPageSettings extends ISettings { export interface ICustomizationSettings extends ISettings { applicationName: string, logo: string, +} + +export interface IAuthenticationSettings extends ISettings { + + allowExternalUsersToAuthenticate: boolean, + // Password + + requiredDigit: boolean, + requiredLength: number, + requiredLowercase: boolean, + requireNonAlphanumeric: boolean, + requireUppercase:boolean, } \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/requests/tvrequest-children.component.ts b/src/Ombi/ClientApp/app/requests/tvrequest-children.component.ts index 92fe9fc13..672d8c089 100644 --- a/src/Ombi/ClientApp/app/requests/tvrequest-children.component.ts +++ b/src/Ombi/ClientApp/app/requests/tvrequest-children.component.ts @@ -23,6 +23,7 @@ export class TvRequestChildrenComponent { } public deny(request: IChildRequests) { + debugger; request.approved = false; request.denied = true; this.requestService.updateChild(request) @@ -30,6 +31,7 @@ export class TvRequestChildrenComponent { } public approve(request: IChildRequests) { + debugger; request.approved = true; request.denied = false; this.requestService.updateChild(request) diff --git a/src/Ombi/ClientApp/app/search/moviesearch.component.html b/src/Ombi/ClientApp/app/search/moviesearch.component.html index b09b1eb25..e9106d264 100644 --- a/src/Ombi/ClientApp/app/search/moviesearch.component.html +++ b/src/Ombi/ClientApp/app/search/moviesearch.component.html @@ -46,7 +46,11 @@ Release Date: {{result.releaseDate | date: 'dd/MM/yyyy'}} + HomePage + + Trailer Available + {{result.quality}}p Processing Request
@@ -58,11 +62,7 @@ - - HomePage - - Trailer
@@ -132,11 +132,12 @@
diff --git a/src/Ombi/ClientApp/app/search/search.component.html b/src/Ombi/ClientApp/app/search/search.component.html index 705ff17ee..e82457259 100644 --- a/src/Ombi/ClientApp/app/search/search.component.html +++ b/src/Ombi/ClientApp/app/search/search.component.html @@ -9,21 +9,10 @@ - - - +
  • TV Shows -
  • - - @@ -33,28 +22,8 @@ - - -
    -
    diff --git a/src/Ombi/ClientApp/app/search/tvsearch.component.html b/src/Ombi/ClientApp/app/search/tvsearch.component.html index eeb45f419..b5c727764 100644 --- a/src/Ombi/ClientApp/app/search/tvsearch.component.html +++ b/src/Ombi/ClientApp/app/search/tvsearch.component.html @@ -44,7 +44,7 @@ diff --git a/src/Ombi/ClientApp/app/services/applications/tester.service.ts b/src/Ombi/ClientApp/app/services/applications/tester.service.ts index cdce6a72a..1c6fa4208 100644 --- a/src/Ombi/ClientApp/app/services/applications/tester.service.ts +++ b/src/Ombi/ClientApp/app/services/applications/tester.service.ts @@ -8,7 +8,9 @@ import { IDiscordNotifcationSettings, IEmailNotificationSettings, IPushbulletNotificationSettings, - ISlackNotificationSettings + ISlackNotificationSettings, + IPushoverNotificationSettings, + IMattermostNotifcationSettings } from '../../interfaces/INotifcationSettings' @@ -25,6 +27,13 @@ export class TesterService extends ServiceAuthHelpers { pushbulletTest(settings: IPushbulletNotificationSettings): Observable { return this.http.post(`${this.url}pushbullet`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData); } + pushoverTest(settings: IPushoverNotificationSettings): Observable { + return this.http.post(`${this.url}pushover`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData); + } + + mattermostTest(settings: IMattermostNotifcationSettings): Observable { + return this.http.post(`${this.url}mattermost`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData); + } slackTest(settings: ISlackNotificationSettings): Observable { return this.http.post(`${this.url}slack`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData); diff --git a/src/Ombi/ClientApp/app/services/settings.service.ts b/src/Ombi/ClientApp/app/services/settings.service.ts index bc4aa9afa..ef75a43f7 100644 --- a/src/Ombi/ClientApp/app/services/settings.service.ts +++ b/src/Ombi/ClientApp/app/services/settings.service.ts @@ -11,13 +11,16 @@ import { ISonarrSettings, ILandingPageSettings, ICustomizationSettings, - IRadarrSettings + IRadarrSettings, + IAuthenticationSettings } from '../interfaces/ISettings'; import { IEmailNotificationSettings, IDiscordNotifcationSettings, IPushbulletNotificationSettings, - ISlackNotificationSettings + ISlackNotificationSettings, + IPushoverNotificationSettings, + IMattermostNotifcationSettings } from '../interfaces/INotifcationSettings'; @Injectable() @@ -31,23 +34,31 @@ export class SettingsService extends ServiceAuthHelpers { } saveOmbi(settings: IOmbiSettings): Observable { - return this.httpAuth.post(`${this.url}/Ombi/`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth.post(`${this.url}/Ombi/`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); + } + + resetOmbiApi(): Observable { + return this.httpAuth.post(`${this.url}/Ombi/resetApi`, { headers: this.headers }).map(this.extractData) + .catch(this.handleError); } getEmby(): Observable { - return this.httpAuth.get(`${this.url}/Emby/`).map(this.extractData).catch(this.handleError) + return this.httpAuth.get(`${this.url}/Emby/`).map(this.extractData).catch(this.handleError); } saveEmby(settings: IEmbySettings): Observable { - return this.httpAuth.post(`${this.url}/Emby/`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth.post(`${this.url}/Emby/`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } getPlex(): Observable { - return this.httpAuth.get(`${this.url}/Plex/`).map(this.extractData).catch(this.handleError) + return this.httpAuth.get(`${this.url}/Plex/`).map(this.extractData).catch(this.handleError); } savePlex(settings: IPlexSettings): Observable { - return this.httpAuth.post(`${this.url}/Plex/`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth.post(`${this.url}/Plex/`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } getSonarr(): Observable { @@ -56,7 +67,8 @@ export class SettingsService extends ServiceAuthHelpers { } saveSonarr(settings: ISonarrSettings): Observable { - return this.httpAuth.post(`${this.url}/Sonarr`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth.post(`${this.url}/Sonarr`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } getRadarr(): Observable { @@ -65,16 +77,29 @@ export class SettingsService extends ServiceAuthHelpers { } saveRadarr(settings: IRadarrSettings): Observable { - return this.httpAuth.post(`${this.url}/Radarr`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth.post(`${this.url}/Radarr`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); + } + + + getAuthentication(): Observable { + return this.httpAuth.get(`${this.url}/Authentication`).map(this.extractData) + .catch(this.handleError); + } + + saveAuthentication(settings: IAuthenticationSettings): Observable { + return this.httpAuth.post(`${this.url}/Authentication`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } // Using http since we need it not to be authenticated to get the landing page settings getLandingPage(): Observable { - return this.nonAuthHttp.get(`${this.url}/LandingPage`).map(this.extractData).catch(this.handleError) + return this.nonAuthHttp.get(`${this.url}/LandingPage`).map(this.extractData).catch(this.handleError); } saveLandingPage(settings: ILandingPageSettings): Observable { - return this.httpAuth.post(`${this.url}/LandingPage`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth.post(`${this.url}/LandingPage`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } // Using http since we need it not to be authenticated to get the customization settings @@ -83,7 +108,8 @@ export class SettingsService extends ServiceAuthHelpers { } saveCustomization(settings: ICustomizationSettings): Observable { - return this.httpAuth.post(`${this.url}/customization`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth.post(`${this.url}/customization`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } getEmailNotificationSettings(): Observable { @@ -91,22 +117,46 @@ export class SettingsService extends ServiceAuthHelpers { } saveEmailNotificationSettings(settings: IEmailNotificationSettings): Observable { - return this.httpAuth.post(`${this.url}/notifications/email`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth + .post(`${this.url}/notifications/email`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } getDiscordNotificationSettings(): Observable { return this.httpAuth.get(`${this.url}/notifications/discord`).map(this.extractData).catch(this.handleError) } + getMattermostNotificationSettings(): Observable { + return this.httpAuth.get(`${this.url}/notifications/mattermost`).map(this.extractData).catch(this.handleError) + } + saveDiscordNotificationSettings(settings: IDiscordNotifcationSettings): Observable { - return this.httpAuth.post(`${this.url}/notifications/discord`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth + .post(`${this.url}/notifications/discord`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); + } + + saveMattermostNotificationSettings(settings: IMattermostNotifcationSettings): Observable { + return this.httpAuth + .post(`${this.url}/notifications/mattermost`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } getPushbulletNotificationSettings(): Observable { return this.httpAuth.get(`${this.url}/notifications/pushbullet`).map(this.extractData).catch(this.handleError) } + getPushoverNotificationSettings(): Observable { + return this.httpAuth.get(`${this.url}/notifications/pushover`).map(this.extractData).catch(this.handleError) + } savePushbulletNotificationSettings(settings: IPushbulletNotificationSettings): Observable { - return this.httpAuth.post(`${this.url}/notifications/pushbullet`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth + .post(`${this.url}/notifications/pushbullet`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); + } + savePushoverNotificationSettings(settings: IPushoverNotificationSettings): Observable { + return this.httpAuth + .post(`${this.url}/notifications/pushover`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } getSlackNotificationSettings(): Observable { @@ -114,6 +164,8 @@ export class SettingsService extends ServiceAuthHelpers { } saveSlackNotificationSettings(settings: ISlackNotificationSettings): Observable { - return this.httpAuth.post(`${this.url}/notifications/slack`, JSON.stringify(settings), { headers: this.headers }).map(this.extractData).catch(this.handleError) + return this.httpAuth + .post(`${this.url}/notifications/slack`, JSON.stringify(settings), { headers: this.headers }) + .map(this.extractData).catch(this.handleError); } } \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/emby/emby.component.html b/src/Ombi/ClientApp/app/settings/emby/emby.component.html index f64f7dbd0..1ecb4bb1c 100644 --- a/src/Ombi/ClientApp/app/settings/emby/emby.component.html +++ b/src/Ombi/ClientApp/app/settings/emby/emby.component.html @@ -24,7 +24,7 @@

    -
    +

    diff --git a/src/Ombi/ClientApp/app/settings/notifications/mattermost.component.html b/src/Ombi/ClientApp/app/settings/notifications/mattermost.component.html new file mode 100644 index 000000000..a8400535b --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/mattermost.component.html @@ -0,0 +1,76 @@ + + +
    +
    + Mattermost Notifications +
    +
    + +
    +
    + + +
    +
    + +
    +
    The Incoming Webhook Url is required
    +
    + +
    + Mattermost > Integrations > Incoming Webhook > Add Incoming Webhook. You will then have a Webhook + +
    + +
    +
    + + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    +
    +
    + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/notifications/mattermost.component.ts b/src/Ombi/ClientApp/app/settings/notifications/mattermost.component.ts new file mode 100644 index 000000000..30072057a --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/mattermost.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, Validators, FormBuilder } from '@angular/forms'; + +import { INotificationTemplates, IMattermostNotifcationSettings, NotificationType } from '../../interfaces/INotifcationSettings'; +import { SettingsService } from '../../services/settings.service'; +import { NotificationService } from "../../services/notification.service"; +import { TesterService } from "../../services/applications/tester.service"; + +@Component({ + templateUrl: './mattermost.component.html' +}) +export class MattermostComponent implements OnInit { + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private fb: FormBuilder, + private testerService : TesterService) { } + + NotificationType = NotificationType; + templates: INotificationTemplates[]; + + form: FormGroup; + + ngOnInit(): void { + this.settingsService.getMattermostNotificationSettings().subscribe(x => { + this.templates = x.notificationTemplates; + + this.form = this.fb.group({ + enabled: [x.enabled], + username: [x.username], + webhookUrl: [x.webhookUrl, [Validators.required]], + channel: [x.channel], + iconUrl:[x.iconUrl] + + }); + }); + } + + onSubmit(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Validation", "Please check your entered values"); + return; + } + + var settings = form.value; + settings.notificationTemplates = this.templates; + + this.settingsService.saveMattermostNotificationSettings(settings).subscribe(x => { + if (x) { + this.notificationService.success("Settings Saved", "Successfully saved the Mattermost settings"); + } else { + this.notificationService.success("Settings Saved", "There was an error when saving the Mattermost settings"); + } + }); + + } + + test(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Validation", "Please check your entered values"); + return; + } + + this.testerService.mattermostTest(form.value).subscribe(x => { + if (x) { + this.notificationService.success("Successful", "Successfully sent a Mattermost message, please check the discord channel"); + } else { + this.notificationService.success("Error", "There was an error when sending the Mattermost message. Please check your settings"); + } + }) + + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/notifications/pushbullet.component.html b/src/Ombi/ClientApp/app/settings/notifications/pushbullet.component.html index 4f4d0f62b..3b2dc30b8 100644 --- a/src/Ombi/ClientApp/app/settings/notifications/pushbullet.component.html +++ b/src/Ombi/ClientApp/app/settings/notifications/pushbullet.component.html @@ -2,7 +2,7 @@
    - Pushbyllet Notifications + Pushbullet Notifications
    diff --git a/src/Ombi/ClientApp/app/settings/notifications/pushover.component.html b/src/Ombi/ClientApp/app/settings/notifications/pushover.component.html new file mode 100644 index 000000000..e07316583 --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/pushover.component.html @@ -0,0 +1,59 @@ + + +
    +
    + Pushover Notifications +
    + + +
    +
    + + +
    +
    + +
    +
    The Access Token is required
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + +
    + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/notifications/pushover.component.ts b/src/Ombi/ClientApp/app/settings/notifications/pushover.component.ts new file mode 100644 index 000000000..f33fceda0 --- /dev/null +++ b/src/Ombi/ClientApp/app/settings/notifications/pushover.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, Validators, FormBuilder } from '@angular/forms'; + +import { INotificationTemplates, IPushoverNotificationSettings, NotificationType } from '../../interfaces/INotifcationSettings'; +import { SettingsService } from '../../services/settings.service'; +import { NotificationService } from "../../services/notification.service"; +import { TesterService } from "../../services/applications/tester.service"; + +@Component({ + templateUrl: './pushover.component.html', +}) +export class PushoverComponent implements OnInit { + constructor(private settingsService: SettingsService, + private notificationService: NotificationService, + private fb: FormBuilder, + private testerService : TesterService) { } + + NotificationType = NotificationType; + templates: INotificationTemplates[]; + + form: FormGroup; + + ngOnInit(): void { + this.settingsService.getPushoverNotificationSettings().subscribe(x => { + this.templates = x.notificationTemplates; + + this.form = this.fb.group({ + enabled: [x.enabled], + userToken: [x.userToken], + accessToken: [x.accessToken, [Validators.required]], + }); + }); + } + + onSubmit(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Validation", "Please check your entered values"); + return; + } + + var settings = form.value; + settings.notificationTemplates = this.templates; + + this.settingsService.savePushoverNotificationSettings(settings).subscribe(x => { + if (x) { + this.notificationService.success("Settings Saved", "Successfully saved the Pushover settings"); + } else { + this.notificationService.success("Settings Saved", "There was an error when saving the Pushover settings"); + } + }); + + } + + test(form: FormGroup) { + if (form.invalid) { + this.notificationService.error("Validation", "Please check your entered values"); + return; + } + + this.testerService.pushoverTest(form.value).subscribe(x => { + if (x) { + this.notificationService.success("Successful", "Successfully sent a Pushbullet message, please check the discord channel"); + } else { + this.notificationService.success("Error", "There was an error when sending the Pushbullet message. Please check your settings"); + } + }) + + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/app/settings/ombi/ombi.component.html b/src/Ombi/ClientApp/app/settings/ombi/ombi.component.html index 9f3896e2b..82e594a4b 100644 --- a/src/Ombi/ClientApp/app/settings/ombi/ombi.component.html +++ b/src/Ombi/ClientApp/app/settings/ombi/ombi.component.html @@ -11,7 +11,7 @@
    -
    +
    @@ -29,20 +29,20 @@
    - +
    diff --git a/src/Ombi/ClientApp/app/settings/ombi/ombi.component.ts b/src/Ombi/ClientApp/app/settings/ombi/ombi.component.ts index 5afe186e4..b7e3d762a 100644 --- a/src/Ombi/ClientApp/app/settings/ombi/ombi.component.ts +++ b/src/Ombi/ClientApp/app/settings/ombi/ombi.component.ts @@ -29,13 +29,17 @@ export class OmbiComponent implements OnInit { refreshApiKey() { - + this.settingsService.resetOmbiApi().subscribe(x => { + this.form.patchValue({ + apiKey: x + }); + }); } onSubmit(form: FormGroup) { if (form.invalid) { this.notificationService.error("Validation", "Please check your entered values"); - return + return; } this.settingsService.saveOmbi(form.value).subscribe(x => { if (x) { diff --git a/src/Ombi/ClientApp/app/settings/plex/plex.component.html b/src/Ombi/ClientApp/app/settings/plex/plex.component.html index 8f2b19328..4777c3538 100644 --- a/src/Ombi/ClientApp/app/settings/plex/plex.component.html +++ b/src/Ombi/ClientApp/app/settings/plex/plex.component.html @@ -27,7 +27,7 @@

    -
    +

    diff --git a/src/Ombi/ClientApp/app/settings/settings.module.ts b/src/Ombi/ClientApp/app/settings/settings.module.ts index f825a3cc0..1320bc6c3 100644 --- a/src/Ombi/ClientApp/app/settings/settings.module.ts +++ b/src/Ombi/ClientApp/app/settings/settings.module.ts @@ -22,6 +22,9 @@ import { CustomizationComponent } from './customization/customization.component' import { EmailNotificationComponent } from './notifications/emailnotification.component'; import { DiscordComponent } from './notifications/discord.component'; import { SlackComponent } from './notifications/slack.component'; +import { PushoverComponent } from './notifications/pushover.component'; +import { PushbulletComponent } from './notifications/pushbullet.component'; +import { MattermostComponent } from './notifications/mattermost.component'; import { NotificationTemplate } from './notifications/notificationtemplate.component'; import { SettingsMenuComponent } from './settingsmenu.component'; @@ -40,6 +43,9 @@ const routes: Routes = [ { path: 'Settings/Email', component: EmailNotificationComponent, canActivate: [AuthGuard] }, { path: 'Settings/Discord', component: DiscordComponent, canActivate: [AuthGuard] }, { path: 'Settings/Slack', component: SlackComponent, canActivate: [AuthGuard] }, + { path: 'Settings/Pushover', component: PushoverComponent, canActivate: [AuthGuard] }, + { path: 'Settings/Pushbullet', component: PushbulletComponent, canActivate: [AuthGuard] }, + { path: 'Settings/Mattermost', component: MattermostComponent, canActivate: [AuthGuard] }, ]; @NgModule({ @@ -72,6 +78,9 @@ const routes: Routes = [ EmailNotificationComponent, HumanizePipe, NotificationTemplate, + PushoverComponent, + MattermostComponent, + PushbulletComponent ], exports: [ RouterModule diff --git a/src/Ombi/ClientApp/app/settings/settingsmenu.component.html b/src/Ombi/ClientApp/app/settings/settingsmenu.component.html index 11791d7f4..34e3d52fd 100644 --- a/src/Ombi/ClientApp/app/settings/settingsmenu.component.html +++ b/src/Ombi/ClientApp/app/settings/settingsmenu.component.html @@ -47,7 +47,8 @@
  • Discord
  • Slack
  • Pushbullet
  • - +
  • Pushover
  • +
  • Mattermost
  • diff --git a/src/Ombi/Controllers/BaseV1ApiController.cs b/src/Ombi/Controllers/ApiV1Attribute.cs similarity index 100% rename from src/Ombi/Controllers/BaseV1ApiController.cs rename to src/Ombi/Controllers/ApiV1Attribute.cs diff --git a/src/Ombi/Controllers/External/PlexController.cs b/src/Ombi/Controllers/External/PlexController.cs index 170df0451..ec62d36f4 100644 --- a/src/Ombi/Controllers/External/PlexController.cs +++ b/src/Ombi/Controllers/External/PlexController.cs @@ -47,7 +47,7 @@ namespace Ombi.Controllers.External settings.Enable = true; settings.Servers = new List { new PlexServers{ -PlexAuthToken = result.user.authentication_token, + PlexAuthToken = result.user.authentication_token, Id = new Random().Next(), Ip = servers.LocalAddresses.Split(new []{','}, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(), MachineIdentifier = servers.MachineIdentifier, @@ -87,7 +87,7 @@ PlexAuthToken = result.user.authentication_token, /// The settings. /// [HttpPost("Libraries")] - public async Task GetPlexLibraries([FromBody] PlexServers settings) + public async Task GetPlexLibraries([FromBody] PlexServers settings) { var libs = await PlexApi.GetLibrarySections(settings.PlexAuthToken, settings.FullUri); diff --git a/src/Ombi/Controllers/External/TesterController.cs b/src/Ombi/Controllers/External/TesterController.cs index c4ad1a517..30ab1065d 100644 --- a/src/Ombi/Controllers/External/TesterController.cs +++ b/src/Ombi/Controllers/External/TesterController.cs @@ -27,13 +27,15 @@ namespace Ombi.Controllers.External /// The pushbullet. /// The slack. public TesterController(INotificationService service, IDiscordNotification notification, IEmailNotification emailN, - IPushbulletNotification pushbullet, ISlackNotification slack) + IPushbulletNotification pushbullet, ISlackNotification slack, IPushoverNotification po, IMattermostNotification mm) { Service = service; DiscordNotification = notification; EmailNotification = emailN; PushbulletNotification = pushbullet; SlackNotification = slack; + PushoverNotification = po; + MattermostNotification = mm; } private INotificationService Service { get; } @@ -41,6 +43,8 @@ namespace Ombi.Controllers.External private IEmailNotification EmailNotification { get; } private IPushbulletNotification PushbulletNotification { get; } private ISlackNotification SlackNotification { get; } + private IPushoverNotification PushoverNotification { get; } + private IMattermostNotification MattermostNotification { get; } /// @@ -73,6 +77,36 @@ namespace Ombi.Controllers.External return true; } + /// + /// Sends a test message to Pushover using the provided settings + /// + /// The settings. + /// + [HttpPost("pushover")] + public bool Pushover([FromBody] PushoverSettings settings) + { + settings.Enabled = true; + PushoverNotification.NotifyAsync( + new NotificationOptions { NotificationType = NotificationType.Test, RequestId = -1 }, settings); + + return true; + } + + /// + /// Sends a test message to mattermost using the provided settings + /// + /// The settings. + /// + [HttpPost("mattermost")] + public bool Mattermost([FromBody] MattermostNotificationSettings settings) + { + settings.Enabled = true; + MattermostNotification.NotifyAsync( + new NotificationOptions { NotificationType = NotificationType.Test, RequestId = -1 }, settings); + + return true; + } + /// /// Sends a test message to Slack using the provided settings diff --git a/src/Ombi/Controllers/IdentityController.cs b/src/Ombi/Controllers/IdentityController.cs index 8a82d2d19..fc329ecf4 100644 --- a/src/Ombi/Controllers/IdentityController.cs +++ b/src/Ombi/Controllers/IdentityController.cs @@ -89,16 +89,32 @@ namespace Ombi.Controllers var result = await UserManager.CreateAsync(userToCreate, user.Password); if (result.Succeeded) { - if (!await RoleManager.RoleExistsAsync(OmbiRoles.Admin)) - { - await RoleManager.CreateAsync(new IdentityRole(OmbiRoles.Admin)); - } + await CreateRoles(); await UserManager.AddToRoleAsync(userToCreate, OmbiRoles.Admin); } return true; } + private async Task CreateRoles() + { + await CreateRole(OmbiRoles.AutoApproveMovie); + await CreateRole(OmbiRoles.Admin); + await CreateRole(OmbiRoles.AutoApproveTv); + await CreateRole(OmbiRoles.PowerUser); + await CreateRole(OmbiRoles.RequestMovie); + await CreateRole(OmbiRoles.RequestTv); + await CreateRole(OmbiRoles.Disabled); + } + + private async Task CreateRole(string role) + { + if (!await RoleManager.RoleExistsAsync(role)) + { + await RoleManager.CreateAsync(new IdentityRole(role)); + } + } + /// /// Gets all users. /// diff --git a/src/Ombi/Controllers/SettingsController.cs b/src/Ombi/Controllers/SettingsController.cs index f53d1645b..a8946eab0 100644 --- a/src/Ombi/Controllers/SettingsController.cs +++ b/src/Ombi/Controllers/SettingsController.cs @@ -65,6 +65,16 @@ namespace Ombi.Controllers return await Save(ombi); } + [HttpPost("ombi/resetApi")] + public async Task ResetApiKey() + { + var currentSettings = await Get(); + currentSettings.ApiKey = Guid.NewGuid().ToString("N"); + await Save(currentSettings); + + return currentSettings.ApiKey; + } + /// /// Gets the Plex Settings. /// @@ -182,6 +192,27 @@ namespace Ombi.Controllers return await Get(); } + /// + /// Save the Authentication settings. + /// + /// The settings. + /// + [HttpPost("authentication")] + public async Task AuthenticationsSettings([FromBody]AuthenticationSettings settings) + { + return await Save(settings); + } + + /// + /// Gets the Authentication Settings. + /// + /// + [HttpGet("authentication")] + public async Task AuthenticationsSettings() + { + return await Get(); + } + /// /// Save the Radarr settings. /// @@ -295,6 +326,40 @@ namespace Ombi.Controllers return model; } + /// + /// Saves the pushover notification settings. + /// + /// The model. + /// + [HttpPost("notifications/pushover")] + public async Task PushoverNotificationSettings([FromBody] PushoverNotificationViewModel model) + { + // Save the email settings + var settings = Mapper.Map(model); + var result = await Save(settings); + + // Save the templates + await TemplateRepository.UpdateRange(model.NotificationTemplates); + + return result; + } + + /// + /// Gets the pushover Notification Settings. + /// + /// + [HttpGet("notifications/pushover")] + public async Task PushoverNotificationSettings() + { + var settings = await Get(); + var model = Mapper.Map(settings); + + // Lookup to see if we have any templates saved + model.NotificationTemplates = await BuildTemplates(NotificationAgent.Pushover); + + return model; + } + /// /// Saves the slack notification settings. @@ -330,6 +395,40 @@ namespace Ombi.Controllers return model; } + /// + /// Saves the Mattermost notification settings. + /// + /// The model. + /// + [HttpPost("notifications/mattermost")] + public async Task MattermostNotificationSettings([FromBody] MattermostNotificationsViewModel model) + { + // Save the email settings + var settings = Mapper.Map(model); + var result = await Save(settings); + + // Save the templates + await TemplateRepository.UpdateRange(model.NotificationTemplates); + + return result; + } + + /// + /// Gets the Mattermost Notification Settings. + /// + /// + [HttpGet("notifications/mattermost")] + public async Task MattermostNotificationSettings() + { + var settings = await Get(); + var model = Mapper.Map(settings); + + // Lookup to see if we have any templates saved + model.NotificationTemplates = await BuildTemplates(NotificationAgent.Mattermost); + + return model; + } + private async Task> BuildTemplates(NotificationAgent agent) { var templates = await TemplateRepository.GetAllTemplates(agent); diff --git a/src/Ombi/Controllers/TokenController.cs b/src/Ombi/Controllers/TokenController.cs index fa10ecf77..fc7af8ae8 100644 --- a/src/Ombi/Controllers/TokenController.cs +++ b/src/Ombi/Controllers/TokenController.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; +using Ombi.Core.Claims; using Ombi.Models; using Ombi.Models.Identity; using Ombi.Store.Entities; @@ -58,6 +59,11 @@ namespace Ombi.Controllers { var roles = await _userManager.GetRolesAsync(user); + if (roles.Contains(OmbiRoles.Disabled)) + { + return new UnauthorizedResult(); + } + var claims = new List { new Claim(JwtRegisteredClaimNames.Sub, user.UserName), diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index b7a20b914..cf5527676 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -4,6 +4,7 @@ netcoreapp1.1 win10-x64;osx.10.12-x64;ubuntu.16.04-x64;debian.8-x64;centos.7-x64; True + 2.3 diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 480551082..7b715f0ef 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Linq; using System.Security.Principal; using System.Text; using AutoMapper; @@ -31,8 +30,6 @@ using Ombi.Store.Context; using Ombi.Store.Entities; using Serilog; using Serilog.Events; -using StackExchange.Profiling; -using StackExchange.Profiling.Storage; using Swashbuckle.AspNetCore.Swagger; namespace Ombi @@ -80,17 +77,6 @@ namespace Ombi .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - - //services.AddIdentityServer() - // .AddTemporarySigningCredential() - // .AddInMemoryPersistedGrants() - // .AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources()) - // .AddInMemoryApiResources(IdentityConfig.GetApiResources()) - // .AddInMemoryClients(IdentityConfig.GetClients()) - // .AddAspNetIdentity() - // .Services.AddTransient() - // .AddTransient(); - services.Configure(options => { options.Password.RequireDigit = false; @@ -144,8 +130,6 @@ namespace Ombi c.DescribeAllParametersInCamelCase(); }); - - services.AddSingleton(); services.AddScoped(sp => sp.GetService().HttpContext.User); @@ -156,21 +140,10 @@ namespace Ombi services.AddHangfire(x => { - x.UseMemoryStorage(new MemoryStorageOptions()); - //x.UseSQLiteStorage("Data Source=Ombi.db;"); - x.UseActivator(new IoCJobActivator(services.BuildServiceProvider())); }); - -#if DEBUG - // Note .AddMiniProfiler() returns a IMiniProfilerBuilder for easy intellisense - //services.AddMiniProfiler(); -#endif - // Make sure you have memory cache available unless you're using another storage provider - services.AddMemoryCache(); - } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -181,20 +154,6 @@ namespace Ombi var ctx = (IOmbiContext)app.ApplicationServices.GetService(typeof(IOmbiContext)); - // Get the url - var url = ctx.ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.Url); - var port = ctx.ApplicationConfigurations.FirstOrDefault(x => x.Type == ConfigurationTypes.Port); - - Console.WriteLine($"Using Url {url.Value}:{port.Value} for Identity Server"); - app.UseIdentity(); - -#if !DEBUG - var audience = $"{url.Value}:{port.Value}"; -#else - - var audience = $"http://localhost:52038/"; -#endif - var tokenValidationParameters = new TokenValidationParameters { @@ -212,28 +171,10 @@ namespace Ombi { Audience = "Ombi", AutomaticAuthenticate = true, - TokenValidationParameters = tokenValidationParameters + TokenValidationParameters = tokenValidationParameters, + }); - // app.UseIdentityServer(); - // app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions - // { - //#if !DEBUG - // Authority = $"{url.Value}:{port.Value}", - //#else - // Authority = $"http://localhost:52038/", - //#endif - // ApiName = "api", - // ApiSecret = "secret", - - // EnableCaching = true, - // CacheDuration = TimeSpan.FromMinutes(10), // that's the default - // RequireHttpsMetadata = options.Value.UseHttps, // FOR DEV set to false - // AutomaticAuthenticate = true, - // AutomaticChallenge = true, - - - // }); loggerFactory.AddSerilog(); @@ -244,20 +185,6 @@ namespace Ombi { HotModuleReplacement = true }); - - //app.UseMiniProfiler(new MiniProfilerOptions - //{ - // // Path to use for profiler URLs - // RouteBasePath = "~/profiler", - - // // (Optional) Control which SQL formatter to use - // // (default is no formatter) - // SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter(), - - // // (Optional) Control storage - // // (default is 30 minutes in MemoryCacheStorage) - // Storage = new MemoryCacheStorage(cache, TimeSpan.FromMinutes(60)), - //}); } app.UseHangfireServer();