From 450548828940b4b831f7510377c7eabd0add75a4 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 1 Apr 2021 10:11:47 +0100 Subject: [PATCH 01/27] added the api to get libs --- src/Ombi.Api.Jellyfin/IJellyfinApi.cs | 1 + src/Ombi.Api.Jellyfin/JellyfinApi.cs | 16 +++++++++++++--- src/Ombi.Api.Jellyfin/Models/MediaFolders.cs | 10 ++++++++++ .../V1/External/JellyfinController.cs | 8 ++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 src/Ombi.Api.Jellyfin/Models/MediaFolders.cs diff --git a/src/Ombi.Api.Jellyfin/IJellyfinApi.cs b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs index 72d45877c..5dcf5741f 100644 --- a/src/Ombi.Api.Jellyfin/IJellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs @@ -6,5 +6,6 @@ namespace Ombi.Api.Jellyfin public interface IJellyfinApi : IBaseJellyfinApi { Task LoginConnectUser(string username, string password); + Task> GetLibraries(string apiKey, string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/JellyfinApi.cs b/src/Ombi.Api.Jellyfin/JellyfinApi.cs index a8d94eca6..9ec6c8241 100644 --- a/src/Ombi.Api.Jellyfin/JellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/JellyfinApi.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json; using Ombi.Api.Jellyfin.Models; using Ombi.Api.Jellyfin.Models.Media.Tv; using Ombi.Api.Jellyfin.Models.Movie; -using Ombi.Helpers; namespace Ombi.Api.Jellyfin { @@ -87,6 +87,16 @@ namespace Ombi.Api.Jellyfin return await Api.Request>(request); } + public async Task> GetLibraries(string apiKey, string baseUrl) + { + var request = new Request("library/mediafolders", baseUrl, HttpMethod.Get); + AddHeaders(request, apiKey); + + var response = await Api.Request>(request); + response.Items = response.Items.Where(x => !x.CollectionType.Equals("playlists", StringComparison.InvariantCultureIgnoreCase)).ToList(); + return response; + } + public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) { return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); diff --git a/src/Ombi.Api.Jellyfin/Models/MediaFolders.cs b/src/Ombi.Api.Jellyfin/Models/MediaFolders.cs new file mode 100644 index 000000000..caa9b6813 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/MediaFolders.cs @@ -0,0 +1,10 @@ +namespace Ombi.Api.Jellyfin.Models +{ + public class MediaFolders + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string CollectionType { get; set; } + } +} diff --git a/src/Ombi/Controllers/V1/External/JellyfinController.cs b/src/Ombi/Controllers/V1/External/JellyfinController.cs index 27663a5b6..5e8b87414 100644 --- a/src/Ombi/Controllers/V1/External/JellyfinController.cs +++ b/src/Ombi/Controllers/V1/External/JellyfinController.cs @@ -66,6 +66,14 @@ namespace Ombi.Controllers.V1.External return result; } + [HttpPost("Library")] + public async Task> GetLibaries([FromBody] JellyfinServers server) + { + var client = await JellyfinApi.CreateClient(); + var result = await client.GetLibraries(server.ApiKey, server.FullUri); + return result; + } + /// /// Gets the jellyfin users. /// From f8c11144012695ea6bf3aba86bf5e998fc73c2ca Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 25 Jun 2021 11:54:07 +0100 Subject: [PATCH 02/27] Fixed #4234 --- src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs | 34 ++++++++++++++------ src/Ombi/ClientApp/src/app/app.component.ts | 1 - 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs index d7942f495..2569029f3 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/NewsletterJob.cs @@ -130,12 +130,11 @@ namespace Ombi.Schedule.Jobs.Ombi var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes).AsNoTracking(); var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable); - var addedLog = _recentlyAddedLog.GetAll(); + var addedLog = _recentlyAddedLog.GetAll().ToList(); - var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent)?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet(); - var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet(); - var addedJellyfinMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet(); - var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId).ToHashSet(); + HashSet addedPlexMovieLogIds, addedEmbyMoviesLogIds, addedJellyfinMoviesLogIds; + HashSet addedAlbumLogIds; + GetRecentlyAddedMoviesData(addedLog, out addedPlexMovieLogIds, out addedEmbyMoviesLogIds, out addedJellyfinMoviesLogIds, out addedAlbumLogIds); var addedPlexEpisodesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode); @@ -375,6 +374,21 @@ namespace Ombi.Schedule.Jobs.Ombi .SendAsync(NotificationHub.NotificationEvent, "Newsletter Finished"); } + private void GetRecentlyAddedMoviesData(List addedLog, out HashSet addedPlexMovieLogIds, out HashSet addedEmbyMoviesLogIds, out HashSet addedJellyfinMoviesLogIds, out HashSet addedAlbumLogIds) + { + var plexParent = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent).ToList(); + addedPlexMovieLogIds = plexParent != null && plexParent.Any() ? (plexParent?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet()) : new HashSet(); + + var embyParent = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent); + addedEmbyMoviesLogIds = embyParent != null && embyParent.Any() ? (embyParent?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet()) : new HashSet(); + + var jellyFinParent = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent); + addedJellyfinMoviesLogIds = jellyFinParent != null && jellyFinParent.Any() ? (jellyFinParent?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet()) : new HashSet(); + + var lidarrParent = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album); + addedAlbumLogIds = lidarrParent != null && lidarrParent.Any() ? (lidarrParent?.Select(x => x.AlbumId)?.ToHashSet() ?? new HashSet()) : new HashSet(); + } + public static string GenerateUnsubscribeLink(string applicationUrl, string id) { if (!applicationUrl.HasValue()) @@ -487,7 +501,7 @@ namespace Ombi.Schedule.Jobs.Ombi await Start(newsletterSettings, false); } - private HashSet FilterPlexEpisodes(IEnumerable source, IQueryable recentlyAdded) + private HashSet FilterPlexEpisodes(IEnumerable source, IEnumerable recentlyAdded) { var itemsToReturn = new HashSet(); foreach (var ep in source.Where(x => x.Series.HasTvDb)) @@ -504,7 +518,7 @@ namespace Ombi.Schedule.Jobs.Ombi return itemsToReturn; } - private HashSet FilterEmbyEpisodes(IEnumerable source, IQueryable recentlyAdded) + private HashSet FilterEmbyEpisodes(IEnumerable source, IEnumerable recentlyAdded) { var itemsToReturn = new HashSet(); foreach (var ep in source.Where(x => x.Series.HasTvDb)) @@ -521,7 +535,7 @@ namespace Ombi.Schedule.Jobs.Ombi return itemsToReturn; } - private HashSet FilterJellyfinEpisodes(IEnumerable source, IQueryable recentlyAdded) + private HashSet FilterJellyfinEpisodes(IEnumerable source, IEnumerable recentlyAdded) { var itemsToReturn = new HashSet(); foreach (var ep in source.Where(x => x.Series.HasTvDb)) @@ -558,7 +572,7 @@ namespace Ombi.Schedule.Jobs.Ombi var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie); var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie); var jellyfinMovies = jellyfinContentToSend.Where(x => x.Type == JellyfinMediaType.Movie); - if ((plexMovies.Any() || embyMovies.Any()) || jellyfinMovies.Any() && !settings.DisableMovies) + if ((plexMovies.Any() || embyMovies.Any() || jellyfinMovies.Any()) && !settings.DisableMovies) { sb.Append("

New Movies



"); sb.Append( @@ -589,7 +603,7 @@ namespace Ombi.Schedule.Jobs.Ombi sb.Append(""); } - if ((plexEpisodes.Any() || embyEp.Any()) || jellyfinEp.Any() && !settings.DisableTv) + if ((plexEpisodes.Any() || embyEp.Any() || jellyfinEp.Any()) && !settings.DisableTv) { sb.Append("

New TV



"); sb.Append( diff --git a/src/Ombi/ClientApp/src/app/app.component.ts b/src/Ombi/ClientApp/src/app/app.component.ts index 038fcc7d7..ef5840c2e 100644 --- a/src/Ombi/ClientApp/src/app/app.component.ts +++ b/src/Ombi/ClientApp/src/app/app.component.ts @@ -28,7 +28,6 @@ export class AppComponent implements OnInit { public showNav: boolean; public updateAvailable: boolean; public currentUrl: string; - public userAccessToken: string; public voteEnabled = false; public applicationName: string = "Ombi" public isAdmin: boolean; From bd857c1e4f3e48c68f6f088643327130e5c67f50 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 28 Jun 2021 23:27:58 +0100 Subject: [PATCH 03/27] Added some more logging and resilience around the Plex OAuth login #4232 --- src/Ombi.Api.Plex/IPlexApi.cs | 2 +- src/Ombi.Api.Plex/Models/OAuth/OAuthPin.cs | 18 +++++++++++++++ src/Ombi.Api.Plex/PlexApi.cs | 22 ++++++++++++++++--- src/Ombi.Api/Api.cs | 2 +- .../Authentication/PlexOAuthManager.cs | 21 +++++++++++++++--- .../services/applications/plextv.service.ts | 1 + 6 files changed, 58 insertions(+), 8 deletions(-) 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/Api.cs b/src/Ombi.Api/Api.cs index 7a8e7678b..66dec8b0b 100644 --- a/src/Ombi.Api/Api.cs +++ b/src/Ombi.Api/Api.cs @@ -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.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/ClientApp/src/app/services/applications/plextv.service.ts b/src/Ombi/ClientApp/src/app/services/applications/plextv.service.ts index f214ac291..3ce0e0a8b 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/plextv.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/plextv.service.ts @@ -20,6 +20,7 @@ export class PlexTvService { "X-Plex-Device": "Ombi (Web)", "X-Plex-Platform": "Web", "Accept": "application/json", + 'X-Plex-Model': 'Plex OAuth', }); return this.http.post("https://plex.tv/api/v2/pins?strong=true", null, {headers}); } From 5db455e899dacfdee657ce4d586304687c652c57 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 1 Jul 2021 21:10:10 +0100 Subject: [PATCH 04/27] Added back in the ability to mark movies as unavailable --- .../components/movie/movie-details.component.html | 5 +++++ .../components/movie/movie-details.component.ts | 11 +++++++++++ .../shared/top-banner/top-banner.component.html | 3 ++- .../shared/top-banner/top-banner.component.ts | 6 ++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.html b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.html index 0fd13097c..4e088bd69 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.html @@ -82,6 +82,11 @@ {{ 'Requests.MarkAvailable' | translate }} + + diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts index 01c8a5783..9cb446656 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/movie-details.component.ts @@ -168,6 +168,17 @@ export class MovieDetailsComponent { } } + + public async markUnavailable() { + const result = await this.requestService.markMovieUnavailable({ id: this.movieRequest.id }).toPromise(); + if (result.result) { + this.movie.available = false; + this.messageService.send(result.message, "Ok"); + } else { + this.messageService.send(result.errorMessage, "Ok"); + } + } + public setAdvancedOptions(data: IAdvancedData) { this.advancedOptions = data; if (data.rootFolderId) { diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.html b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.html index 55550160e..ba45ea5fa 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.html @@ -9,7 +9,8 @@
-

{{title}} +

{{title}} + ({{releaseDate | amLocal | amDateFormat: 'YYYY'}})

