diff --git a/.azuredevops/pipelines/templates/variables.yml b/.azuredevops/pipelines/templates/variables.yml index 205e68eaa..e90ecc049 100644 --- a/.azuredevops/pipelines/templates/variables.yml +++ b/.azuredevops/pipelines/templates/variables.yml @@ -27,4 +27,4 @@ variables: value: "4.0.$(Build.BuildId)" - name: isMain - value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))] \ No newline at end of file + value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/develop'), eq(variables['Build.SourceBranch'], 'refs/heads/master'))] diff --git a/.gitignore b/.gitignore index 1a1a229b8..fc31d399b 100644 --- a/.gitignore +++ b/.gitignore @@ -247,5 +247,6 @@ _Pvt_Extensions # Ignore local vscode config *.vscode /src/Ombi/database.json +/src/Ombi/databases.json /src/Ombi/healthchecksdb /src/Ombi/ClientApp/package-lock.json diff --git a/README.md b/README.md index 2ff3e0ccd..60f886a72 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ Search the existing requests to see if your suggestion has already been submitte ___ Get it on Google Play
-_**Note:** There is no longer an iOS app due to complications outside of our control._ - +Get it on the App Store +
# Features Here are some of the features Ombi has: diff --git a/src/Ombi.Api.Plex/IPlexApi.cs b/src/Ombi.Api.Plex/IPlexApi.cs index c79ec50c9..a4597765e 100644 --- a/src/Ombi.Api.Plex/IPlexApi.cs +++ b/src/Ombi.Api.Plex/IPlexApi.cs @@ -23,7 +23,7 @@ namespace Ombi.Api.Plex Task GetUsers(string authToken); Task GetAccount(string authToken); Task GetRecentlyAdded(string authToken, string uri, string sectionId); - Task GetPin(int pinId); + Task GetPin(int pinId); Task GetOAuthUrl(string code, string applicationUrl); Task AddUser(string emailAddress, string serverId, string authToken, int[] libs); } diff --git a/src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs b/src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs index e65cd91d4..3ab857ed3 100644 --- a/src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs +++ b/src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs @@ -1,7 +1,14 @@ using System; +using System.Collections.Generic; namespace Ombi.Api.Plex.Models.OAuth { + public class OAuthContainer + { + public OAuthPin Result { get; set; } + public OAuthErrorsContainer Errors { get; set; } + } + public class OAuthPin { public int id { get; set; } @@ -24,4 +31,15 @@ namespace Ombi.Api.Plex.Models.OAuth public string coordinates { get; set; } } + public class OAuthErrorsContainer + { + public List errors { get; set; } + } + + public class OAuthErrors + { + public int code { get; set; } + public string message { get; set; } + } + } \ No newline at end of file diff --git a/src/Ombi.Api.Plex/PlexApi.cs b/src/Ombi.Api.Plex/PlexApi.cs index 0d6356457..b80534bb9 100644 --- a/src/Ombi.Api.Plex/PlexApi.cs +++ b/src/Ombi.Api.Plex/PlexApi.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; -using System.Reflection; using System.Threading.Tasks; +using Newtonsoft.Json; using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models.Friends; using Ombi.Api.Plex.Models.OAuth; @@ -208,12 +208,28 @@ namespace Ombi.Api.Plex return await Api.Request(request); } - public async Task GetPin(int pinId) + public async Task GetPin(int pinId) { var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get); await AddHeaders(request); - return await Api.Request(request); + var response = await Api.RequestContent(request); + + if (response.Contains("errors")) + { + var errors = JsonConvert.DeserializeObject(response, Ombi.Api.Api.Settings); + return new OAuthContainer + { + Errors = errors + }; + } + + var pinResult = JsonConvert.DeserializeObject(response, Ombi.Api.Api.Settings); + + return new OAuthContainer + { + Result = pinResult + }; } public async Task GetOAuthUrl(string code, string applicationUrl) diff --git a/src/Ombi.Api.Radarr/Models/V2/MovieResponse.cs b/src/Ombi.Api.Radarr/Models/V2/MovieResponse.cs index 74d5c75ea..6eb2f1c5a 100644 --- a/src/Ombi.Api.Radarr/Models/V2/MovieResponse.cs +++ b/src/Ombi.Api.Radarr/Models/V2/MovieResponse.cs @@ -1,39 +1,122 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Net.Mime; namespace Ombi.Api.Radarr.Models -{ +{ public class MovieResponse { public string title { get; set; } + public string originalTitle { get; set; } + public Alternatetitle[] alternateTitles { get; set; } + public int secondaryYearSourceId { get; set; } public string sortTitle { get; set; } - public double sizeOnDisk { get; set; } + public long sizeOnDisk { get; set; } public string status { get; set; } public string overview { get; set; } - public string inCinemas { get; set; } - public string physicalRelease { get; set; } - public List images { get; set; } + public DateTime inCinemas { get; set; } + public DateTime physicalRelease { get; set; } + public DateTime digitalRelease { get; set; } + public Image[] images { get; set; } public string website { get; set; } - public bool downloaded { get; set; } public int year { get; set; } public bool hasFile { get; set; } public string youTubeTrailerId { get; set; } public string studio { get; set; } public string path { get; set; } - public int profileId { get; set; } - public string minimumAvailability { get; set; } + public int qualityProfileId { get; set; } public bool monitored { get; set; } + public string minimumAvailability { get; set; } + public bool isAvailable { get; set; } + public string folderName { get; set; } public int runtime { get; set; } - public string lastInfoSync { get; set; } public string cleanTitle { get; set; } public string imdbId { get; set; } public int tmdbId { get; set; } public string titleSlug { get; set; } - public List genres { get; set; } - public List tags { get; set; } - public string added { get; set; } + public string certification { get; set; } + public string[] genres { get; set; } + public object[] tags { get; set; } + public DateTime added { get; set; } public Ratings ratings { get; set; } - //public List alternativeTitles { get; set; } - public int qualityProfileId { get; set; } + public Moviefile movieFile { get; set; } + public Collection collection { get; set; } + public int id { get; set; } + } + + + public class Moviefile + { + public int movieId { get; set; } + public string relativePath { get; set; } + public string path { get; set; } + public long size { get; set; } + public DateTime dateAdded { get; set; } + public string sceneName { get; set; } + public int indexerFlags { get; set; } + public V3.Quality quality { get; set; } + public Mediainfo mediaInfo { get; set; } + public string originalFilePath { get; set; } + public bool qualityCutoffNotMet { get; set; } + public Language[] languages { get; set; } + public string releaseGroup { get; set; } + public string edition { get; set; } + public int id { get; set; } + } + + public class Revision + { + public int version { get; set; } + public int real { get; set; } + public bool isRepack { get; set; } + } + + public class Mediainfo + { + public string audioAdditionalFeatures { get; set; } + public int audioBitrate { get; set; } + public float audioChannels { get; set; } + public string audioCodec { get; set; } + public string audioLanguages { get; set; } + public int audioStreamCount { get; set; } + public int videoBitDepth { get; set; } + public int videoBitrate { get; set; } + public string videoCodec { get; set; } + public float videoFps { get; set; } + public string resolution { get; set; } + public string runTime { get; set; } + public string scanType { get; set; } + public string subtitles { get; set; } + } + + public class Language + { + public int id { get; set; } + public string name { get; set; } + } + + public class Collection + { + public string name { get; set; } + public int tmdbId { get; set; } + public object[] images { get; set; } + } + + public class Alternatetitle + { + public string sourceType { get; set; } + public int movieId { get; set; } + public string title { get; set; } + public int sourceId { get; set; } + public int votes { get; set; } + public int voteCount { get; set; } + public Language1 language { get; set; } + public int id { get; set; } + } + + public class Language1 + { public int id { get; set; } + public string name { get; set; } } -} +} \ No newline at end of file diff --git a/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs b/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs index b56049c9a..09e985f43 100644 --- a/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs +++ b/src/Ombi.Api.Radarr/Models/V2/RadarrAddMovie.cs @@ -28,5 +28,6 @@ namespace Ombi.Api.Radarr.Models public string titleSlug { get; set; } public int year { get; set; } public string minimumAvailability { get; set; } + public long sizeOnDisk { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Api.Radarr/RadarrApi.cs b/src/Ombi.Api.Radarr/RadarrApi.cs index b461ccda8..e1879bba3 100644 --- a/src/Ombi.Api.Radarr/RadarrApi.cs +++ b/src/Ombi.Api.Radarr/RadarrApi.cs @@ -82,7 +82,8 @@ namespace Ombi.Api.Radarr titleSlug = title + year, monitored = true, year = year, - minimumAvailability = minimumAvailability + minimumAvailability = minimumAvailability, + sizeOnDisk = 0 }; if (searchNow) diff --git a/src/Ombi.Api.Radarr/RadarrV3Api.cs b/src/Ombi.Api.Radarr/RadarrV3Api.cs index dd1d0b279..9e7e0f4c2 100644 --- a/src/Ombi.Api.Radarr/RadarrV3Api.cs +++ b/src/Ombi.Api.Radarr/RadarrV3Api.cs @@ -41,7 +41,7 @@ namespace Ombi.Api.Radarr public async Task SystemStatus(string apiKey, string baseUrl) { - var request = new Request("/api/v3/status", baseUrl, HttpMethod.Get); + var request = new Request("/api/v3/system/status", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); return await Api.Request(request); @@ -65,7 +65,7 @@ namespace Ombi.Api.Radarr public async Task UpdateMovie(MovieResponse movie, string apiKey, string baseUrl) { - var request = new Request($"/api/v3/movie/", baseUrl, HttpMethod.Put); + var request = new Request($"/api/v3/movie/{movie.id}", baseUrl, HttpMethod.Put); AddHeaders(request, apiKey); request.AddJsonBody(movie); @@ -85,7 +85,8 @@ namespace Ombi.Api.Radarr titleSlug = title + year, monitored = true, year = year, - minimumAvailability = minimumAvailability + minimumAvailability = minimumAvailability, + sizeOnDisk = 0 }; if (searchNow) diff --git a/src/Ombi.Api.Webhook/WebhookApi.cs b/src/Ombi.Api.Webhook/WebhookApi.cs index 8b6b35ca0..f2faaa18b 100644 --- a/src/Ombi.Api.Webhook/WebhookApi.cs +++ b/src/Ombi.Api.Webhook/WebhookApi.cs @@ -19,7 +19,7 @@ namespace Ombi.Api.Webhook public async Task PushAsync(string baseUrl, string accessToken, IDictionary parameters) { - var request = new Request("/", baseUrl, HttpMethod.Post); + var request = new Request("", baseUrl, HttpMethod.Post); if (!string.IsNullOrWhiteSpace(accessToken)) { diff --git a/src/Ombi.Api/Api.cs b/src/Ombi.Api/Api.cs index 764da5a13..66dec8b0b 100644 --- a/src/Ombi.Api/Api.cs +++ b/src/Ombi.Api/Api.cs @@ -16,14 +16,14 @@ namespace Ombi.Api { public class Api : IApi { - public Api(ILogger log, IOmbiHttpClient client) + public Api(ILogger log, HttpClient client) { Logger = log; _client = client; } private ILogger Logger { get; } - private readonly IOmbiHttpClient _client; + private readonly HttpClient _client; public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { @@ -73,7 +73,7 @@ namespace Ombi.Api } // do something with the response - var receivedString = await httpResponseMessage.Content.ReadAsStringAsync(); + var receivedString = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken); LogDebugContent(receivedString); if (request.ContentType == ContentType.Json) { diff --git a/src/Ombi.Api/IOmbiHttpClient.cs b/src/Ombi.Api/IOmbiHttpClient.cs deleted file mode 100644 index 1f6ff9514..000000000 --- a/src/Ombi.Api/IOmbiHttpClient.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Ombi.Api -{ - public interface IOmbiHttpClient - { - Task SendAsync(HttpRequestMessage request); - Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); - Task GetStringAsync(Uri requestUri); - } -} \ No newline at end of file diff --git a/src/Ombi.Api/Ombi.Api.csproj b/src/Ombi.Api/Ombi.Api.csproj index 98da29c2a..91821c83b 100644 --- a/src/Ombi.Api/Ombi.Api.csproj +++ b/src/Ombi.Api/Ombi.Api.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Ombi.Api/OmbiHttpClient.cs b/src/Ombi.Api/OmbiHttpClient.cs deleted file mode 100644 index 978476cf7..000000000 --- a/src/Ombi.Api/OmbiHttpClient.cs +++ /dev/null @@ -1,110 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2017 Jamie Rees -// File: OmbiHttpClient.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion - -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Ombi.Core.Settings; -using Ombi.Helpers; -using Ombi.Settings.Settings.Models; - -namespace Ombi.Api -{ - /// - /// The purpose of this class is simple, keep one instance of the HttpClient in play. - /// There are many articles related to when using multiple HttpClient's keeping the socket in a WAIT state - /// https://blogs.msdn.microsoft.com/alazarev/2017/12/29/disposable-finalizers-and-httpclient/ - /// https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ - /// - public class OmbiHttpClient : IOmbiHttpClient - { - public OmbiHttpClient(ICacheService cache, ISettingsService s) - { - _cache = cache; - _settings = s; - _runtimeVersion = AssemblyHelper.GetRuntimeVersion(); - } - - private static HttpClient _client; - private static HttpMessageHandler _handler; - - private readonly ICacheService _cache; - private readonly ISettingsService _settings; - private readonly string _runtimeVersion; - - - public async Task SendAsync(HttpRequestMessage request) - { - await Setup(); - return await _client.SendAsync(request); - } - - public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - await Setup(); - return await _client.SendAsync(request, cancellationToken); - } - - public async Task GetStringAsync(Uri requestUri) - { - await Setup(); - return await _client.GetStringAsync(requestUri); - } - - private async Task Setup() - { - if (_client == null) - { - if (_handler == null) - { - // Get the handler - _handler = await GetHandler(); - } - _client = new HttpClient(_handler); - _client.DefaultRequestHeaders.Add("User-Agent", $"Ombi/{_runtimeVersion} (https://ombi.io/)"); - } - } - - private async Task GetHandler() - { - if (_cache == null) - { - return new HttpClientHandler(); - } - var settings = await _cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await _settings.GetSettingsAsync(), DateTime.Now.AddHours(1)); - if (settings.IgnoreCertificateErrors) - { - return new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true, - }; - } - return new HttpClientHandler(); - } - } -} \ No newline at end of file diff --git a/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs b/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs index 805440837..3c651b167 100644 --- a/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs +++ b/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs @@ -42,8 +42,9 @@ namespace Ombi.Core.Tests.Engine.V2 var cache = new Mock(); var ombiSettings = new Mock>(); var requestSubs = new Mock>(); + var mediaCache = new Mock(); _engine = new MovieRequestEngine(movieApi.Object, requestService.Object, user.Object, notificationHelper.Object, rules.Object, movieSender.Object, - logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object); + logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object, mediaCache.Object); } [Test] diff --git a/src/Ombi.Core.Tests/Rule/Request/ExistingPlexRequestRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/ExistingPlexRequestRuleTests.cs new file mode 100644 index 000000000..f5a362303 --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Request/ExistingPlexRequestRuleTests.cs @@ -0,0 +1,209 @@ +using MockQueryable.Moq; +using Moq; +using NUnit.Framework; +using Ombi.Core.Rule.Rules.Request; +using Ombi.Store.Entities; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Core.Tests.Rule.Request +{ + [TestFixture] + public class ExistingPlexRequestRuleTests + { + private ExistingPlexRequestRule Rule; + private Mock PlexContentRepo; + + [SetUp] + public void SetUp() + { + PlexContentRepo = new Mock(); + Rule = new ExistingPlexRequestRule(PlexContentRepo.Object); + } + + [Test] + public async Task RequestShow_DoesNotExistAtAll_IsSuccessful() + { + PlexContentRepo.Setup(x => x.GetAll()).Returns(new List().AsQueryable().BuildMock().Object); + var req = new ChildRequests + { + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + } + }, + SeasonNumber = 1 + } + } + }; + var result = await Rule.Execute(req); + + + Assert.That(result.Success, Is.True); + } + + [Test] + public async Task RequestShow_AllEpisodesAreaRequested_IsNotSuccessful() + { + SetupMockData(); + + var req = new ChildRequests + { + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + }, + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 2, + }, + }, + SeasonNumber = 1 + } + }, + Id = 1, + }; + var result = await Rule.Execute(req); + + + Assert.That(result.Success, Is.False); + } + + + [Test] + public async Task RequestShow_SomeEpisodesAreaRequested_IsSuccessful() + { + SetupMockData(); + + var req = new ChildRequests + { + RequestType = RequestType.TvShow, + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + }, + new EpisodeRequests + { + Id = 2, + EpisodeNumber = 2, + }, + new EpisodeRequests + { + Id = 3, + EpisodeNumber = 3, + }, + }, + SeasonNumber = 1 + } + }, + Id = 1, + }; + var result = await Rule.Execute(req); + + + Assert.That(result.Success, Is.True); + + var episodes = req.SeasonRequests.SelectMany(x => x.Episodes); + Assert.That(episodes.Count() == 1, "We didn't remove the episodes that have already been requested!"); + Assert.That(episodes.First().EpisodeNumber == 3, "We removed the wrong episode"); + } + + [Test] + public async Task RequestShow_NewSeasonRequest_IsSuccessful() + { + SetupMockData(); + + var req = new ChildRequests + { + RequestType = RequestType.TvShow, + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + }, + new EpisodeRequests + { + Id = 2, + EpisodeNumber = 2, + }, + new EpisodeRequests + { + Id = 3, + EpisodeNumber = 3, + }, + }, + SeasonNumber = 2 + } + }, + Id = 1, + }; + var result = await Rule.Execute(req); + + Assert.That(result.Success, Is.True); + } + + private void SetupMockData() + { + var childRequests = new List + { + new PlexServerContent + { + Type = PlexMediaTypeEntity.Show, + TheMovieDbId = "1", + Title = "Test", + ReleaseYear = "2001", + Episodes = new List + { + new PlexEpisode + { + EpisodeNumber = 1, + Id = 1, + SeasonNumber = 1, + }, + new PlexEpisode + { + EpisodeNumber = 2, + Id = 2, + SeasonNumber = 1, + }, + } + } + }; + PlexContentRepo.Setup(x => x.GetAll()).Returns(childRequests.AsQueryable().BuildMock().Object); + } + } +} diff --git a/src/Ombi.Core.Tests/Rule/Request/ExistingTvRequestRuleTests.cs b/src/Ombi.Core.Tests/Rule/Request/ExistingTvRequestRuleTests.cs new file mode 100644 index 000000000..d5ca903bf --- /dev/null +++ b/src/Ombi.Core.Tests/Rule/Request/ExistingTvRequestRuleTests.cs @@ -0,0 +1,215 @@ +using MockQueryable.Moq; +using Moq; +using NUnit.Framework; +using Ombi.Core.Rule.Rules.Request; +using Ombi.Store.Entities; +using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository.Requests; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Core.Tests.Rule.Request +{ + [TestFixture] + public class ExistingTvRequestRuleTests + { + private ExistingTvRequestRule Rule; + private Mock TvRequestRepo; + + [SetUp] + public void SetUp() + { + TvRequestRepo = new Mock(); + Rule = new ExistingTvRequestRule(TvRequestRepo.Object); + } + + [Test] + public async Task RequestShow_DoesNotExistAtAll_IsSuccessful() + { + TvRequestRepo.Setup(x => x.GetChild()).Returns(new List().AsQueryable().BuildMock().Object); + var req = new ChildRequests + { + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + } + }, + SeasonNumber = 1 + } + } + }; + var result = await Rule.Execute(req); + + + Assert.That(result.Success, Is.True); + } + + [Test] + public async Task RequestShow_AllEpisodesAreaRequested_IsNotSuccessful() + { + SetupMockData(); + + var req = new ChildRequests + { + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + }, + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 2, + }, + }, + SeasonNumber = 1 + } + }, + Id = 1, + }; + var result = await Rule.Execute(req); + + + Assert.That(result.Success, Is.False); + } + + + [Test] + public async Task RequestShow_SomeEpisodesAreaRequested_IsSuccessful() + { + SetupMockData(); + + var req = new ChildRequests + { + RequestType = RequestType.TvShow, + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + }, + new EpisodeRequests + { + Id = 2, + EpisodeNumber = 2, + }, + new EpisodeRequests + { + Id = 3, + EpisodeNumber = 3, + }, + }, + SeasonNumber = 1 + } + }, + Id = 1, + }; + var result = await Rule.Execute(req); + + + Assert.That(result.Success, Is.True); + + var episodes = req.SeasonRequests.SelectMany(x => x.Episodes); + Assert.That(episodes.Count() == 1, "We didn't remove the episodes that have already been requested!"); + Assert.That(episodes.First().EpisodeNumber == 3, "We removed the wrong episode"); + } + + [Test] + public async Task RequestShow_NewSeasonRequest_IsSuccessful() + { + SetupMockData(); + + var req = new ChildRequests + { + RequestType = RequestType.TvShow, + SeasonRequests = new List + { + new SeasonRequests + { + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + }, + new EpisodeRequests + { + Id = 2, + EpisodeNumber = 2, + }, + new EpisodeRequests + { + Id = 3, + EpisodeNumber = 3, + }, + }, + SeasonNumber = 2 + } + }, + Id = 1, + }; + var result = await Rule.Execute(req); + + Assert.That(result.Success, Is.True); + } + + private void SetupMockData() + { + var childRequests = new List + { + new ChildRequests + { + ParentRequest = new TvRequests + { + Id = 1, + ExternalProviderId = 1, + }, + SeasonRequests = new List + { + new SeasonRequests + { + Id = 1, + SeasonNumber = 1, + Episodes = new List + { + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 1, + }, + new EpisodeRequests + { + Id = 1, + EpisodeNumber = 2, + } + } + } + } + } + }; + TvRequestRepo.Setup(x => x.GetChild()).Returns(childRequests.AsQueryable().BuildMock().Object); + } + } +} diff --git a/src/Ombi.Core.Tests/Rule/Search/LidarrAlbumCacheRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/LidarrAlbumCacheRuleTests.cs index 27dbee614..7bf84d05f 100644 --- a/src/Ombi.Core.Tests/Rule/Search/LidarrAlbumCacheRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/LidarrAlbumCacheRuleTests.cs @@ -30,7 +30,7 @@ namespace Ombi.Core.Tests.Rule.Search public async Task Should_Not_Be_Monitored_Or_Available() { var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.False(request.Approved); @@ -49,7 +49,7 @@ namespace Ombi.Core.Tests.Rule.Search } }.AsQueryable()); var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.False(request.Approved); @@ -71,7 +71,7 @@ namespace Ombi.Core.Tests.Rule.Search } }.AsQueryable()); var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.False(request.Approved); @@ -93,7 +93,7 @@ namespace Ombi.Core.Tests.Rule.Search } }.AsQueryable()); var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.False(request.Approved); @@ -114,7 +114,7 @@ namespace Ombi.Core.Tests.Rule.Search } }.AsQueryable()); var request = new SearchAlbumViewModel { ForeignAlbumId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.False(request.Approved); diff --git a/src/Ombi.Core.Tests/Rule/Search/LidarrArtistCacheRuleTests.cs b/src/Ombi.Core.Tests/Rule/Search/LidarrArtistCacheRuleTests.cs index c17400acb..e13d8d1cd 100644 --- a/src/Ombi.Core.Tests/Rule/Search/LidarrArtistCacheRuleTests.cs +++ b/src/Ombi.Core.Tests/Rule/Search/LidarrArtistCacheRuleTests.cs @@ -29,7 +29,7 @@ namespace Ombi.Core.Tests.Rule.Search public async Task Should_Not_Be_Monitored() { var request = new SearchArtistViewModel { ForignArtistId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.False(request.Monitored); @@ -46,7 +46,7 @@ namespace Ombi.Core.Tests.Rule.Search } }.AsQueryable()); var request = new SearchArtistViewModel { ForignArtistId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.True(request.Monitored); @@ -64,7 +64,7 @@ namespace Ombi.Core.Tests.Rule.Search } }.AsQueryable()); var request = new SearchArtistViewModel { ForignArtistId = "abc" }; - var result = await Rule.Execute(request); + var result = await Rule.Execute(request, string.Empty); Assert.True(result.Success); Assert.True(request.Monitored); diff --git a/src/Ombi.Core/Authentication/PlexOAuthManager.cs b/src/Ombi.Core/Authentication/PlexOAuthManager.cs index 76b1b5d97..78f33cee8 100644 --- a/src/Ombi.Core/Authentication/PlexOAuthManager.cs +++ b/src/Ombi.Core/Authentication/PlexOAuthManager.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Ombi.Api.Plex; using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models.OAuth; @@ -11,24 +13,37 @@ namespace Ombi.Core.Authentication { public class PlexOAuthManager : IPlexOAuthManager { - public PlexOAuthManager(IPlexApi api, ISettingsService settings) + public PlexOAuthManager(IPlexApi api, ISettingsService settings, ILogger logger) { _api = api; _customizationSettingsService = settings; + _logger = logger; } private readonly IPlexApi _api; private readonly ISettingsService _customizationSettingsService; + private readonly ILogger _logger; public async Task GetAccessTokenFromPin(int pinId) { var pin = await _api.GetPin(pinId); - if (pin.expiresAt < DateTime.UtcNow) + if (pin.Errors != null) { + foreach (var err in pin.Errors?.errors ?? new List()) + { + _logger.LogError($"Code: '{err.code}' : '{err.message}'"); + } + + return string.Empty; + } + + if (pin.Result.expiresIn <= 0) + { + _logger.LogError("Pin has expired"); return string.Empty; } - return pin.authToken; + return pin.Result.authToken; } public async Task GetAccount(string accessToken) diff --git a/src/Ombi.Core/Engine/BaseMediaEngine.cs b/src/Ombi.Core/Engine/BaseMediaEngine.cs index d2e18d2b2..71a756543 100644 --- a/src/Ombi.Core/Engine/BaseMediaEngine.cs +++ b/src/Ombi.Core/Engine/BaseMediaEngine.cs @@ -48,6 +48,8 @@ namespace Ombi.Core.Engine protected readonly ISettingsService OmbiSettings; protected readonly IRepository _subscriptionRepository; + private bool _demo = DemoSingleton.Instance.Demo; + protected async Task> GetMovieRequests() { var now = DateTime.Now.Ticks; @@ -125,7 +127,7 @@ namespace Ombi.Core.Engine UserId = user.Id }; } - var settings = await Cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await OmbiSettings.GetSettingsAsync()); + var settings = await Cache.GetOrAddAsync(CacheKeys.OmbiSettings, () => OmbiSettings.GetSettingsAsync()); var result = new HideResult { Hide = settings.HideRequestsUsers, @@ -193,6 +195,23 @@ namespace Ombi.Core.Engine return ombiSettings ?? (ombiSettings = await OmbiSettings.GetSettingsAsync()); } + protected bool DemoCheck(string title) + { + if (!title.HasValue()) + { + return false; + } + if (_demo) + { + if (ExcludedDemo.ExcludedContent.Any(x => title.Contains(x, System.Globalization.CompareOptions.OrdinalIgnoreCase))) + { + return true; + } + return false; + } + return false; + } + public class HideResult { public bool Hide { get; set; } diff --git a/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs b/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs index 8ddf27656..68a4335cb 100644 --- a/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/BaseEngine.cs @@ -9,6 +9,7 @@ using Ombi.Store.Entities.Requests; using Ombi.Store.Entities; using Microsoft.EntityFrameworkCore; using Ombi.Core.Authentication; +using Ombi.Helpers; namespace Ombi.Core.Engine.Interfaces { @@ -29,6 +30,10 @@ namespace Ombi.Core.Engine.Interfaces private OmbiUser _user; protected async Task GetUser() { + if(!Username.HasValue()) + { + return null; + } var username = Username.ToUpper(); return _user ?? (_user = await UserManager.Users.FirstOrDefaultAsync(x => x.NormalizedUserName == username)); } @@ -54,9 +59,9 @@ namespace Ombi.Core.Engine.Interfaces var ruleResults = await Rules.StartSearchRules(model); return ruleResults; } - public async Task RunSpecificRule(object model, SpecificRules rule) + public async Task RunSpecificRule(object model, SpecificRules rule, string requestOnBehalf) { - var ruleResults = await Rules.StartSpecificRules(model, rule); + var ruleResults = await Rules.StartSpecificRules(model, rule, requestOnBehalf); return ruleResults; } } diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index c4bd0aa0e..5a3624c5e 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -19,7 +19,7 @@ namespace Ombi.Core.Engine.Interfaces Task> NowPlayingMovies(int currentPosition, int amountToLoad); Task GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null); Task GetTvDbId(int theMovieDbId); - Task> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); + Task> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken, string langCustomCode = null); Task> TopRatedMovies(int currentlyLoaded, int toLoad); Task> UpcomingMovies(int currentlyLoaded, int toLoad); Task GetMoviesByActor(int actorId, string langCode); @@ -28,5 +28,6 @@ namespace Ombi.Core.Engine.Interfaces Task GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted); Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); Task> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); + Task> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs index 64c76b18f..dfcd1b1da 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieRequestEngine.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Ombi.Core.Models.Requests; using Ombi.Core.Models.UI; @@ -11,6 +12,7 @@ namespace Ombi.Core.Engine.Interfaces Task RequestMovie(MovieRequestViewModel model); Task> SearchMovieRequest(string search); + Task RequestCollection(int collectionId, CancellationToken cancellationToken); Task RemoveMovieRequest(int requestId); Task RemoveAllMovieRequests(); diff --git a/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs b/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs index c8b7746f0..f4eeb6fc3 100644 --- a/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs +++ b/src/Ombi.Core/Engine/Interfaces/IRequestEngine.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Ombi.Core.Models; using Ombi.Core.Models.Requests; @@ -24,5 +25,6 @@ namespace Ombi.Core.Engine.Interfaces Task UnSubscribeRequest(int requestId, RequestType type); Task SubscribeToRequest(int requestId, RequestType type); Task GetRemainingRequests(OmbiUser user = null); + Task ReProcessRequest(int requestId, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs index 2fc78b6bb..25a3c2621 100644 --- a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs @@ -11,8 +11,9 @@ namespace Ombi.Core Task GetShowInformation(string tvdbid, CancellationToken token); Task GetShowByRequest(int requestId, CancellationToken token); Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); - Task> Popular(int currentlyLoaded, int amountToLoad); + Task> Popular(int currentlyLoaded, int amountToLoad, string langCustomCode = null); Task> Anticipated(int currentlyLoaded, int amountToLoad); Task> Trending(int currentlyLoaded, int amountToLoad); + Task> RecentlyRequestedShows(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 00a97f766..88958971e 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -21,6 +21,7 @@ using Ombi.Settings.Settings.Models; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; using Ombi.Core.Models; +using System.Threading; namespace Ombi.Core.Engine { @@ -29,7 +30,7 @@ namespace Ombi.Core.Engine public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user, INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger log, OmbiUserManager manager, IRepository rl, ICacheService cache, - ISettingsService ombiSettings, IRepository sub) + ISettingsService ombiSettings, IRepository sub, IMediaCacheService mediaCacheService) : base(user, requestService, r, manager, cache, ombiSettings, sub) { MovieApi = movieApi; @@ -37,6 +38,7 @@ namespace Ombi.Core.Engine Sender = sender; Logger = log; _requestLog = rl; + _mediaCacheService = mediaCacheService; } private IMovieDbApi MovieApi { get; } @@ -44,6 +46,7 @@ namespace Ombi.Core.Engine private IMovieSender Sender { get; } private ILogger Logger { get; } private readonly IRepository _requestLog; + private readonly IMediaCacheService _mediaCacheService; /// /// Requests the movie. @@ -70,7 +73,7 @@ namespace Ombi.Core.Engine var canRequestOnBehalf = model.RequestOnBehalf.HasValue(); var isAdmin = await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin); - if (model.RequestOnBehalf.HasValue() && !isAdmin) + if (canRequestOnBehalf && !isAdmin) { return new RequestEngineResult { @@ -370,7 +373,6 @@ namespace Ombi.Core.Engine }; } - public async Task UpdateAdvancedOptions(MediaAdvancedOptions options) { var request = await MovieRepository.Find(options.RequestId); @@ -526,6 +528,7 @@ namespace Ombi.Core.Engine // We are denying a request await NotificationHelper.Notify(request, NotificationType.RequestDeclined); await MovieRepository.Update(request); + await _mediaCacheService.Purge(); return new RequestEngineResult { @@ -549,12 +552,42 @@ namespace Ombi.Core.Engine request.Denied = false; await MovieRepository.Update(request); - var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification); + var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty); if (canNotify.Success) { await NotificationHelper.Notify(request, NotificationType.RequestApproved); } + await _mediaCacheService.Purge(); + + return await ProcessSendingMovie(request); + } + + public async Task RequestCollection(int collectionId, CancellationToken cancellationToken) + { + var langCode = await DefaultLanguageCode(null); + var collections = await Cache.GetOrAddAsync($"GetCollection{collectionId}{langCode}", + () => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1)); + + var results = new List(); + foreach (var collection in collections.parts) + { + results.Add(await RequestMovie(new MovieRequestViewModel + { + TheMovieDbId = collection.id + })); + } + + + if (results.All(x => x.IsError)) + { + new RequestEngineResult { Result = false, ErrorMessage = $"The whole collection {collections.name} Is already monitored or requested!" }; + } + + return new RequestEngineResult { Result = true, Message = $"The collection {collections.name} has been successfully added!", RequestId = results.FirstOrDefault().RequestId}; + } + private async Task ProcessSendingMovie(MovieRequests request) + { if (request.Approved) { var result = await Sender.Send(request); @@ -609,6 +642,7 @@ namespace Ombi.Core.Engine results.RootPathOverride = request.RootPathOverride; await MovieRepository.Update(results); + await _mediaCacheService.Purge(); return results; } @@ -621,12 +655,14 @@ namespace Ombi.Core.Engine { var request = await MovieRepository.GetAll().FirstOrDefaultAsync(x => x.Id == requestId); await MovieRepository.Delete(request); + await _mediaCacheService.Purge(); } public async Task RemoveAllMovieRequests() { var request = MovieRepository.GetAll(); await MovieRepository.DeleteRange(request); + await _mediaCacheService.Purge(); } public async Task UserHasRequest(string userId) @@ -634,6 +670,21 @@ namespace Ombi.Core.Engine return await MovieRepository.GetAll().AnyAsync(x => x.RequestedUserId == userId); } + public async Task ReProcessRequest(int requestId, CancellationToken cancellationToken) + { + var request = await MovieRepository.Find(requestId); + if (request == null) + { + return new RequestEngineResult + { + Result = false, + ErrorMessage = "Request does not exist" + }; + } + + return await ProcessSendingMovie(request); + } + public async Task MarkUnavailable(int modelId) { var request = await MovieRepository.Find(modelId); @@ -647,6 +698,7 @@ namespace Ombi.Core.Engine request.Available = false; await MovieRepository.Update(request); + await _mediaCacheService.Purge(); return new RequestEngineResult { @@ -670,6 +722,7 @@ namespace Ombi.Core.Engine request.MarkedAsAvailable = DateTime.Now; await NotificationHelper.Notify(request, NotificationType.RequestAvailable); await MovieRepository.Update(request); + await _mediaCacheService.Purge(); return new RequestEngineResult { @@ -682,12 +735,14 @@ namespace Ombi.Core.Engine { await MovieRepository.Add(model); - var result = await RunSpecificRule(model, SpecificRules.CanSendNotification); + var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf); if (result.Success) { await NotificationHelper.NewRequest(model); } + await _mediaCacheService.Purge(); + await _requestLog.Add(new RequestLog { UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id, diff --git a/src/Ombi.Core/Engine/MovieSearchEngine.cs b/src/Ombi.Core/Engine/MovieSearchEngine.cs index 11bcfbfa2..d911c1cf5 100644 --- a/src/Ombi.Core/Engine/MovieSearchEngine.cs +++ b/src/Ombi.Core/Engine/MovieSearchEngine.cs @@ -45,9 +45,9 @@ namespace Ombi.Core.Engine public async Task LookupImdbInformation(int theMovieDbId, string langCode = null) { langCode = await DefaultLanguageCode(langCode); - var movieInfo = await Cache.GetOrAdd(nameof(LookupImdbInformation) + langCode + theMovieDbId, - async () => await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode), - DateTime.Now.AddHours(12)); + var movieInfo = await Cache.GetOrAddAsync(nameof(LookupImdbInformation) + langCode + theMovieDbId, + () => MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode), + DateTimeOffset.Now.AddHours(12)); var viewMovie = Mapper.Map(movieInfo); return await ProcessSingleMovie(viewMovie, true); @@ -121,11 +121,11 @@ namespace Ombi.Core.Engine public async Task> PopularMovies() { - var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.PopularMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.PopularMovies(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API @@ -139,11 +139,11 @@ namespace Ombi.Core.Engine /// public async Task> TopRatedMovies() { - var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.TopRatedMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.TopRated(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API @@ -157,11 +157,11 @@ namespace Ombi.Core.Engine /// public async Task> UpcomingMovies() { - var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.Upcoming(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { Logger.LogDebug("Search Result: {result}", result); @@ -176,11 +176,11 @@ namespace Ombi.Core.Engine /// public async Task> NowPlayingMovies() { - var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.NowPlayingMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.NowPlaying(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API diff --git a/src/Ombi.Core/Engine/MusicRequestEngine.cs b/src/Ombi.Core/Engine/MusicRequestEngine.cs index e473c1fb1..032b2ae7d 100644 --- a/src/Ombi.Core/Engine/MusicRequestEngine.cs +++ b/src/Ombi.Core/Engine/MusicRequestEngine.cs @@ -362,7 +362,7 @@ namespace Ombi.Core.Engine await MusicRepository.Update(request); - var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification); + var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty); if (canNotify.Success) { await NotificationHelper.Notify(request, NotificationType.RequestApproved); @@ -506,7 +506,7 @@ namespace Ombi.Core.Engine { await MusicRepository.Add(model); - var result = await RunSpecificRule(model, SpecificRules.CanSendNotification); + var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, string.Empty); if (result.Success) { await NotificationHelper.NewRequest(model); diff --git a/src/Ombi.Core/Engine/MusicSearchEngine.cs b/src/Ombi.Core/Engine/MusicSearchEngine.cs index a9af03ecf..89bae7069 100644 --- a/src/Ombi.Core/Engine/MusicSearchEngine.cs +++ b/src/Ombi.Core/Engine/MusicSearchEngine.cs @@ -151,7 +151,7 @@ namespace Ombi.Core.Engine } - await Rules.StartSpecificRules(vm, SpecificRules.LidarrArtist); + await Rules.StartSpecificRules(vm, SpecificRules.LidarrArtist, string.Empty); return vm; } @@ -190,7 +190,7 @@ namespace Ombi.Core.Engine vm.Cover = a.images?.FirstOrDefault(x => x.coverType.Equals("cover"))?.url?.ToHttpsUrl(); - await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum); + await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty); await RunSearchRules(vm); @@ -230,7 +230,7 @@ namespace Ombi.Core.Engine vm.Cover = a.remoteCover; } - await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum); + await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty); await RunSearchRules(vm); @@ -258,7 +258,7 @@ namespace Ombi.Core.Engine vm.Cover = fullAlbum.remoteCover; } - await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum); + await Rules.StartSpecificRules(vm, SpecificRules.LidarrAlbum, string.Empty); await RunSearchRules(vm); diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 9a2ccbf1e..086eff0c9 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -25,28 +25,35 @@ using Ombi.Settings.Settings.Models; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; using Ombi.Core.Models; +using System.Threading; +using Microsoft.Extensions.Logging; namespace Ombi.Core.Engine { public class TvRequestEngine : BaseMediaEngine, ITvRequestEngine { public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user, - INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, + INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger logger, ITvSender sender, IRepository rl, ISettingsService settings, ICacheService cache, - IRepository sub) : base(user, requestService, rule, manager, cache, settings, sub) + IRepository sub, IMediaCacheService mediaCacheService) : base(user, requestService, rule, manager, cache, settings, sub) { TvApi = tvApi; MovieDbApi = movApi; NotificationHelper = helper; + _logger = logger; TvSender = sender; _requestLog = rl; + _mediaCacheService = mediaCacheService; } private INotificationHelper NotificationHelper { get; } private ITvMazeApi TvApi { get; } private IMovieDbApi MovieDbApi { get; } private ITvSender TvSender { get; } + + private readonly ILogger _logger; private readonly IRepository _requestLog; + private readonly IMediaCacheService _mediaCacheService; public async Task RequestTvShow(TvRequestViewModel tv) { @@ -68,7 +75,7 @@ namespace Ombi.Core.Engine } } - var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi); + var tvBuilder = new TvShowRequestBuilder(TvApi, MovieDbApi, _logger); (await tvBuilder .GetShowInfo(tv.TvDbId)) .CreateTvList(tv) @@ -164,7 +171,7 @@ namespace Ombi.Core.Engine }; } - if ((tv.RootFolderOverride.HasValue || tv.QualityPathOverride.HasValue) && !isAdmin) + if ((tv.RootFolderOverride.HasValue || tv.QualityPathOverride.HasValue || tv.LanguageProfile.HasValue) && !isAdmin) { return new RequestEngineResult { @@ -250,7 +257,7 @@ namespace Ombi.Core.Engine } // This is a new request - var newRequest = tvBuilder.CreateNewRequest(tv, tv.RootFolderOverride.GetValueOrDefault(), tv.QualityPathOverride.GetValueOrDefault()); + var newRequest = tvBuilder.CreateNewRequest(tv, tv.RootFolderOverride.GetValueOrDefault(), tv.QualityPathOverride.GetValueOrDefault(), tv.LanguageProfile.GetValueOrDefault()); return await AddRequest(newRequest.NewRequest, tv.RequestOnBehalf); } @@ -324,6 +331,7 @@ namespace Ombi.Core.Engine Collection = allRequests }; } + public async Task> GetRequests() { var shouldHide = await HideFromOtherUsers(); @@ -343,7 +351,6 @@ namespace Ombi.Core.Engine return allRequests; } - public async Task> GetRequests(int count, int position, string sortProperty, string sortOrder) { var shouldHide = await HideFromOtherUsers(); @@ -399,7 +406,7 @@ namespace Ombi.Core.Engine }; } - public async Task> GetRequests(int count, int position, string sortProperty, string sortOrder, RequestStatus status) + public async Task> GetRequests(int count, int position, string sortProperty, string sortOrder, RequestStatus status) { var shouldHide = await HideFromOtherUsers(); List allRequests; @@ -471,6 +478,7 @@ namespace Ombi.Core.Engine Total = total, }; } + public async Task> GetUnavailableRequests(int count, int position, string sortProperty, string sortOrder) { var shouldHide = await HideFromOtherUsers(); @@ -524,7 +532,6 @@ namespace Ombi.Core.Engine }; } - public async Task> GetRequestsLite() { var shouldHide = await HideFromOtherUsers(); @@ -694,6 +701,7 @@ namespace Ombi.Core.Engine } await TvRepository.UpdateChild(request); + await _mediaCacheService.Purge(); if (request.Approved) { @@ -720,6 +728,7 @@ namespace Ombi.Core.Engine request.Denied = true; request.DeniedReason = reason; await TvRepository.UpdateChild(request); + await _mediaCacheService.Purge(); await NotificationHelper.Notify(request, NotificationType.RequestDeclined); return new RequestEngineResult { @@ -730,6 +739,7 @@ namespace Ombi.Core.Engine public async Task UpdateChildRequest(ChildRequests request) { await TvRepository.UpdateChild(request); + await _mediaCacheService.Purge(); return request; } @@ -749,12 +759,14 @@ namespace Ombi.Core.Engine } await TvRepository.Db.SaveChangesAsync(); + await _mediaCacheService.Purge(); } public async Task RemoveTvRequest(int requestId) { var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId); await TvRepository.Delete(request); + await _mediaCacheService.Purge(); } public async Task UserHasRequest(string userId) @@ -781,6 +793,7 @@ namespace Ombi.Core.Engine } } await TvRepository.UpdateChild(request); + await _mediaCacheService.Purge(); return new RequestEngineResult { Result = true, @@ -809,6 +822,7 @@ namespace Ombi.Core.Engine } await TvRepository.UpdateChild(request); await NotificationHelper.Notify(request, NotificationType.RequestAvailable); + await _mediaCacheService.Purge(); return new RequestEngineResult { Result = true, @@ -883,22 +897,25 @@ namespace Ombi.Core.Engine return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf); } - private static List SortEpisodes(List items) + public async Task ReProcessRequest(int requestId, CancellationToken cancellationToken) { - foreach (var value in items) + var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId, cancellationToken); + if (request == null) { - foreach (var requests in value.SeasonRequests) + return new RequestEngineResult { - requests.Episodes = requests.Episodes.OrderBy(x => x.EpisodeNumber).ToList(); - } + Result = false, + ErrorMessage = "Request does not exist" + }; } - return items; + + return await ProcessSendingShow(request); } private async Task AfterRequest(ChildRequests model, string requestOnBehalf) { - var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification); + var sendRuleResult = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf); if (sendRuleResult.Success) { await NotificationHelper.NewRequest(model); @@ -912,7 +929,13 @@ namespace Ombi.Core.Engine RequestType = RequestType.TvShow, EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(), }); + await _mediaCacheService.Purge(); + + return await ProcessSendingShow(model); + } + private async Task ProcessSendingShow(ChildRequests model) + { if (model.Approved) { // Autosend @@ -997,6 +1020,10 @@ namespace Ombi.Core.Engine request.QualityOverride = options.QualityOverride; request.RootFolder = options.RootPathOverride; + if (options.LanguageProfile > 0) + { + request.LanguageProfile = options.LanguageProfile; + } await TvRepository.Update(request); diff --git a/src/Ombi.Core/Engine/TvSearchEngine.cs b/src/Ombi.Core/Engine/TvSearchEngine.cs index d549f9f56..518b97720 100644 --- a/src/Ombi.Core/Engine/TvSearchEngine.cs +++ b/src/Ombi.Core/Engine/TvSearchEngine.cs @@ -77,16 +77,16 @@ namespace Ombi.Core.Engine public async Task GetShowInformation(string tvdbid, CancellationToken token) { - var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvdbid, - async () => await TvMazeApi.ShowLookupByTheTvDbId(int.Parse(tvdbid)), DateTime.Now.AddHours(12)); + var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + tvdbid, + () => TvMazeApi.ShowLookupByTheTvDbId(int.Parse(tvdbid)), DateTimeOffset.Now.AddHours(12)); if (show == null) { // We don't have enough information return null; } - var episodes = await Cache.GetOrAdd("TvMazeEpisodeLookup" + show.id, - async () => await TvMazeApi.EpisodeLookup(show.id), DateTime.Now.AddHours(12)); + var episodes = await Cache.GetOrAddAsync("TvMazeEpisodeLookup" + show.id, + () => TvMazeApi.EpisodeLookup(show.id), DateTimeOffset.Now.AddHours(12)); if (episodes == null || !episodes.Any()) { // We don't have enough information @@ -133,7 +133,7 @@ namespace Ombi.Core.Engine public async Task> Popular() { - var result = await Cache.GetOrAdd(CacheKeys.PopularTv, async () => await TraktApi.GetPopularShows(null, ResultLimit), DateTime.Now.AddHours(12)); + var result = await Cache.GetOrAddAsync(CacheKeys.PopularTv, () => TraktApi.GetPopularShows(null, ResultLimit), DateTimeOffset.Now.AddHours(12)); var processed = ProcessResults(result); return await processed; } @@ -146,8 +146,8 @@ namespace Ombi.Core.Engine var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(Popular) + langCode + pagesToLoad.Page, - async () => await TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(Popular) + langCode + pagesToLoad.Page, + () => TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } @@ -158,7 +158,7 @@ namespace Ombi.Core.Engine public async Task> Anticipated() { - var result = await Cache.GetOrAdd(CacheKeys.AnticipatedTv, async () => await TraktApi.GetAnticipatedShows(null, ResultLimit), DateTime.Now.AddHours(12)); + var result = await Cache.GetOrAddAsync(CacheKeys.AnticipatedTv, () => TraktApi.GetAnticipatedShows(null, ResultLimit), DateTimeOffset.Now.AddHours(12)); var processed = ProcessResults(result); return await processed; } @@ -171,8 +171,8 @@ namespace Ombi.Core.Engine var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(Anticipated) + langCode + pagesToLoad.Page, - async () => await TraktApi.GetAnticipatedShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(Anticipated) + langCode + pagesToLoad.Page, + () => TraktApi.GetAnticipatedShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } var processed = ProcessResults(results); @@ -181,7 +181,7 @@ namespace Ombi.Core.Engine public async Task> Trending() { - var result = await Cache.GetOrAdd(CacheKeys.TrendingTv, async () => await TraktApi.GetTrendingShows(null, ResultLimit), DateTime.Now.AddHours(12)); + var result = await Cache.GetOrAddAsync(CacheKeys.TrendingTv, () => TraktApi.GetTrendingShows(null, ResultLimit), DateTimeOffset.Now.AddHours(12)); var processed = ProcessResults(result); return await processed; } @@ -195,8 +195,8 @@ namespace Ombi.Core.Engine var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(Trending) + langCode + pagesToLoad.Page, - async () => await TraktApi.GetTrendingShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page, + () => TraktApi.GetTrendingShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } var processed = ProcessResults(results); diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index 3714e58a8..25c0fc2a2 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -19,6 +19,7 @@ using Ombi.Store.Repository; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; @@ -29,7 +30,7 @@ namespace Ombi.Core.Engine.V2 { public MovieSearchEngineV2(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, ILogger logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService s, IRepository sub, - ISettingsService customizationSettings, IMovieRequestEngine movieRequestEngine) + ISettingsService customizationSettings, IMovieRequestEngine movieRequestEngine, IHttpClientFactory httpClientFactory) : base(identity, service, r, um, mem, s, sub) { MovieApi = movApi; @@ -37,6 +38,7 @@ namespace Ombi.Core.Engine.V2 Logger = logger; _customizationSettings = customizationSettings; _movieRequestEngine = movieRequestEngine; + _client = httpClientFactory.CreateClient(); } private IMovieDbApi MovieApi { get; } @@ -44,12 +46,13 @@ namespace Ombi.Core.Engine.V2 private ILogger Logger { get; } private readonly ISettingsService _customizationSettings; private readonly IMovieRequestEngine _movieRequestEngine; + private readonly HttpClient _client; public async Task GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null) { langCode = await DefaultLanguageCode(langCode); - var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + theMovieDbId + langCode, - async () => await MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken); + var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + theMovieDbId + langCode, + () => MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12)); return await ProcessSingleMovie(movieInfo); } @@ -58,8 +61,8 @@ namespace Ombi.Core.Engine.V2 { langCode = await DefaultLanguageCode(langCode); var request = await RequestService.MovieRequestService.Find(requestId); - var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode, - async () => await MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken); + var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode, + () => MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12)); return await ProcessSingleMovie(movieInfo); } @@ -67,8 +70,8 @@ namespace Ombi.Core.Engine.V2 public async Task GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null) { langCode = await DefaultLanguageCode(langCode); - var collections = await Cache.GetOrAdd(nameof(GetCollection) + collectionId + langCode, - async () => await MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTime.Now.AddDays(1), cancellationToken); + var collections = await Cache.GetOrAddAsync(nameof(GetCollection) + collectionId + langCode, + () => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1)); var c = await ProcessCollection(collections); c.Collection = c.Collection.OrderBy(x => x.ReleaseDate).ToList(); @@ -105,11 +108,11 @@ namespace Ombi.Core.Engine.V2 public async Task> PopularMovies() { - var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.PopularMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.PopularMovies(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API @@ -124,17 +127,17 @@ namespace Ombi.Core.Engine.V2 /// Gets popular movies by paging /// /// - public async Task> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken) + public async Task> PopularMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken, string langCustomCode = null) { - var langCode = await DefaultLanguageCode(null); + var langCode = await DefaultLanguageCode(langCustomCode); var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(PopularMovies) + pagesToLoad.Page + langCode, - async () => await MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTime.Now.AddHours(12), cancellationToken); + var apiResult = await Cache.GetOrAddAsync(nameof(PopularMovies) + pagesToLoad.Page + langCode, + () => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } return await TransformMovieResultsToResponse(results); @@ -146,11 +149,11 @@ namespace Ombi.Core.Engine.V2 /// public async Task> TopRatedMovies() { - var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.TopRatedMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.TopRated(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API @@ -167,8 +170,8 @@ namespace Ombi.Core.Engine.V2 var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(TopRatedMovies) + pagesToLoad.Page + langCode, - async () => await MovieApi.TopRated(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(TopRatedMovies) + pagesToLoad.Page + langCode, + () => MovieApi.TopRated(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } return await TransformMovieResultsToResponse(results); @@ -183,8 +186,32 @@ namespace Ombi.Core.Engine.V2 var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode, - async () => await MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode, + () => MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12)); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + + public async Task> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken) + { + var langCode = await DefaultLanguageCode(null); + + var result = await _client.GetAsync("https://raw.githubusercontent.com/Ombi-app/Ombi.News/main/Seasonal.md"); + var keyWordIds = await result.Content.ReadAsStringAsync(); + + if (string.IsNullOrEmpty(keyWordIds) || keyWordIds.Equals("\n")) + { + return new List(); + } + + var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await Cache.GetOrAddAsync(nameof(SeasonalList) + pagesToLoad.Page + langCode + keyWordIds, + () => MovieApi.GetMoviesViaKeywords(keyWordIds, langCode, cancellationToken, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } return await TransformMovieResultsToResponse(results); @@ -200,16 +227,16 @@ namespace Ombi.Core.Engine.V2 var results = new List(); - var requestResult = await Cache.GetOrAdd(nameof(RecentlyRequestedMovies) + "Requests" + toLoad + langCode, + var requestResult = await Cache.GetOrAddAsync(nameof(RecentlyRequestedMovies) + "Requests" + toLoad + langCode, async () => { return await _movieRequestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel { OrderType = OrderType.RequestedDateDesc }); - }, DateTime.Now.AddMinutes(15), cancellationToken); + }, DateTimeOffset.Now.AddMinutes(15)); - var movieDBResults = await Cache.GetOrAdd(nameof(RecentlyRequestedMovies) + toLoad + langCode, + var movieDBResults = await Cache.GetOrAddAsync(nameof(RecentlyRequestedMovies) + toLoad + langCode, async () => { var responses = new List(); @@ -218,7 +245,7 @@ namespace Ombi.Core.Engine.V2 responses.Add(await MovieApi.GetMovieInformation(movie.TheMovieDbId)); } return responses; - }, DateTime.Now.AddHours(12), cancellationToken); + }, DateTimeOffset.Now.AddHours(12)); results.AddRange(movieDBResults); @@ -232,11 +259,11 @@ namespace Ombi.Core.Engine.V2 /// public async Task> UpcomingMovies() { - var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.Upcoming(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { Logger.LogDebug("Search Result: {result}", result); @@ -254,8 +281,8 @@ namespace Ombi.Core.Engine.V2 var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(UpcomingMovies) + pagesToLoad.Page + langCode, - async () => await MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(UpcomingMovies) + pagesToLoad.Page + langCode, + () => MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } return await TransformMovieResultsToResponse(results); @@ -267,11 +294,11 @@ namespace Ombi.Core.Engine.V2 /// public async Task> NowPlayingMovies() { - var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => + var result = await Cache.GetOrAddAsync(CacheKeys.NowPlayingMovies, async () => { var langCode = await DefaultLanguageCode(null); return await MovieApi.NowPlaying(langCode); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); if (result != null) { return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API @@ -281,8 +308,8 @@ namespace Ombi.Core.Engine.V2 public async Task GetMoviesByActor(int actorId, string langCode) { - var result = await Cache.GetOrAdd(nameof(GetMoviesByActor) + actorId + langCode, - async () => await MovieApi.GetActorMovieCredits(actorId, langCode)); + var result = await Cache.GetOrAddAsync(nameof(GetMoviesByActor) + actorId + langCode, + () => MovieApi.GetActorMovieCredits(actorId, langCode), DateTimeOffset.Now.AddHours(12)); // Later we run this through the rules engine return result; } @@ -315,6 +342,12 @@ namespace Ombi.Core.Engine.V2 foreach (var movie in movies) { var result = await ProcessSingleMovie(movie); + + if (DemoCheck(result.Title)) + { + continue; + } + if (settings.HideAvailableFromDiscover && result.Available) { continue; @@ -333,6 +366,14 @@ namespace Ombi.Core.Engine.V2 private async Task ProcessSingleMovie(FullMovieInfo movie) { var viewMovie = Mapper.Map(movie); + var user = await GetUser(); + var digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == user.StreamingCountry); + if (digitalReleaseDate == null) + { + digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US"); + } + viewMovie.DigitalReleaseDate = digitalReleaseDate?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate; + await RunSearchRules(viewMovie); // This requires the rules to be run first to populate the RequestId property @@ -348,6 +389,7 @@ namespace Ombi.Core.Engine.V2 mapped.JellyfinUrl = viewMovie.JellyfinUrl; mapped.Subscribed = viewMovie.Subscribed; mapped.ShowSubscribe = viewMovie.ShowSubscribe; + mapped.DigitalReleaseDate = viewMovie.DigitalReleaseDate; return mapped; } @@ -371,6 +413,7 @@ namespace Ombi.Core.Engine.V2 mapped.Requested = movie.Requested; mapped.PlexUrl = movie.PlexUrl; mapped.EmbyUrl = movie.EmbyUrl; + mapped.JellyfinUrl = movie.JellyfinUrl; mapped.Subscribed = movie.Subscribed; mapped.ShowSubscribe = movie.ShowSubscribe; mapped.ReleaseDate = movie.ReleaseDate; @@ -382,12 +425,21 @@ namespace Ombi.Core.Engine.V2 { if (viewMovie.ImdbId.IsNullOrEmpty()) { - var showInfo = await Cache.GetOrAdd("GetMovieInformationWIthImdbId" + viewMovie.Id, - async () => await MovieApi.GetMovieInformation(viewMovie.Id), DateTime.Now.AddHours(12)); + var showInfo = await Cache.GetOrAddAsync("GetMovieInformationWIthImdbId" + viewMovie.Id, + () => MovieApi.GetMovieInformation(viewMovie.Id), DateTimeOffset.Now.AddHours(12)); viewMovie.Id = showInfo.Id; // TheMovieDbId viewMovie.ImdbId = showInfo.ImdbId; } + var user = await GetUser(); + var digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == user.StreamingCountry); + if (digitalReleaseDate == null) + { + digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US"); + } + viewMovie.DigitalReleaseDate = digitalReleaseDate?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate; + + viewMovie.TheMovieDbId = viewMovie.Id.ToString(); await RunSearchRules(viewMovie); @@ -424,12 +476,12 @@ namespace Ombi.Core.Engine.V2 public async Task GetMovieInfoByImdbId(string imdbId, CancellationToken cancellationToken) { var langCode = await DefaultLanguageCode(null); - var findResult = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + imdbId + langCode, - async () => await MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTime.Now.AddHours(12), cancellationToken); + var findResult = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + imdbId + langCode, + () => MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTimeOffset.Now.AddHours(12)); var movie = findResult.movie_results.FirstOrDefault(); - var movieInfo = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + movie.id + langCode, - async () => await MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken); + var movieInfo = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + movie.id + langCode, + () => MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12)); return await ProcessSingleMovie(movieInfo); } diff --git a/src/Ombi.Core/Engine/V2/MultiSearchEngine.cs b/src/Ombi.Core/Engine/V2/MultiSearchEngine.cs index b24f5a42c..fc2595676 100644 --- a/src/Ombi.Core/Engine/V2/MultiSearchEngine.cs +++ b/src/Ombi.Core/Engine/V2/MultiSearchEngine.cs @@ -35,6 +35,8 @@ namespace Ombi.Core.Engine.V2 private readonly ISettingsService _lidarrSettings; private readonly IMusicBrainzApi _musicApi; + private bool _demo = DemoSingleton.Instance.Demo; + public async Task> MultiSearch(string searchTerm, MultiSearchFilter filter, CancellationToken cancellationToken) { @@ -60,6 +62,12 @@ namespace Ombi.Core.Engine.V2 foreach (var multiSearch in movieDbData) { + + if (DemoCheck(multiSearch.title) || DemoCheck(multiSearch.name)) + { + continue; + } + var result = new MultiSearchResult { MediaType = multiSearch.media_type, diff --git a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs index 9b9b40906..26cba7c14 100644 --- a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs @@ -22,6 +22,9 @@ using Microsoft.EntityFrameworkCore; using System.Threading; using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb.Models; +using System.Diagnostics; +using Ombi.Core.Engine.Interfaces; +using Ombi.Core.Models.UI; namespace Ombi.Core.Engine.V2 { @@ -32,10 +35,11 @@ namespace Ombi.Core.Engine.V2 private readonly ITraktApi _traktApi; private readonly IMovieDbApi _movieApi; private readonly ISettingsService _customization; + private readonly ITvRequestEngine _requestEngine; public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService s, - IRepository sub, IMovieDbApi movieApi, ISettingsService customization) + IRepository sub, IMovieDbApi movieApi, ISettingsService customization, ITvRequestEngine requestEngine) : base(identity, service, r, um, memCache, s, sub) { _tvMaze = tvMaze; @@ -43,25 +47,43 @@ namespace Ombi.Core.Engine.V2 _traktApi = trakt; _movieApi = movieApi; _customization = customization; + _requestEngine = requestEngine; } public async Task GetShowByRequest(int requestId, CancellationToken token) { var request = await RequestService.TvRequestService.Get().FirstOrDefaultAsync(x => x.Id == requestId); - return await GetShowInformation(request.ExternalProviderId.ToString(), token); // TODO + return await GetShowInformation(request.ExternalProviderId.ToString(), token); } public async Task GetShowInformation(string tvdbid, CancellationToken token) { - var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvdbid, - async () => await _movieApi.GetTVInfo(tvdbid), DateTime.Now.AddHours(12)); + var langCode = await DefaultLanguageCode(null); + var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + langCode + tvdbid, + async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTimeOffset.Now.AddHours(12)); if (show == null || show.name == null) { // We don't have enough information return null; } + if (!show.Images?.Posters?.Any() ?? false && !string.Equals(langCode, "en", StringComparison.OrdinalIgnoreCase)) + { + // There's no regional assets for this, so + // lookup the en-us version to get them + var enShow = await Cache.GetOrAddAsync(nameof(GetShowInformation) + "en" + tvdbid, + async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTimeOffset.Now.AddHours(12)); + + // For some of the more obsecure cases + if (!show.overview.HasValue()) + { + show.overview = enShow.overview; + } + + show.Images = enShow.Images; + } + var mapped = _mapper.Map(show); @@ -69,54 +91,22 @@ namespace Ombi.Core.Engine.V2 { var seasonEpisodes = (await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, token)); - foreach (var episode in seasonEpisodes.episodes) - { - var season = mapped.SeasonRequests.FirstOrDefault(x => x.SeasonNumber == episode.season_number); - if (season == null) - { - var newSeason = new SeasonRequests - { - SeasonNumber = episode.season_number, - Overview = tvSeason.overview, - Episodes = new List() - }; - newSeason.Episodes.Add(new EpisodeRequests - { - //Url = episode...ToHttpsUrl(), - Title = episode.name, - AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue, - EpisodeNumber = episode.episode_number, - - }); - mapped.SeasonRequests.Add(newSeason); - } - else - { - // We already have the season, so just add the episode - season.Episodes.Add(new EpisodeRequests - { - //Url = e.url.ToHttpsUrl(), - Title = episode.name, - AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue, - EpisodeNumber = episode.episode_number, - }); - } - } + MapSeasons(mapped.SeasonRequests, tvSeason, seasonEpisodes); } return await ProcessResult(mapped); } - public async Task> Popular(int currentlyLoaded, int amountToLoad) + public async Task> Popular(int currentlyLoaded, int amountToLoad, string langCustomCode = null) { - var langCode = await DefaultLanguageCode(null); + var langCode = await DefaultLanguageCode(langCustomCode); var pages = PaginationHelper.GetNextPages(currentlyLoaded, amountToLoad, ResultLimit); var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(Popular) + langCode + pagesToLoad.Page, - async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(Popular) + langCode + pagesToLoad.Page, + async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } @@ -132,8 +122,8 @@ namespace Ombi.Core.Engine.V2 var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(Anticipated) + langCode + pagesToLoad.Page, - async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(Anticipated) + langCode + pagesToLoad.Page, + async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } var processed = ProcessResults(results); @@ -148,10 +138,11 @@ namespace Ombi.Core.Engine.V2 var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(Trending) + langCode + pagesToLoad.Page, - async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); + var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page, + async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } + var processed = ProcessResults(results); return await processed; } @@ -177,22 +168,118 @@ namespace Ombi.Core.Engine.V2 return data; } - private async Task> ProcessResults(IEnumerable items) + + public async Task> RecentlyRequestedShows(int currentlyLoaded, int toLoad, CancellationToken cancellationToken) + { + var langCode = await DefaultLanguageCode(null); + + var results = new List(); + + var requestResult = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + "Requests" + toLoad + langCode, + async () => + { + return await _requestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel + { + OrderType = OrderType.RequestedDateDesc + }); + }, DateTimeOffset.Now.AddMinutes(15)); + + var movieDBResults = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + toLoad + langCode, + async () => + { + var responses = new List(); + foreach (var movie in requestResult.Collection) + { + responses.Add(await _movieApi.GetTVInfo(movie.ExternalProviderId.ToString())); + } + return responses; + }, DateTimeOffset.Now.AddHours(12)); + + var mapped = _mapper.Map>(movieDBResults); + + foreach(var map in mapped) + { + var processed = await ProcessResult(map); + results.Add(processed); + } + return results; + } + + private async Task> ProcessResults(List items) { var retVal = new List(); var settings = await _customization.GetSettingsAsync(); + foreach (var tvMazeSearch in items) { + if (DemoCheck(tvMazeSearch.Title)) + { + continue; + } + if (settings.HideAvailableFromDiscover) + { + // To hide, we need to know if it's fully available, the only way to do this is to lookup it's episodes to check if we have every episode + var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + tvMazeSearch.Id.ToString(), + async () => await _movieApi.GetTVInfo(tvMazeSearch.Id.ToString()), DateTime.Now.AddHours(12)); + foreach (var tvSeason in show.seasons.Where(x => x.season_number != 0)) // skip the first season + { + var seasonEpisodes = await Cache.GetOrAddAsync("SeasonEpisodes" + show.id + tvSeason.season_number, async () => + { + return await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, CancellationToken.None); + }, DateTimeOffset.Now.AddHours(12)); + + MapSeasons(tvMazeSearch.SeasonRequests, tvSeason, seasonEpisodes); + } + } + var result = await ProcessResult(tvMazeSearch); - if (result == null || settings.HideAvailableFromDiscover && result.Available) + if (result == null || settings.HideAvailableFromDiscover && result.FullyAvailable) { continue; } retVal.Add(result); } + return retVal; } + private static void MapSeasons(List seasonRequests, Season tvSeason, SeasonDetails seasonEpisodes) + { + foreach (var episode in seasonEpisodes.episodes) + { + var season = seasonRequests.FirstOrDefault(x => x.SeasonNumber == episode.season_number); + if (season == null) + { + var newSeason = new SeasonRequests + { + SeasonNumber = episode.season_number, + Overview = tvSeason.overview, + Episodes = new List() + }; + newSeason.Episodes.Add(new EpisodeRequests + { + //Url = episode...ToHttpsUrl(), + Title = episode.name, + AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue, + EpisodeNumber = episode.episode_number, + + }); + seasonRequests.Add(newSeason); + } + else + { + // We already have the season, so just add the episode + season.Episodes.Add(new EpisodeRequests + { + //Url = e.url.ToHttpsUrl(), + Title = episode.name, + AirDate = episode.air_date.HasValue() ? DateTime.Parse(episode.air_date) : DateTime.MinValue, + EpisodeNumber = episode.episode_number, + }); + } + } + } + private async Task ProcessResult(T tvMazeSearch) { var item = _mapper.Map(tvMazeSearch); @@ -216,6 +303,9 @@ namespace Ombi.Core.Engine.V2 item.Approved = oldModel.Approved; item.SeasonRequests = oldModel.SeasonRequests; item.RequestId = oldModel.RequestId; + item.PlexUrl = oldModel.PlexUrl; + item.EmbyUrl = oldModel.EmbyUrl; + item.JellyfinUrl = oldModel.JellyfinUrl; if (!string.IsNullOrEmpty(item.Images?.Medium)) { diff --git a/src/Ombi.Core/Helpers/TvShowRequestBuilder.cs b/src/Ombi.Core/Helpers/TvShowRequestBuilder.cs index 39667eaca..fc66d197d 100644 --- a/src/Ombi.Core/Helpers/TvShowRequestBuilder.cs +++ b/src/Ombi.Core/Helpers/TvShowRequestBuilder.cs @@ -12,16 +12,19 @@ using Ombi.Helpers; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository.Requests; +using Microsoft.Extensions.Logging; namespace Ombi.Core.Helpers { public class TvShowRequestBuilder { + private readonly ILogger _logger; - public TvShowRequestBuilder(ITvMazeApi tvApi, IMovieDbApi movApi) + public TvShowRequestBuilder(ITvMazeApi tvApi, IMovieDbApi movApi, ILogger logger) { TvApi = tvApi; MovieDbApi = movApi; + _logger = logger; } private ITvMazeApi TvApi { get; } @@ -45,6 +48,7 @@ namespace Ombi.Core.Helpers { if (result.Name.Equals(ShowInfo.name, StringComparison.InvariantCultureIgnoreCase)) { + _logger.LogInformation($"Found matching MovieDb entry for show name {ShowInfo.name}"); TheMovieDbRecord = result; var showIds = await MovieDbApi.GetTvExternals(result.Id); ShowInfo.externals.imdb = showIds.imdb_id; @@ -237,18 +241,19 @@ namespace Ombi.Core.Helpers public TvShowRequestBuilder CreateNewRequest(TvRequestViewModel tv) { + _logger.LogInformation($"Building Request for {ShowInfo.name} with Provider ID {TheMovieDbRecord?.Id ?? 0}"); NewRequest = new TvRequests { Overview = ShowInfo.summary.RemoveHtml(), PosterPath = PosterPath, Title = ShowInfo.name, ReleaseDate = FirstAir, - ExternalProviderId = TheMovieDbRecord.Id, + ExternalProviderId = TheMovieDbRecord?.Id ?? 0, Status = ShowInfo.status, ImdbId = ShowInfo.externals?.imdb ?? string.Empty, TvDbId = tv.TvDbId, ChildRequests = new List(), - TotalSeasons = tv.Seasons.Count(), + TotalSeasons = tv.Seasons?.Count ?? 0, Background = BackdropPath }; NewRequest.ChildRequests.Add(ChildRequest); diff --git a/src/Ombi.Core/Helpers/TvShowRequestBuilderV2.cs b/src/Ombi.Core/Helpers/TvShowRequestBuilderV2.cs index 02150cc4d..7a45e9f09 100644 --- a/src/Ombi.Core/Helpers/TvShowRequestBuilderV2.cs +++ b/src/Ombi.Core/Helpers/TvShowRequestBuilderV2.cs @@ -33,6 +33,14 @@ namespace Ombi.Core.Helpers public async Task GetShowInfo(int id) { TheMovieDbRecord = await MovieDbApi.GetTVInfo(id.ToString()); + + // Remove 'Specials Season' + var firstSeason = TheMovieDbRecord.seasons.OrderBy(x => x.season_number).FirstOrDefault(); + if (firstSeason?.season_number == 0) + { + TheMovieDbRecord.seasons.Remove(firstSeason); + } + BackdropPath = TheMovieDbRecord.Images?.Backdrops?.OrderBy(x => x.VoteCount).ThenBy(x => x.VoteAverage).FirstOrDefault()?.FilePath; ; DateTime.TryParse(TheMovieDbRecord.first_air_date, out var dt); @@ -149,6 +157,10 @@ namespace Ombi.Core.Helpers else if (tv.FirstSeason) { var first = allEpisodes.OrderBy(x => x.season_number).FirstOrDefault(); + if (first.season_number == 0) + { + first = allEpisodes.OrderBy(x => x.season_number).Skip(1).FirstOrDefault(); + } var episodesRequests = new List(); foreach (var ep in allEpisodes) { @@ -217,7 +229,7 @@ namespace Ombi.Core.Helpers } - public TvShowRequestBuilderV2 CreateNewRequest(TvRequestViewModelV2 tv, int rootPathOverride, int qualityOverride) + public TvShowRequestBuilderV2 CreateNewRequest(TvRequestViewModelV2 tv, int rootPathOverride, int qualityOverride, int langProfile) { int.TryParse(TheMovieDbRecord.ExternalIds?.TvDbId, out var tvdbId); NewRequest = new TvRequests @@ -234,7 +246,8 @@ namespace Ombi.Core.Helpers TotalSeasons = tv.Seasons.Count(), Background = BackdropPath, RootFolder = rootPathOverride, - QualityOverride = qualityOverride + QualityOverride = qualityOverride, + LanguageProfile = langProfile }; NewRequest.ChildRequests.Add(ChildRequest); diff --git a/src/Ombi.Core/ImageService.cs b/src/Ombi.Core/ImageService.cs index e22605441..f9c8b40b9 100644 --- a/src/Ombi.Core/ImageService.cs +++ b/src/Ombi.Core/ImageService.cs @@ -23,8 +23,8 @@ namespace Ombi.Core public async Task GetTvBackground(string tvdbId) { - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await _configRepository.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); - var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvdbId}", async () => await _fanartTvApi.GetTvImages(int.Parse(tvdbId), key.Value), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => _configRepository.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); + var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvdbId}", () => _fanartTvApi.GetTvImages(int.Parse(tvdbId), key.Value), DateTimeOffset.Now.AddDays(1)); if (images == null) { diff --git a/src/Ombi.Core/Models/Requests/MovieAdvancedOptions.cs b/src/Ombi.Core/Models/Requests/MovieAdvancedOptions.cs index 02ce0a6bd..570c3f979 100644 --- a/src/Ombi.Core/Models/Requests/MovieAdvancedOptions.cs +++ b/src/Ombi.Core/Models/Requests/MovieAdvancedOptions.cs @@ -5,5 +5,6 @@ public int RequestId { get; set; } public int RootPathOverride { get; set; } public int QualityOverride { get; set; } + public int LanguageProfile { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs b/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs index 960b30ab9..1f56fb9c0 100644 --- a/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs +++ b/src/Ombi.Core/Models/Requests/TvRequestViewModel.cs @@ -24,6 +24,7 @@ namespace Ombi.Core.Models.Requests { public bool RequestAll { get; set; } public bool LatestSeason { get; set; } + public int? LanguageProfile { get; set; } public bool FirstSeason { get; set; } public List Seasons { get; set; } = new List(); [JsonIgnore] diff --git a/src/Ombi.Core/Models/Search/SearchTvShowViewModel.cs b/src/Ombi.Core/Models/Search/SearchTvShowViewModel.cs index dfd038a91..1a3f47175 100644 --- a/src/Ombi.Core/Models/Search/SearchTvShowViewModel.cs +++ b/src/Ombi.Core/Models/Search/SearchTvShowViewModel.cs @@ -58,9 +58,6 @@ namespace Ombi.Core.Models.Search public bool PartlyAvailable { get; set; } public override RequestType Type => RequestType.TvShow; - /// - /// Only set on the images call - /// public string BackdropPath { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Models/Search/V2/MovieCollectionsViewModel.cs b/src/Ombi.Core/Models/Search/V2/MovieCollectionsViewModel.cs index 47870c18b..0cecd83e5 100644 --- a/src/Ombi.Core/Models/Search/V2/MovieCollectionsViewModel.cs +++ b/src/Ombi.Core/Models/Search/V2/MovieCollectionsViewModel.cs @@ -16,7 +16,7 @@ namespace Ombi.Core.Models.Search.V2 public string Overview { get; set; } public string PosterPath { get; set; } public string Title { get; set; } - public DateTime ReleaseDate { get; set; } + public DateTime? ReleaseDate { get; set; } public override RequestType Type => RequestType.Movie; diff --git a/src/Ombi.Core/Rule/Interfaces/IRuleEvaluator.cs b/src/Ombi.Core/Rule/Interfaces/IRuleEvaluator.cs index c32342146..5b438124e 100644 --- a/src/Ombi.Core/Rule/Interfaces/IRuleEvaluator.cs +++ b/src/Ombi.Core/Rule/Interfaces/IRuleEvaluator.cs @@ -9,6 +9,6 @@ namespace Ombi.Core.Rule.Interfaces { Task> StartRequestRules(BaseRequest obj); Task> StartSearchRules(SearchViewModel obj); - Task StartSpecificRules(object obj, SpecificRules selectedRule); + Task StartSpecificRules(object obj, SpecificRules selectedRule, string requestOnBehalf); } } \ No newline at end of file diff --git a/src/Ombi.Core/Rule/Interfaces/ISpecificRule.cs b/src/Ombi.Core/Rule/Interfaces/ISpecificRule.cs index 065b2f990..f76464888 100644 --- a/src/Ombi.Core/Rule/Interfaces/ISpecificRule.cs +++ b/src/Ombi.Core/Rule/Interfaces/ISpecificRule.cs @@ -5,7 +5,7 @@ namespace Ombi.Core.Rule.Interfaces { public interface ISpecificRule where T : new() { - Task Execute(T obj); + Task Execute(T obj, string requestOnBehalf); SpecificRules Rule { get; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Rule/RuleEvaluator.cs b/src/Ombi.Core/Rule/RuleEvaluator.cs index a0c272c00..712d6031e 100644 --- a/src/Ombi.Core/Rule/RuleEvaluator.cs +++ b/src/Ombi.Core/Rule/RuleEvaluator.cs @@ -58,13 +58,13 @@ namespace Ombi.Core.Rule return results; } - public async Task StartSpecificRules(object obj, SpecificRules selectedRule) + public async Task StartSpecificRules(object obj, SpecificRules selectedRule, string requestOnBehalf) { foreach (var rule in SpecificRules) { if (selectedRule == rule.Rule) { - var result = await rule.Execute(obj); + var result = await rule.Execute(obj, requestOnBehalf); return result; } } diff --git a/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs b/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs index 927ebf1be..10ab93bed 100644 --- a/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/ExistingPlexRequestRule.cs @@ -7,6 +7,7 @@ using Ombi.Core.Rule.Interfaces; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; +using Ombi.Store.Repository.Requests; namespace Ombi.Core.Rule.Rules.Request { @@ -60,6 +61,7 @@ namespace Ombi.Core.Rule.Rules.Request { foreach (var season in child.SeasonRequests) { + var episodesToRemove = new List(); var currentSeasonRequest = content.Episodes.Where(x => x.SeasonNumber == season.SeasonNumber).ToList(); if (!currentSeasonRequest.Any()) @@ -68,12 +70,24 @@ namespace Ombi.Core.Rule.Rules.Request } foreach (var e in season.Episodes) { - var hasEpisode = currentSeasonRequest.Any(x => x.EpisodeNumber == e.EpisodeNumber); - if (hasEpisode) + var existingEpRequest = currentSeasonRequest.FirstOrDefault(x => x.EpisodeNumber == e.EpisodeNumber); + if (existingEpRequest != null) { - return Fail($"We already have episodes requested from series {child.Title}"); + episodesToRemove.Add(e); } } + + episodesToRemove.ForEach(x => + { + season.Episodes.Remove(x); + }); + } + + var anyEpisodes = child.SeasonRequests.SelectMany(x => x.Episodes).Any(); + + if (!anyEpisodes) + { + return Fail($"We already have episodes requested from series {child.Title}"); } return Success(); diff --git a/src/Ombi.Core/Rule/Rules/Request/ExistingTVRequestRule.cs b/src/Ombi.Core/Rule/Rules/Request/ExistingTVRequestRule.cs index 02546d356..7973f664b 100644 --- a/src/Ombi.Core/Rule/Rules/Request/ExistingTVRequestRule.cs +++ b/src/Ombi.Core/Rule/Rules/Request/ExistingTVRequestRule.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Core.Rule.Interfaces; @@ -41,15 +42,30 @@ namespace Ombi.Core.Rule.Rules.Request { continue; } + + var episodesToRemove = new List(); foreach (var e in season.Episodes) { - var hasEpisode = currentSeasonRequest.Episodes.Any(x => x.EpisodeNumber == e.EpisodeNumber); - if (hasEpisode) + var existingEpRequest = currentSeasonRequest.Episodes.FirstOrDefault(x => x.EpisodeNumber == e.EpisodeNumber); + if (existingEpRequest != null) { - return Fail($"We already have episodes requested from series {tv.Title}"); + episodesToRemove.Add(e); } } + + episodesToRemove.ForEach(x => + { + season.Episodes.Remove(x); + }); + } + + var anyEpisodes = tv.SeasonRequests.SelectMany(x => x.Episodes).Any(); + + if (!anyEpisodes) + { + return Fail($"We already have episodes requested from series {tv.Title}"); } + } return Success(); } diff --git a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs index 71331c51d..fe062e851 100644 --- a/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs +++ b/src/Ombi.Core/Rule/Rules/Search/AvailabilityRuleHelper.cs @@ -13,12 +13,12 @@ namespace Ombi.Core.Rule.Rules.Search { public static void CheckForUnairedEpisodes(SearchTvShowViewModel search) { - foreach (var season in search.SeasonRequests) + foreach (var season in search.SeasonRequests.ToList()) { // If we have all the episodes for this season, then this season is available if (season.Episodes.All(x => x.Available)) { - season.SeasonAvailable = true; + season.SeasonAvailable = true; } } if (search.SeasonRequests.All(x => x.Episodes.All(e => e.Available))) diff --git a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs index 3fe11cbc4..7f6718a6b 100644 --- a/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/EmbyAvailabilityRule.cs @@ -67,7 +67,7 @@ namespace Ombi.Core.Rule.Rules.Search var s = await EmbySettings.GetSettingsAsync(); if (s.Enable) { - var server = s.Servers.FirstOrDefault(x => x.ServerHostname != null); + var server = s.Servers.FirstOrDefault(); if ((server?.ServerHostname ?? string.Empty).HasValue()) { obj.EmbyUrl = EmbyHelper.GetEmbyMediaUrl(item.EmbyId, server?.ServerId, server?.ServerHostname); diff --git a/src/Ombi.Core/Rule/Rules/Search/LidarrAlbumCacheRule.cs b/src/Ombi.Core/Rule/Rules/Search/LidarrAlbumCacheRule.cs index 3ca57d635..ca0c0cd97 100644 --- a/src/Ombi.Core/Rule/Rules/Search/LidarrAlbumCacheRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/LidarrAlbumCacheRule.cs @@ -9,7 +9,7 @@ using Ombi.Store.Repository; namespace Ombi.Core.Rule.Rules.Search { - public class LidarrAlbumCacheRule : BaseSearchRule, IRules + public class LidarrAlbumCacheRule : SpecificRule, ISpecificRule { public LidarrAlbumCacheRule(IExternalRepository db) { @@ -18,7 +18,9 @@ namespace Ombi.Core.Rule.Rules.Search private readonly IExternalRepository _db; - public Task Execute(SearchViewModel objec) + public override SpecificRules Rule => SpecificRules.LidarrAlbum; + + public Task Execute(object objec, string requestOnBehalf) { if (objec is SearchAlbumViewModel obj) { diff --git a/src/Ombi.Core/Rule/Rules/Search/LidarrArtistCacheRule.cs b/src/Ombi.Core/Rule/Rules/Search/LidarrArtistCacheRule.cs index a254ea2d3..06ca3f06c 100644 --- a/src/Ombi.Core/Rule/Rules/Search/LidarrArtistCacheRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/LidarrArtistCacheRule.cs @@ -17,7 +17,7 @@ namespace Ombi.Core.Rule.Rules.Search private readonly IExternalRepository _db; - public Task Execute(object objec) + public Task Execute(object objec, string requestOnBehalf) { var obj = (SearchArtistViewModel) objec; // Check if it's in Lidarr @@ -30,6 +30,7 @@ namespace Ombi.Core.Rule.Rules.Search return Task.FromResult(Success()); } + public override SpecificRules Rule => SpecificRules.LidarrArtist; } } \ 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 2a239d1d3..5a2b6ed98 100644 --- a/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/PlexAvailabilityRule.cs @@ -25,10 +25,14 @@ namespace Ombi.Core.Rule.Rules.Search PlexServerContent item = null; var useImdb = false; var useTheMovieDb = false; + var useId = false; var useTvDb = false; + + PlexMediaTypeEntity type = ConvertType(obj.Type); + if (obj.ImdbId.HasValue()) { - item = await PlexContentRepository.Get(obj.ImdbId); + item = await PlexContentRepository.GetByType(obj.ImdbId, ProviderType.ImdbId, type); if (item != null) { useImdb = true; @@ -36,9 +40,17 @@ namespace Ombi.Core.Rule.Rules.Search } if (item == null) { + if (obj.Id > 0) + { + item = await PlexContentRepository.GetByType(obj.Id.ToString(), ProviderType.TheMovieDbId, type); + if (item != null) + { + useId = true; + } + } if (obj.TheMovieDbId.HasValue()) { - item = await PlexContentRepository.Get(obj.TheMovieDbId); + item = await PlexContentRepository.GetByType(obj.TheMovieDbId, ProviderType.TheMovieDbId, type); if (item != null) { useTheMovieDb = true; @@ -49,7 +61,7 @@ namespace Ombi.Core.Rule.Rules.Search { if (obj.TheTvDbId.HasValue()) { - item = await PlexContentRepository.Get(obj.TheTvDbId); + item = await PlexContentRepository.GetByType(obj.TheTvDbId, ProviderType.TvDbId, type); if (item != null) { useTvDb = true; @@ -60,6 +72,11 @@ namespace Ombi.Core.Rule.Rules.Search if (item != null) { + if (useId) + { + obj.TheMovieDbId = obj.Id.ToString(); + useTheMovieDb = true; + } obj.Available = true; obj.PlexUrl = item.Url; obj.Quality = item.Quality; @@ -71,9 +88,9 @@ namespace Ombi.Core.Rule.Rules.Search if (search.SeasonRequests.Any()) { var allEpisodes = PlexContentRepository.GetAllEpisodes(); - foreach (var season in search.SeasonRequests) + foreach (var season in search.SeasonRequests.ToList()) { - foreach (var episode in season.Episodes) + foreach (var episode in season.Episodes.ToList()) { await AvailabilityRuleHelper.SingleEpisodeCheck(useImdb, allEpisodes, episode, season, item, useTheMovieDb, useTvDb, Log); } @@ -86,6 +103,12 @@ namespace Ombi.Core.Rule.Rules.Search return Success(); } - + private PlexMediaTypeEntity ConvertType(RequestType type) => + type switch + { + RequestType.Movie => PlexMediaTypeEntity.Movie, + RequestType.TvShow => PlexMediaTypeEntity.Show, + _ => PlexMediaTypeEntity.Movie, + }; } } \ No newline at end of file diff --git a/src/Ombi.Core/Rule/Rules/SonarrCacheRule.cs b/src/Ombi.Core/Rule/Rules/SonarrCacheRule.cs index 0e4313ac7..325b7cc8d 100644 --- a/src/Ombi.Core/Rule/Rules/SonarrCacheRule.cs +++ b/src/Ombi.Core/Rule/Rules/SonarrCacheRule.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Ombi.Core.Models.Search; @@ -6,6 +7,7 @@ using Ombi.Helpers; using Ombi.Store.Context; using Ombi.Store.Entities; using Ombi.Store.Entities.Requests; +using Ombi.Store.Repository.Requests; namespace Ombi.Core.Rule.Rules { @@ -23,7 +25,7 @@ namespace Ombi.Core.Rule.Rules if (obj.RequestType == RequestType.TvShow) { var vm = (ChildRequests) obj; - var result = await _ctx.SonarrCache.FirstOrDefaultAsync(x => x.TvDbId == vm.Id); // TODO lookup the external provider in the sonarr sync to use themoviedb + var result = await _ctx.SonarrCache.FirstOrDefaultAsync(x => x.TheMovieDbId == vm.Id); if (result != null) { if (vm.SeasonRequests.Any()) @@ -31,17 +33,30 @@ namespace Ombi.Core.Rule.Rules var sonarrEpisodes = _ctx.SonarrEpisodeCache; foreach (var season in vm.SeasonRequests) { + var toRemove = new List(); foreach (var ep in season.Episodes) { // Check if we have it - var monitoredInSonarr = sonarrEpisodes.Any(x => + var monitoredInSonarr = sonarrEpisodes.FirstOrDefault(x => x.EpisodeNumber == ep.EpisodeNumber && x.SeasonNumber == season.SeasonNumber - && x.TvDbId == vm.Id); - if (monitoredInSonarr) + && x.MovieDbId == vm.Id); + if (monitoredInSonarr != null) { - return new RuleResult{Message = "We already have this request, please choose the \"Select...\" option to refine your request"}; - } + toRemove.Add(ep); + } } + + toRemove.ForEach(x => + { + season.Episodes.Remove(x); + }); + + } + var anyEpisodes = vm.SeasonRequests.SelectMany(x => x.Episodes).Any(); + + if (!anyEpisodes) + { + return new RuleResult { Message = $"We already have episodes requested from series {vm.Title}" }; } } } diff --git a/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs b/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs index 30ec9b14a..af71df9ac 100644 --- a/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs +++ b/src/Ombi.Core/Rule/Rules/Specific/SendNotificationRule.cs @@ -22,11 +22,20 @@ namespace Ombi.Core.Rule.Rules.Specific private OmbiUserManager UserManager { get; } private ISettingsService Settings { get; } - public async Task Execute(object obj) + public async Task Execute(object obj, string requestOnBehalf) { var req = (BaseRequest)obj; + var canRequestonBehalf = requestOnBehalf.HasValue(); var settings = await Settings.GetSettingsAsync(); var sendNotification = true; + + if (settings.DoNotSendNotificationsForAutoApprove && canRequestonBehalf) + { + return new RuleResult + { + Success = false + }; + } var requestedUser = await UserManager.Users.FirstOrDefaultAsync(x => x.Id == req.RequestedUserId); if (req.RequestType == RequestType.Movie) { diff --git a/src/Ombi.Core/Senders/MovieSender.cs b/src/Ombi.Core/Senders/MovieSender.cs index 33d6afd25..87d686d35 100644 --- a/src/Ombi.Core/Senders/MovieSender.cs +++ b/src/Ombi.Core/Senders/MovieSender.cs @@ -125,7 +125,6 @@ namespace Ombi.Core.Senders private async Task SendToRadarr(MovieRequests model, RadarrSettings settings) { - var v3 = settings.V3; var qualityToUse = int.Parse(settings.DefaultQualityProfile); var rootFolderPath = settings.DefaultRootPath; @@ -159,30 +158,16 @@ namespace Ombi.Core.Senders List movies; // Check if the movie already exists? Since it could be unmonitored - if (settings.V3) - { - movies = await _radarrV3Api.GetMovies(settings.ApiKey, settings.FullUri); - } - else - { - movies = await _radarrV2Api.GetMovies(settings.ApiKey, settings.FullUri); - } + + movies = await _radarrV3Api.GetMovies(settings.ApiKey, settings.FullUri); + var existingMovie = movies.FirstOrDefault(x => x.tmdbId == model.TheMovieDbId); if (existingMovie == null) { - RadarrAddMovie result; - if (v3) - { - result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, - qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, - settings.MinimumAvailability); - } - else - { - result = await _radarrV2Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, - qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, - settings.MinimumAvailability); - } + var result = await _radarrV3Api.AddMovie(model.TheMovieDbId, model.Title, model.ReleaseDate.Year, + qualityToUse, rootFolderPath, settings.ApiKey, settings.FullUri, !settings.AddOnly, + settings.MinimumAvailability); + if (!string.IsNullOrEmpty(result.Error?.message)) { _log.LogError(LoggingEvents.RadarrCacher, result.Error.message); @@ -199,23 +184,12 @@ namespace Ombi.Core.Senders { // let's set it to monitored and search for it existingMovie.monitored = true; - if (v3) - { - await _radarrV3Api.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri); - // Search for it - if (!settings.AddOnly) - { - await _radarrV3Api.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri); - } - } - else + + await _radarrV3Api.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri); + // Search for it + if (!settings.AddOnly) { - await _radarrV2Api.UpdateMovie(existingMovie, settings.ApiKey, settings.FullUri); - // Search for it - if (!settings.AddOnly) - { - await _radarrV2Api.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri); - } + await _radarrV3Api.MovieSearch(new[] { existingMovie.id }, settings.ApiKey, settings.FullUri); } return new SenderResult { Success = true, Sent = true }; @@ -226,18 +200,9 @@ namespace Ombi.Core.Senders private async Task RadarrRootPath(int overrideId, RadarrSettings settings) { - if (settings.V3) - { - var paths = await _radarrV3Api.GetRootFolders(settings.ApiKey, settings.FullUri); - var selectedPath = paths.FirstOrDefault(x => x.id == overrideId); - return selectedPath?.path ?? string.Empty; - } - else - { - var paths = await _radarrV2Api.GetRootFolders(settings.ApiKey, settings.FullUri); - var selectedPath = paths.FirstOrDefault(x => x.id == overrideId); - return selectedPath?.path ?? string.Empty; - } + var paths = await _radarrV3Api.GetRootFolders(settings.ApiKey, settings.FullUri); + var selectedPath = paths.FirstOrDefault(x => x.id == overrideId); + return selectedPath?.path ?? string.Empty; } } } \ No newline at end of file diff --git a/src/Ombi.Core/Senders/TvSender.cs b/src/Ombi.Core/Senders/TvSender.cs index 8187cad59..2f42a12c0 100644 --- a/src/Ombi.Core/Senders/TvSender.cs +++ b/src/Ombi.Core/Senders/TvSender.cs @@ -158,6 +158,8 @@ namespace Ombi.Core.Senders } int qualityToUse; + var sonarrV3 = s.V3; + var languageProfileId = s.LanguageProfile; string rootFolderPath; string seriesType; @@ -167,8 +169,17 @@ namespace Ombi.Core.Senders { // Get the root path from the rootfolder selected. // For some reason, if we haven't got one use the first root folder in Sonarr - rootFolderPath = await GetSonarrRootPath(int.Parse(s.RootPathAnime), s); - int.TryParse(s.QualityProfileAnime, out qualityToUse); + if (!int.TryParse(s.RootPathAnime, out int animePath)) + { + animePath = int.Parse(s.RootPath); // Set it to the main root folder if we have no anime folder. + } + rootFolderPath = await GetSonarrRootPath(animePath, s); + languageProfileId = s.LanguageProfileAnime > 0 ? s.LanguageProfileAnime : s.LanguageProfile; + + if (!int.TryParse(s.QualityProfileAnime, out qualityToUse)) + { + qualityToUse = int.Parse(s.QualityProfile); + } if (profiles != null) { if (profiles.SonarrRootPathAnime > 0) @@ -181,7 +192,6 @@ namespace Ombi.Core.Senders } } seriesType = "anime"; - } else { @@ -220,11 +230,16 @@ namespace Ombi.Core.Senders rootFolderPath = await GetSonarrRootPath(rootfolderOverride, s); } } - - // Are we using v3 sonarr? - var sonarrV3 = s.V3; - var languageProfileId = s.LanguageProfile; + if (model.ParentRequest.LanguageProfile.HasValue) + { + var languageProfile = model.ParentRequest.LanguageProfile.Value; + if (languageProfile > 0) + { + languageProfileId = languageProfile; + } + } + try { // Does the series actually exist? @@ -264,6 +279,10 @@ namespace Ombi.Core.Senders var seasonsToAdd = GetSeasonsToCreate(model); newSeries.seasons = seasonsToAdd; var result = await SonarrApi.AddSeries(newSeries, s.ApiKey, s.FullUri); + if (result?.ErrorMessages?.Any() ?? false) + { + throw new Exception(string.Join(',', result.ErrorMessages)); + } existingSeries = await SonarrApi.GetSeriesById(result.id, s.ApiKey, s.FullUri); await SendToSonarr(model, existingSeries, s); } @@ -407,7 +426,6 @@ namespace Ombi.Core.Senders await SonarrApi.SeasonPass(s.ApiKey, s.FullUri, result); } - if (!s.AddOnly) { await SearchForRequest(model, sonarrEpList, result, s, episodesToUpdate); diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index ef9c9230c..62686deec 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -23,7 +23,6 @@ using Ombi.Notifications; using Ombi.Schedule; using Ombi.Schedule.Jobs; using Ombi.Settings.Settings; -using Ombi.Store.Context; using Ombi.Store.Repository; using Ombi.Notifications.Agents; using Ombi.Schedule.Jobs.Radarr; @@ -68,6 +67,8 @@ using Ombi.Api.MusicBrainz; using Ombi.Api.Twilio; using Ombi.Api.CloudService; using Ombi.Api.RottenTomatoes; +using System.Net.Http; +using Microsoft.Extensions.Logging; namespace Ombi.DependencyInjection { @@ -119,14 +120,24 @@ namespace Ombi.DependencyInjection public static void RegisterHttp(this IServiceCollection services) { + var runtimeVersion = AssemblyHelper.GetRuntimeVersion(); services.AddSingleton(); services.AddScoped(sp => sp.GetService().HttpContext.User); + services.AddHttpClient("OmbiClient", client => + { + client.DefaultRequestHeaders.Add("User-Agent", $"Ombi/{runtimeVersion} (https://ombi.io/)"); + }).ConfigurePrimaryHttpMessageHandler(() => + { + var httpClientHandler = new HttpClientHandler(); + httpClientHandler.ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true; + + return httpClientHandler; + }); } public static void RegisterApi(this IServiceCollection services) { - services.AddScoped(); - services.AddScoped(); // https://blogs.msdn.microsoft.com/alazarev/2017/12/29/disposable-finalizers-and-httpclient/ + services.AddScoped(s => new Api.Api(s.GetRequiredService>(), s.GetRequiredService().CreateClient("OmbiClient"))); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -195,6 +206,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddTransient(); diff --git a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj index ed1e9e4a2..49d1dd194 100644 --- a/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj +++ b/src/Ombi.DependencyInjection/Ombi.DependencyInjection.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Ombi.HealthChecks/Checks/RadarrHealthCheck.cs b/src/Ombi.HealthChecks/Checks/RadarrHealthCheck.cs index e365c73bb..9d5b31e5f 100644 --- a/src/Ombi.HealthChecks/Checks/RadarrHealthCheck.cs +++ b/src/Ombi.HealthChecks/Checks/RadarrHealthCheck.cs @@ -21,7 +21,7 @@ namespace Ombi.HealthChecks.Checks using (var scope = CreateScope()) { var settingsProvider = scope.ServiceProvider.GetRequiredService>(); - var api = scope.ServiceProvider.GetRequiredService(); + var api = scope.ServiceProvider.GetRequiredService(); var settings = await settingsProvider.GetSettingsAsync(); if (!settings.Enabled) { diff --git a/src/Ombi.Helpers.Tests/PlexHelperTests.cs b/src/Ombi.Helpers.Tests/PlexHelperTests.cs index 82b59fdf7..e7d35a583 100644 --- a/src/Ombi.Helpers.Tests/PlexHelperTests.cs +++ b/src/Ombi.Helpers.Tests/PlexHelperTests.cs @@ -61,6 +61,7 @@ namespace Ombi.Helpers.Tests get { yield return new TestCaseData("plex://movie/5e1632df2d4d84003e48e54e|imdb://tt9178402|tmdb://610201", new ProviderId { ImdbId = "tt9178402", TheMovieDb = "610201" }).SetName("V2 Regular Plex Id"); + yield return new TestCaseData("plex://movie/5e1632df2d4d84003e48e54e|imdb://tt9178402|tmdb://610201|thetvdb://12345", new ProviderId { ImdbId = "tt9178402", TheMovieDb = "610201", TheTvDb = "12345" }).SetName("V2 Regular Plex Id w/ tvdb"); yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|imdb://tt0119567|tmdb://330", new ProviderId { ImdbId = "tt0119567", TheMovieDb = "330" }).SetName("V2 Regular Plex Id Another"); yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|imdb://tt0119567", new ProviderId { ImdbId = "tt0119567" }).SetName("V2 Regular Plex Id Single Imdb"); yield return new TestCaseData("plex://movie/5d7768253c3c2a001fbcab72|tmdb://330", new ProviderId { TheMovieDb = "330" }).SetName("V2 Regular Plex Id Single Tmdb"); diff --git a/src/Ombi.Helpers/AssemblyHelper.cs b/src/Ombi.Helpers/AssemblyHelper.cs index eb4dc55fd..9abc3bf92 100644 --- a/src/Ombi.Helpers/AssemblyHelper.cs +++ b/src/Ombi.Helpers/AssemblyHelper.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.PlatformAbstractions; +using System.Linq; using System.Reflection; namespace Ombi.Helpers @@ -8,7 +9,8 @@ namespace Ombi.Helpers public static string GetRuntimeVersion() { ApplicationEnvironment app = PlatformServices.Default.Application; - return app.ApplicationVersion; + var split = app.ApplicationVersion.Split('.'); + return string.Join('.', split.Take(3)); } } } \ No newline at end of file diff --git a/src/Ombi.Helpers/CacheService.cs b/src/Ombi.Helpers/CacheService.cs index 4f2f69dc8..3cf392880 100644 --- a/src/Ombi.Helpers/CacheService.cs +++ b/src/Ombi.Helpers/CacheService.cs @@ -1,48 +1,26 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; -using Nito.AsyncEx; +using LazyCache; namespace Ombi.Helpers { public class CacheService : ICacheService { - private readonly IMemoryCache _memoryCache; - private readonly AsyncLock _mutex = new AsyncLock(); - public CacheService(IMemoryCache memoryCache) + protected readonly IAppCache _memoryCache; + public CacheService(IAppCache memoryCache) { - _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); + _memoryCache = memoryCache; } - public async Task GetOrAdd(string cacheKey, Func> factory, DateTime absoluteExpiration = default(DateTime), CancellationToken cancellationToken = default(CancellationToken)) + public virtual async Task GetOrAddAsync(string cacheKey, Func> factory, DateTimeOffset absoluteExpiration = default) { - if (absoluteExpiration == default(DateTime)) + if (absoluteExpiration == default) { - absoluteExpiration = DateTime.Now.AddHours(1); + absoluteExpiration = DateTimeOffset.Now.AddHours(1); } - // locks get and set internally - if (_memoryCache.TryGetValue(cacheKey, out var result)) - { - return result; - } - - if (_memoryCache.TryGetValue(cacheKey, out result)) - { - return result; - } - - if (cancellationToken.CanBeCanceled) - { - cancellationToken.ThrowIfCancellationRequested(); - } - - result = await factory(); - _memoryCache.Set(cacheKey, result, absoluteExpiration); - return result; + return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); } public void Remove(string key) @@ -50,28 +28,10 @@ namespace Ombi.Helpers _memoryCache.Remove(key); } - - - public T GetOrAdd(string cacheKey, Func factory, DateTime absoluteExpiration) + public T GetOrAdd(string cacheKey, Func factory, DateTimeOffset absoluteExpiration) { // locks get and set internally - if (_memoryCache.TryGetValue(cacheKey, out var result)) - { - return result; - } - - lock (TypeLock.Lock) - { - if (_memoryCache.TryGetValue(cacheKey, out result)) - { - return result; - } - - result = factory(); - _memoryCache.Set(cacheKey, result, absoluteExpiration); - - return result; - } + return _memoryCache.GetOrAdd(cacheKey, () => factory(), absoluteExpiration); } private static class TypeLock diff --git a/src/Ombi.Helpers/DemoSingleton.cs b/src/Ombi.Helpers/DemoSingleton.cs index 22b6b2f31..b94c7dbb3 100644 --- a/src/Ombi.Helpers/DemoSingleton.cs +++ b/src/Ombi.Helpers/DemoSingleton.cs @@ -1,4 +1,6 @@ -namespace Ombi.Helpers +using System.Collections.Generic; + +namespace Ombi.Helpers { public class DemoSingleton { @@ -10,4 +12,460 @@ public bool Demo { get; set; } } + + public static class ExcludedDemo + { + public static HashSet ExcludedContent => new HashSet + { + "101 Dalmatians", + "102 Dalmatians", + "20,000 Leagues Under the Sea", + "A Bug's Life", + "A Far Off Place", + "A Goofy Movie", + "A Kid in King Arthur's Court", + "A Tale of Two Critters", + "A Tiger Walks", + "A Wrinkle in Time", + "ABCD 2", + "African Cats", + "Air Bud", + "Air Bud: Golden Receiver", + "Aladdin", + "Aladdin", + "Alexander and the Terrible, Horrible, No Good, Very Bad Day", + "Alice Through the Looking Glass", + "Alice in Wonderland", + "Alice in Wonderland", + "Aliens of the Deep", + "Almost Angels", + "America's Heart and Soul", + "Amy", + "Anaganaga O Dheerudu", + "Angels in the Outfield", + "Arjun: The Warrior Prince", + "Around the World in 80 Days", + "Artemis Fowl", + "Atlantis: The Lost Empire", + "Babes in Toyland", + "Bambi", + "Bears", + "Beauty and the Beast", + "Beauty and the Beast", + "Bedknobs and Broomsticks", + "Bedtime Stories", + "Benji the Hunted", + "Beverly Hills Chihuahua", + "Big Hero 6", + "Big Red", + "Blackbeard's Ghost", + "Blank Check", + "Blue", + "Bolt", + "Bon Voyage!", + "Born in China", + "Brave", + "Bridge to Terabithia", + "Brother Bear", + "Candleshoe", + "Cars", + "Cars 2", + "Cars 3", + "Charley and the Angel", + "Charlie, the Lonesome Cougar", + "Cheetah", + "Chicken Little", + "Chimpanzee", + "Christopher Robin", + "Cinderella", + "Cinderella", + "Coco", + "College Road Trip", + "Condorman", + "Confessions of a Teenage Drama Queen", + "Cool Runnings", + "D2: The Mighty Ducks", + "D3: The Mighty Ducks", + "Dangal", + "Darby O'Gill and the Little People", + "Dasavathaaram", + "Davy Crockett and the River Pirates", + "Davy Crockett, King of the Wild Frontier", + "Dinosaur", + "Disney's A Christmas Carol", + "Disney's The Kid", + "Do Dooni Chaar", + "Dolphin Reef", + "Doug's 1st Movie", + "Dragonslayer", + "DuckTales the Movie: Treasure of the Lost Lamp", + "Dumbo", + "Dumbo", + "Earth", + "Eight Below", + "Emil and the Detectives", + "Enchanted", + "Endurance", + "Escape to Witch Mountain", + "Expedition China", + "Fantasia", + "Fantasia 2000", + "Finding Dory", + "Finding Nemo", + "First Kid", + "Flight of the Navigator", + "Flubber", + "Follow Me, Boys!", + "Frank and Ollie", + "Frankenweenie", + "Freaky Friday", + "Freaky Friday", + "Frozen", + "Frozen II", + "Onward", + "Star Wars", + "Raya", + "Mandalorian", + "Fun and Fancy Free", + "G-Force", + "George of the Jungle", + "Ghost in the Shell 2: Innocence GITS2", + "Ghost of the Mountains", + "Ghosts of the Abyss", + "Glory Road", + "Greyfriars Bobby", + "Growing Up Wild", + "Gus", + "Hannah Montana and Miley Cyrus: Best of Both Worlds Concert", + "Hannah Montana: The Movie", + "Heavyweights", + "Herbie Goes Bananas", + "Herbie Goes to Monte Carlo", + "Herbie Rides Again", + "Herbie: Fully Loaded", + "Hercules", + "High School Musical 3: Senior Year", + "Hocus Pocus", + "Holes", + "Home on the Range", + "Homeward Bound II: Lost in San Francisco", + "Homeward Bound: The Incredible Journey", + "Honey, I Blew Up the Kid", + "Honey, I Shrunk the Kids", + "Hot Lead and Cold Feet", + "I'll Be Home for Christmas", + "Ice Princess", + "In Search of the Castaways", + "Incredibles 2", + "Inside Out", + "Inspector Gadget", + "Into the Woods", + "Invincible", + "Iron Will", + "Jagga Jasoos", + "James and the Giant Peach", + "John Carter", + "Johnny Tremain", + "Jonas Brothers: The 3D Concert Experience", + "Jungle 2 Jungle", + "Jungle Cat", + "Khoobsurat", + "Kidnapped", + "King of the Grizzlies", + "L'Empereur - March of the Penguins 2: The Next Step[a]", + "Lady and the Tramp", + "Lady and the Tramp", + "Lilly the Witch: The Dragon and the Magic Book", + "Lilly the Witch: The Journey to Mandolan", + "Lilo & Stitch", + "Lt. Robin Crusoe, U.S.N.", + "Make Mine Music", + "Maleficent", + "Maleficent: Mistress of Evil", + "Man of the House", + "Mars Needs Moms", + "Mary Poppins", + "Mary Poppins Returns", + "Max Keeble's Big Move", + "McFarland, USA", + "Meet the Deedles", + "Meet the Robinsons", + "Melody Time", + "Midnight Madness", + "Mighty Joe Young", + "Million Dollar Arm", + "Miracle", + "Miracle of the White Stallions", + "Moana", + "Monkey Kingdom", + "Monkeys, Go Home!", + "Monsters University", + "Monsters, Inc.", + "Moon Pilot", + "Morning Light", + "Mr. Magoo", + "Mulan", + "Muppet Treasure Island", + "Muppets Most Wanted", + "My Favorite Martian", + "Napoleon and Samantha", + "National Treasure", + "National Treasure: Book of Secrets", + "Never Cry Wolf", + "Never a Dull Moment", + "Newsies", + "Night Crossing", + "Nikki, Wild Dog of the North", + "No Deposit, No Return", + "Now You See Him, Now You Don't", + "Oceans", + "Old Dogs", + "Old Yeller", + "Oliver & Company", + "One Hundred and One Dalmatians", + "One Little Indian", + "One Magic Christmas", + "One of Our Dinosaurs Is Missing", + "Operation Dumbo Drop", + "Oz the Great and Powerful", + "Penguins", + "Perri", + "Pete's Dragon", + "Pete's Dragon", + "Peter Pan", + "Piglet's Big Movie", + "Pinocchio", + "Pirates of the Caribbean: At World's End", + "Pirates of the Caribbean: Dead Man's Chest", + "Pirates of the Caribbean: Dead Men Tell No Tales", + "Pirates of the Caribbean: On Stranger Tides", + "Pirates of the Caribbean: The Curse of the Black Pearl", + "Planes", + "Planes: Fire & Rescue", + "Pocahontas", + "Pollyanna", + "Pooh's Heffalump Movie", + "Popeye", + "Prince of Persia: The Sands of Time", + "Prom", + "Queen of Katwe", + "Race to Witch Mountain", + "Ralph Breaks the Internet", + "Rascal", + "Ratatouille", + "Recess: School's Out", + "Remember the Titans", + "Return from Witch Mountain", + "Return to Never Land", + "Return to Oz", + "Return to Snowy River", + "Ride a Wild Pony", + "Roadside Romeo", + "Rob Roy, the Highland Rogue", + "Robin Hood", + "RocketMan", + "Roving Mars", + "Run, Cougar, Run", + "Sacred Planet", + "Saludos Amigos", + "Savage Sam", + "Saving Mr. Banks", + "Scandalous John", + "Secretariat", + "Secrets of Life", + "Shipwrecked", + "Sky High", + "Sleeping Beauty", + "Smith!", + "Snow Dogs", + "Snow White and the Seven Dwarfs", + "Snowball Express", + "So Dear to My Heart", + "Something Wicked This Way Comes", + "Son of Flubber", + "Song of the South", + "Squanto: A Warrior's Tale", + "Summer Magic", + "Superdad", + "Swiss Family Robinson", + "Tall Tale", + "Tangled", + "Tarzan", + "Teacher's Pet", + "Ten Who Dared", + "Tex", + "That Darn Cat", + "That Darn Cat!", + "The Absent-Minded Professor", + "The Adventures of Bullwhip Griffin", + "The Adventures of Huck Finn", + "The Adventures of Ichabod and Mr. Toad", + "The African Lion", + "The Apple Dumpling Gang", + "The Apple Dumpling Gang Rides Again", + "The Aristocats", + "The BFG", + "The Barefoot Executive", + "The Bears and I", + "The Best of Walt Disney's True-Life Adventures", + "The Big Green", + "The Biscuit Eater", + "The Black Cauldron", + "The Black Hole", + "The Boatniks", + "The Book of Masters", + "The Boys: The Sherman Brothers' Story", + "The Castaway Cowboy", + "The Cat from Outer Space", + "The Chronicles of Narnia: Prince Caspian", + "The Chronicles of Narnia: The Lion, the Witch and the Wardrobe", + "The Computer Wore Tennis Shoes", + "The Country Bears", + "The Crimson Wing: Mystery of the Flamingos", + "The Devil and Max Devlin", + "The Emperor's New Groove", + "The Fighting Prince of Donegal", + "The Finest Hours", + "The Fox and the Hound", + "The Game Plan", + "The Gnome-Mobile", + "The Good Dinosaur", + "The Great Locomotive Chase", + "The Great Mouse Detective", + "The Greatest Game Ever Played", + "The Happiest Millionaire", + "The Haunted Mansion", + "The Horse in the Gray Flannel Suit", + "The Hunchback of Notre Dame", + "The Incredible Journey", + "The Incredibles", + "The Island at the Top of the World", + "The Journey of Natty Gann", + "The Jungle Book", + "The Jungle Book", + "The Jungle Book", + "The Jungle Book 2", + "The Last Flight of Noah's Ark", + "The Legend of Lobo", + "The Light in the Forest", + "The Lion King", + "The Lion King", + "The Little Mermaid", + "The Littlest Horse Thieves", + "The Littlest Outlaw", + "The Living Desert", + "The Lizzie McGuire Movie", + "The London Connection", + "The Lone Ranger", + "The Love Bug", + "The Many Adventures of Winnie the Pooh", + "The Mighty Ducks", + "The Million Dollar Duck", + "The Misadventures of Merlin Jones", + "The Monkey's Uncle", + "The Moon-Spinners", + "The Muppet Christmas Carol", + "The Muppets", + "The Nightmare Before Christmas 3D TNBC", + "The North Avenue Irregulars", + "The Nutcracker and the Four Realms", + "The Odd Life of Timothy Green", + "The One and Only, Genuine, Original Family Band", + "The Pacifier", + "The Parent Trap", + "The Parent Trap", + "The Pixar Story", + "The Princess Diaries", + "The Princess Diaries 2: Royal Engagement", + "The Princess and the Frog", + "The Reluctant Dragon", + "The Rescuers", + "The Rescuers Down Under", + "The Rocketeer TR", + "The Rookie", + "The Santa Clause 2", + "The Santa Clause 3: The Escape Clause", + "The Santa Clause TSC", + "The Shaggy D.A.", + "The Shaggy Dog", + "The Shaggy Dog", + "The Sign of Zorro", + "The Sorcerer's Apprentice", + "The Story of Robin Hood and His Merrie Men", + "The Straight Story", + "The Strongest Man in the World", + "The Sword and the Rose", + "The Sword in the Stone", + "The Three Caballeros", + "The Three Lives of Thomasina", + "The Three Musketeers", + "The Tigger Movie", + "The Ugly Dachshund", + "The Vanishing Prairie", + "The Watcher in the Woods", + "The Wild", + "The Wild Country", + "The World's Greatest Athlete", + "The Young Black Stallion", + "Third Man on the Mountain", + "Those Calloways", + "Toby Tyler", + "Tom and Huck", + "Tomorrowland", + "Tonka", + "Toy Story", + "Toy Story 2", + "Toy Story 3", + "Toy Story 4", + "Trail of the Panda", + "Treasure Island", + "Treasure Planet", + "Treasure of Matecumbe", + "Trenchcoat", + "Tron", + "Tron: Legacy", + "Tuck Everlasting", + "Underdog", + "Unidentified Flying Oddball", + "Up", + "Valiant", + "Victory Through Air Power", + "WALL-E", + "Waking Sleeping Beauty", + "Walt & El Grupo", + "Westward Ho the Wagons!", + "White Fang", + "White Fang 2: Myth of the White Wolf", + "White Wilderness", + "Wild Hearts Can't Be Broken", + "Wings of Life", + "Winnie the Pooh", + "Wreck-It Ralph", + "Zokkomon", + "Zootopia", + "Zorro the Avenger", + "Iron Man", + "Hulk", + "Thor", + "Avengers", + "Guardians of the Galaxy", + "Ant-Man", + "Captain America", + "Doctor Strange", + "Guardians of the Galaxy", + "Spider-Man", + "Black Panther", + "Marvel", + "Spider Man", + "SpiderMan", + "Loki", + "Winter Soldier", + "Wanda", + "Small Fry", + "Rex", + "Lamp life", + "Toy", + "Hawaiian" + }; + } } \ No newline at end of file diff --git a/src/Ombi.Helpers/ICacheService.cs b/src/Ombi.Helpers/ICacheService.cs index e3996a0e1..f22355ee7 100644 --- a/src/Ombi.Helpers/ICacheService.cs +++ b/src/Ombi.Helpers/ICacheService.cs @@ -6,8 +6,8 @@ namespace Ombi.Helpers { public interface ICacheService { - Task GetOrAdd(string cacheKey, Func> factory, DateTime absoluteExpiration = default(DateTime), CancellationToken cancellationToken = default(CancellationToken)); - T GetOrAdd(string cacheKey, Func factory, DateTime absoluteExpiration); + Task GetOrAddAsync(string cacheKey, Func> factory, DateTimeOffset absoluteExpiration = default); + T GetOrAdd(string cacheKey, Func factory, DateTimeOffset absoluteExpiration); void Remove(string key); } } \ No newline at end of file diff --git a/src/Ombi.Helpers/MediaCacheService.cs b/src/Ombi.Helpers/MediaCacheService.cs new file mode 100644 index 000000000..69ad8ec09 --- /dev/null +++ b/src/Ombi.Helpers/MediaCacheService.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LazyCache; + +namespace Ombi.Helpers +{ + public interface IMediaCacheService + { + Task GetOrAddAsync(string cacheKey, System.Func> factory, DateTimeOffset absoluteExpiration = default); + Task Purge(); + } + public class MediaCacheService : CacheService, IMediaCacheService + { + private const string CacheKey = "MediaCacheServiceKeys"; + + public MediaCacheService(IAppCache memoryCache) : base(memoryCache) + { + } + + public async override Task GetOrAddAsync(string cacheKey, System.Func> factory, DateTimeOffset absoluteExpiration = default) + { + if (absoluteExpiration == default) + { + absoluteExpiration = DateTimeOffset.Now.AddHours(1); + } + + if (_memoryCache.TryGetValue($"MediaCacheService_{cacheKey}", out var result)) + { + return (T)result; + } + + // Not in the cache, so add this Key into our MediaServiceCache + await UpdateLocalCache(cacheKey); + + return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + } + + private async Task UpdateLocalCache(string cacheKey) + { + var mediaServiceCache = await _memoryCache.GetAsync>(CacheKey); + if (mediaServiceCache == null) + { + mediaServiceCache = new List(); + } + mediaServiceCache.Add(cacheKey); + _memoryCache.Remove(CacheKey); + _memoryCache.Add(CacheKey, mediaServiceCache); + } + + public async Task Purge() + { + var keys = await _memoryCache.GetAsync>(CacheKey); + foreach (var key in keys) + { + base.Remove(key); + } + } + + } +} diff --git a/src/Ombi.Helpers/Ombi.Helpers.csproj b/src/Ombi.Helpers/Ombi.Helpers.csproj index 48969ac1a..7492444d1 100644 --- a/src/Ombi.Helpers/Ombi.Helpers.csproj +++ b/src/Ombi.Helpers/Ombi.Helpers.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Ombi.Helpers/PlexHelper.cs b/src/Ombi.Helpers/PlexHelper.cs index 08548b8c9..506e0939f 100644 --- a/src/Ombi.Helpers/PlexHelper.cs +++ b/src/Ombi.Helpers/PlexHelper.cs @@ -107,7 +107,7 @@ namespace Ombi.Helpers public static string GetPlexMediaUrl(string machineId, int mediaId) { var url = - $"https://app.plex.tv/web/app#!/server/{machineId}/details?key=library%2Fmetadata%2F{mediaId}"; + $"https://app.plex.tv/web/app#!/server/{machineId}/details?key=%2flibrary%2Fmetadata%2F{mediaId}"; return url; } diff --git a/src/Ombi.Mapping/Profiles/MovieProfile.cs b/src/Ombi.Mapping/Profiles/MovieProfile.cs index 161cb1fda..0551abd58 100644 --- a/src/Ombi.Mapping/Profiles/MovieProfile.cs +++ b/src/Ombi.Mapping/Profiles/MovieProfile.cs @@ -104,7 +104,7 @@ namespace Ombi.Mapping.Profiles .ForMember(x => x.Id, o => o.MapFrom(s => s.id)) .ForMember(x => x.Overview, o => o.MapFrom(s => s.overview)) .ForMember(x => x.PosterPath, o => o.MapFrom(s => s.poster_path)) - .ForMember(x => x.ReleaseDate, o => o.MapFrom(s => DateTime.Parse(s.release_date))) + .ForMember(x => x.ReleaseDate, o => o.MapFrom(s => string.IsNullOrEmpty(s.release_date) ? (DateTime?)null : DateTime.Parse(s.release_date))) .ForMember(x => x.Title, o => o.MapFrom(s => s.title)); CreateMap().ReverseMap(); diff --git a/src/Ombi.Mapping/Profiles/TvProfile.cs b/src/Ombi.Mapping/Profiles/TvProfile.cs index 55f5ccf9e..0aceffc4f 100644 --- a/src/Ombi.Mapping/Profiles/TvProfile.cs +++ b/src/Ombi.Mapping/Profiles/TvProfile.cs @@ -81,7 +81,9 @@ namespace Ombi.Mapping.Profiles .ForMember(dest => dest.Rating, opts => opts.MapFrom(src => src.VoteAverage.ToString())) .ForMember(dest => dest.BackdropPath, opts => opts.MapFrom(src => src.PosterPath)) //.ForMember(dest => dest.Runtime, opts => opts.MapFrom(src => src.Runtime.ToString())) - .ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title)); + .ForMember(dest => dest.Title, opts => opts.MapFrom(src => src.Title)) + .ForMember(dest => dest.SeasonRequests, opts => opts.MapFrom(src => src.SeasonRequests)) + ; //.ForMember(dest => dest.Status, opts => opts.MapFrom(src => TraktEnumHelper.GetDescription(src.Status))) //.ForMember(dest => dest.Trailer, // opts => opts.MapFrom(src => src.Trailer != null ? src.Trailer.ToString().ToHttpsUrl() : string.Empty)) diff --git a/src/Ombi.Mapping/Profiles/TvProfileV2.cs b/src/Ombi.Mapping/Profiles/TvProfileV2.cs index d54321d01..09021efb2 100644 --- a/src/Ombi.Mapping/Profiles/TvProfileV2.cs +++ b/src/Ombi.Mapping/Profiles/TvProfileV2.cs @@ -32,7 +32,7 @@ namespace Ombi.Mapping.Profiles .ForMember(dest => dest.Images, opts => opts.MapFrom(src => src.Images)) .ForMember(dest => dest.Cast, opts => opts.MapFrom(src => src.Credits.cast)) .ForMember(dest => dest.Crew, opts => opts.MapFrom(src => src.Credits.crew)) - .ForMember(dest => dest.Banner, opts => opts.MapFrom(src => GetBanner(src.Images))) + .ForMember(dest => dest.Banner, opts => opts.MapFrom(src => GetBanner(src.Images, src.backdrop_path))) .ForMember(dest => dest.Genres, opts => opts.MapFrom(src => src.genres)) .ForMember(dest => dest.Keywords, opts => opts.MapFrom(src => src.Keywords)) .ForMember(dest => dest.Tagline, opts => opts.MapFrom(src => src.tagline)) @@ -78,20 +78,20 @@ namespace Ombi.Mapping.Profiles CreateMap().ReverseMap(); } - private string GetBanner(Api.TheMovieDb.Models.Images images) + private string GetBanner(Api.TheMovieDb.Models.Images images, string backdropPath) { var hasBackdrop = images?.Backdrops?.Any(); if (hasBackdrop ?? false) { return images.Backdrops?.OrderBy(x => x.VoteCount).ThenBy(x => x.VoteAverage).Select(x => x.FilePath).FirstOrDefault(); } - else if (images != null) + else if (images?.Posters?.Any() ?? false) { return images.Posters?.OrderBy(x => x.VoteCount).ThenBy(x => x.VoteAverage).Select(x => x.FilePath).FirstOrDefault(); } else { - return string.Empty; + return backdropPath; } } diff --git a/src/Ombi.Notifications.Templates/INewsletterTemplate.cs b/src/Ombi.Notifications.Templates/INewsletterTemplate.cs index e3302710d..620cf3999 100644 --- a/src/Ombi.Notifications.Templates/INewsletterTemplate.cs +++ b/src/Ombi.Notifications.Templates/INewsletterTemplate.cs @@ -2,6 +2,6 @@ { public interface INewsletterTemplate { - string LoadTemplate(string subject, string intro, string tableHtml, string logo); + string LoadTemplate(string subject, string intro, string tableHtml, string logo, string unsubscribeLink); } } \ No newline at end of file diff --git a/src/Ombi.Notifications.Templates/NewsletterTemplate.cs b/src/Ombi.Notifications.Templates/NewsletterTemplate.cs index 84d92c21c..f2f0cbf12 100644 --- a/src/Ombi.Notifications.Templates/NewsletterTemplate.cs +++ b/src/Ombi.Notifications.Templates/NewsletterTemplate.cs @@ -13,7 +13,7 @@ namespace Ombi.Notifications.Templates if (string.IsNullOrEmpty(_templateLocation)) { #if DEBUG - _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "netcoreapp3.0", "Templates", "NewsletterTemplate.html"); + _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "bin", "Debug", "net5.0", "Templates", "NewsletterTemplate.html"); #else _templateLocation = Path.Combine(Directory.GetCurrentDirectory(), "Templates", "NewsletterTemplate.html"); #endif @@ -29,9 +29,10 @@ namespace Ombi.Notifications.Templates private const string Logo = "{@LOGO}"; private const string TableLocation = "{@RECENTLYADDED}"; private const string IntroText = "{@INTRO}"; + private const string Unsubscribe = "{@UNSUBSCRIBE}"; - public string LoadTemplate(string subject, string intro, string tableHtml, string logo) + public string LoadTemplate(string subject, string intro, string tableHtml, string logo, string unsubscribeLink) { var sb = new StringBuilder(File.ReadAllText(TemplateLocation)); sb.Replace(SubjectKey, subject); @@ -39,6 +40,7 @@ namespace Ombi.Notifications.Templates sb.Replace(IntroText, intro); sb.Replace(DateKey, DateTime.Now.ToString("f")); sb.Replace(Logo, string.IsNullOrEmpty(logo) ? OmbiLogo : logo); + sb.Replace(Unsubscribe, string.IsNullOrEmpty(unsubscribeLink) ? string.Empty : unsubscribeLink); return sb.ToString(); } diff --git a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html index 10b76dd30..71666ba67 100644 --- a/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html +++ b/src/Ombi.Notifications.Templates/Templates/NewsletterTemplate.html @@ -451,6 +451,11 @@