diff --git a/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.ts b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.ts index c7db74305..ec2379b0b 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.ts +++ b/src/Ombi/ClientApp/src/app/media-details/components/shared/top-banner/top-banner.component.ts @@ -13,6 +13,12 @@ export class TopBannerComponent { @Input() available: boolean; @Input() background: any; + get releaseDateFormat(): Date|null { + if (this.releaseDate && this.releaseDate instanceof Date && this.releaseDate.getFullYear() !== 1) { + return this.releaseDate; + } + return null; + } constructor(private sanitizer:DomSanitizer){ } From a9bbd55c0599d3cd0352bf22dd94bab82a456cbc Mon Sep 17 00:00:00 2001 From: stefangross Date: Sat, 3 Jul 2021 10:43:15 +0200 Subject: [PATCH 05/27] Fixed issue with couchpotato profiles not loading correctly --- .../src/app/settings/couchpotato/couchpotato.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html b/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html index 396fe12ad..0c4fec2d2 100644 --- a/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html +++ b/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html @@ -1,4 +1,4 @@ - +
@@ -55,7 +55,7 @@ Quality Profiles - {{profile.label}} + {{profile.label}} A Default Quality Profile is required @@ -85,4 +85,4 @@
-
\ No newline at end of file +

From 0bbf5c1b562112d9b226226f19f9f55daf843dd2 Mon Sep 17 00:00:00 2001 From: stefangross Date: Sat, 3 Jul 2021 11:20:42 +0200 Subject: [PATCH 06/27] added null check to to prevent crash when loading couchpotato settings for the first time --- .../src/app/settings/couchpotato/couchpotato.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html b/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html index 0c4fec2d2..d1ab4edb6 100644 --- a/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html +++ b/src/Ombi/ClientApp/src/app/settings/couchpotato/couchpotato.component.html @@ -55,7 +55,7 @@ Quality Profiles - {{profile.label}} + {{profile.label}} A Default Quality Profile is required From 8c7087452559fa49963584f81b05b9668930fd4d Mon Sep 17 00:00:00 2001 From: stefangross Date: Sun, 4 Jul 2021 10:29:17 +0200 Subject: [PATCH 07/27] fixed issue with sorting movie requests --- src/Ombi.Core/Engine/MovieRequestEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 269961c2f..7507389b7 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -315,7 +315,7 @@ namespace Ombi.Core.Engine // TODO fix this so we execute this on the server var requests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) - ? allRequests.ToList().OrderBy(x => x.RequestedDate).ToList() + ? allRequests.ToList().OrderBy(x => prop.GetValue(x)).ToList() : allRequests.ToList().OrderByDescending(x => prop.GetValue(x)).ToList(); var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); From d01da2d28811b6a6df60552e6c9b45f8e9a5d671 Mon Sep 17 00:00:00 2001 From: stefangross Date: Sun, 4 Jul 2021 10:29:17 +0200 Subject: [PATCH 08/27] fixed issue with sorting movie requests --- src/Ombi.Core/Engine/MovieRequestEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 269961c2f..7507389b7 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -315,7 +315,7 @@ namespace Ombi.Core.Engine // TODO fix this so we execute this on the server var requests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) - ? allRequests.ToList().OrderBy(x => x.RequestedDate).ToList() + ? allRequests.ToList().OrderBy(x => prop.GetValue(x)).ToList() : allRequests.ToList().OrderByDescending(x => prop.GetValue(x)).ToList(); var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); From b406dc0118b2a4f4de7ac5db5d3ce29c1b26038b Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 5 Jul 2021 14:20:25 +0100 Subject: [PATCH 09/27] Fixed the issue where the first scan has the potential to not get all the shows depending on the order of the plex libraries --- src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs index 8333eed56..928baa0c3 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs @@ -175,7 +175,7 @@ namespace Ombi.Schedule.Jobs.Plex var allEps = Repo.GetAllEpisodes(); - foreach (var content in allContent) + foreach (var content in allContent.OrderByDescending(x => x.viewGroup)) { Logger.LogDebug($"Got type '{content.viewGroup}' to process"); if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.InvariantCultureIgnoreCase)) From bc59a5051eabf6d5dfd2abee4fa943cf47314b17 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 5 Jul 2021 14:57:16 +0100 Subject: [PATCH 10/27] Added the ability to show seasonal content ;) --- .../Engine/Interfaces/IMovieEngineV2.cs | 1 + .../Engine/V2/MovieSearchEngineV2.cs | 29 ++++++++++++++++++- src/Ombi.TheMovieDbApi/IMovieDbApi.cs | 1 + src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 26 +++++++++++++++++ .../carousel-list.component.html | 2 +- .../carousel-list/carousel-list.component.ts | 8 ++++- .../discover/discover.component.html | 8 +++++ .../components/discover/discover.component.ts | 7 +++++ .../src/app/services/searchV2.service.ts | 4 +++ src/Ombi/Controllers/V2/SearchController.cs | 13 +++++++++ src/Ombi/wwwroot/translations/en.json | 1 + 11 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index 442359f0f..5a3624c5e 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -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/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index f3023c848..b29171eaf 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,6 +46,7 @@ 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) { @@ -190,6 +193,30 @@ namespace Ombi.Core.Engine.V2 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)) + { + return new List(); + } + + var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await Cache.GetOrAdd(nameof(SeasonalList) + pagesToLoad.Page + langCode + keyWordIds, + async () => await MovieApi.GetMoviesViaKeywords(keyWordIds, langCode, cancellationToken, pagesToLoad.Page), DateTime.Now.AddHours(12)); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + /// /// Gets recently requested movies /// diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index fb15aab5f..7a0f7e385 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -18,6 +18,7 @@ namespace Ombi.Api.TheMovieDb Task> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task> PopularTv(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task> SearchMovie(string searchTerm, int? year, string languageCode); + Task> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null); Task> SearchTv(string searchTerm, string year = default); Task> TopRated(string languageCode, int? page = null); Task> Upcoming(string languageCode, int? page = null); diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index c5a290e04..70e4a94c7 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -331,6 +331,32 @@ namespace Ombi.Api.TheMovieDb return await Api.Request(request, token); } + public async Task> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null) + { + var request = new Request($"discover/movie", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("language", langCode); + request.AddQueryString("sort_by", "vote_average.desc"); + + request.AddQueryString("with_keywords", keywordId); + + // `vote_count` consideration isn't explicitly documented, but using only the `sort_by` filter + // does not provide the same results as `/movie/top_rated`. This appears to be adequate enough + // to filter out extremely high-rated movies due to very little votes + request.AddQueryString("vote_count.gte", "250"); + + if (page != null) + { + request.AddQueryString("page", page.ToString()); + } + + await AddDiscoverSettings(request); + await AddGenreFilter(request, "movie"); + AddRetry(request); + var result = await Api.Request>(request, cancellationToken); + return Mapper.Map>(result.results); + } + public async Task> SearchKeyword(string searchTerm) { var request = new Request("search/keyword", BaseUri, HttpMethod.Get); diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html index 73e68336e..aa9ffc5f2 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.html @@ -1,4 +1,4 @@ -
+
{{'Discovery.Combined' | translate}} {{'Discovery.Movies' | translate}} diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts index 2c397e12e..b45f0ccbb 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from "@angular/core"; import { DiscoverOption, IDiscoverCardResult } from "../../interfaces"; import { ISearchMovieResult, ISearchTvResult, RequestType } from "../../../interfaces"; import { SearchV2Service } from "../../../services"; @@ -11,6 +11,7 @@ export enum DiscoverType { Trending, Popular, RecentlyRequested, + Seasonal, } @Component({ @@ -23,6 +24,7 @@ export class CarouselListComponent implements OnInit { @Input() public discoverType: DiscoverType; @Input() public id: string; @Input() public isAdmin: boolean; + @Output() public movieCount: EventEmitter = new EventEmitter(); @ViewChild('carousel', {static: false}) carousel: Carousel; public DiscoverOption = DiscoverOption; @@ -33,6 +35,7 @@ export class CarouselListComponent implements OnInit { public responsiveOptions: any; public RequestType = RequestType; public loadingFlag: boolean; + public DiscoverType = DiscoverType; get mediaTypeStorageKey() { return "DiscoverOptions" + this.discoverType.toString(); @@ -220,7 +223,10 @@ export class CarouselListComponent implements OnInit { break case DiscoverType.RecentlyRequested: this.movies = await this.searchService.recentlyRequestedMoviesByPage(this.currentlyLoaded, this.amountToLoad); + case DiscoverType.Seasonal: + this.movies = await this.searchService.seasonalMoviesByPage(this.currentlyLoaded, this.amountToLoad); } + this.movieCount.emit(this.movies.length); this.currentlyLoaded += this.amountToLoad; } diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html index 701a6b204..89f5dda07 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.html @@ -1,4 +1,12 @@
+ +
+

{{'Discovery.SeasonalTab' | translate}}

+
+ +
+
+

{{'Discovery.PopularTab' | translate}}

diff --git a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts index ac5a6a9dc..155a69cef 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/discover/discover.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from "@angular/core"; + import { AuthService } from "../../../auth/auth.service"; import { DiscoverType } from "../carousel-list/carousel-list.component"; @@ -11,6 +12,7 @@ export class DiscoverComponent implements OnInit { public DiscoverType = DiscoverType; public isAdmin: boolean; + public showSeasonal: boolean; constructor(private authService: AuthService) { } @@ -18,4 +20,9 @@ export class DiscoverComponent implements OnInit { this.isAdmin = this.authService.isAdmin(); } + public setSeasonalMovieCount(count: number) { + if (count > 0) { + this.showSeasonal = true; + } + } } diff --git a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts index f7c287fd2..ece4a26c9 100644 --- a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts +++ b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts @@ -63,6 +63,10 @@ export class SearchV2Service extends ServiceHelpers { return this.http.get(`${this.url}/Movie/requested/${currentlyLoaded}/${toLoad}`).toPromise(); } + public seasonalMoviesByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/Movie/seasonal/${currentlyLoaded}/${toLoad}`).toPromise(); + } + public nowPlayingMovies(): Observable { return this.http.get(`${this.url}/Movie/nowplaying`); } diff --git a/src/Ombi/Controllers/V2/SearchController.cs b/src/Ombi/Controllers/V2/SearchController.cs index 4fbdf50f1..6d911ec71 100644 --- a/src/Ombi/Controllers/V2/SearchController.cs +++ b/src/Ombi/Controllers/V2/SearchController.cs @@ -164,6 +164,19 @@ namespace Ombi.Controllers.V2 return await _movieEngineV2.PopularMovies(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); } + /// + /// Returns Seasonal Movies + /// + /// We use TheMovieDb as the Movie Provider + /// + [HttpGet("movie/seasonal/{currentPosition}/{amountToLoad}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public async Task> Seasonal(int currentPosition, int amountToLoad) + { + return await _movieEngineV2.SeasonalList(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); + } + /// /// Returns Recently Requested Movies using Paging /// diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index 853927d4f..b497c784e 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -295,6 +295,7 @@ "PopularTab": "Popular", "TrendingTab": "Trending", "UpcomingTab": "Upcoming", + "SeasonalTab": "Seasonal", "RecentlyRequestedTab": "Recently Requested", "Movies": "Movies", "Combined": "Combined", From d417526b265c5b454682fe73e865432f917ea50b Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 5 Jul 2021 15:21:55 +0100 Subject: [PATCH 11/27] WIP on recently requested --- .../Engine/Interfaces/ITvSearchEngineV2.cs | 1 + .../Engine/V2/MovieSearchEngineV2.cs | 2 +- src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs | 43 ++++++++++++++++++- .../carousel-list/carousel-list.component.ts | 5 +++ .../src/app/services/searchV2.service.ts | 4 ++ src/Ombi/Controllers/V2/SearchController.cs | 12 ++++++ 6 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs index 3e2d859c8..25a3c2621 100644 --- a/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/ITvSearchEngineV2.cs @@ -14,5 +14,6 @@ namespace Ombi.Core 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/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index b29171eaf..4ff99225a 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -200,7 +200,7 @@ namespace Ombi.Core.Engine.V2 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)) + if (string.IsNullOrEmpty(keyWordIds) || keyWordIds.Equals("\n")) { return new List(); } diff --git a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs index 865693244..6bb67debd 100644 --- a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs @@ -23,6 +23,8 @@ 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 { @@ -33,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; @@ -44,6 +47,7 @@ namespace Ombi.Core.Engine.V2 _traktApi = trakt; _movieApi = movieApi; _customization = customization; + _requestEngine = requestEngine; } @@ -164,6 +168,43 @@ namespace Ombi.Core.Engine.V2 return data; } + + public async Task> RecentlyRequestedShows(int currentlyLoaded, int toLoad, CancellationToken cancellationToken) + { + var langCode = await DefaultLanguageCode(null); + + var results = new List(); + + var requestResult = await Cache.GetOrAdd(nameof(RecentlyRequestedShows) + "Requests" + toLoad + langCode, + async () => + { + return await _requestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel + { + OrderType = OrderType.RequestedDateDesc + }); + }, DateTime.Now.AddMinutes(15), cancellationToken); + + var movieDBResults = await Cache.GetOrAdd(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; + }, DateTime.Now.AddHours(12), cancellationToken); + + 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(); diff --git a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts index b45f0ccbb..3ab5bbe17 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/carousel-list/carousel-list.component.ts @@ -223,8 +223,10 @@ export class CarouselListComponent implements OnInit { break case DiscoverType.RecentlyRequested: this.movies = await this.searchService.recentlyRequestedMoviesByPage(this.currentlyLoaded, this.amountToLoad); + break; case DiscoverType.Seasonal: this.movies = await this.searchService.seasonalMoviesByPage(this.currentlyLoaded, this.amountToLoad); + break; } this.movieCount.emit(this.movies.length); this.currentlyLoaded += this.amountToLoad; @@ -241,6 +243,9 @@ export class CarouselListComponent implements OnInit { case DiscoverType.Upcoming: this.tvShows = await this.searchService.anticipatedTvByPage(this.currentlyLoaded, this.amountToLoad); break + case DiscoverType.RecentlyRequested: + // this.tvShows = await this.searchService.recentlyRequestedMoviesByPage(this.currentlyLoaded, this.amountToLoad); // TODO need to do some more mapping + break; } this.currentlyLoaded += this.amountToLoad; } diff --git a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts index ece4a26c9..99e5afb39 100644 --- a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts +++ b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts @@ -63,6 +63,10 @@ export class SearchV2Service extends ServiceHelpers { return this.http.get(`${this.url}/Movie/requested/${currentlyLoaded}/${toLoad}`).toPromise(); } + public recentlyRequestedTvByPage(currentlyLoaded: number, toLoad: number): Promise { + return this.http.get(`${this.url}/tv/requested/${currentlyLoaded}/${toLoad}`).toPromise(); + } + public seasonalMoviesByPage(currentlyLoaded: number, toLoad: number): Promise { return this.http.get(`${this.url}/Movie/seasonal/${currentlyLoaded}/${toLoad}`).toPromise(); } diff --git a/src/Ombi/Controllers/V2/SearchController.cs b/src/Ombi/Controllers/V2/SearchController.cs index 6d911ec71..d59bf8fcd 100644 --- a/src/Ombi/Controllers/V2/SearchController.cs +++ b/src/Ombi/Controllers/V2/SearchController.cs @@ -189,6 +189,18 @@ namespace Ombi.Controllers.V2 { return await _movieEngineV2.RecentlyRequestedMovies(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); } + /// + /// Returns Recently Requested Tv using Paging + /// + /// We use TheMovieDb as the Movie Provider + /// + [HttpGet("tv/requested/{currentPosition}/{amountToLoad}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public async Task> RecentlyRequestedTv(int currentPosition, int amountToLoad) + { + return await _tvEngineV2.RecentlyRequestedShows(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); + } /// /// Returns Now Playing Movies From 4082f6ad4132c8be067c0b9490a2b5eff95b1e13 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 5 Jul 2021 15:22:20 +0100 Subject: [PATCH 12/27] Missing commit --- src/Ombi/Controllers/V2/SearchController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ombi/Controllers/V2/SearchController.cs b/src/Ombi/Controllers/V2/SearchController.cs index d59bf8fcd..9391815dd 100644 --- a/src/Ombi/Controllers/V2/SearchController.cs +++ b/src/Ombi/Controllers/V2/SearchController.cs @@ -189,6 +189,7 @@ namespace Ombi.Controllers.V2 { return await _movieEngineV2.RecentlyRequestedMovies(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); } + /// /// Returns Recently Requested Tv using Paging /// From 0c4bd10b0f002fd67b55a07247828a7f40682433 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 7 Jul 2021 11:59:27 +0100 Subject: [PATCH 13/27] Added the Digital Release Date back in. Improved performance and system load when loading content #4082 --- src/Ombi.Core/Engine/BaseMediaEngine.cs | 2 +- src/Ombi.Core/Engine/MovieRequestEngine.cs | 18 ++- src/Ombi.Core/Engine/MovieSearchEngine.cs | 22 +-- src/Ombi.Core/Engine/TvRequestEngine.cs | 31 ++--- src/Ombi.Core/Engine/TvSearchEngine.cs | 26 ++-- .../Engine/V2/MovieSearchEngineV2.cs | 90 ++++++++----- src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs | 34 ++--- src/Ombi.Core/ImageService.cs | 4 +- src/Ombi.DependencyInjection/IocExtensions.cs | 1 + src/Ombi.Helpers/CacheService.cs | 60 ++------- src/Ombi.Helpers/ICacheService.cs | 4 +- src/Ombi.Helpers/MediaCacheService.cs | 61 +++++++++ src/Ombi.Helpers/Ombi.Helpers.csproj | 1 + src/Ombi.Settings/Settings/SettingsService.cs | 6 +- .../movie-information-panel.component.html | 2 +- .../V1/External/LidarrController.cs | 8 +- src/Ombi/Controllers/V1/IdentityController.cs | 4 +- src/Ombi/Controllers/V1/ImagesController.cs | 34 ++--- src/Ombi/Controllers/V1/JobController.cs | 2 +- src/Ombi/Controllers/V1/UpdateController.cs | 4 +- src/Ombi/Controllers/V2/RequestsController.cs | 1 + src/Ombi/Controllers/V2/SearchController.cs | 127 ++++++++++++------ src/Ombi/Extensions/StartupExtensions.cs | 2 +- src/Ombi/Ombi.csproj | 1 + src/Ombi/Startup.cs | 2 +- 25 files changed, 320 insertions(+), 227 deletions(-) create mode 100644 src/Ombi.Helpers/MediaCacheService.cs diff --git a/src/Ombi.Core/Engine/BaseMediaEngine.cs b/src/Ombi.Core/Engine/BaseMediaEngine.cs index deef9ebea..71a756543 100644 --- a/src/Ombi.Core/Engine/BaseMediaEngine.cs +++ b/src/Ombi.Core/Engine/BaseMediaEngine.cs @@ -127,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, diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 269961c2f..88958971e 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -30,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; @@ -38,6 +38,7 @@ namespace Ombi.Core.Engine Sender = sender; Logger = log; _requestLog = rl; + _mediaCacheService = mediaCacheService; } private IMovieDbApi MovieApi { get; } @@ -45,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. @@ -371,7 +373,6 @@ namespace Ombi.Core.Engine }; } - public async Task UpdateAdvancedOptions(MediaAdvancedOptions options) { var request = await MovieRepository.Find(options.RequestId); @@ -527,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 { @@ -555,6 +557,7 @@ namespace Ombi.Core.Engine { await NotificationHelper.Notify(request, NotificationType.RequestApproved); } + await _mediaCacheService.Purge(); return await ProcessSendingMovie(request); } @@ -562,8 +565,8 @@ namespace Ombi.Core.Engine public async Task RequestCollection(int collectionId, CancellationToken cancellationToken) { var langCode = await DefaultLanguageCode(null); - var collections = await Cache.GetOrAdd($"GetCollection{collectionId}{langCode}", - async () => await MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTime.Now.AddDays(1), cancellationToken); + 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) @@ -639,6 +642,7 @@ namespace Ombi.Core.Engine results.RootPathOverride = request.RootPathOverride; await MovieRepository.Update(results); + await _mediaCacheService.Purge(); return results; } @@ -651,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) @@ -692,6 +698,7 @@ namespace Ombi.Core.Engine request.Available = false; await MovieRepository.Update(request); + await _mediaCacheService.Purge(); return new RequestEngineResult { @@ -715,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 { @@ -733,6 +741,8 @@ namespace Ombi.Core.Engine 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/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 531fa161d..086eff0c9 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -35,7 +35,7 @@ namespace Ombi.Core.Engine public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user, 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; @@ -43,6 +43,7 @@ namespace Ombi.Core.Engine _logger = logger; TvSender = sender; _requestLog = rl; + _mediaCacheService = mediaCacheService; } private INotificationHelper NotificationHelper { get; } @@ -52,6 +53,7 @@ namespace Ombi.Core.Engine private readonly ILogger _logger; private readonly IRepository _requestLog; + private readonly IMediaCacheService _mediaCacheService; public async Task RequestTvShow(TvRequestViewModel tv) { @@ -329,6 +331,7 @@ namespace Ombi.Core.Engine Collection = allRequests }; } + public async Task> GetRequests() { var shouldHide = await HideFromOtherUsers(); @@ -348,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(); @@ -404,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; @@ -476,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(); @@ -529,7 +532,6 @@ namespace Ombi.Core.Engine }; } - public async Task> GetRequestsLite() { var shouldHide = await HideFromOtherUsers(); @@ -699,6 +701,7 @@ namespace Ombi.Core.Engine } await TvRepository.UpdateChild(request); + await _mediaCacheService.Purge(); if (request.Approved) { @@ -725,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 { @@ -735,6 +739,7 @@ namespace Ombi.Core.Engine public async Task UpdateChildRequest(ChildRequests request) { await TvRepository.UpdateChild(request); + await _mediaCacheService.Purge(); return request; } @@ -754,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) @@ -786,6 +793,7 @@ namespace Ombi.Core.Engine } } await TvRepository.UpdateChild(request); + await _mediaCacheService.Purge(); return new RequestEngineResult { Result = true, @@ -814,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, @@ -888,19 +897,6 @@ namespace Ombi.Core.Engine return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf); } - private static List SortEpisodes(List items) - { - foreach (var value in items) - { - foreach (var requests in value.SeasonRequests) - { - requests.Episodes = requests.Episodes.OrderBy(x => x.EpisodeNumber).ToList(); - } - } - return items; - } - - public async Task ReProcessRequest(int requestId, CancellationToken cancellationToken) { var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId, cancellationToken); @@ -933,6 +929,7 @@ namespace Ombi.Core.Engine RequestType = RequestType.TvShow, EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(), }); + await _mediaCacheService.Purge(); return await ProcessSendingShow(model); } 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 4ff99225a..25c0fc2a2 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -51,8 +51,8 @@ namespace Ombi.Core.Engine.V2 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); } @@ -61,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); } @@ -70,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(); @@ -108,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 @@ -136,8 +136,8 @@ namespace Ombi.Core.Engine.V2 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); @@ -149,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 @@ -170,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); @@ -186,8 +186,8 @@ 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); @@ -210,8 +210,8 @@ namespace Ombi.Core.Engine.V2 var results = new List(); foreach (var pagesToLoad in pages) { - var apiResult = await Cache.GetOrAdd(nameof(SeasonalList) + pagesToLoad.Page + langCode + keyWordIds, - async () => await MovieApi.GetMoviesViaKeywords(keyWordIds, langCode, cancellationToken, pagesToLoad.Page), DateTime.Now.AddHours(12)); + 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); @@ -227,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(); @@ -245,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); @@ -259,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); @@ -281,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); @@ -294,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 @@ -308,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; } @@ -366,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 @@ -381,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; } @@ -416,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); @@ -458,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/TvSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs index 6bb67debd..26cba7c14 100644 --- a/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/TvSearchEngineV2.cs @@ -60,8 +60,8 @@ namespace Ombi.Core.Engine.V2 public async Task GetShowInformation(string tvdbid, CancellationToken token) { var langCode = await DefaultLanguageCode(null); - var show = await Cache.GetOrAdd(nameof(GetShowInformation) + langCode + tvdbid, - async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTime.Now.AddHours(12)); + 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 @@ -72,8 +72,8 @@ namespace Ombi.Core.Engine.V2 { // There's no regional assets for this, so // lookup the en-us version to get them - var enShow = await Cache.GetOrAdd(nameof(GetShowInformation) + "en" + tvdbid, - async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTime.Now.AddHours(12)); + 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()) @@ -105,8 +105,8 @@ namespace Ombi.Core.Engine.V2 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)); } @@ -122,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); @@ -138,8 +138,8 @@ 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)); } @@ -175,16 +175,16 @@ namespace Ombi.Core.Engine.V2 var results = new List(); - var requestResult = await Cache.GetOrAdd(nameof(RecentlyRequestedShows) + "Requests" + toLoad + langCode, + var requestResult = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + "Requests" + toLoad + langCode, async () => { return await _requestEngine.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(RecentlyRequestedShows) + toLoad + langCode, + var movieDBResults = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + toLoad + langCode, async () => { var responses = new List(); @@ -193,7 +193,7 @@ namespace Ombi.Core.Engine.V2 responses.Add(await _movieApi.GetTVInfo(movie.ExternalProviderId.ToString())); } return responses; - }, DateTime.Now.AddHours(12), cancellationToken); + }, DateTimeOffset.Now.AddHours(12)); var mapped = _mapper.Map>(movieDBResults); @@ -219,14 +219,14 @@ namespace Ombi.Core.Engine.V2 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.GetOrAdd(nameof(GetShowInformation) + tvMazeSearch.Id.ToString(), + 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.GetOrAdd("SeasonEpisodes" + show.id + tvSeason.season_number, async () => + var seasonEpisodes = await Cache.GetOrAddAsync("SeasonEpisodes" + show.id + tvSeason.season_number, async () => { return await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, CancellationToken.None); - }, DateTime.Now.AddHours(12)); + }, DateTimeOffset.Now.AddHours(12)); MapSeasons(tvMazeSearch.SeasonRequests, tvSeason, seasonEpisodes); } 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.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 73a69203a..62686deec 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -206,6 +206,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddTransient(); 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/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.Settings/Settings/SettingsService.cs b/src/Ombi.Settings/Settings/SettingsService.cs index d2f2fdef6..70313430f 100644 --- a/src/Ombi.Settings/Settings/SettingsService.cs +++ b/src/Ombi.Settings/Settings/SettingsService.cs @@ -41,12 +41,12 @@ namespace Ombi.Settings.Settings var model = obj; return model; - }, DateTime.Now.AddHours(2)); + }, DateTimeOffset.Now.AddHours(2)); } public async Task GetSettingsAsync() { - return await _cache.GetOrAdd(CacheName, async () => + return await _cache.GetOrAddAsync(CacheName, async () => { var result = await Repo.GetAsync(EntityName); if (result == null) @@ -61,7 +61,7 @@ namespace Ombi.Settings.Settings var model = obj; return model; - }, DateTime.Now.AddHours(5)); + }, DateTimeOffset.Now.AddHours(5)); } public bool SaveSettings(T model) diff --git a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.html b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.html index 6b0da8604..62908b361 100644 --- a/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.html +++ b/src/Ombi/ClientApp/src/app/media-details/components/movie/panels/movie-information-panel.component.html @@ -44,7 +44,7 @@
- {{'Requests.RequestedBy' | translate }}: + {{'Requests.RequestedBy' | translate }}: {{request.requestedUser.userAlias}}
diff --git a/src/Ombi/Controllers/V1/External/LidarrController.cs b/src/Ombi/Controllers/V1/External/LidarrController.cs index 33af536d7..616f01918 100644 --- a/src/Ombi/Controllers/V1/External/LidarrController.cs +++ b/src/Ombi/Controllers/V1/External/LidarrController.cs @@ -69,7 +69,7 @@ namespace Ombi.Controllers.V1.External [HttpGet("Profiles")] public async Task> GetProfiles() { - return await Cache.GetOrAdd(CacheKeys.LidarrQualityProfiles, async () => + return await Cache.GetOrAddAsync(CacheKeys.LidarrQualityProfiles, async () => { var settings = await _lidarrSettings.GetSettingsAsync(); if (settings.Enabled) @@ -77,7 +77,7 @@ namespace Ombi.Controllers.V1.External return await _lidarrApi.GetProfiles(settings.ApiKey, settings.FullUri); } return null; - }, DateTime.Now.AddHours(1)); + }, DateTimeOffset.Now.AddHours(1)); } /// @@ -88,7 +88,7 @@ namespace Ombi.Controllers.V1.External [HttpGet("RootFolders")] public async Task> GetRootFolders() { - return await Cache.GetOrAdd(CacheKeys.LidarrRootFolders, async () => + return await Cache.GetOrAddAsync(CacheKeys.LidarrRootFolders, async () => { var settings = await _lidarrSettings.GetSettingsAsync(); if (settings.Enabled) @@ -96,7 +96,7 @@ namespace Ombi.Controllers.V1.External return await _lidarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); } return null; - }, DateTime.Now.AddHours(1)); + }, DateTimeOffset.Now.AddHours(1)); } } } \ No newline at end of file diff --git a/src/Ombi/Controllers/V1/IdentityController.cs b/src/Ombi/Controllers/V1/IdentityController.cs index 70cb73e74..ca10902b8 100644 --- a/src/Ombi/Controllers/V1/IdentityController.cs +++ b/src/Ombi/Controllers/V1/IdentityController.cs @@ -293,8 +293,8 @@ namespace Ombi.Controllers.V1 [PowerUser] public async Task> GetAllUsersDropdown() { - var users = await _cacheService.GetOrAdd(CacheKeys.UsersDropdown, - async () => await UserManager.Users.Where(x => x.UserType != UserType.SystemUser).ToListAsync()); + var users = await _cacheService.GetOrAddAsync(CacheKeys.UsersDropdown, + () => UserManager.Users.Where(x => x.UserType != UserType.SystemUser).ToListAsync()); var model = new List(); diff --git a/src/Ombi/Controllers/V1/ImagesController.cs b/src/Ombi/Controllers/V1/ImagesController.cs index 847cd8777..9683c54b0 100644 --- a/src/Ombi/Controllers/V1/ImagesController.cs +++ b/src/Ombi/Controllers/V1/ImagesController.cs @@ -45,9 +45,9 @@ namespace Ombi.Controllers.V1 { return string.Empty; } - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); - var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvdbid}", async () => await FanartTvApi.GetTvImages(tvdbid, key.Value), DateTime.Now.AddDays(1)); + var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvdbid}", () => FanartTvApi.GetTvImages(tvdbid, key.Value), DateTimeOffset.Now.AddDays(1)); if (images == null) { return string.Empty; @@ -70,7 +70,7 @@ namespace Ombi.Controllers.V1 [HttpGet("poster")] public async Task GetRandomPoster() { - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); var rand = new Random(); var val = rand.Next(1, 3); if (val == 1) @@ -79,7 +79,7 @@ namespace Ombi.Controllers.V1 var selectedMovieIndex = rand.Next(movies.Count()); var movie = movies[selectedMovieIndex]; - var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{movie.Id}", async () => await FanartTvApi.GetMovieImages(movie.Id.ToString(), key.Value), DateTime.Now.AddDays(1)); + var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{movie.Id}", () => FanartTvApi.GetMovieImages(movie.Id.ToString(), key.Value), DateTimeOffset.Now.AddDays(1)); if (images == null) { return string.Empty; @@ -114,9 +114,9 @@ namespace Ombi.Controllers.V1 [HttpGet("poster/movie/{movieDbId}")] public async Task GetMoviePoster(string movieDbId) { - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); - var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{movieDbId}", async () => await FanartTvApi.GetMovieImages(movieDbId, key.Value), DateTime.Now.AddDays(1)); + var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{movieDbId}", () => FanartTvApi.GetMovieImages(movieDbId, key.Value), DateTimeOffset.Now.AddDays(1)); if (images == null) { @@ -148,9 +148,9 @@ namespace Ombi.Controllers.V1 { return string.Empty; } - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); - var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvdbid}", async () => await FanartTvApi.GetTvImages(tvdbid, key.Value), DateTime.Now.AddDays(1)); + var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvdbid}", () => FanartTvApi.GetTvImages(tvdbid, key.Value), DateTimeOffset.Now.AddDays(1)); if (images == null) { @@ -178,9 +178,9 @@ namespace Ombi.Controllers.V1 [HttpGet("background/movie/{movieDbId}")] public async Task GetMovieBackground(string movieDbId) { - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); - var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{movieDbId}", async () => await FanartTvApi.GetMovieImages(movieDbId, key.Value), DateTime.Now.AddDays(1)); + var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{movieDbId}", () => FanartTvApi.GetMovieImages(movieDbId, key.Value), DateTimeOffset.Now.AddDays(1)); if (images == null) { @@ -203,9 +203,9 @@ namespace Ombi.Controllers.V1 [HttpGet("banner/movie/{movieDbId}")] public async Task GetMovieBanner(string movieDbId) { - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); - var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{movieDbId}", async () => await FanartTvApi.GetMovieImages(movieDbId, key.Value), DateTime.Now.AddDays(1)); + var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{movieDbId}", () => FanartTvApi.GetMovieImages(movieDbId, key.Value), DateTimeOffset.Now.AddDays(1)); if (images == null) { @@ -246,17 +246,17 @@ namespace Ombi.Controllers.V1 var movieUrl = string.Empty; var tvUrl = string.Empty; - var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1)); + var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1)); if (moviesArray.Length > 0) { var item = rand.Next(moviesArray.Length); - var result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{moviesArray[item]}", async () => await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTime.Now.AddDays(1)); + var result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{moviesArray[item]}", () => FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTimeOffset.Now.AddDays(1)); while (!result.moviebackground?.Any() ?? true) { item = rand.Next(moviesArray.Length); - result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{moviesArray[item]}", async () => await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTime.Now.AddDays(1)); + result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{moviesArray[item]}", () => FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTimeOffset.Now.AddDays(1)); } @@ -268,12 +268,12 @@ namespace Ombi.Controllers.V1 if (tvArray.Length > 0) { var item = rand.Next(tvArray.Length); - var result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvArray[item]}", async () => await FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTime.Now.AddDays(1)); + var result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvArray[item]}", () => FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTimeOffset.Now.AddDays(1)); while (!result.showbackground?.Any() ?? true) { item = rand.Next(tvArray.Length); - result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvArray[item]}", async () => await FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTime.Now.AddDays(1)); + result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvArray[item]}", () => FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTimeOffset.Now.AddDays(1)); } var otherRand = new Random(); var res = otherRand.Next(result.showbackground.Length); diff --git a/src/Ombi/Controllers/V1/JobController.cs b/src/Ombi/Controllers/V1/JobController.cs index 668a795a1..2dd694619 100644 --- a/src/Ombi/Controllers/V1/JobController.cs +++ b/src/Ombi/Controllers/V1/JobController.cs @@ -66,7 +66,7 @@ namespace Ombi.Controllers.V1 [HttpGet("updateCached")] public async Task CheckForUpdateCached() { - var val = await _memCache.GetOrAdd(CacheKeys.Update, async () => + var val = await _memCache.GetOrAddAsync(CacheKeys.Update, async () => { var productArray = _updater.GetVersion(); diff --git a/src/Ombi/Controllers/V1/UpdateController.cs b/src/Ombi/Controllers/V1/UpdateController.cs index 419442205..fc654933f 100644 --- a/src/Ombi/Controllers/V1/UpdateController.cs +++ b/src/Ombi/Controllers/V1/UpdateController.cs @@ -21,10 +21,10 @@ namespace Ombi.Controllers.V1 private readonly ICacheService _cache; private readonly IChangeLogProcessor _processor; - [HttpGet()] + [HttpGet] public async Task UpdateAvailable() { - return await _cache.GetOrAdd("Update", async () => await _processor.Process()); + return await _cache.GetOrAddAsync("Update", () => _processor.Process()); } } } \ No newline at end of file diff --git a/src/Ombi/Controllers/V2/RequestsController.cs b/src/Ombi/Controllers/V2/RequestsController.cs index 6449db438..9f26e95bf 100644 --- a/src/Ombi/Controllers/V2/RequestsController.cs +++ b/src/Ombi/Controllers/V2/RequestsController.cs @@ -12,6 +12,7 @@ using Ombi.Store.Entities; using System.Linq; using Microsoft.Extensions.Logging; using Ombi.Attributes; +using Ombi.Helpers; namespace Ombi.Controllers.V2 { diff --git a/src/Ombi/Controllers/V2/SearchController.cs b/src/Ombi/Controllers/V2/SearchController.cs index 9391815dd..4bf8f15da 100644 --- a/src/Ombi/Controllers/V2/SearchController.cs +++ b/src/Ombi/Controllers/V2/SearchController.cs @@ -1,11 +1,9 @@ using System; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; using System.Collections.Generic; -using System.Threading; using Ombi.Core; using Ombi.Api.TheMovieDb.Models; using Ombi.Core.Engine.V2; @@ -16,14 +14,15 @@ using Ombi.Core.Models.Search.V2.Music; using Ombi.Models; using Ombi.Api.RottenTomatoes.Models; using Ombi.Api.RottenTomatoes; -using Hqub.MusicBrainz.API.Entities.Collections; +using Ombi.Helpers; namespace Ombi.Controllers.V2 { public class SearchController : V2Controller { public SearchController(IMultiSearchEngine multiSearchEngine, - IMovieEngineV2 v2Movie, ITVSearchEngineV2 v2Tv, IMusicSearchEngineV2 musicEngine, IRottenTomatoesApi rottenTomatoesApi) + IMovieEngineV2 v2Movie, ITVSearchEngineV2 v2Tv, IMusicSearchEngineV2 musicEngine, IRottenTomatoesApi rottenTomatoesApi, + IMediaCacheService mediaCacheService) { _multiSearchEngine = multiSearchEngine; _movieEngineV2 = v2Movie; @@ -31,6 +30,7 @@ namespace Ombi.Controllers.V2 _tvEngineV2 = v2Tv; _musicEngine = musicEngine; _rottenTomatoesApi = rottenTomatoesApi; + _mediaCacheService = mediaCacheService; } private readonly IMultiSearchEngine _multiSearchEngine; @@ -38,6 +38,7 @@ namespace Ombi.Controllers.V2 private readonly ITVSearchEngineV2 _tvEngineV2; private readonly IMusicSearchEngineV2 _musicEngine; private readonly IRottenTomatoesApi _rottenTomatoesApi; + private readonly IMediaCacheService _mediaCacheService; /// /// Returns search results for both TV and Movies @@ -59,24 +60,30 @@ namespace Ombi.Controllers.V2 /// /// The MovieDB Id [HttpGet("movie/{movieDbId}")] - public async Task GetMovieInfo(int movieDbId) + public Task GetMovieInfo(int movieDbId) { - return await _movieEngineV2.GetFullMovieInformation(movieDbId, Request.HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetMovieInfo) + movieDbId, + () => _movieEngineV2.GetFullMovieInformation(movieDbId, Request.HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } [HttpGet("movie/imdb/{imdbid}")] - public async Task GetMovieInfoByImdbId(string imdbId) + public Task GetMovieInfoByImdbId(string imdbId) { - return await _movieEngineV2.GetMovieInfoByImdbId(imdbId, Request.HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + imdbId, + () => _movieEngineV2.GetMovieInfoByImdbId(imdbId, Request.HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } /// /// Returns details for a single movie /// [HttpGet("movie/request/{requestId}")] - public async Task GetMovieByRequest(int requestId) + public Task GetMovieByRequest(int requestId) { - return await _movieEngineV2.GetMovieInfoByRequestId(requestId, Request.HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetMovieByRequest) + requestId, + () => _movieEngineV2.GetMovieInfoByRequestId(requestId, Request.HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } /// @@ -85,9 +92,11 @@ namespace Ombi.Controllers.V2 /// The collection id from TheMovieDb /// [HttpGet("movie/collection/{collectionId}")] - public async Task GetMovieCollections(int collectionId) + public Task GetMovieCollections(int collectionId) { - return await _movieEngineV2.GetCollection(collectionId, Request.HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetMovieCollections) + collectionId, + () => _movieEngineV2.GetCollection(collectionId, Request.HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } /// @@ -96,9 +105,11 @@ namespace Ombi.Controllers.V2 /// TVMaze is the TV Show Provider /// The TVDB Id [HttpGet("tv/{tvdbId}")] - public async Task GetTvInfo(string tvdbid) + public Task GetTvInfo(string tvdbid) { - return await _tvEngineV2.GetShowInformation(tvdbid, HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetTvInfo) + tvdbid, + () => _tvEngineV2.GetShowInformation(tvdbid, HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } /// @@ -107,9 +118,11 @@ namespace Ombi.Controllers.V2 /// TVMaze is the TV Show Provider /// [HttpGet("tv/request/{requestId}")] - public async Task GetTvInfoByRequest(int requestId) + public Task GetTvInfoByRequest(int requestId) { - return await _tvEngineV2.GetShowByRequest(requestId, HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetTvInfoByRequest) + requestId, + () => _tvEngineV2.GetShowByRequest(requestId, HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } /// @@ -117,9 +130,11 @@ namespace Ombi.Controllers.V2 /// /// [HttpGet("tv/moviedb/{moviedbid}")] - public async Task GetTvInfoByMovieId(string moviedbid) + public Task GetTvInfoByMovieId(string moviedbid) { - return await _tvEngineV2.GetShowInformation(moviedbid, HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetTvInfoByMovieId) + moviedbid, + () => _tvEngineV2.GetShowInformation(moviedbid, HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } /// @@ -131,9 +146,11 @@ namespace Ombi.Controllers.V2 [HttpPost("movie/similar")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> SimilarMovies([FromBody] SimilarMoviesRefineModel model) + public Task> SimilarMovies([FromBody] SimilarMoviesRefineModel model) { - return await _movieEngineV2.SimilarMovies(model.TheMovieDbId, model.LanguageCode); + return _mediaCacheService.GetOrAddAsync(nameof(SimilarMovies) + model.TheMovieDbId + model.LanguageCode, + () => _movieEngineV2.SimilarMovies(model.TheMovieDbId, model.LanguageCode), + DateTimeOffset.Now.AddHours(12)); } @@ -159,9 +176,11 @@ namespace Ombi.Controllers.V2 [HttpGet("movie/popular/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> Popular(int currentPosition, int amountToLoad) + public Task> Popular(int currentPosition, int amountToLoad) { - return await _movieEngineV2.PopularMovies(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(Popular) + "Movies" + currentPosition + amountToLoad, + () => _movieEngineV2.PopularMovies(currentPosition, amountToLoad, Request.HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } /// @@ -172,9 +191,11 @@ namespace Ombi.Controllers.V2 [HttpGet("movie/seasonal/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> Seasonal(int currentPosition, int amountToLoad) + public Task> Seasonal(int currentPosition, int amountToLoad) { - return await _movieEngineV2.SeasonalList(currentPosition, amountToLoad, Request.HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(Seasonal) + "Movies" + currentPosition + amountToLoad, + () => _movieEngineV2.SeasonalList(currentPosition, amountToLoad, Request.HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(1)); } /// @@ -224,9 +245,11 @@ namespace Ombi.Controllers.V2 [HttpGet("movie/nowplaying/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> NowPlayingMovies(int currentPosition, int amountToLoad) + public Task> NowPlayingMovies(int currentPosition, int amountToLoad) { - return await _movieEngineV2.NowPlayingMovies(currentPosition, amountToLoad); + return _mediaCacheService.GetOrAddAsync(nameof(NowPlayingMovies) + currentPosition + amountToLoad, + () => _movieEngineV2.NowPlayingMovies(currentPosition, amountToLoad), + DateTimeOffset.Now.AddHours(12)); } /// @@ -250,9 +273,11 @@ namespace Ombi.Controllers.V2 [HttpGet("movie/toprated/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> TopRatedMovies(int currentPosition, int amountToLoad) + public Task> TopRatedMovies(int currentPosition, int amountToLoad) { - return await _movieEngineV2.TopRatedMovies(currentPosition, amountToLoad); + return _mediaCacheService.GetOrAddAsync(nameof(TopRatedMovies) + currentPosition + amountToLoad, + () => _movieEngineV2.TopRatedMovies(currentPosition, amountToLoad), + DateTimeOffset.Now.AddHours(12)); } /// @@ -276,9 +301,11 @@ namespace Ombi.Controllers.V2 [HttpGet("movie/upcoming/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> UpcomingMovies(int currentPosition, int amountToLoad) + public Task> UpcomingMovies(int currentPosition, int amountToLoad) { - return await _movieEngineV2.UpcomingMovies(currentPosition, amountToLoad); + return _mediaCacheService.GetOrAddAsync(nameof(UpcomingMovies) + currentPosition + amountToLoad, + () => _movieEngineV2.UpcomingMovies(currentPosition, amountToLoad), + DateTimeOffset.Now.AddHours(12)); } /// @@ -289,9 +316,11 @@ namespace Ombi.Controllers.V2 [HttpGet("tv/popular/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> PopularTv(int currentPosition, int amountToLoad) + public Task> PopularTv(int currentPosition, int amountToLoad) { - return await _tvEngineV2.Popular(currentPosition, amountToLoad); + return _mediaCacheService.GetOrAddAsync(nameof(PopularTv) + currentPosition + amountToLoad, + () => _tvEngineV2.Popular(currentPosition, amountToLoad), + DateTimeOffset.Now.AddHours(12)); } /// @@ -302,9 +331,11 @@ namespace Ombi.Controllers.V2 [HttpGet("tv/anticipated/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> AnticipatedTv(int currentPosition, int amountToLoad) + public Task> AnticipatedTv(int currentPosition, int amountToLoad) { - return await _tvEngineV2.Anticipated(currentPosition, amountToLoad); + return _mediaCacheService.GetOrAddAsync(nameof(AnticipatedTv) + currentPosition + amountToLoad, + () => _tvEngineV2.Anticipated(currentPosition, amountToLoad), + DateTimeOffset.Now.AddHours(12)); } /// @@ -316,9 +347,11 @@ namespace Ombi.Controllers.V2 [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] [Obsolete("This method is obsolete, Trakt API no longer supports this")] - public async Task> MostWatched(int currentPosition, int amountToLoad) + public Task> MostWatched(int currentPosition, int amountToLoad) { - return await _tvEngineV2.Popular(currentPosition, amountToLoad); + return _mediaCacheService.GetOrAddAsync(nameof(MostWatched) + currentPosition + amountToLoad, + () => _tvEngineV2.Popular(currentPosition, amountToLoad), + DateTimeOffset.Now.AddHours(12)); } @@ -330,9 +363,11 @@ namespace Ombi.Controllers.V2 [HttpGet("tv/trending/{currentPosition}/{amountToLoad}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesDefaultResponseType] - public async Task> Trending(int currentPosition, int amountToLoad) + public Task> Trending(int currentPosition, int amountToLoad) { - return await _tvEngineV2.Trending(currentPosition, amountToLoad); + return _mediaCacheService.GetOrAddAsync(nameof(Trending) + currentPosition + amountToLoad, + () => _tvEngineV2.Trending(currentPosition, amountToLoad), + DateTimeOffset.Now.AddHours(12)); } /// @@ -386,7 +421,9 @@ namespace Ombi.Controllers.V2 [ProducesDefaultResponseType] public Task GetRottenMovieRatings(string name, int year) { - return _rottenTomatoesApi.GetMovieRatings(name, year); + return _mediaCacheService.GetOrAddAsync(nameof(GetRottenMovieRatings) + name + year, + () => _rottenTomatoesApi.GetMovieRatings(name, year), + DateTimeOffset.Now.AddHours(12)); } [HttpGet("ratings/tv/{name}/{year}")] @@ -394,7 +431,9 @@ namespace Ombi.Controllers.V2 [ProducesDefaultResponseType] public Task GetRottenTvRatings(string name, int year) { - return _rottenTomatoesApi.GetTvRatings(name, year); + return _mediaCacheService.GetOrAddAsync(nameof(GetRottenTvRatings) + name + year, + () => _rottenTomatoesApi.GetTvRatings(name, year), + DateTimeOffset.Now.AddHours(12)); } [HttpGet("stream/movie/{movieDbId}")] @@ -402,7 +441,9 @@ namespace Ombi.Controllers.V2 [ProducesDefaultResponseType] public Task> GetMovieStreams(int movieDBId) { - return _movieEngineV2.GetStreamInformation(movieDBId, HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetMovieStreams) + movieDBId, + () => _movieEngineV2.GetStreamInformation(movieDBId, HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } [HttpGet("stream/tv/{movieDbId}")] @@ -410,7 +451,9 @@ namespace Ombi.Controllers.V2 [ProducesDefaultResponseType] public Task> GetTvStreams(int movieDbId) { - return _tvEngineV2.GetStreamInformation(movieDbId, HttpContext.RequestAborted); + return _mediaCacheService.GetOrAddAsync(nameof(GetTvStreams) + movieDbId, + () => _tvEngineV2.GetStreamInformation(movieDbId, HttpContext.RequestAborted), + DateTimeOffset.Now.AddHours(12)); } } } \ No newline at end of file diff --git a/src/Ombi/Extensions/StartupExtensions.cs b/src/Ombi/Extensions/StartupExtensions.cs index 5149c4fea..e3253045a 100644 --- a/src/Ombi/Extensions/StartupExtensions.cs +++ b/src/Ombi/Extensions/StartupExtensions.cs @@ -116,7 +116,7 @@ namespace Ombi { var userid = context.Principal?.Claims?.Where(x => x.Type.Equals("id", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault()?.Value ?? default; var cache = context.HttpContext.RequestServices.GetRequiredService(); - var user = await cache.GetOrAdd(userid + "token", async () => + var user = await cache.GetOrAddAsync(userid + "token", async () => { var um = context.HttpContext.RequestServices.GetRequiredService(); return await um.FindByIdAsync(userid); diff --git a/src/Ombi/Ombi.csproj b/src/Ombi/Ombi.csproj index 63c4f5a87..53c98534b 100644 --- a/src/Ombi/Ombi.csproj +++ b/src/Ombi/Ombi.csproj @@ -56,6 +56,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index bb10c22a2..4b82c3124 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -84,7 +84,7 @@ namespace Ombi //{ // setup.AddHealthCheckEndpoint("Ombi", "/health"); //}); - services.AddMemoryCache(); + services.AddLazyCache(); services.AddHttpClient(); services.AddJwtAuthentication(); From 5585958731e645a3c32e36ba8f57ebd597c0d609 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 7 Jul 2021 13:48:09 +0100 Subject: [PATCH 14/27] Cleanup commit --- src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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] From 90da69a34222bd8616c9057aaa065e09f66fef31 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 8 Jul 2021 21:47:29 +0100 Subject: [PATCH 15/27] Plex OAuth window will now auto close --- .../src/app/login/login.component.ts | 386 ++++++++++-------- 1 file changed, 213 insertions(+), 173 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/login/login.component.ts b/src/Ombi/ClientApp/src/app/login/login.component.ts index e9260d685..aeeb5c5e7 100644 --- a/src/Ombi/ClientApp/src/app/login/login.component.ts +++ b/src/Ombi/ClientApp/src/app/login/login.component.ts @@ -18,208 +18,248 @@ import { StorageService } from "../shared/storage/storage-service"; import { MatSnackBar } from "@angular/material/snack-bar"; @Component({ - templateUrl: "./login.component.html", - animations: [fadeInOutAnimation], - styleUrls: ["./login.component.scss"], + templateUrl: "./login.component.html", + animations: [fadeInOutAnimation], + styleUrls: ["./login.component.scss"], }) export class LoginComponent implements OnDestroy, OnInit { + public form: FormGroup; + public customizationSettings: ICustomizationSettings; + public authenticationSettings: IAuthenticationSettings; + public plexEnabled: boolean; + public background: any; + public landingFlag: boolean; + public baseUrl: string; + public loginWithOmbi: boolean; + public pinTimer: any; + public oauthLoading: boolean; - public form: FormGroup; - public customizationSettings: ICustomizationSettings; - public authenticationSettings: IAuthenticationSettings; - public plexEnabled: boolean; - public background: any; - public landingFlag: boolean; - public baseUrl: string; - public loginWithOmbi: boolean; - public pinTimer: any; - public oauthLoading: boolean; - - public get appName(): string { - if (this.customizationSettings.applicationName) { - return this.customizationSettings.applicationName; - } else { - return "Ombi"; - } + public get appName(): string { + if (this.customizationSettings.applicationName) { + return this.customizationSettings.applicationName; + } else { + return "Ombi"; } + } - public get appNameTranslate(): object { - return { appName: this.appName }; - } - private timer: any; - private clientId: string; - - private errorBody: string; - private errorValidation: string; - private href: string; - - private oAuthWindow: Window|null; - - constructor(private authService: AuthService, private router: Router, private status: StatusService, - private fb: FormBuilder, private settingsService: SettingsService, private images: ImageService, private sanitizer: DomSanitizer, - private route: ActivatedRoute, @Inject(APP_BASE_HREF) href:string, private translate: TranslateService, private plexTv: PlexTvService, - private store: StorageService, private readonly notify: MatSnackBar) { - this.href = href; - this.route.params - .subscribe((params: any) => { - this.landingFlag = params.landing; - if (!this.landingFlag) { - this.settingsService.getLandingPage().subscribe(x => { - if (x.enabled && !this.landingFlag) { - this.router.navigate(["landingpage"]); - } - }); - } - }); + public get appNameTranslate(): object { + return { appName: this.appName }; + } + private timer: any; + private clientId: string; - this.form = this.fb.group({ - username: ["", Validators.required], - password: [""], - rememberMe: [false], - }); + private errorBody: string; + private errorValidation: string; + private href: string; - this.status.getWizardStatus().subscribe(x => { - if (!x.result) { - this.router.navigate(["Wizard"]); - } + private oAuthWindow: Window | null; + + constructor( + private authService: AuthService, + private router: Router, + private status: StatusService, + private fb: FormBuilder, + private settingsService: SettingsService, + private images: ImageService, + private sanitizer: DomSanitizer, + private route: ActivatedRoute, + @Inject(APP_BASE_HREF) href: string, + private translate: TranslateService, + private plexTv: PlexTvService, + private store: StorageService, + private readonly notify: MatSnackBar + ) { + this.href = href; + this.route.params.subscribe((params: any) => { + this.landingFlag = params.landing; + if (!this.landingFlag) { + this.settingsService.getLandingPage().subscribe((x) => { + if (x.enabled && !this.landingFlag) { + this.router.navigate(["landingpage"]); + } }); + } + }); - if (authService.loggedIn()) { - this.router.navigate(["/"]); - } + this.form = this.fb.group({ + username: ["", Validators.required], + password: [""], + rememberMe: [false], + }); + + this.status.getWizardStatus().subscribe((x) => { + if (!x.result) { + this.router.navigate(["Wizard"]); + } + }); + + if (authService.loggedIn()) { + this.router.navigate(["/"]); } + } - public ngOnInit() { - this.settingsService.getAuthentication().subscribe(x => this.authenticationSettings = x); - this.settingsService.getClientId().subscribe(x => this.clientId = x); - this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); - this.images.getRandomBackground().subscribe(x => { - this.background = this.sanitizer.bypassSecurityTrustStyle("url(" + x.url + ")"); - }); - this.timer = setInterval(() => { - this.cycleBackground(); - }, 30000); + public ngOnInit() { + this.settingsService + .getAuthentication() + .subscribe((x) => (this.authenticationSettings = x)); + this.settingsService.getClientId().subscribe((x) => (this.clientId = x)); + this.settingsService + .getCustomization() + .subscribe((x) => (this.customizationSettings = x)); + this.images.getRandomBackground().subscribe((x) => { + this.background = this.sanitizer.bypassSecurityTrustStyle( + "url(" + x.url + ")" + ); + }); + this.timer = setInterval(() => { + this.cycleBackground(); + }, 30000); - const base = this.href; - if (base.length > 1) { - this.baseUrl = base; - } + const base = this.href; + if (base.length > 1) { + this.baseUrl = base; + } - this.translate.get("Login.Errors.IncorrectCredentials").subscribe(x => this.errorBody = x); - this.translate.get("Common.Errors.Validation").subscribe(x => this.errorValidation = x); + this.translate + .get("Login.Errors.IncorrectCredentials") + .subscribe((x) => (this.errorBody = x)); + this.translate + .get("Common.Errors.Validation") + .subscribe((x) => (this.errorValidation = x)); + } + + public onSubmit(form: FormGroup) { + if (form.invalid) { + this.notify.open(this.errorValidation, "OK", { + duration: 300000, + }); + return; } + const value = form.value; + const user = { + password: value.password, + username: value.username, + rememberMe: value.rememberMe, + usePlexOAuth: false, + plexTvPin: { id: 0, code: "" }, + }; + this.authService.requiresPassword(user).subscribe((x) => { + if (x && this.authenticationSettings.allowNoPassword) { + // Looks like this user requires a password + this.authenticationSettings.allowNoPassword = false; + return; + } + this.authService.login(user).subscribe( + (x) => { + this.store.save("id_token", x.access_token); - public onSubmit(form: FormGroup) { - if (form.invalid) { - this.notify.open(this.errorValidation, "OK", { - duration: 300000 + if (this.authService.loggedIn()) { + this.ngOnDestroy(); + this.router.navigate(["/"]); + } else { + this.notify.open(this.errorBody, "OK", { + duration: 3000, }); - return; + } + }, + (err) => { + this.notify.open(this.errorBody, "OK", { + duration: 3000000, + }); } - const value = form.value; - const user = { password: value.password, username: value.username, rememberMe: value.rememberMe, usePlexOAuth: false, plexTvPin: { id: 0, code: "" } }; - this.authService.requiresPassword(user).subscribe(x => { - if (x && this.authenticationSettings.allowNoPassword) { - // Looks like this user requires a password - this.authenticationSettings.allowNoPassword = false; - return; - } - this.authService.login(user) - .subscribe(x => { - this.store.save("id_token", x.access_token); - - if (this.authService.loggedIn()) { - this.ngOnDestroy(); - this.router.navigate(["/"]); - } else { - this.notify.open(this.errorBody, "OK", { - duration: 3000 - }); - } - - }, err => { - this.notify.open(this.errorBody, "OK", { - duration: 3000000 - }) - }); - }); - } + ); + }); + } - public oauth() { - if (this.oAuthWindow) { - this.oAuthWindow.close(); - } - this.oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0, + public oauth() { + if (this.oAuthWindow) { + this.oAuthWindow.close(); + } + this.oAuthWindow = window.open( + window.location.toString(), + "_blank", + `toolbar=0, location=0, status=0, menubar=0, scrollbars=1, resizable=1, width=500, - height=500`); - this.plexTv.GetPin(this.clientId, this.appName).subscribe((pin: any) => { - - this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => { - this.oAuthWindow!.location.replace(x.url); - - if (this.pinTimer) { - clearInterval(this.pinTimer); - } - - this.pinTimer = setInterval(() => { - if(this.oAuthWindow.closed) { - this.oauthLoading = true; - this.getPinResult(x.pinId); - } - }, 1000); - }); - }); - } + height=500` + ); + this.plexTv.GetPin(this.clientId, this.appName).subscribe((pin: any) => { + this.authService + .login({ + usePlexOAuth: true, + password: "", + rememberMe: true, + username: "", + plexTvPin: pin, + }) + .subscribe((x) => { + this.oAuthWindow!.location.replace(x.url); - public getPinResult(pinId: number) { - clearInterval(this.pinTimer); - this.authService.oAuth(pinId).subscribe(x => { - if(x.access_token) { - this.store.save("id_token", x.access_token); - - if (this.authService.loggedIn()) { - this.ngOnDestroy(); - - if (this.oAuthWindow) { - this.oAuthWindow.close(); - } - this.oauthLoading = false; - this.router.navigate(["search"]); - return; - } + if (this.pinTimer) { + clearInterval(this.pinTimer); } - this.notify.open("Could not log you in!", "OK", { - duration: 3000 + + this.pinTimer = setInterval(() => { + this.oauthLoading = true; + this.getPinResult(x.pinId); + }, 1000); }); - this.oauthLoading = false; + }); + } - }, err => { - console.log(err); - this.notify.open(err.body, "OK", { - duration: 3000 - }); + public getPinResult(pinId: number) { + this.authService.oAuth(pinId).subscribe( + (x) => { + if (x.access_token) { + clearInterval(this.pinTimer); + this.store.save("id_token", x.access_token); - this.router.navigate(["login"]); - }); - } + if (this.authService.loggedIn()) { + this.ngOnDestroy(); - public ngOnDestroy() { - clearInterval(this.timer); - clearInterval(this.pinTimer); - } - - private cycleBackground() { - this.images.getRandomBackground().subscribe(x => { - this.background = ""; - }); - this.images.getRandomBackground().subscribe(x => { - this.background = this.sanitizer - .bypassSecurityTrustStyle("url(" + x.url + ")"); + if (this.oAuthWindow) { + this.oAuthWindow.close(); + } + this.oauthLoading = false; + this.router.navigate(["search"]); + return; + } + } + // if (notifyUser) { + // this.notify.open("Could not log you in!", "OK", { + // duration: 3000, + // }); + // } + this.oauthLoading = false; + }, + (err) => { + console.log(err); + this.notify.open("You are not authenticated with Ombi", "OK", { + duration: 3000, }); - } + + this.router.navigate(["login"]); + } + ); + } + + public ngOnDestroy() { + clearInterval(this.timer); + clearInterval(this.pinTimer); + } + + private cycleBackground() { + this.images.getRandomBackground().subscribe((x) => { + this.background = ""; + }); + this.images.getRandomBackground().subscribe((x) => { + this.background = this.sanitizer.bypassSecurityTrustStyle( + "url(" + x.url + ")" + ); + }); + } } From fc7df0e11b912afdf4532db7804d5731d2d30f90 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 9 Jul 2021 15:16:57 +0100 Subject: [PATCH 16/27] Added the ability to select certain libraries in jellyfin --- src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs | 7 +- src/Ombi.Api.Jellyfin/JellyfinApi.cs | 20 ++-- .../Jobs/Jellyfin/JellyfinContentSync.cs | 103 ++++++++++++------ .../Jobs/Jellyfin/JellyfinEpisodeSync.cs | 21 +++- .../Settings/Models/External/EmbySettings.cs | 8 ++ .../Models/External/JellyfinSettings.cs | 9 ++ .../ClientApp/src/app/interfaces/ISettings.ts | 19 ++++ .../services/applications/jellyfin.service.ts | 7 +- .../settings/jellyfin/jellyfin.component.html | 35 ++++-- .../settings/jellyfin/jellyfin.component.ts | 30 ++++- .../app/wizard/jellyfin/jellyfin.component.ts | 6 +- 11 files changed, 198 insertions(+), 67 deletions(-) diff --git a/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs b/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs index 7a9ee8a5a..4bb997cb5 100644 --- a/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/IBaseJellyfinApi.cs @@ -13,13 +13,12 @@ namespace Ombi.Api.Jellyfin Task> GetUsers(string baseUri, string apiKey); Task LogIn(string username, string password, string apiKey, string baseUri); - Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, - string baseUri); + Task> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); - Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, + Task> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); - Task> GetAllShows(string apiKey, int startIndex, int count, string userId, + Task> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); Task> GetCollection(string mediaId, diff --git a/src/Ombi.Api.Jellyfin/JellyfinApi.cs b/src/Ombi.Api.Jellyfin/JellyfinApi.cs index 9ec6c8241..c16620710 100644 --- a/src/Ombi.Api.Jellyfin/JellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/JellyfinApi.cs @@ -97,19 +97,19 @@ namespace Ombi.Api.Jellyfin return response; } - public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); + return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count, parentIdFilder); } - public async Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder); } - public async Task> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder); } public async Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) @@ -152,15 +152,19 @@ namespace Ombi.Api.Jellyfin var obj = await Api.Request>(request); return obj; } - private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count, string parentIdFilder = default) { var request = new Request($"users/{userId}/items", baseUri, HttpMethod.Get); request.AddQueryString("Recursive", true.ToString()); request.AddQueryString("IncludeItemTypes", type); - request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); + request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview,ParentId" : "ProviderIds,ParentId"); request.AddQueryString("startIndex", startIndex.ToString()); request.AddQueryString("limit", count.ToString()); + if(!string.IsNullOrEmpty(parentIdFilder)) + { + request.AddQueryString("ParentId", parentIdFilder); + } request.AddQueryString("IsVirtualItem", "False"); diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs index ff96e2130..0920eed42 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs @@ -52,7 +52,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin { try { - await StartServerCache(server, jellyfinSettings); + await StartServerCache(server); } catch (Exception e) { @@ -70,55 +70,47 @@ namespace Ombi.Schedule.Jobs.Jellyfin } - private async Task StartServerCache(JellyfinServers server, JellyfinSettings settings) + private async Task StartServerCache(JellyfinServers server) { if (!ValidateSettings(server)) + { return; + } //await _repo.ExecuteSql("DELETE FROM JellyfinEpisode"); //await _repo.ExecuteSql("DELETE FROM JellyfinContent"); - var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); - var totalCount = movies.TotalRecordCount; - var processed = 1; - - var mediaToAdd = new HashSet(); - - while (processed < totalCount) + if (server.JellyfinSelectedLibraries.Any()) { - foreach (var movie in movies.Items) - { - if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) - { - var movieInfo = - await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); - foreach (var item in movieInfo.Items) - { - await ProcessMovies(item, mediaToAdd, server); - } + var movieLibsToFilter = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies"); - processed++; - } - else - { - processed++; - // Regular movie - await ProcessMovies(movie, mediaToAdd, server); - } + foreach (var movieParentIdFilder in movieLibsToFilter) + { + _logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'"); + await ProcessMovies(server, movieParentIdFilder.Key); } - // Get the next batch - movies = await Api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); - await _repo.AddRange(mediaToAdd); - mediaToAdd.Clear(); - + var tvLibsToFilter = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); + foreach (var tvParentIdFilter in tvLibsToFilter) + { + _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); + await ProcessTv(server, tvParentIdFilter.Key); + } } + else + { + await ProcessMovies(server); + await ProcessTv(server); + } + } - + private async Task ProcessTv(JellyfinServers server, string parentId = default) + { // TV Time - var tv = await Api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var mediaToAdd = new HashSet(); + var tv = await Api.GetAllShows(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri); var totalTv = tv.TotalRecordCount; - processed = 1; + var processed = 1; while (processed < totalTv) { foreach (var tvShow in tv.Items) @@ -162,13 +154,52 @@ namespace Ombi.Schedule.Jobs.Jellyfin } } // Get the next batch - tv = await Api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + tv = await Api.GetAllShows(server.ApiKey, parentId, processed, 200, server.AdministratorId, server.FullUri); await _repo.AddRange(mediaToAdd); mediaToAdd.Clear(); } if (mediaToAdd.Any()) + { + await _repo.AddRange(mediaToAdd); + } + } + + private async Task ProcessMovies(JellyfinServers server, string parentId = default) + { + var movies = await Api.GetAllMovies(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri); + var totalCount = movies.TotalRecordCount; + var processed = 1; + var mediaToAdd = new HashSet(); + while (processed < totalCount) + { + foreach (var movie in movies.Items) + { + if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) + { + var movieInfo = + await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); + foreach (var item in movieInfo.Items) + { + await ProcessMovies(item, mediaToAdd, server); + } + + processed++; + } + else + { + processed++; + // Regular movie + await ProcessMovies(movie, mediaToAdd, server); + } + } + + // Get the next batch + movies = await Api.GetAllMovies(server.ApiKey, parentId, processed, 200, server.AdministratorId, server.FullUri); await _repo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + + } } private async Task ProcessMovies(JellyfinMovie movieInfo, ICollection content, JellyfinServers server) diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs index 11c7ec7af..4cbe7dca9 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs @@ -72,7 +72,20 @@ namespace Ombi.Schedule.Jobs.Jellyfin .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Started"); foreach (var server in settings.Servers) { - await CacheEpisodes(server); + + if (server.JellyfinSelectedLibraries.Any()) + { + var tvLibsToFilter = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); + foreach (var tvParentIdFilter in tvLibsToFilter) + { + _logger.LogInformation($"Scanning Lib for episodes '{tvParentIdFilter.Title}'"); + await CacheEpisodes(server, tvParentIdFilter.Key); + } + } + else + { + await CacheEpisodes(server, string.Empty); + } } await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) @@ -81,9 +94,9 @@ namespace Ombi.Schedule.Jobs.Jellyfin await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); } - private async Task CacheEpisodes(JellyfinServers server) + private async Task CacheEpisodes(JellyfinServers server, string parentIdFilter) { - var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, 0, 200, server.AdministratorId, server.FullUri); var total = allEpisodes.TotalRecordCount; var processed = 1; var epToAdd = new HashSet(); @@ -150,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin await _repo.AddRange(epToAdd); epToAdd.Clear(); - allEpisodes = await Api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, 200, server.AdministratorId, server.FullUri); } if (epToAdd.Any()) diff --git a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs index 5bd7cea93..2b165daf8 100644 --- a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs @@ -17,5 +17,13 @@ namespace Ombi.Core.Settings.Models.External public string AdministratorId { get; set; } public string ServerHostname { get; set; } public bool EnableEpisodeSearching { get; set; } + public List EmbySelectedLibraries { get; set; } = new List(); + } + + public class EmbySelectedLibraries + { + public int Key { get; set; } + public string Title { get; set; } // Name is for display purposes + public bool Enabled { get; set; } } } diff --git a/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs b/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs index 3bee56848..99df8afe0 100644 --- a/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/JellyfinSettings.cs @@ -17,5 +17,14 @@ namespace Ombi.Core.Settings.Models.External public string AdministratorId { get; set; } public string ServerHostname { get; set; } public bool EnableEpisodeSearching { get; set; } + public List JellyfinSelectedLibraries { get; set; } = new List(); + } + + public class JellyfinSelectedLibraries + { + public string Key { get; set; } + public string Title { get; set; } // Name is for display purposes + public string CollectionType { get; set; } + public bool Enabled { get; set; } } } diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts index 910f440f3..95b5f9dab 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts @@ -64,6 +64,25 @@ export interface IJellyfinServer extends IExternalSettings { administratorId: string; enableEpisodeSearching: boolean; serverHostname: string; + jellyfinSelectedLibraries: IJellyfinLibrariesSettings[]; +} +export interface IJellyfinLibrariesSettings { + key: string; + title: string; + enabled: boolean; + collectionType: string; +} + +export interface IJellyfinContainer { + items: T[]; + totalRecordCount: number; +} + +export interface IJellyfinLibrary { + name: string; + serverId: string; + id: string; + collectionType: string; } export interface IPublicInfo { diff --git a/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts b/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts index 1bd27ef99..25ff15fde 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts @@ -5,7 +5,7 @@ import { Observable } from "rxjs"; import { ServiceHelpers } from "../service.helpers"; -import { IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces"; +import { IEmbyServer, IJellyfinContainer, IJellyfinLibrary, IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces"; @Injectable() export class JellyfinService extends ServiceHelpers { @@ -20,9 +20,12 @@ export class JellyfinService extends ServiceHelpers { public getUsers(): Observable { return this.http.get(`${this.url}users`, {headers: this.headers}); } - + public getPublicInfo(server: IJellyfinServer): Observable { return this.http.post(`${this.url}info`, JSON.stringify(server), {headers: this.headers}); } + public getLibraries(settings: IJellyfinServer): Observable> { + return this.http.post>(`${this.url}Library`, JSON.stringify(settings), {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html index ada4f5aa6..6b3ebfdbc 100644 --- a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.html @@ -9,11 +9,11 @@
-
-
- Enable -
+
+ Enable +
+
@@ -75,7 +75,28 @@
- + +
+ Note: if nothing is selected, we will monitor all libraries +
+
+ +
+
+
+
+
+
+
+ {{lib.title}} +
+
+
+
@@ -87,10 +108,10 @@
- + - +
diff --git a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts index fd65ba43c..346d23ac8 100644 --- a/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/jellyfin/jellyfin.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from "@angular/core"; - -import { IJellyfinServer, IJellyfinSettings } from "../../interfaces"; +import { IEmbyServer, IJellyfinLibrariesSettings, IJellyfinServer, IJellyfinSettings } from "../../interfaces"; import { JellyfinService, JobService, NotificationService, SettingsService, TesterService } from "../../services"; -import { MatTabChangeEvent } from "@angular/material/tabs"; + import {FormControl} from '@angular/forms'; +import { MatTabChangeEvent } from "@angular/material/tabs"; @Component({ templateUrl: "./jellyfin.component.html", @@ -101,4 +101,28 @@ export class JellyfinComponent implements OnInit { } }); } + + public loadLibraries(server: IJellyfinServer) { + if (server.ip == null) { + this.notificationService.error("Jellyfin is not yet configured correctly"); + return; + } + this.jellyfinService.getLibraries(server).subscribe(x => { + server.jellyfinSelectedLibraries = []; + if (x.totalRecordCount > 0) { + x.items.forEach((item) => { + const lib: IJellyfinLibrariesSettings = { + key: item.id, + title: item.name, + enabled: false, + collectionType: item.collectionType + }; + server.jellyfinSelectedLibraries.push(lib); + }); + } else { + this.notificationService.error("Couldn't find any libraries"); + } + }, + err => { this.notificationService.error(err); }); + } } diff --git a/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts index f3779b6be..d1200360e 100644 --- a/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/jellyfin/jellyfin.component.ts @@ -1,10 +1,9 @@ import { Component, OnInit } from "@angular/core"; +import { IJellyfinSettings } from "../../interfaces"; import { JellyfinService } from "../../services"; import { NotificationService } from "../../services"; -import { IJellyfinSettings } from "../../interfaces"; - @Component({ selector: "wizard-jellyfin", templateUrl: "./jellyfin.component.html", @@ -35,7 +34,8 @@ export class JellyfinComponent implements OnInit { ssl: false, subDir: "", serverHostname: "", - serverId: undefined + serverId: undefined, + jellyfinSelectedLibraries: [] }); } From 6d70010777b2f173d5a46d23b2ad9a45d77fca1a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Fri, 9 Jul 2021 16:22:09 +0100 Subject: [PATCH 17/27] Added the option to select libaries to monitor in Jellyfin and Emby #2389 --- src/Ombi.Api.Emby/EmbyApi.cs | 34 +++++-- src/Ombi.Api.Emby/IBaseEmbyApi.cs | 6 +- src/Ombi.Api.Emby/IEmbyApi.cs | 2 + .../Models/Media/MediaFolders.cs | 16 ++++ src/Ombi.Helpers/MediaCacheService.cs | 4 + .../Jobs/Emby/EmbyContentSync.cs | 96 ++++++++++++------- .../Jobs/Emby/EmbyEpisodeSync.cs | 21 +++- .../Jobs/Jellyfin/JellyfinContentSync.cs | 4 +- .../Jobs/Jellyfin/JellyfinEpisodeSync.cs | 2 +- .../Jobs/Ombi/MediaDatabaseRefresh.cs | 19 ++-- .../Jobs/Ombi/RefreshMetadata.cs | 7 +- .../Jobs/Plex/PlexContentSync.cs | 6 +- .../Settings/Models/External/EmbySettings.cs | 3 +- .../ClientApp/src/app/interfaces/ISettings.ts | 15 ++- .../app/services/applications/emby.service.ts | 8 +- .../services/applications/jellyfin.service.ts | 6 +- .../src/app/settings/emby/emby.component.html | 37 +++++-- .../src/app/settings/emby/emby.component.ts | 30 +++++- .../src/app/wizard/emby/emby.component.ts | 8 +- .../Controllers/V1/External/EmbyController.cs | 10 +- 20 files changed, 249 insertions(+), 85 deletions(-) create mode 100644 src/Ombi.Api.Emby/Models/Media/MediaFolders.cs diff --git a/src/Ombi.Api.Emby/EmbyApi.cs b/src/Ombi.Api.Emby/EmbyApi.cs index 7cb702fbc..b24783e3c 100644 --- a/src/Ombi.Api.Emby/EmbyApi.cs +++ b/src/Ombi.Api.Emby/EmbyApi.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json; using Ombi.Api.Emby.Models; +using Ombi.Api.Emby.Models.Media; using Ombi.Api.Emby.Models.Media.Tv; using Ombi.Api.Emby.Models.Movie; using Ombi.Helpers; @@ -112,19 +115,30 @@ namespace Ombi.Api.Emby return await Api.Request>(request); } - public async Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetLibraries(string apiKey, string baseUrl) { - return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count); + var request = new Request("library/mediafolders", baseUrl, HttpMethod.Get); + AddHeaders(request, apiKey); + + var response = await Api.Request>(request); + response.Items = response.Items.Where(x => !x.CollectionType.Equals("playlists", StringComparison.InvariantCultureIgnoreCase)).ToList(); + return response; + } + + + public async Task> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) + { + return await GetAll("Movie", apiKey, userId, baseUri, true, startIndex, count, parentIdFilder); } - public async Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Episode", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder); } - public async Task> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) + public async Task> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) { - return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count); + return await GetAll("Series", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder); } public async Task GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) @@ -167,7 +181,7 @@ namespace Ombi.Api.Emby var obj = await Api.Request>(request); return obj; } - private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) + private async Task> GetAll(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count, string parentIdFilder = default) { var request = new Request($"emby/users/{userId}/items", baseUri, HttpMethod.Get); @@ -176,6 +190,10 @@ namespace Ombi.Api.Emby request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); request.AddQueryString("startIndex", startIndex.ToString()); request.AddQueryString("limit", count.ToString()); + if (!string.IsNullOrEmpty(parentIdFilder)) + { + request.AddQueryString("ParentId", parentIdFilder); + } request.AddQueryString("IsVirtualItem", "False"); diff --git a/src/Ombi.Api.Emby/IBaseEmbyApi.cs b/src/Ombi.Api.Emby/IBaseEmbyApi.cs index 6f9a6bc7f..4de81073d 100644 --- a/src/Ombi.Api.Emby/IBaseEmbyApi.cs +++ b/src/Ombi.Api.Emby/IBaseEmbyApi.cs @@ -13,13 +13,13 @@ namespace Ombi.Api.Emby Task> GetUsers(string baseUri, string apiKey); Task LogIn(string username, string password, string apiKey, string baseUri); - Task> GetAllMovies(string apiKey, int startIndex, int count, string userId, + Task> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); - Task> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, + Task> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); - Task> GetAllShows(string apiKey, int startIndex, int count, string userId, + Task> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); Task> GetCollection(string mediaId, diff --git a/src/Ombi.Api.Emby/IEmbyApi.cs b/src/Ombi.Api.Emby/IEmbyApi.cs index e7803116d..66c281de0 100644 --- a/src/Ombi.Api.Emby/IEmbyApi.cs +++ b/src/Ombi.Api.Emby/IEmbyApi.cs @@ -1,10 +1,12 @@ using System.Threading.Tasks; using Ombi.Api.Emby.Models; +using Ombi.Api.Emby.Models.Media; namespace Ombi.Api.Emby { public interface IEmbyApi : IBaseEmbyApi { Task LoginConnectUser(string username, string password); + Task> GetLibraries(string apiKey, string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Emby/Models/Media/MediaFolders.cs b/src/Ombi.Api.Emby/Models/Media/MediaFolders.cs new file mode 100644 index 000000000..fee589404 --- /dev/null +++ b/src/Ombi.Api.Emby/Models/Media/MediaFolders.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Api.Emby.Models.Media +{ + public class MediaFolders + { + public string Name { get; set; } + public string ServerId { get; set; } + public string Id { get; set; } + public string CollectionType { get; set; } + } +} diff --git a/src/Ombi.Helpers/MediaCacheService.cs b/src/Ombi.Helpers/MediaCacheService.cs index 69ad8ec09..1cb969b60 100644 --- a/src/Ombi.Helpers/MediaCacheService.cs +++ b/src/Ombi.Helpers/MediaCacheService.cs @@ -51,6 +51,10 @@ namespace Ombi.Helpers public async Task Purge() { var keys = await _memoryCache.GetAsync>(CacheKey); + if (keys == null) + { + return; + } foreach (var key in keys) { base.Remove(key); diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index 866216fe4..4d7e271d6 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -35,6 +35,7 @@ namespace Ombi.Schedule.Jobs.Emby private readonly IEmbyApiFactory _apiFactory; private readonly IEmbyContentRepository _repo; private readonly IHubContext _notification; + private IEmbyApi Api { get; set; } public async Task Execute(IJobExecutionContext job) @@ -78,47 +79,37 @@ namespace Ombi.Schedule.Jobs.Emby //await _repo.ExecuteSql("DELETE FROM EmbyEpisode"); //await _repo.ExecuteSql("DELETE FROM EmbyContent"); - var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); - var totalCount = movies.TotalRecordCount; - var processed = 1; - - var mediaToAdd = new HashSet(); - - while (processed < totalCount) + if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) { - foreach (var movie in movies.Items) - { - if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) - { - var movieInfo = - await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); - foreach (var item in movieInfo.Items) - { - await ProcessMovies(item, mediaToAdd, server); - } + var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies"); - processed++; - } - else - { - processed++; - // Regular movie - await ProcessMovies(movie, mediaToAdd, server); - } + foreach (var movieParentIdFilder in movieLibsToFilter) + { + _logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'"); + await ProcessMovies(server, movieParentIdFilder.Key); } - // Get the next batch - movies = await Api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); - await _repo.AddRange(mediaToAdd); - mediaToAdd.Clear(); - + var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); + foreach (var tvParentIdFilter in tvLibsToFilter) + { + _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); + await ProcessTv(server, tvParentIdFilter.Key); + } } + else + { + await ProcessMovies(server); + await ProcessTv(server); + } + } - + private async Task ProcessTv(EmbyServers server, string parentId = default) + { // TV Time - var tv = await Api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var mediaToAdd = new HashSet(); + var tv = await Api.GetAllShows(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri); var totalTv = tv.TotalRecordCount; - processed = 1; + var processed = 1; while (processed < totalTv) { foreach (var tvShow in tv.Items) @@ -162,7 +153,7 @@ namespace Ombi.Schedule.Jobs.Emby } } // Get the next batch - tv = await Api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + tv = await Api.GetAllShows(server.ApiKey, parentId, processed, 200, server.AdministratorId, server.FullUri); await _repo.AddRange(mediaToAdd); mediaToAdd.Clear(); } @@ -171,6 +162,43 @@ namespace Ombi.Schedule.Jobs.Emby await _repo.AddRange(mediaToAdd); } + private async Task ProcessMovies(EmbyServers server, string parentId = default) + { + var movies = await Api.GetAllMovies(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri); + var totalCount = movies.TotalRecordCount; + var processed = 1; + var mediaToAdd = new HashSet(); + while (processed < totalCount) + { + foreach (var movie in movies.Items) + { + if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) + { + var movieInfo = + await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); + foreach (var item in movieInfo.Items) + { + await ProcessMovies(item, mediaToAdd, server); + } + + processed++; + } + else + { + processed++; + // Regular movie + await ProcessMovies(movie, mediaToAdd, server); + } + } + + // Get the next batch + movies = await Api.GetAllMovies(server.ApiKey, parentId, processed, 200, server.AdministratorId, server.FullUri); + await _repo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + + } + } + private async Task ProcessMovies(EmbyMovie movieInfo, ICollection content, EmbyServers server) { // Check if it exists diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs index 2cc03a16c..1a55da6c9 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs @@ -60,6 +60,7 @@ namespace Ombi.Schedule.Jobs.Emby private readonly ILogger _logger; private readonly IEmbyContentRepository _repo; private readonly IHubContext _notification; + private IEmbyApi Api { get; set; } @@ -72,7 +73,19 @@ namespace Ombi.Schedule.Jobs.Emby .SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started"); foreach (var server in settings.Servers) { - await CacheEpisodes(server); + if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) + { + var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); + foreach (var tvParentIdFilter in tvLibsToFilter) + { + _logger.LogInformation($"Scanning Lib for episodes '{tvParentIdFilter.Title}'"); + await CacheEpisodes(server, tvParentIdFilter.Key); + } + } + else + { + await CacheEpisodes(server, string.Empty); + } } await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) @@ -81,9 +94,9 @@ namespace Ombi.Schedule.Jobs.Emby await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); } - private async Task CacheEpisodes(EmbyServers server) + private async Task CacheEpisodes(EmbyServers server, string parentIdFilter) { - var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); + var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, 0, 200, server.AdministratorId, server.FullUri); var total = allEpisodes.TotalRecordCount; var processed = 1; var epToAdd = new HashSet(); @@ -150,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Emby await _repo.AddRange(epToAdd); epToAdd.Clear(); - allEpisodes = await Api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); + allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, 200, server.AdministratorId, server.FullUri); } if (epToAdd.Any()) diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs index 0920eed42..c806a5e95 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs @@ -35,6 +35,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin private readonly IJellyfinApiFactory _apiFactory; private readonly IJellyfinContentRepository _repo; private readonly IHubContext _notification; + private IJellyfinApi Api { get; set; } public async Task Execute(IJobExecutionContext job) @@ -61,7 +62,6 @@ namespace Ombi.Schedule.Jobs.Jellyfin _logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name); } } - await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Finished"); // Episodes @@ -80,7 +80,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin //await _repo.ExecuteSql("DELETE FROM JellyfinEpisode"); //await _repo.ExecuteSql("DELETE FROM JellyfinContent"); - if (server.JellyfinSelectedLibraries.Any()) + if (server.JellyfinSelectedLibraries.Any() && server.JellyfinSelectedLibraries.Any(x => x.Enabled)) { var movieLibsToFilter = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies"); diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs index 4cbe7dca9..60f500d19 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs @@ -73,7 +73,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin foreach (var server in settings.Servers) { - if (server.JellyfinSelectedLibraries.Any()) + if (server.JellyfinSelectedLibraries.Any() && server.JellyfinSelectedLibraries.Any(x => x.Enabled)) { var tvLibsToFilter = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); foreach (var tvParentIdFilter in tvLibsToFilter) diff --git a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs index d1b2d3f99..46f3a7e56 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs @@ -16,21 +16,26 @@ namespace Ombi.Schedule.Jobs.Ombi public class MediaDatabaseRefresh : IMediaDatabaseRefresh { public MediaDatabaseRefresh(ISettingsService s, ILogger log, - IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo) + IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo, + ISettingsService embySettings, ISettingsService jellyfinSettings) { - _settings = s; + _plexSettings = s; _log = log; _plexRepo = plexRepo; _embyRepo = embyRepo; _jellyfinRepo = jellyfinRepo; - _settings.ClearCache(); + _embySettings = embySettings; + _jellyfinSettings = jellyfinSettings; + _plexSettings.ClearCache(); } - private readonly ISettingsService _settings; + private readonly ISettingsService _plexSettings; private readonly ILogger _log; private readonly IPlexContentRepository _plexRepo; private readonly IEmbyContentRepository _embyRepo; private readonly IJellyfinContentRepository _jellyfinRepo; + private readonly ISettingsService _embySettings; + private readonly ISettingsService _jellyfinSettings; public async Task Execute(IJobExecutionContext job) { @@ -51,7 +56,7 @@ namespace Ombi.Schedule.Jobs.Ombi { try { - var s = await _settings.GetSettingsAsync(); + var s = await _embySettings.GetSettingsAsync(); if (!s.Enable) { return; @@ -73,7 +78,7 @@ namespace Ombi.Schedule.Jobs.Ombi { try { - var s = await _settings.GetSettingsAsync(); + var s = await _jellyfinSettings.GetSettingsAsync(); if (!s.Enable) { return; @@ -95,7 +100,7 @@ namespace Ombi.Schedule.Jobs.Ombi { try { - var s = await _settings.GetSettingsAsync(); + var s = await _plexSettings.GetSettingsAsync(); if (!s.Enable) { return; diff --git a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs index 65c85b586..a24d07da0 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/RefreshMetadata.cs @@ -30,7 +30,7 @@ namespace Ombi.Schedule.Jobs.Ombi IMovieDbApi movieApi, ISettingsService embySettings, IEmbyApiFactory embyApi, ISettingsService jellyfinSettings, IJellyfinApiFactory jellyfinApi, - IHubContext notification) + IHubContext notification, IMediaCacheService mediaCacheService) { _plexRepo = plexRepo; _embyRepo = embyRepo; @@ -44,6 +44,7 @@ namespace Ombi.Schedule.Jobs.Ombi _jellyfinSettings = jellyfinSettings; _jellyfinApiFactory = jellyfinApi; _notification = notification; + _mediaCacheService = mediaCacheService; } private readonly IPlexContentRepository _plexRepo; @@ -58,6 +59,8 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly IEmbyApiFactory _embyApiFactory; private readonly IJellyfinApiFactory _jellyfinApiFactory; private readonly IHubContext _notification; + private readonly IMediaCacheService _mediaCacheService; + private IEmbyApi EmbyApi { get; set; } private IJellyfinApi JellyfinApi { get; set; } @@ -102,6 +105,8 @@ namespace Ombi.Schedule.Jobs.Ombi return; } + await _mediaCacheService.Purge(); + _log.LogInformation("Metadata refresh finished"); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Metadata Refresh Finished"); diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs index 928baa0c3..dbad7ac84 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs @@ -52,8 +52,10 @@ namespace Ombi.Schedule.Jobs.Plex public class PlexContentSync : IPlexContentSync { private readonly IMovieDbApi _movieApi; + private readonly IMediaCacheService _mediaCacheService; + public PlexContentSync(ISettingsService plex, IPlexApi plexApi, ILogger logger, IPlexContentRepository repo, - IPlexEpisodeSync epsiodeSync, IHubContext hub, IMovieDbApi movieDbApi) + IPlexEpisodeSync epsiodeSync, IHubContext hub, IMovieDbApi movieDbApi, IMediaCacheService mediaCacheService) { Plex = plex; PlexApi = plexApi; @@ -62,6 +64,7 @@ namespace Ombi.Schedule.Jobs.Plex EpisodeSync = epsiodeSync; Notification = hub; _movieApi = movieDbApi; + _mediaCacheService = mediaCacheService; Plex.ClearCache(); } @@ -121,6 +124,7 @@ namespace Ombi.Schedule.Jobs.Plex { await NotifyClient("Plex Sync - Checking if any requests are now available"); Logger.LogInformation("Kicking off Plex Availability Checker"); + await _mediaCacheService.Purge(); await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex"); } var processedCont = processedContent?.Content?.Count() ?? 0; diff --git a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs index 2b165daf8..fe95a96d6 100644 --- a/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs +++ b/src/Ombi.Settings/Settings/Models/External/EmbySettings.cs @@ -22,8 +22,9 @@ namespace Ombi.Core.Settings.Models.External public class EmbySelectedLibraries { - public int Key { get; set; } + public string Key { get; set; } public string Title { get; set; } // Name is for display purposes + public string CollectionType { get; set; } public bool Enabled { get; set; } } } diff --git a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts index 95b5f9dab..236dd12d6 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/ISettings.ts @@ -45,6 +45,7 @@ export interface IEmbyServer extends IExternalSettings { administratorId: string; enableEpisodeSearching: boolean; serverHostname: string; + embySelectedLibraries: IEmbyLibrariesSettings[]; } export interface IPublicInfo { @@ -72,8 +73,14 @@ export interface IJellyfinLibrariesSettings { enabled: boolean; collectionType: string; } +export interface IEmbyLibrariesSettings { + key: string; + title: string; + enabled: boolean; + collectionType: string; +} -export interface IJellyfinContainer { +export interface IMediaServerMediaContainer { items: T[]; totalRecordCount: number; } @@ -84,6 +91,12 @@ export interface IJellyfinLibrary { id: string; collectionType: string; } +export interface IEmbyLibrary { + name: string; + serverId: string; + id: string; + collectionType: string; +} export interface IPublicInfo { id: string; diff --git a/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts b/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts index 734568d95..6e7bdc555 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/emby.service.ts @@ -5,7 +5,7 @@ import { Observable } from "rxjs"; import { ServiceHelpers } from "../service.helpers"; -import { IEmbyServer, IEmbySettings, IPublicInfo, IUsersModel } from "../../interfaces"; +import { IEmbyLibrary, IEmbyServer, IEmbySettings, IMediaServerMediaContainer, IPublicInfo, IUsersModel } from "../../interfaces"; @Injectable() export class EmbyService extends ServiceHelpers { @@ -20,9 +20,13 @@ export class EmbyService extends ServiceHelpers { public getUsers(): Observable { return this.http.get(`${this.url}users`, {headers: this.headers}); } - + public getPublicInfo(server: IEmbyServer): Observable { return this.http.post(`${this.url}info`, JSON.stringify(server), {headers: this.headers}); } + public getLibraries(settings: IEmbyServer): Observable> { + return this.http.post>(`${this.url}Library`, JSON.stringify(settings), {headers: this.headers}); + } + } diff --git a/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts b/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts index 25ff15fde..19fc52883 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/jellyfin.service.ts @@ -5,7 +5,7 @@ import { Observable } from "rxjs"; import { ServiceHelpers } from "../service.helpers"; -import { IEmbyServer, IJellyfinContainer, IJellyfinLibrary, IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces"; +import { IEmbyServer, IMediaServerMediaContainer, IJellyfinLibrary, IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces"; @Injectable() export class JellyfinService extends ServiceHelpers { @@ -25,7 +25,7 @@ export class JellyfinService extends ServiceHelpers { return this.http.post(`${this.url}info`, JSON.stringify(server), {headers: this.headers}); } - public getLibraries(settings: IJellyfinServer): Observable> { - return this.http.post>(`${this.url}Library`, JSON.stringify(settings), {headers: this.headers}); + public getLibraries(settings: IJellyfinServer): Observable> { + return this.http.post>(`${this.url}Library`, JSON.stringify(settings), {headers: this.headers}); } } diff --git a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html index a079534f2..14aaada64 100644 --- a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html +++ b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.html @@ -8,13 +8,11 @@
-
-
-
- Enable -
-
-
+
+
+ Enable + +
@@ -74,8 +72,29 @@ Current URL: "https://app.emby.media/#!/item/item.html?id=1
+ +
+ Note: if nothing is selected, we will monitor all libraries +
+
+ +
+
+
+
+
+
+
+ {{lib.title}} +
+
+
+
-
@@ -87,7 +106,7 @@
- + diff --git a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts index c01b5636f..b50175f61 100644 --- a/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/emby/emby.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit } from "@angular/core"; - -import { IEmbyServer, IEmbySettings } from "../../interfaces"; import { EmbyService, JobService, NotificationService, SettingsService, TesterService } from "../../services"; -import { MatTabChangeEvent } from "@angular/material/tabs"; +import { IEmbyLibrariesSettings, IEmbyServer, IEmbySettings } from "../../interfaces"; + import {FormControl} from '@angular/forms'; +import { MatTabChangeEvent } from "@angular/material/tabs"; @Component({ templateUrl: "./emby.component.html", @@ -100,4 +100,28 @@ export class EmbyComponent implements OnInit { } }); } + + public loadLibraries(server: IEmbyServer) { + if (server.ip == null) { + this.notificationService.error("Emby is not yet configured correctly"); + return; + } + this.embyService.getLibraries(server).subscribe(x => { + server.embySelectedLibraries = []; + if (x.totalRecordCount > 0) { + x.items.forEach((item) => { + const lib: IEmbyLibrariesSettings = { + key: item.id, + title: item.name, + enabled: false, + collectionType: item.collectionType + }; + server.embySelectedLibraries.push(lib); + }); + } else { + this.notificationService.error("Couldn't find any libraries"); + } + }, + err => { this.notificationService.error(err); }); + } } diff --git a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts index fb0c167a6..356a4c1e4 100644 --- a/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts +++ b/src/Ombi/ClientApp/src/app/wizard/emby/emby.component.ts @@ -1,9 +1,8 @@ import { Component, OnInit } from "@angular/core"; import { EmbyService } from "../../services"; -import { NotificationService } from "../../services"; - import { IEmbySettings } from "../../interfaces"; +import { NotificationService } from "../../services"; @Component({ selector: "wizard-emby", @@ -35,7 +34,8 @@ export class EmbyComponent implements OnInit { ssl: false, subDir: "", serverHostname: "", - serverId: undefined + serverId: undefined, + embySelectedLibraries: [] }); } @@ -45,7 +45,7 @@ export class EmbyComponent implements OnInit { this.notificationService.error("Username or password was incorrect. Could not authenticate with Emby."); return; } - + this.notificationService.success("Done! Please press next"); }); } diff --git a/src/Ombi/Controllers/V1/External/EmbyController.cs b/src/Ombi/Controllers/V1/External/EmbyController.cs index 29ad84eb2..faed4bc2b 100644 --- a/src/Ombi/Controllers/V1/External/EmbyController.cs +++ b/src/Ombi/Controllers/V1/External/EmbyController.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Ombi.Api.Emby; using Ombi.Api.Emby.Models; -using Ombi.Api.Plex; +using Ombi.Api.Emby.Models.Media; using Ombi.Attributes; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; @@ -92,5 +92,13 @@ namespace Ombi.Controllers.V1.External // Filter out any dupes return vm.DistinctBy(x => x.Id); } + + [HttpPost("Library")] + public async Task> GetLibaries([FromBody] EmbyServers server) + { + var client = await EmbyApi.CreateClient(); + var result = await client.GetLibraries(server.ApiKey, server.FullUri); + return result; + } } } From 309f21420357851d8792ceb4620ef80e0e5a7d1f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 20 Jul 2021 20:48:29 +0100 Subject: [PATCH 18/27] Change the API to use the virtual folders call and also take into consideration mixed content folders #2389 --- src/Ombi.Api.Emby/EmbyApi.cs | 7 ++-- src/Ombi.Api.Emby/IEmbyApi.cs | 5 ++- .../Models/LibraryVirtualFolders.cs | 26 ++++++++++++ .../Jobs/Emby/EmbyContentSync.cs | 9 +++++ .../Controllers/V1/External/EmbyController.cs | 40 ++++++++++++++++++- 5 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 src/Ombi.Api.Emby/Models/LibraryVirtualFolders.cs diff --git a/src/Ombi.Api.Emby/EmbyApi.cs b/src/Ombi.Api.Emby/EmbyApi.cs index b24783e3c..c413ecded 100644 --- a/src/Ombi.Api.Emby/EmbyApi.cs +++ b/src/Ombi.Api.Emby/EmbyApi.cs @@ -115,13 +115,12 @@ namespace Ombi.Api.Emby return await Api.Request>(request); } - public async Task> GetLibraries(string apiKey, string baseUrl) + public async Task> GetLibraries(string apiKey, string baseUrl) { - var request = new Request("library/mediafolders", baseUrl, HttpMethod.Get); + var request = new Request("library/VirtualFolders", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); - var response = await Api.Request>(request); - response.Items = response.Items.Where(x => !x.CollectionType.Equals("playlists", StringComparison.InvariantCultureIgnoreCase)).ToList(); + var response = await Api.Request>(request); return response; } diff --git a/src/Ombi.Api.Emby/IEmbyApi.cs b/src/Ombi.Api.Emby/IEmbyApi.cs index 66c281de0..bbb03421e 100644 --- a/src/Ombi.Api.Emby/IEmbyApi.cs +++ b/src/Ombi.Api.Emby/IEmbyApi.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models.Media; @@ -7,6 +8,6 @@ namespace Ombi.Api.Emby public interface IEmbyApi : IBaseEmbyApi { Task LoginConnectUser(string username, string password); - Task> GetLibraries(string apiKey, string baseUrl); + Task> GetLibraries(string apiKey, string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Emby/Models/LibraryVirtualFolders.cs b/src/Ombi.Api.Emby/Models/LibraryVirtualFolders.cs new file mode 100644 index 000000000..a75a655a2 --- /dev/null +++ b/src/Ombi.Api.Emby/Models/LibraryVirtualFolders.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Api.Emby.Models +{ + public class LibraryVirtualFolders + { + public string Name { get; set; } + public Libraryoptions LibraryOptions { get; set; } + public string ItemId { get; set; } + } + + public class Libraryoptions + { + public List TypeOptions { get; set; } = new List(); + } + + public class Typeoption + { + public string Type { get; set; } + } + +} diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index 4d7e271d6..0630c0b28 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -95,6 +95,15 @@ namespace Ombi.Schedule.Jobs.Emby _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); await ProcessTv(server, tvParentIdFilter.Key); } + + + var mixedLibs = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed"); + foreach (var m in mixedLibs) + { + _logger.LogInformation($"Scanning Lib '{m.Title}'"); + await ProcessTv(server, m.Key); + await ProcessMovies(server, m.Key); + } } else { diff --git a/src/Ombi/Controllers/V1/External/EmbyController.cs b/src/Ombi/Controllers/V1/External/EmbyController.cs index faed4bc2b..cc7c6fc47 100644 --- a/src/Ombi/Controllers/V1/External/EmbyController.cs +++ b/src/Ombi/Controllers/V1/External/EmbyController.cs @@ -98,7 +98,45 @@ namespace Ombi.Controllers.V1.External { var client = await EmbyApi.CreateClient(); var result = await client.GetLibraries(server.ApiKey, server.FullUri); - return result; + var mediaFolders = new EmbyItemContainer + { + TotalRecordCount = result.Count, + Items = new List() + }; + + foreach(var folder in result) + { + var toAdd = new MediaFolders + { + Name = folder.Name, + Id = folder.ItemId, + ServerId = server.ServerId + }; + + var types = folder?.LibraryOptions?.TypeOptions?.Select(x => x.Type); + + if (!types.Any()) + { + continue; + } + + if (types.Where(x => x.Equals("Movie", System.StringComparison.InvariantCultureIgnoreCase) + || x.Equals("Episode", System.StringComparison.InvariantCultureIgnoreCase)).Count() >= 2) + { + toAdd.CollectionType = "mixed"; + } + else if (types.Where(x => x.Equals("Movie", System.StringComparison.InvariantCultureIgnoreCase)).Any()) + { + toAdd.CollectionType = "movies"; + } + else if (types.Where(x => x.Equals("Episode", System.StringComparison.InvariantCultureIgnoreCase)).Any()) + { + toAdd.CollectionType = "tvshows"; + } + + mediaFolders.Items.Add(toAdd); + } + return mediaFolders; } } } From a702e22b048eee578542ca2883d69fc5a89b2eea Mon Sep 17 00:00:00 2001 From: Jamie Date: Tue, 20 Jul 2021 20:54:11 +0000 Subject: [PATCH 19/27] added russian into the language #4259 --- src/Ombi/ClientApp/src/app/app.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ombi/ClientApp/src/app/app.component.ts b/src/Ombi/ClientApp/src/app/app.component.ts index ef5840c2e..0a63c0e51 100644 --- a/src/Ombi/ClientApp/src/app/app.component.ts +++ b/src/Ombi/ClientApp/src/app/app.component.ts @@ -50,7 +50,7 @@ export class AppComponent implements OnInit { private readonly identity: IdentityService, @Inject(DOCUMENT) private document: HTMLDocument) { - this.translate.addLangs(["en", "de", "fr", "da", "es", "it", "nl", "sk", "sv", "no", "pl", "pt"]); + this.translate.addLangs(["en", "de", "fr", "da", "es", "it", "nl", "sk", "sv", "no", "pl", "ru", "pt"]); if (this.authService.loggedIn()) { this.user = this.authService.claims(); @@ -78,7 +78,7 @@ export class AppComponent implements OnInit { // See if we can match the supported langs with the current browser lang const browserLang: string = translate.getBrowserLang(); - this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|sk|sv|no|pl|pt/) ? browserLang : "en"); + this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|ru|sk|sv|no|pl|pt/) ? browserLang : "en"); } From 71194071412ab4576ff7282474149a8e4ae3de3c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Sat, 24 Jul 2021 22:58:35 +0100 Subject: [PATCH 20/27] Fixed the jellyfin load libraries issue #4266 --- src/Ombi.Api.Jellyfin/IJellyfinApi.cs | 5 ++- src/Ombi.Api.Jellyfin/JellyfinApi.cs | 7 ++- .../Models/LibraryVirtualFolders.cs | 26 +++++++++++ .../Jobs/Jellyfin/JellyfinContentSync.cs | 8 ++++ .../V1/External/JellyfinController.cs | 43 ++++++++++++++++++- 5 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/Ombi.Api.Jellyfin/Models/LibraryVirtualFolders.cs diff --git a/src/Ombi.Api.Jellyfin/IJellyfinApi.cs b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs index 5dcf5741f..f84396b52 100644 --- a/src/Ombi.Api.Jellyfin/IJellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/IJellyfinApi.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using Ombi.Api.Jellyfin.Models; namespace Ombi.Api.Jellyfin @@ -6,6 +7,6 @@ namespace Ombi.Api.Jellyfin public interface IJellyfinApi : IBaseJellyfinApi { Task LoginConnectUser(string username, string password); - Task> GetLibraries(string apiKey, string baseUrl); + Task> GetLibraries(string apiKey, string baseUrl); } } \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/JellyfinApi.cs b/src/Ombi.Api.Jellyfin/JellyfinApi.cs index c16620710..2fafbfd86 100644 --- a/src/Ombi.Api.Jellyfin/JellyfinApi.cs +++ b/src/Ombi.Api.Jellyfin/JellyfinApi.cs @@ -87,13 +87,12 @@ namespace Ombi.Api.Jellyfin return await Api.Request>(request); } - public async Task> GetLibraries(string apiKey, string baseUrl) + public async Task> GetLibraries(string apiKey, string baseUrl) { - var request = new Request("library/mediafolders", baseUrl, HttpMethod.Get); + var request = new Request("library/virtualfolders", baseUrl, HttpMethod.Get); AddHeaders(request, apiKey); - var response = await Api.Request>(request); - response.Items = response.Items.Where(x => !x.CollectionType.Equals("playlists", StringComparison.InvariantCultureIgnoreCase)).ToList(); + var response = await Api.Request>(request); return response; } diff --git a/src/Ombi.Api.Jellyfin/Models/LibraryVirtualFolders.cs b/src/Ombi.Api.Jellyfin/Models/LibraryVirtualFolders.cs new file mode 100644 index 000000000..fbe56a398 --- /dev/null +++ b/src/Ombi.Api.Jellyfin/Models/LibraryVirtualFolders.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Api.Jellyfin.Models +{ + public class LibraryVirtualFolders + { + public string Name { get; set; } + public Libraryoptions LibraryOptions { get; set; } + public string ItemId { get; set; } + } + + public class Libraryoptions + { + public List TypeOptions { get; set; } = new List(); + } + + public class Typeoption + { + public string Type { get; set; } + } + +} diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs index c806a5e95..2a950fcf4 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinContentSync.cs @@ -96,6 +96,14 @@ namespace Ombi.Schedule.Jobs.Jellyfin _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); await ProcessTv(server, tvParentIdFilter.Key); } + + var mixedLibs = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed"); + foreach (var m in mixedLibs) + { + _logger.LogInformation($"Scanning Lib '{m.Title}'"); + await ProcessTv(server, m.Key); + await ProcessMovies(server, m.Key); + } } else { diff --git a/src/Ombi/Controllers/V1/External/JellyfinController.cs b/src/Ombi/Controllers/V1/External/JellyfinController.cs index 5e8b87414..275ef30f6 100644 --- a/src/Ombi/Controllers/V1/External/JellyfinController.cs +++ b/src/Ombi/Controllers/V1/External/JellyfinController.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -71,7 +72,45 @@ namespace Ombi.Controllers.V1.External { var client = await JellyfinApi.CreateClient(); var result = await client.GetLibraries(server.ApiKey, server.FullUri); - return result; + var mediaFolders = new JellyfinItemContainer + { + TotalRecordCount = result.Count, + Items = new List() + }; + + foreach (var folder in result) + { + var toAdd = new MediaFolders + { + Name = folder.Name, + Id = folder.ItemId, + ServerId = server.ServerId + }; + + var types = folder?.LibraryOptions?.TypeOptions?.Select(x => x.Type).ToList(); + + if (!types.Any()) + { + continue; + } + + if (types.Where(x => x.Equals("Movie", System.StringComparison.InvariantCultureIgnoreCase) + || x.Equals("Episode", System.StringComparison.InvariantCultureIgnoreCase)).Count() >= 2) + { + toAdd.CollectionType = "mixed"; + } + else if (types.Any(x => x.Equals("Movie", StringComparison.InvariantCultureIgnoreCase))) + { + toAdd.CollectionType = "movies"; + } + else if (types.Any(x => x.Equals("Episode", StringComparison.InvariantCultureIgnoreCase))) + { + toAdd.CollectionType = "tvshows"; + } + + mediaFolders.Items.Add(toAdd); + } + return mediaFolders; } /// From 83ee2dae1a5e5d7e38f9db533b45e085efaaa369 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 26 Jul 2021 14:39:06 +0100 Subject: [PATCH 21/27] Made a start on the advanced options --- src/Ombi.TheMovieDbApi/IMovieDbApi.cs | 8 +- .../{Keyword.cs => TheMovidDbKeyValue.cs} | 2 +- src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 14 +- src/Ombi/ClientApp/src/app/app.module.ts | 120 +++++++++++------- .../search-results.component.html | 1 + .../src/app/my-nav/my-nav.component.html | 1 + .../src/app/my-nav/my-nav.component.ts | 7 + .../applications/themoviedb.service.ts | 4 +- .../themoviedb/themoviedb.component.ts | 12 +- .../advanced-search-dialog.component.html | 58 +++++++++ .../advanced-search-dialog.component.scss | 8 ++ .../advanced-search-dialog.component.ts | 36 ++++++ .../genre-select/genre-select.component.html | 25 ++++ .../genre-select/genre-select.component.ts | 85 +++++++++++++ .../keyword-search.component.html | 37 ++++++ .../keyword-search.component.ts | 64 ++++++++++ .../ClientApp/src/app/shared/shared.module.ts | 58 +++++---- .../V1/External/TheMovieDbController.cs | 16 +-- 18 files changed, 457 insertions(+), 99 deletions(-) rename src/Ombi.TheMovieDbApi/Models/{Keyword.cs => TheMovidDbKeyValue.cs} (84%) create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts create mode 100644 src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts create mode 100644 src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 7a0f7e385..202392c5f 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -26,7 +26,7 @@ namespace Ombi.Api.TheMovieDb Task> UpcomingTv(string languageCode, int? page = null); Task> SimilarMovies(int movieId, string langCode); Task Find(string externalId, ExternalSource source); - Task GetTvExternals(int theMovieDbId); + Task GetTvExternals(int theMovieDbId); Task GetSeasonEpisodes(int theMovieDbId, int seasonNumber, CancellationToken token, string langCode = "en"); Task GetTVInfo(string themoviedbid, string langCode = "en"); Task> SearchByActor(string searchTerm, string langCode); @@ -35,10 +35,10 @@ namespace Ombi.Api.TheMovieDb Task> DiscoverMovies(string langCode, int keywordId); Task GetFullMovieInfo(int movieId, CancellationToken cancellationToken, string langCode); Task GetCollection(string langCode, int collectionId, CancellationToken cancellationToken); - Task> SearchKeyword(string searchTerm); - Task GetKeyword(int keywordId); + Task> SearchKeyword(string searchTerm); + Task GetKeyword(int keywordId); Task GetMovieWatchProviders(int theMoviedbId, CancellationToken token); Task GetTvWatchProviders(int theMoviedbId, CancellationToken token); - Task> GetGenres(string media); + Task> GetGenres(string media, CancellationToken cancellationToken); } } diff --git a/src/Ombi.TheMovieDbApi/Models/Keyword.cs b/src/Ombi.TheMovieDbApi/Models/TheMovidDbKeyValue.cs similarity index 84% rename from src/Ombi.TheMovieDbApi/Models/Keyword.cs rename to src/Ombi.TheMovieDbApi/Models/TheMovidDbKeyValue.cs index 770eebc94..a5f3fc0db 100644 --- a/src/Ombi.TheMovieDbApi/Models/Keyword.cs +++ b/src/Ombi.TheMovieDbApi/Models/TheMovidDbKeyValue.cs @@ -2,7 +2,7 @@ namespace Ombi.Api.TheMovieDb.Models { - public sealed class Keyword + public sealed class TheMovidDbKeyValue { [DataMember(Name = "id")] public int Id { get; set; } diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 70e4a94c7..a8fdc3269 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -357,34 +357,34 @@ namespace Ombi.Api.TheMovieDb return Mapper.Map>(result.results); } - public async Task> SearchKeyword(string searchTerm) + public async Task> SearchKeyword(string searchTerm) { var request = new Request("search/keyword", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); request.AddQueryString("query", searchTerm); AddRetry(request); - var result = await Api.Request>(request); - return result.results ?? new List(); + var result = await Api.Request>(request); + return result.results ?? new List(); } - public async Task GetKeyword(int keywordId) + public async Task GetKeyword(int keywordId) { var request = new Request($"keyword/{keywordId}", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); - var keyword = await Api.Request(request); + var keyword = await Api.Request(request); return keyword == null || keyword.Id == 0 ? null : keyword; } - public async Task> GetGenres(string media) + public async Task> GetGenres(string media, CancellationToken cancellationToken) { var request = new Request($"genre/{media}/list", BaseUri, HttpMethod.Get); request.AddQueryString("api_key", ApiToken); AddRetry(request); - var result = await Api.Request>(request); + var result = await Api.Request>(request, cancellationToken); return result.genres ?? new List(); } diff --git a/src/Ombi/ClientApp/src/app/app.module.ts b/src/Ombi/ClientApp/src/app/app.module.ts index 8e44d59bd..1716b5c5e 100644 --- a/src/Ombi/ClientApp/src/app/app.module.ts +++ b/src/Ombi/ClientApp/src/app/app.module.ts @@ -1,73 +1,98 @@ -import { CommonModule, PlatformLocation, APP_BASE_HREF } from "@angular/common"; -import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; -import { NgModule } from "@angular/core"; +import { APP_BASE_HREF, CommonModule, PlatformLocation } from "@angular/common"; +import { CardsFreeModule, MDBBootstrapModule, NavbarModule } from "angular-bootstrap-md"; +import { CustomPageService, ImageService, RequestService, SettingsService } from "./services"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http"; +import { IdentityService, IssuesService, JobService, MessageService, PlexTvService, SearchService, StatusService } from "./services"; import { RouterModule, Routes } from "@angular/router"; - -import { JwtModule } from "@auth0/angular-jwt"; import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; -import { TranslateHttpLoader } from "@ngx-translate/http-loader"; -import { CookieService } from "ng2-cookies"; +import { AppComponent } from "./app.component"; +import { AuthGuard } from "./auth/auth.guard"; +import { AuthService } from "./auth/auth.service"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { BrowserModule } from "@angular/platform-browser"; import { ButtonModule } from "primeng/button"; import { ConfirmDialogModule } from "primeng/confirmdialog"; +import { CookieComponent } from "./auth/cookie.component"; +import { CookieService } from "ng2-cookies"; +import { CustomPageComponent } from "./custompage/custompage.component"; import { DataViewModule } from "primeng/dataview"; import { DialogModule } from "primeng/dialog"; -import { OverlayPanelModule } from "primeng/overlaypanel"; -import { TooltipModule } from "primeng/tooltip"; -import { SidebarModule } from "primeng/sidebar"; - +import { FilterService } from "./discover/services/filter-service"; +import { JwtModule } from "@auth0/angular-jwt"; +import { LandingPageComponent } from "./landingpage/landingpage.component"; +import { LandingPageService } from "./services"; +import { LayoutModule } from '@angular/cdk/layout'; +import { LoginComponent } from "./login/login.component"; +import { LoginOAuthComponent } from "./login/loginoauth.component"; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from "@angular/material/card"; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatNativeDateModule } from '@angular/material/core'; +import { MatChipsModule } from "@angular/material/chips"; +import { MatDialogModule } from "@angular/material/dialog"; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from "@angular/material/input"; import { MatListModule } from '@angular/material/list'; +import { MatMenuModule } from "@angular/material/menu"; +import { MatNativeDateModule } from '@angular/material/core'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatCardModule } from "@angular/material/card"; -import { MatInputModule } from "@angular/material/input"; import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTabsModule } from "@angular/material/tabs"; +import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from "@angular/material/tooltip"; +import { MyNavComponent } from './my-nav/my-nav.component'; +import { NavSearchComponent } from "./my-nav/nav-search.component"; +import { NgModule } from "@angular/core"; +import { NotificationService } from "./services"; +import { OverlayModule } from "@angular/cdk/overlay"; +import { OverlayPanelModule } from "primeng/overlaypanel"; +import { PageNotFoundComponent } from "./errors/not-found.component"; +import { RemainingRequestsComponent } from "./shared/remaining-requests/remaining-requests.component"; +import { ResetPasswordComponent } from "./login/resetpassword.component"; +import { SearchV2Service } from "./services/searchV2.service"; +import { SidebarModule } from "primeng/sidebar"; +import { SignalRNotificationService } from "./services/signlarnotification.service"; +import { StorageService } from "./shared/storage/storage-service"; +import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component"; +import { TooltipModule } from "primeng/tooltip"; +import { TranslateHttpLoader } from "@ngx-translate/http-loader"; +import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor"; + +// Components + + + + + + -import { MDBBootstrapModule, CardsFreeModule, NavbarModule } from "angular-bootstrap-md"; -// Components -import { AppComponent } from "./app.component"; -import { CookieComponent } from "./auth/cookie.component"; -import { CustomPageComponent } from "./custompage/custompage.component"; -import { PageNotFoundComponent } from "./errors/not-found.component"; -import { LandingPageComponent } from "./landingpage/landingpage.component"; -import { LoginComponent } from "./login/login.component"; -import { LoginOAuthComponent } from "./login/loginoauth.component"; -import { ResetPasswordComponent } from "./login/resetpassword.component"; -import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component"; // Services -import { AuthGuard } from "./auth/auth.guard"; -import { AuthService } from "./auth/auth.service"; -import { ImageService, SettingsService, CustomPageService, RequestService } from "./services"; -import { LandingPageService } from "./services"; -import { NotificationService } from "./services"; -import { IssuesService, JobService, PlexTvService, StatusService, SearchService, IdentityService, MessageService } from "./services"; -import { MyNavComponent } from './my-nav/my-nav.component'; -import { LayoutModule } from '@angular/cdk/layout'; -import { SearchV2Service } from "./services/searchV2.service"; -import { NavSearchComponent } from "./my-nav/nav-search.component"; -import { OverlayModule } from "@angular/cdk/overlay"; -import { StorageService } from "./shared/storage/storage-service"; -import { SignalRNotificationService } from "./services/signlarnotification.service"; -import { MatMenuModule } from "@angular/material/menu"; -import { RemainingRequestsComponent } from "./shared/remaining-requests/remaining-requests.component"; -import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor"; -import { FilterService } from "./discover/services/filter-service"; + + + + + + + + + + + + + + + + + + const routes: Routes = [ { path: "*", component: PageNotFoundComponent }, @@ -135,6 +160,8 @@ export function JwtTokenGetter() { MatMenuModule, MatInputModule, MatTabsModule, + MatChipsModule, + MatDialogModule, ReactiveFormsModule, MatAutocompleteModule, TooltipModule, @@ -146,7 +173,6 @@ export function JwtTokenGetter() { MatCheckboxModule, MatProgressSpinnerModule, MDBBootstrapModule.forRoot(), - // NbThemeModule.forRoot({ name: 'dark'}), JwtModule.forRoot({ config: { tokenGetter: JwtTokenGetter, diff --git a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html index 29a583890..2bd13441d 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html +++ b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.html @@ -1,4 +1,5 @@
+
diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html index 95f31198b..84ae81fe8 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html @@ -78,6 +78,7 @@ {{ 'NavigationBar.Filter.Music' | translate}} + diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts index 57bfaca91..393d12fd4 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts @@ -3,9 +3,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { IUser, RequestType, UserType } from '../interfaces'; import { SettingsService, SettingsStateService } from '../services'; +import { AdvancedSearchDialogComponent } from '../shared/advanced-search-dialog/advanced-search-dialog.component'; import { FilterService } from '../discover/services/filter-service'; import { ILocalUser } from '../auth/IUserLogin'; import { INavBar } from '../interfaces/ICommon'; +import { MatDialog } from '@angular/material/dialog'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { Md5 } from 'ts-md5/dist/md5'; import { Observable } from 'rxjs'; @@ -54,6 +56,7 @@ export class MyNavComponent implements OnInit { private settingsService: SettingsService, private store: StorageService, private filterService: FilterService, + private dialogService: MatDialog, private readonly settingState: SettingsStateService) { } @@ -121,6 +124,10 @@ export class MyNavComponent implements OnInit { this.store.save("searchFilter", JSON.stringify(this.searchFilter)); } + public openAdvancedSearch() { + this.dialogService.open(AdvancedSearchDialogComponent, null); + } + public getUserImage(): string { var fallback = this.applicationLogo ? this.applicationLogo : 'https://raw.githubusercontent.com/Ombi-app/Ombi/gh-pages/img/android-chrome-512x512.png'; return `https://www.gravatar.com/avatar/${this.emailHash}?d=${fallback}`; diff --git a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts index ff76d591b..0af37c508 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts @@ -7,7 +7,9 @@ import { catchError } from "rxjs/operators"; import { IMovieDbKeyword } from "../../interfaces"; import { ServiceHelpers } from "../service.helpers"; -@Injectable() +@Injectable({ + providedIn: 'root', + }) export class TheMovieDbService extends ServiceHelpers { constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { super(http, "/api/v1/TheMovieDb", href); diff --git a/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts b/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts index 49ec3e51f..008e2dfab 100644 --- a/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts +++ b/src/Ombi/ClientApp/src/app/settings/themoviedb/themoviedb.component.ts @@ -1,13 +1,13 @@ import {COMMA, ENTER} from "@angular/cdk/keycodes"; -import { Component, OnInit, ElementRef, ViewChild } from "@angular/core"; -import { MatAutocomplete } from "@angular/material/autocomplete"; +import { Component, ElementRef, OnInit, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; +import { IMovieDbKeyword, ITheMovieDbSettings } from "../../interfaces"; +import { debounceTime, switchMap } from "rxjs/operators"; -import { ITheMovieDbSettings, IMovieDbKeyword } from "../../interfaces"; +import { MatAutocomplete } from "@angular/material/autocomplete"; import { NotificationService } from "../../services"; import { SettingsService } from "../../services"; import { TheMovieDbService } from "../../services"; -import { FormBuilder, FormGroup } from "@angular/forms"; -import { debounceTime, switchMap } from "rxjs/operators"; interface IKeywordTag { id: number; @@ -30,8 +30,6 @@ export class TheMovieDbComponent implements OnInit { public filteredMovieGenres: IMovieDbKeyword[]; public filteredTvGenres: IMovieDbKeyword[]; - @ViewChild('fruitInput') public fruitInput: ElementRef; - constructor(private settingsService: SettingsService, private notificationService: NotificationService, private tmdbService: TheMovieDbService, diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html new file mode 100644 index 000000000..02f9ac35b --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html @@ -0,0 +1,58 @@ +
+

+ Advanced Search +

+
+ + +
+ +
+ +
+
+
+ + Movies + TV Shows + +
+
+
+ + Year + + +
+ +
+ +
+
+ +
+ +
+ +
+ + +
+
diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss new file mode 100644 index 000000000..8d701c21f --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.scss @@ -0,0 +1,8 @@ + +@import "~styles/variables.scss"; + +.alert-info { + background: $accent; + border-color: $ombi-background-primary; + color:white; +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts new file mode 100644 index 000000000..e59f7fab7 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts @@ -0,0 +1,36 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup } from "@angular/forms"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; + + +@Component({ + selector: "advanced-search-dialog", + templateUrl: "advanced-search-dialog.component.html", + styleUrls: [ "advanced-search-dialog.component.scss" ] +}) +export class AdvancedSearchDialogComponent implements OnInit { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private fb: FormBuilder + ) {} + + public form: FormGroup; + + + public async ngOnInit() { + + this.form = this.fb.group({ + keywords: [[]], + genres: [[]], + releaseYear: [], + type: ['movie'], + }) + + this.form.controls.type.valueChanges.subscribe(val => { + this.form.controls.genres.setValue([]); + }); + + + } +} diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html new file mode 100644 index 000000000..523974a90 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html @@ -0,0 +1,25 @@ + + + Genres + + + {{word.name}} + cancel + + + + + + {{word.name}} + + + + diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts new file mode 100644 index 000000000..c80eb4ae8 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts @@ -0,0 +1,85 @@ +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from "rxjs/operators"; + +import { IMovieDbKeyword } from "../../../interfaces"; +import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +import { Observable } from "rxjs"; +import { TheMovieDbService } from "../../../services"; + +@Component({ + selector: "genre-select", + templateUrl: "genre-select.component.html" +}) +export class GenreSelectComponent implements OnInit { + constructor( + private tmdbService: TheMovieDbService + ) {} + + @Input() public form: FormGroup; + + private _mediaType: string; + @Input() set mediaType(type: string) { + this._mediaType = type; + this.tmdbService.getGenres(this._mediaType).subscribe((res) => { + this.genres = res; + this.filteredKeywords = this.control.valueChanges.pipe( + startWith(''), + map((genre: string | null) => genre ? this._filter(genre) : this.genres.slice())); + }); + + } + get mediaType(): string { + return this._mediaType; + } + public genres: IMovieDbKeyword[] = []; + public control = new FormControl(); + public filteredTags: IMovieDbKeyword[]; + public filteredKeywords: Observable; + + @ViewChild('keywordInput') input: ElementRef; + + async ngOnInit() { + + // this.genres = await this.tmdbService.getGenres(this.mediaType).toPromise(); + + + + } + + remove(word: IMovieDbKeyword): void { + const exisiting = this.form.controls.genres.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.genres.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.genres.value; + if(exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + this.form.controls.genres.setValue(exisiting); + this.input.nativeElement.value = ''; + this.control.setValue(null); + } + + private _filter(value: string|IMovieDbKeyword): IMovieDbKeyword[] { + if (typeof value === 'object') { + const filterValue = value.name.toLowerCase(); + return this.genres.filter(g => g.name.toLowerCase().includes(filterValue)); + } else if (typeof value === 'string') { + const filterValue = value.toLowerCase(); + return this.genres.filter(g => g.name.toLowerCase().includes(filterValue)); + } + + return this.genres; + } + + +} diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html new file mode 100644 index 000000000..1ff1874e9 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html @@ -0,0 +1,37 @@ + + + + + Keywords + + + {{word.name}} + cancel + + + + + + {{word.name}} + + + + diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts new file mode 100644 index 000000000..1ea8bcef1 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts @@ -0,0 +1,64 @@ +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { debounceTime, distinctUntilChanged, startWith, switchMap } from "rxjs/operators"; + +import { IMovieDbKeyword } from "../../../interfaces"; +import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +import { Observable } from "rxjs"; +import { TheMovieDbService } from "../../../services"; + +@Component({ + selector: "keyword-search", + templateUrl: "keyword-search.component.html" +}) +export class KeywordSearchComponent implements OnInit { + constructor( + private tmdbService: TheMovieDbService + ) {} + + @Input() public form: FormGroup; + public control = new FormControl(); + public filteredTags: IMovieDbKeyword[]; + public filteredKeywords: Observable; + + @ViewChild('keywordInput') input: ElementRef; + + ngOnInit(): void { + + this.filteredKeywords = this.control.valueChanges.pipe( + startWith(''), + debounceTime(400), + distinctUntilChanged(), + switchMap(val => { + return this.filter(val || '') + }) + ); + } + + filter(val: string): Observable { + return this.tmdbService.getKeywords(val); + }; + + remove(word: IMovieDbKeyword): void { + const exisiting = this.form.controls.keywords.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.keywords.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.keywords.value; + if (exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + this.form.controls.keywords.setValue(exisiting); + this.input.nativeElement.value = ''; + this.control.setValue(null); + } + +} diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index 1189320dd..a76914470 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -1,43 +1,46 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { TranslateModule } from "@ngx-translate/core"; -import { TruncateModule } from "@yellowspot/ng-truncate"; -import { MomentModule } from "ngx-moment"; -import { IssuesReportComponent } from "./issues-report.component"; - -import { SidebarModule } from "primeng/sidebar"; +import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component"; +import { AdvancedSearchDialogComponent } from "./advanced-search-dialog/advanced-search-dialog.component"; +import { CommonModule } from "@angular/common"; +import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component"; +import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; +import { GenreSelectComponent } from "./components/genre-select/genre-select.component"; import { InputSwitchModule } from "primeng/inputswitch"; - +import { IssuesReportComponent } from "./issues-report.component"; +import { KeywordSearchComponent } from "./components/keyword-search/keyword-search.component"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatButtonModule } from '@angular/material/button'; -import { MatNativeDateModule } from '@angular/material/core'; +import { MatCardModule } from "@angular/material/card"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatChipsModule } from "@angular/material/chips"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatExpansionModule } from "@angular/material/expansion"; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from "@angular/material/input"; import { MatListModule } from '@angular/material/list'; +import {MatMenuModule} from '@angular/material/menu'; +import { MatNativeDateModule } from '@angular/material/core'; import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; +import {MatRadioModule} from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSortModule } from '@angular/material/sort'; import { MatStepperModule } from '@angular/material/stepper'; import { MatTableModule } from '@angular/material/table'; -import {MatMenuModule} from '@angular/material/menu'; +import { MatTabsModule } from "@angular/material/tabs"; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTreeModule } from '@angular/material/tree'; - import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatCardModule } from "@angular/material/card"; -import { MatCheckboxModule } from "@angular/material/checkbox"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatExpansionModule } from "@angular/material/expansion"; -import { MatInputModule } from "@angular/material/input"; -import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; -import { MatSlideToggleModule } from "@angular/material/slide-toggle"; -import { MatTabsModule } from "@angular/material/tabs"; -import { EpisodeRequestComponent } from "./episode-request/episode-request.component"; -import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component"; -import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component"; +import { MomentModule } from "ngx-moment"; +import { NgModule } from "@angular/core"; +import { SidebarModule } from "primeng/sidebar"; +import { TheMovieDbService } from "../services"; +import { TranslateModule } from "@ngx-translate/core"; +import { TruncateModule } from "@yellowspot/ng-truncate"; @NgModule({ declarations: [ @@ -45,6 +48,9 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques EpisodeRequestComponent, DetailsGroupComponent, AdminRequestDialogComponent, + AdvancedSearchDialogComponent, + KeywordSearchComponent, + GenreSelectComponent, ], imports: [ SidebarModule, @@ -59,6 +65,7 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques MatAutocompleteModule, MatInputModule, MatTabsModule, + MatRadioModule, MatButtonModule, MatNativeDateModule, MatChipsModule, @@ -89,6 +96,9 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques IssuesReportComponent, EpisodeRequestComponent, AdminRequestDialogComponent, + AdvancedSearchDialogComponent, + GenreSelectComponent, + KeywordSearchComponent, DetailsGroupComponent, TruncateModule, InputSwitchModule, diff --git a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs index ac4bb4eea..3dacdb406 100644 --- a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs +++ b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs @@ -1,7 +1,7 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb.Models; -using Ombi.Attributes; using System.Collections.Generic; using System.Threading.Tasks; @@ -11,10 +11,10 @@ using Genre = Ombi.TheMovieDbApi.Models.Genre; namespace Ombi.Controllers.External { - [Admin] [ApiV1] + [Authorize] [Produces("application/json")] - public sealed class TheMovieDbController : Controller + public sealed class TheMovieDbController : ControllerBase { public TheMovieDbController(IMovieDbApi tmdbApi) => TmdbApi = tmdbApi; @@ -25,7 +25,7 @@ namespace Ombi.Controllers.External ///
/// The search term. [HttpGet("Keywords")] - public async Task> GetKeywords([FromQuery]string searchTerm) => + public async Task> GetKeywords([FromQuery]string searchTerm) => await TmdbApi.SearchKeyword(searchTerm); /// @@ -36,15 +36,15 @@ namespace Ombi.Controllers.External public async Task GetKeywords(int keywordId) { var keyword = await TmdbApi.GetKeyword(keywordId); - return keyword == null ? NotFound() : (IActionResult)Ok(keyword); + return keyword == null ? NotFound() : Ok(keyword); } /// /// Gets the genres for either Tv or Movies depending on media type /// - /// Either `tv` or `movie`. + /// Either `tv` or `movie`. [HttpGet("Genres/{media}")] public async Task> GetGenres(string media) => - await TmdbApi.GetGenres(media); + await TmdbApi.GetGenres(media, HttpContext.RequestAborted); } } From 4118f0de413bc770141da2ec5a238fa689c0c317 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 27 Jul 2021 09:38:20 +0100 Subject: [PATCH 22/27] Fixed the discord notification test issue --- src/Ombi.Helpers/CacheService.cs | 17 +++++++++++++---- src/Ombi.Helpers/MediaCacheService.cs | 15 ++++++++++----- src/Ombi/Startup.cs | 1 + 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Ombi.Helpers/CacheService.cs b/src/Ombi.Helpers/CacheService.cs index 3cf392880..c0acf0dea 100644 --- a/src/Ombi.Helpers/CacheService.cs +++ b/src/Ombi.Helpers/CacheService.cs @@ -2,13 +2,14 @@ using System.Threading; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { public class CacheService : ICacheService { - protected readonly IAppCache _memoryCache; - public CacheService(IAppCache memoryCache) + protected readonly IMemoryCache _memoryCache; + public CacheService(IMemoryCache memoryCache) { _memoryCache = memoryCache; } @@ -20,7 +21,11 @@ namespace Ombi.Helpers absoluteExpiration = DateTimeOffset.Now.AddHours(1); } - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } public void Remove(string key) @@ -31,7 +36,11 @@ namespace Ombi.Helpers public T GetOrAdd(string cacheKey, Func factory, DateTimeOffset absoluteExpiration) { // locks get and set internally - return _memoryCache.GetOrAdd(cacheKey, () => factory(), absoluteExpiration); + return _memoryCache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private static class TypeLock diff --git a/src/Ombi.Helpers/MediaCacheService.cs b/src/Ombi.Helpers/MediaCacheService.cs index 1cb969b60..c514c8e25 100644 --- a/src/Ombi.Helpers/MediaCacheService.cs +++ b/src/Ombi.Helpers/MediaCacheService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { @@ -14,7 +15,7 @@ namespace Ombi.Helpers { private const string CacheKey = "MediaCacheServiceKeys"; - public MediaCacheService(IAppCache memoryCache) : base(memoryCache) + public MediaCacheService(IMemoryCache memoryCache) : base(memoryCache) { } @@ -33,24 +34,28 @@ namespace Ombi.Helpers // Not in the cache, so add this Key into our MediaServiceCache await UpdateLocalCache(cacheKey); - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private async Task UpdateLocalCache(string cacheKey) { - var mediaServiceCache = await _memoryCache.GetAsync>(CacheKey); + var mediaServiceCache = _memoryCache.Get>(CacheKey); if (mediaServiceCache == null) { mediaServiceCache = new List(); } mediaServiceCache.Add(cacheKey); _memoryCache.Remove(CacheKey); - _memoryCache.Add(CacheKey, mediaServiceCache); + _memoryCache.Set(CacheKey, mediaServiceCache); } public async Task Purge() { - var keys = await _memoryCache.GetAsync>(CacheKey); + var keys = _memoryCache.Get>(CacheKey); if (keys == null) { return; diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 4b82c3124..95fd6447f 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -84,6 +84,7 @@ namespace Ombi //{ // setup.AddHealthCheckEndpoint("Ombi", "/health"); //}); + services.AddMemoryCache(); services.AddLazyCache(); services.AddHttpClient(); From 6327782c74e572bd15d93b9ae933a41c807267e5 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 27 Jul 2021 09:38:20 +0100 Subject: [PATCH 23/27] Fixed the discord notification test issue --- src/Ombi.Helpers/CacheService.cs | 17 +++++++++++++---- src/Ombi.Helpers/MediaCacheService.cs | 15 ++++++++++----- src/Ombi/Startup.cs | 1 + 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Ombi.Helpers/CacheService.cs b/src/Ombi.Helpers/CacheService.cs index 3cf392880..c0acf0dea 100644 --- a/src/Ombi.Helpers/CacheService.cs +++ b/src/Ombi.Helpers/CacheService.cs @@ -2,13 +2,14 @@ using System.Threading; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { public class CacheService : ICacheService { - protected readonly IAppCache _memoryCache; - public CacheService(IAppCache memoryCache) + protected readonly IMemoryCache _memoryCache; + public CacheService(IMemoryCache memoryCache) { _memoryCache = memoryCache; } @@ -20,7 +21,11 @@ namespace Ombi.Helpers absoluteExpiration = DateTimeOffset.Now.AddHours(1); } - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } public void Remove(string key) @@ -31,7 +36,11 @@ namespace Ombi.Helpers public T GetOrAdd(string cacheKey, Func factory, DateTimeOffset absoluteExpiration) { // locks get and set internally - return _memoryCache.GetOrAdd(cacheKey, () => factory(), absoluteExpiration); + return _memoryCache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private static class TypeLock diff --git a/src/Ombi.Helpers/MediaCacheService.cs b/src/Ombi.Helpers/MediaCacheService.cs index 1cb969b60..c514c8e25 100644 --- a/src/Ombi.Helpers/MediaCacheService.cs +++ b/src/Ombi.Helpers/MediaCacheService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using LazyCache; +using Microsoft.Extensions.Caching.Memory; namespace Ombi.Helpers { @@ -14,7 +15,7 @@ namespace Ombi.Helpers { private const string CacheKey = "MediaCacheServiceKeys"; - public MediaCacheService(IAppCache memoryCache) : base(memoryCache) + public MediaCacheService(IMemoryCache memoryCache) : base(memoryCache) { } @@ -33,24 +34,28 @@ namespace Ombi.Helpers // Not in the cache, so add this Key into our MediaServiceCache await UpdateLocalCache(cacheKey); - return await _memoryCache.GetOrAddAsync(cacheKey, () => factory(), absoluteExpiration); + return await _memoryCache.GetOrCreateAsync(cacheKey, entry => + { + entry.AbsoluteExpiration = absoluteExpiration; + return factory(); + }); } private async Task UpdateLocalCache(string cacheKey) { - var mediaServiceCache = await _memoryCache.GetAsync>(CacheKey); + var mediaServiceCache = _memoryCache.Get>(CacheKey); if (mediaServiceCache == null) { mediaServiceCache = new List(); } mediaServiceCache.Add(cacheKey); _memoryCache.Remove(CacheKey); - _memoryCache.Add(CacheKey, mediaServiceCache); + _memoryCache.Set(CacheKey, mediaServiceCache); } public async Task Purge() { - var keys = await _memoryCache.GetAsync>(CacheKey); + var keys = _memoryCache.Get>(CacheKey); if (keys == null) { return; diff --git a/src/Ombi/Startup.cs b/src/Ombi/Startup.cs index 4b82c3124..95fd6447f 100644 --- a/src/Ombi/Startup.cs +++ b/src/Ombi/Startup.cs @@ -84,6 +84,7 @@ namespace Ombi //{ // setup.AddHealthCheckEndpoint("Ombi", "/health"); //}); + services.AddMemoryCache(); services.AddLazyCache(); services.AddHttpClient(); From 6ee9606f7cfaccb832e75543db121c557be93f54 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 28 Jul 2021 16:48:45 +0100 Subject: [PATCH 24/27] MORE! --- .../Engine/Interfaces/IMovieEngineV2.cs | 1 + .../Engine/V2/MovieSearchEngineV2.cs | 17 +++- src/Ombi.TheMovieDbApi/IMovieDbApi.cs | 2 + .../Models/DiscoverModel.cs | 18 +++++ src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs | 21 +++++ .../Models/WatchProvidersResults.cs | 16 ++++ src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 39 ++++++++++ .../ClientApp/src/app/interfaces/IMovieDb.ts | 15 ++++ .../src/app/my-nav/my-nav.component.html | 2 +- .../src/app/my-nav/my-nav.component.scss | 4 + .../applications/themoviedb.service.ts | 6 +- .../src/app/services/searchV2.service.ts | 6 +- .../advanced-search-dialog.component.html | 10 ++- .../advanced-search-dialog.component.ts | 21 ++++- .../genre-select/genre-select.component.html | 2 +- .../genre-select/genre-select.component.ts | 18 ++--- .../keyword-search.component.html | 2 +- .../keyword-search.component.ts | 8 +- .../watch-providers-select.component.html | 24 ++++++ .../watch-providers-select.component.ts | 77 +++++++++++++++++++ .../ClientApp/src/app/shared/shared.module.ts | 3 + .../V1/External/TheMovieDbController.cs | 18 +++++ src/Ombi/Controllers/V2/SearchController.cs | 13 ++++ 23 files changed, 314 insertions(+), 29 deletions(-) create mode 100644 src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs create mode 100644 src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs create mode 100644 src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs create mode 100644 src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html create mode 100644 src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts diff --git a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs index 5a3624c5e..2d86443d1 100644 --- a/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs +++ b/src/Ombi.Core/Engine/Interfaces/IMovieEngineV2.cs @@ -29,5 +29,6 @@ namespace Ombi.Core.Engine.Interfaces Task> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); Task> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); Task> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken); + Task> AdvancedSearch(DiscoverModel model, int currentlyLoaded, int toLoad, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index 25c0fc2a2..acc390aec 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -137,7 +137,22 @@ namespace Ombi.Core.Engine.V2 foreach (var pagesToLoad in pages) { var apiResult = await Cache.GetOrAddAsync(nameof(PopularMovies) + pagesToLoad.Page + langCode, - () => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12)); + () => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12)); + results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + } + return await TransformMovieResultsToResponse(results); + } + + public async Task> AdvancedSearch(DiscoverModel model, int currentlyLoaded, int toLoad, CancellationToken cancellationToken) + { + var langCode = await DefaultLanguageCode(null); + + var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); + + var results = new List(); + foreach (var pagesToLoad in pages) + { + var apiResult = await MovieApi.AdvancedSearch(model, cancellationToken); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); } return await TransformMovieResultsToResponse(results); diff --git a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs index 202392c5f..6d0859cef 100644 --- a/src/Ombi.TheMovieDbApi/IMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/IMovieDbApi.cs @@ -40,5 +40,7 @@ namespace Ombi.Api.TheMovieDb Task GetMovieWatchProviders(int theMoviedbId, CancellationToken token); Task GetTvWatchProviders(int theMoviedbId, CancellationToken token); Task> GetGenres(string media, CancellationToken cancellationToken); + Task> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken); + Task> AdvancedSearch(DiscoverModel model, CancellationToken cancellationToken); } } diff --git a/src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs b/src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs new file mode 100644 index 000000000..53f3bfb21 --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/DiscoverModel.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Api.TheMovieDb.Models +{ + public class DiscoverModel + { + public string Type { get; set; } + public int? ReleaseYear { get; set; } + public List GenreIds { get; set; } = new List(); + public List KeywordIds { get; set; } = new List(); + public List WatchProviders { get; set; } = new List(); + public List Companies { get; set; } = new List(); + } +} diff --git a/src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs b/src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs new file mode 100644 index 000000000..3edb7320b --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/DiscoverTv.cs @@ -0,0 +1,21 @@ +namespace Ombi.Api.TheMovieDb.Models { + + public class DiscoverTv + { + public int vote_count { get; set; } + public int id { get; set; } + public bool video { get; set; } + public float vote_average { get; set; } + public string title { get; set; } + public float popularity { get; set; } + public string poster_path { get; set; } + public string original_language { get; set; } + public string original_title { get; set; } + public int[] genre_ids { get; set; } + public string backdrop_path { get; set; } + public bool adult { get; set; } + public string overview { get; set; } + public string release_date { get; set; } + } + +} \ No newline at end of file diff --git a/src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs b/src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs new file mode 100644 index 000000000..44462018b --- /dev/null +++ b/src/Ombi.TheMovieDbApi/Models/WatchProvidersResults.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ombi.Api.TheMovieDb.Models +{ + public class WatchProvidersResults + { + public int provider_id { get; set; } + public string logo_path { get; set; } + public string provider_name { get; set; } + public string origin_country { get; set; } + } +} diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index a8fdc3269..7e89b48fd 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -68,6 +68,34 @@ namespace Ombi.Api.TheMovieDb return await Api.Request>(request); } + + + public async Task> AdvancedSearch(DiscoverModel model, CancellationToken cancellationToken) + { + var request = new Request($"discover/{model.Type}", BaseUri, HttpMethod.Get); + request.FullUri = request.FullUri.AddQueryParameter("api_key", ApiToken); + if(model.ReleaseYear.HasValue && model.ReleaseYear.Value > 1900) + { + request.FullUri = request.FullUri.AddQueryParameter("year", model.ReleaseYear.Value.ToString()); + } + if (model.KeywordIds.Any()) + { + request.FullUri = request.FullUri.AddQueryParameter("with_keyword", string.Join(',', model.KeywordIds)); + } + if (model.GenreIds.Any()) + { + request.FullUri = request.FullUri.AddQueryParameter("with_genres", string.Join(',', model.GenreIds)); + } + if (model.WatchProviders.Any()) + { + request.FullUri = request.FullUri.AddQueryParameter("with_watch_providers", string.Join(',', model.WatchProviders)); + } + request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); + + var result = await Api.Request>(request, cancellationToken); + return Mapper.Map>(result.results); + } + public async Task GetCollection(string langCode, int collectionId, CancellationToken cancellationToken) { // https://developers.themoviedb.org/3/discover/movie-discover @@ -368,6 +396,17 @@ namespace Ombi.Api.TheMovieDb return result.results ?? new List(); } + public async Task> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken) + { + var request = new Request($"/watch/providers/{media}", BaseUri, HttpMethod.Get); + request.AddQueryString("api_key", ApiToken); + request.AddQueryString("query", searchTerm); + AddRetry(request); + + var result = await Api.Request>(request, cancellationToken); + return result.results ?? new List(); + } + public async Task GetKeyword(int keywordId) { var request = new Request($"keyword/{keywordId}", BaseUri, HttpMethod.Get); diff --git a/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts b/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts index 63443ae4c..f82225434 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IMovieDb.ts @@ -2,3 +2,18 @@ id: number; name: string; } + +export interface IWatchProvidersResults { + provider_id: number; + logo_path: string; + provider_name: string; +} + +export interface IDiscoverModel { + type: string; + releaseYear?: number|undefined; + genreIds?: number[]; + keywordIds?: number[]; + watchProviders?: number[]; + companies?: number[]; +} diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html index 84ae81fe8..940002d9f 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.html @@ -78,7 +78,7 @@ {{ 'NavigationBar.Filter.Music' | translate}} - + diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss index 2b018ee8e..4f17a636d 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss @@ -230,4 +230,8 @@ ::ng-deep .mat-sidenav-fixed .mat-list-base .mat-list-item .mat-list-item-content, .mat-list-base .mat-list-option .mat-list-item-content{ padding:0; margin: 0 4em 0 0.5em; +} + +.advanced-search { + margin-left: 10px; } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts index 0af37c508..a49fb4146 100644 --- a/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts +++ b/src/Ombi/ClientApp/src/app/services/applications/themoviedb.service.ts @@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core"; import { empty, Observable, throwError } from "rxjs"; import { catchError } from "rxjs/operators"; -import { IMovieDbKeyword } from "../../interfaces"; +import { IMovieDbKeyword, IWatchProvidersResults } from "../../interfaces"; import { ServiceHelpers } from "../service.helpers"; @Injectable({ @@ -28,4 +28,8 @@ export class TheMovieDbService extends ServiceHelpers { public getGenres(media: string): Observable { return this.http.get(`${this.url}/Genres/${media}`, { headers: this.headers }) } + + public getWatchProviders(media: string): Observable { + return this.http.get(`${this.url}/WatchProviders/${media}`, {headers: this.headers}); + } } diff --git a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts index 99e5afb39..348db936b 100644 --- a/src/Ombi/ClientApp/src/app/services/searchV2.service.ts +++ b/src/Ombi/ClientApp/src/app/services/searchV2.service.ts @@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { Observable } from "rxjs"; -import { IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces"; +import { IDiscoverModel, IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces"; import { ServiceHelpers } from "./service.helpers"; import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2"; @@ -51,6 +51,10 @@ export class SearchV2Service extends ServiceHelpers { return this.http.get(`${this.url}/Movie/Popular/${currentlyLoaded}/${toLoad}`).toPromise(); } + public advancedSearch(model: IDiscoverModel, currentlyLoaded: number, toLoad: number): Promise { + return this.http.post(`${this.url}/advancedSearch/Movie/${currentlyLoaded}/${toLoad}`, model).toPromise(); + } + public upcomingMovies(): Observable { return this.http.get(`${this.url}/Movie/upcoming`); } diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html index 02f9ac35b..67bd1c2d5 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html @@ -1,4 +1,4 @@ -
+

Advanced Search

@@ -34,6 +34,10 @@
+ +
+ +
@@ -49,10 +53,10 @@
diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts index e59f7fab7..a06d43b43 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts @@ -1,6 +1,8 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, FormGroup } from "@angular/forms"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { IDiscoverModel } from "../../interfaces"; +import { SearchV2Service } from "../../services"; @Component({ @@ -12,7 +14,8 @@ export class AdvancedSearchDialogComponent implements OnInit { constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any, - private fb: FormBuilder + private fb: FormBuilder, + private searchService: SearchV2Service, ) {} public form: FormGroup; @@ -21,16 +24,28 @@ export class AdvancedSearchDialogComponent implements OnInit { public async ngOnInit() { this.form = this.fb.group({ - keywords: [[]], - genres: [[]], + keywordIds: [[]], + genreIds: [[]], releaseYear: [], type: ['movie'], + watchProviders: [[]], }) this.form.controls.type.valueChanges.subscribe(val => { this.form.controls.genres.setValue([]); + this.form.controls.watchProviders.setValue([]); }); } + + public async onSubmit() { + const watchProviderIds = this.form.controls.watchProviders.value.map(x => x.provider_id); + const genres = this.form.controls.genreIds.value.map(x => x.id); + await this.searchService.advancedSearch({ + watchProviders: watchProviderIds, + genreIds: genres, + type: this.form.controls.type.value, + }, 0, 30); + } } diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html index 523974a90..9907b002c 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html @@ -3,7 +3,7 @@ Genres {{word.name}} diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts index c80eb4ae8..06c92e4cf 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.ts @@ -11,7 +11,7 @@ import { TheMovieDbService } from "../../../services"; selector: "genre-select", templateUrl: "genre-select.component.html" }) -export class GenreSelectComponent implements OnInit { +export class GenreSelectComponent { constructor( private tmdbService: TheMovieDbService ) {} @@ -39,32 +39,24 @@ export class GenreSelectComponent implements OnInit { @ViewChild('keywordInput') input: ElementRef; - async ngOnInit() { - - // this.genres = await this.tmdbService.getGenres(this.mediaType).toPromise(); - - - - } - remove(word: IMovieDbKeyword): void { - const exisiting = this.form.controls.genres.value; + const exisiting = this.form.controls.genreIds.value; const index = exisiting.indexOf(word); if (index >= 0) { exisiting.splice(index, 1); - this.form.controls.genres.setValue(exisiting); + this.form.controls.genreIds.setValue(exisiting); } } selected(event: MatAutocompleteSelectedEvent): void { const val = event.option.value; - const exisiting = this.form.controls.genres.value; + const exisiting = this.form.controls.genreIds.value; if(exisiting.indexOf(val) < 0) { exisiting.push(val); } - this.form.controls.genres.setValue(exisiting); + this.form.controls.genreIds.setValue(exisiting); this.input.nativeElement.value = ''; this.control.setValue(null); } diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html index 1ff1874e9..631c42596 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.html @@ -15,7 +15,7 @@ Keywords {{word.name}} diff --git a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts index 1ea8bcef1..67427c6e4 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/components/keyword-search/keyword-search.component.ts @@ -40,23 +40,23 @@ export class KeywordSearchComponent implements OnInit { }; remove(word: IMovieDbKeyword): void { - const exisiting = this.form.controls.keywords.value; + const exisiting = this.form.controls.keywordIds.value; const index = exisiting.indexOf(word); if (index >= 0) { exisiting.splice(index, 1); - this.form.controls.keywords.setValue(exisiting); + this.form.controls.keywordIds.setValue(exisiting); } } selected(event: MatAutocompleteSelectedEvent): void { const val = event.option.value; - const exisiting = this.form.controls.keywords.value; + const exisiting = this.form.controls.keywordIds.value; if (exisiting.indexOf(val) < 0) { exisiting.push(val); } - this.form.controls.keywords.setValue(exisiting); + this.form.controls.keywordIds.setValue(exisiting); this.input.nativeElement.value = ''; this.control.setValue(null); } diff --git a/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html new file mode 100644 index 000000000..3c22c7d33 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.html @@ -0,0 +1,24 @@ + + Watch Providers + + + {{word.provider_name}} + cancel + + + + + + {{word.provider_name}} + + + + diff --git a/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts new file mode 100644 index 000000000..2c89756e4 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/components/watch-providers-select/watch-providers-select.component.ts @@ -0,0 +1,77 @@ +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { IMovieDbKeyword, IWatchProvidersResults } from "../../../interfaces"; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from "rxjs/operators"; + +import { MatAutocompleteSelectedEvent } from "@angular/material/autocomplete"; +import { Observable } from "rxjs"; +import { TheMovieDbService } from "../../../services"; + +@Component({ + selector: "watch-providers-select", + templateUrl: "watch-providers-select.component.html" +}) +export class WatchProvidersSelectComponent { + constructor( + private tmdbService: TheMovieDbService + ) {} + + private _mediaType: string; + @Input() set mediaType(type: string) { + this._mediaType = type; + this.tmdbService.getWatchProviders(this._mediaType).subscribe((res) => { + this.watchProviders = res; + this.filteredList = this.control.valueChanges.pipe( + startWith(''), + map((genre: string | null) => genre ? this._filter(genre) : this.watchProviders.slice())); + }); + + } + get mediaType(): string { + return this._mediaType; + } + @Input() public form: FormGroup; + + public watchProviders: IWatchProvidersResults[] = []; + public control = new FormControl(); + public filteredTags: IWatchProvidersResults[]; + public filteredList: Observable; + + @ViewChild('keywordInput') input: ElementRef; + + + remove(word: IWatchProvidersResults): void { + const exisiting = this.form.controls.watchProviders.value; + const index = exisiting.indexOf(word); + + if (index >= 0) { + exisiting.splice(index, 1); + this.form.controls.watchProviders.setValue(exisiting); + } + } + + + selected(event: MatAutocompleteSelectedEvent): void { + const val = event.option.value; + const exisiting = this.form.controls.watchProviders.value; + if (exisiting.indexOf(val) < 0) { + exisiting.push(val); + } + this.form.controls.watchProviders.setValue(exisiting); + this.input.nativeElement.value = ''; + this.control.setValue(null); + } + + private _filter(value: string|IWatchProvidersResults): IWatchProvidersResults[] { + if (typeof value === 'object') { + const filterValue = value.provider_name.toLowerCase(); + return this.watchProviders.filter(g => g.provider_name.toLowerCase().includes(filterValue)); + } else if (typeof value === 'string') { + const filterValue = value.toLowerCase(); + return this.watchProviders.filter(g => g.provider_name.toLowerCase().includes(filterValue)); + } + + return this.watchProviders; + } + +} diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index a76914470..50dfc4538 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -41,6 +41,7 @@ import { SidebarModule } from "primeng/sidebar"; import { TheMovieDbService } from "../services"; import { TranslateModule } from "@ngx-translate/core"; import { TruncateModule } from "@yellowspot/ng-truncate"; +import { WatchProvidersSelectComponent } from "./components/watch-providers-select/watch-providers-select.component"; @NgModule({ declarations: [ @@ -51,6 +52,7 @@ import { TruncateModule } from "@yellowspot/ng-truncate"; AdvancedSearchDialogComponent, KeywordSearchComponent, GenreSelectComponent, + WatchProvidersSelectComponent, ], imports: [ SidebarModule, @@ -99,6 +101,7 @@ import { TruncateModule } from "@yellowspot/ng-truncate"; AdvancedSearchDialogComponent, GenreSelectComponent, KeywordSearchComponent, + WatchProvidersSelectComponent, DetailsGroupComponent, TruncateModule, InputSwitchModule, diff --git a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs index 3dacdb406..f5fdb996f 100644 --- a/src/Ombi/Controllers/V1/External/TheMovieDbController.cs +++ b/src/Ombi/Controllers/V1/External/TheMovieDbController.cs @@ -46,5 +46,23 @@ namespace Ombi.Controllers.External [HttpGet("Genres/{media}")] public async Task> GetGenres(string media) => await TmdbApi.GetGenres(media, HttpContext.RequestAborted); + + /// + /// Searches for the watch providers matching the specified term. + /// + /// The search term. + [HttpGet("WatchProviders/movie")] + public async Task> GetWatchProvidersMovies([FromQuery] string searchTerm) => + await TmdbApi.SearchWatchProviders("movie", searchTerm, HttpContext.RequestAborted); + + + /// + /// Searches for the watch providers matching the specified term. + /// + /// The search term. + [HttpGet("WatchProviders/tv")] + public async Task> GetWatchProvidersTv([FromQuery] string searchTerm) => + await TmdbApi.SearchWatchProviders("tv", searchTerm, HttpContext.RequestAborted); + } } diff --git a/src/Ombi/Controllers/V2/SearchController.cs b/src/Ombi/Controllers/V2/SearchController.cs index 4bf8f15da..0b58244b3 100644 --- a/src/Ombi/Controllers/V2/SearchController.cs +++ b/src/Ombi/Controllers/V2/SearchController.cs @@ -183,6 +183,19 @@ namespace Ombi.Controllers.V2 DateTimeOffset.Now.AddHours(12)); } + /// + /// Returns Advanced Searched Media using paging + /// + /// We use TheMovieDb as the Movie Provider + /// + [HttpPost("advancedSearch/movie/{currentPosition}/{amountToLoad}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public Task> AdvancedSearchMovie([FromBody]DiscoverModel model, int currentPosition, int amountToLoad) + { + return _movieEngineV2.AdvancedSearch(model, currentPosition, amountToLoad, Request.HttpContext.RequestAborted); + } + /// /// Returns Seasonal Movies /// From 924a562c57f4126345b1a51dfd625136af67a2f6 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 28 Jul 2021 21:40:49 +0100 Subject: [PATCH 25/27] Added the basic advanced search --- .../Engine/V2/MovieSearchEngineV2.cs | 12 +-- src/Ombi.TheMovieDbApi/TheMovieDbApi.cs | 2 +- .../src/app/discover/components/index.ts | 17 +++-- .../search-results.component.ts | 76 +++++++++++++++++-- .../src/app/discover/discover.module.ts | 14 ++-- .../src/app/my-nav/my-nav.component.scss | 10 +++ .../src/app/my-nav/my-nav.component.ts | 14 +++- .../advanced-search-dialog-data.service.ts | 27 +++++++ .../advanced-search-dialog.component.html | 30 +++++--- .../advanced-search-dialog.component.ts | 32 +++++--- .../genre-select/genre-select.component.html | 2 +- .../keyword-search.component.html | 15 +--- .../watch-providers-select.component.html | 4 +- src/Ombi/wwwroot/translations/en.json | 2 + 14 files changed, 185 insertions(+), 72 deletions(-) create mode 100644 src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts diff --git a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs index acc390aec..e865c6465 100644 --- a/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs +++ b/src/Ombi.Core/Engine/V2/MovieSearchEngineV2.cs @@ -147,15 +147,15 @@ namespace Ombi.Core.Engine.V2 { var langCode = await DefaultLanguageCode(null); - var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); + //var pages = PaginationHelper.GetNextPages(currentlyLoaded, toLoad, _theMovieDbMaxPageItems); var results = new List(); - foreach (var pagesToLoad in pages) - { + //foreach (var pagesToLoad in pages) + //{ var apiResult = await MovieApi.AdvancedSearch(model, cancellationToken); - results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); - } - return await TransformMovieResultsToResponse(results); + //results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); + //} + return await TransformMovieResultsToResponse(apiResult); } /// diff --git a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs index 7e89b48fd..aa46b0c8e 100644 --- a/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs +++ b/src/Ombi.TheMovieDbApi/TheMovieDbApi.cs @@ -90,7 +90,7 @@ namespace Ombi.Api.TheMovieDb { request.FullUri = request.FullUri.AddQueryParameter("with_watch_providers", string.Join(',', model.WatchProviders)); } - request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); + //request.FullUri = request.FullUri.AddQueryParameter("sort_by", "popularity.desc"); var result = await Api.Request>(request, cancellationToken); return Mapper.Map>(result.results); diff --git a/src/Ombi/ClientApp/src/app/discover/components/index.ts b/src/Ombi/ClientApp/src/app/discover/components/index.ts index 2d399cd76..835ef702c 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/index.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/index.ts @@ -1,15 +1,15 @@ -import { DiscoverComponent } from "./discover/discover.component"; -import { DiscoverCollectionsComponent } from "./collections/discover-collections.component"; +import { RadarrService, RequestService, SearchService, SonarrService } from "../../services"; + +import { AuthGuard } from "../../auth/auth.guard"; +import { CarouselListComponent } from "./carousel-list/carousel-list.component"; import { DiscoverActorComponent } from "./actor/discover-actor.component"; import { DiscoverCardComponent } from "./card/discover-card.component"; -import { Routes } from "@angular/router"; -import { AuthGuard } from "../../auth/auth.guard"; -import { SearchService, RequestService, SonarrService, RadarrService } from "../../services"; -import { MatDialog } from "@angular/material/dialog"; +import { DiscoverCollectionsComponent } from "./collections/discover-collections.component"; +import { DiscoverComponent } from "./discover/discover.component"; import { DiscoverSearchResultsComponent } from "./search-results/search-results.component"; -import { CarouselListComponent } from "./carousel-list/carousel-list.component"; +import { MatDialog } from "@angular/material/dialog"; import { RequestServiceV2 } from "../../services/requestV2.service"; - +import { Routes } from "@angular/router"; export const components: any[] = [ DiscoverComponent, @@ -34,4 +34,5 @@ export const routes: Routes = [ { path: "collection/:collectionId", component: DiscoverCollectionsComponent, canActivate: [AuthGuard] }, { path: "actor/:actorId", component: DiscoverActorComponent, canActivate: [AuthGuard] }, { path: ":searchTerm", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] }, + { path: "advanced/search", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] }, ]; \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts index d9e046da9..02415a970 100644 --- a/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts +++ b/src/Ombi/ClientApp/src/app/discover/components/search-results/search-results.component.ts @@ -1,14 +1,15 @@ +import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { SearchV2Service } from "../../../services"; -import { IDiscoverCardResult } from "../../interfaces"; -import { IMultiSearchResult, RequestType } from "../../../interfaces"; +import { IMultiSearchResult, ISearchMovieResult, RequestType } from "../../../interfaces"; + +import { AdvancedSearchDialogDataService } from "../../../shared/advanced-search-dialog/advanced-search-dialog-data.service"; +import { AuthService } from "../../../auth/auth.service"; import { FilterService } from "../../services/filter-service"; +import { IDiscoverCardResult } from "../../interfaces"; import { SearchFilter } from "../../../my-nav/SearchFilter"; +import { SearchV2Service } from "../../../services"; import { StorageService } from "../../../shared/storage/storage-service"; - import { isEqual } from "lodash"; -import { AuthService } from "../../../auth/auth.service"; @Component({ templateUrl: "./search-results.component.html", @@ -25,22 +26,41 @@ export class DiscoverSearchResultsComponent implements OnInit { public filter: SearchFilter; + private isAdvancedSearch: boolean; + constructor(private searchService: SearchV2Service, private route: ActivatedRoute, private filterService: FilterService, + private router: Router, + private advancedDataService: AdvancedSearchDialogDataService, private store: StorageService, private authService: AuthService) { this.route.params.subscribe((params: any) => { + this.isAdvancedSearch = this.router.url === '/discover/advanced/search'; + if (this.isAdvancedSearch) { + this.loadAdvancedData(); + return; + } this.searchTerm = params.searchTerm; this.clear(); this.init(); }); + + this.advancedDataService.onDataChange.subscribe(() => { + this.clear(); + this.loadAdvancedData(); + }); + } public async ngOnInit() { - this.loadingFlag = true; this.isAdmin = this.authService.isAdmin(); + if (this.advancedDataService) { + return; + } + this.loadingFlag = true; + this.filterService.onFilterChange.subscribe(async x => { if (!isEqual(this.filter, x)) { this.filter = { ...x }; @@ -115,6 +135,48 @@ export class DiscoverSearchResultsComponent implements OnInit { this.discoverResults = []; } + private loadAdvancedData() { + const advancedData = this.advancedDataService.getData(); + this.mapAdvancedData(advancedData); + return; + } + + public mapAdvancedData(advancedData: ISearchMovieResult[]) { + this.finishLoading(); + const type = this.advancedDataService.getType(); + advancedData.forEach(m => { + + let mediaType = type; + + let poster = `https://image.tmdb.org/t/p/w300/${m.posterPath}`; + if (!m.posterPath) { + if (mediaType === RequestType.movie) { + poster = "images/default_movie_poster.png" + } + if (mediaType === RequestType.tvShow) { + poster = "images/default_tv_poster.png" + } + } + + this.discoverResults.push({ + posterPath: poster, + requested: false, + title: m.title, + type: mediaType, + id: m.id, + url: "", + rating: 0, + overview: m.overview, + approved: false, + imdbid: "", + denied: false, + background: "", + available: false, + tvMovieDb: false + }); + }); + } + private async search() { this.clear(); this.results = await this.searchService diff --git a/src/Ombi/ClientApp/src/app/discover/discover.module.ts b/src/Ombi/ClientApp/src/app/discover/discover.module.ts index 2514f0636..414c881e6 100644 --- a/src/Ombi/ClientApp/src/app/discover/discover.module.ts +++ b/src/Ombi/ClientApp/src/app/discover/discover.module.ts @@ -1,16 +1,14 @@ -import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; +import * as fromComponents from './components'; + +import { CarouselModule } from 'primeng/carousel'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; - -import { SharedModule } from "../shared/shared.module"; +import { NgModule } from "@angular/core"; import { PipeModule } from "../pipes/pipe.module"; -import { CarouselModule } from 'primeng/carousel'; +import { RouterModule } from "@angular/router"; +import { SharedModule } from "../shared/shared.module"; import { SkeletonModule } from 'primeng/skeleton'; -import * as fromComponents from './components'; - - @NgModule({ imports: [ RouterModule.forChild(fromComponents.routes), diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss index 4f17a636d..40406fe05 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.scss @@ -234,4 +234,14 @@ .advanced-search { margin-left: 10px; +} + +::ng-deep .dialog-responsive { + width: 40%; +} + +@media only screen and (max-width: 760px) { + ::ng-deep .dialog-responsive { + width: 100%; + } } \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts index 393d12fd4..10c2b5998 100644 --- a/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts +++ b/src/Ombi/ClientApp/src/app/my-nav/my-nav.component.ts @@ -11,6 +11,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { Md5 } from 'ts-md5/dist/md5'; import { Observable } from 'rxjs'; +import { Router } from '@angular/router'; import { SearchFilter } from './SearchFilter'; import { StorageService } from '../shared/storage/storage-service'; import { map } from 'rxjs/operators'; @@ -57,7 +58,8 @@ export class MyNavComponent implements OnInit { private store: StorageService, private filterService: FilterService, private dialogService: MatDialog, - private readonly settingState: SettingsStateService) { + private readonly settingState: SettingsStateService, + private router: Router) { } public async ngOnInit() { @@ -125,7 +127,15 @@ export class MyNavComponent implements OnInit { } public openAdvancedSearch() { - this.dialogService.open(AdvancedSearchDialogComponent, null); + const dialogRef = this.dialogService.open(AdvancedSearchDialogComponent, { panelClass: 'dialog-responsive' }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.router.navigate([`discover/advanced/search`]); + } + + return; + }); } public getUserImage(): string { diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts new file mode 100644 index 000000000..c6312915a --- /dev/null +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog-data.service.ts @@ -0,0 +1,27 @@ +import { EventEmitter, Injectable, Output } from "@angular/core"; + +import { RequestType } from "../../interfaces"; + +@Injectable({ + providedIn: "root" +}) +export class AdvancedSearchDialogDataService { + + @Output() public onDataChange = new EventEmitter(); + private _data: any; + private _type: RequestType; + + setData(data: any, type: RequestType) { + this._data = data; + this._type = type; + this.onDataChange.emit(this._data); + } + + getData(): any { + return this._data; + } + + getType(): RequestType { + return this._type; + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html index 67bd1c2d5..4c8e58821 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.html @@ -4,8 +4,8 @@
@@ -13,7 +13,10 @@
-
+
+ Please choose what type of media you are searching for: +
+
Movies @@ -21,23 +24,26 @@
-
- - Year +
+ + Year of Release
-
- -
-
+ +
- -
+ +
+
+ + Please note that Keyword Searching is very hit and miss due to the inconsistent data in TheMovieDb + +
diff --git a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts index a06d43b43..614d2d103 100644 --- a/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts +++ b/src/Ombi/ClientApp/src/app/shared/advanced-search-dialog/advanced-search-dialog.component.ts @@ -1,9 +1,9 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, FormGroup } from "@angular/forms"; import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; -import { IDiscoverModel } from "../../interfaces"; +import { RequestType } from "../../interfaces"; import { SearchV2Service } from "../../services"; - +import { AdvancedSearchDialogDataService } from "./advanced-search-dialog-data.service"; @Component({ selector: "advanced-search-dialog", @@ -12,15 +12,14 @@ import { SearchV2Service } from "../../services"; }) export class AdvancedSearchDialogComponent implements OnInit { constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, private fb: FormBuilder, private searchService: SearchV2Service, + private advancedSearchDialogService: AdvancedSearchDialogDataService ) {} public form: FormGroup; - public async ngOnInit() { this.form = this.fb.group({ @@ -35,17 +34,28 @@ export class AdvancedSearchDialogComponent implements OnInit { this.form.controls.genres.setValue([]); this.form.controls.watchProviders.setValue([]); }); - - } public async onSubmit() { - const watchProviderIds = this.form.controls.watchProviders.value.map(x => x.provider_id); - const genres = this.form.controls.genreIds.value.map(x => x.id); - await this.searchService.advancedSearch({ + const formData = this.form.value; + const watchProviderIds = formData.watchProviders.map(x => x.provider_id); + const genres = formData.genreIds.map(x => x.id); + const keywords = formData.keywordIds.map(x => x.id); + const data = await this.searchService.advancedSearch({ watchProviders: watchProviderIds, genreIds: genres, - type: this.form.controls.type.value, + keywordIds: keywords, + releaseYear: formData.releaseYear, + type: formData.type, }, 0, 30); + + this.advancedSearchDialogService.setData(data, formData.type === 'movie' ? RequestType.movie : RequestType.tvShow); + + this.dialogRef.close(true); + } + + public onClose() { + this.dialogRef.close(false); } + } diff --git a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html index 9907b002c..93ed4618e 100644 --- a/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html +++ b/src/Ombi/ClientApp/src/app/shared/components/genre-select/genre-select.component.html @@ -1,5 +1,5 @@ - + Genres - - - - {{option.name}} - - - --> - - - + Keywords - Watch Providers + + Watch Providers Date: Thu, 29 Jul 2021 11:18:14 +0100 Subject: [PATCH 26/27] Small fix around the detection of available Jellyfin movies --- .../Rule/Rules/Search/JellyfinAvailabilityRule.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs index 0447458d9..c51645112 100644 --- a/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs +++ b/src/Ombi.Core/Rule/Rules/Search/JellyfinAvailabilityRule.cs @@ -28,6 +28,7 @@ namespace Ombi.Core.Rule.Rules.Search var useImdb = false; var useTheMovieDb = false; var useTvDb = false; + var useId = false; if (obj.ImdbId.HasValue()) { @@ -39,6 +40,14 @@ namespace Ombi.Core.Rule.Rules.Search } if (item == null) { + if (obj.Id > 0) + { + item = await JellyfinContentRepository.GetByTheMovieDbId(obj.Id.ToString()); + if (item != null) + { + useId = true; + } + } if (obj.TheMovieDbId.HasValue()) { item = await JellyfinContentRepository.GetByTheMovieDbId(obj.TheMovieDbId); @@ -63,6 +72,11 @@ namespace Ombi.Core.Rule.Rules.Search if (item != null) { + if (useId) + { + obj.TheMovieDbId = obj.Id.ToString(); + useTheMovieDb = true; + } obj.Available = true; var s = await JellyfinSettings.GetSettingsAsync(); if (s.Enable) From 45b692c8d216d4e3367c6a9b80843cf3fd5165d5 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 17 Aug 2021 08:27:10 +0100 Subject: [PATCH 27/27] Fixed #4290 --- src/Ombi.Core/Engine/BaseMediaEngine.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Ombi.Core/Engine/BaseMediaEngine.cs b/src/Ombi.Core/Engine/BaseMediaEngine.cs index 71a756543..91f8a58b0 100644 --- a/src/Ombi.Core/Engine/BaseMediaEngine.cs +++ b/src/Ombi.Core/Engine/BaseMediaEngine.cs @@ -173,6 +173,10 @@ namespace Ombi.Core.Engine return currentCode; } var user = await GetUser(); + if (user == null) + { + return "en"; + } if (string.IsNullOrEmpty(user.Language)) {