Merge pull request #4301 from Ombi-app/develop

release
pull/4382/head v4.0.1468
Jamie 3 years ago committed by GitHub
commit 3e1a73214c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,9 +1,12 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Internal;
using Newtonsoft.Json; using Newtonsoft.Json;
using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models;
using Ombi.Api.Emby.Models.Media;
using Ombi.Api.Emby.Models.Media.Tv; using Ombi.Api.Emby.Models.Media.Tv;
using Ombi.Api.Emby.Models.Movie; using Ombi.Api.Emby.Models.Movie;
using Ombi.Helpers; using Ombi.Helpers;
@ -112,19 +115,29 @@ namespace Ombi.Api.Emby
return await Api.Request<EmbyItemContainer<EmbyMovie>>(request); return await Api.Request<EmbyItemContainer<EmbyMovie>>(request);
} }
public async Task<EmbyItemContainer<EmbyMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) public async Task<List<LibraryVirtualFolders>> GetLibraries(string apiKey, string baseUrl)
{ {
return await GetAll<EmbyMovie>("Movie", apiKey, userId, baseUri, true, startIndex, count); var request = new Request("library/VirtualFolders", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
var response = await Api.Request<List<LibraryVirtualFolders>>(request);
return response;
}
public async Task<EmbyItemContainer<EmbyMovie>> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri)
{
return await GetAll<EmbyMovie>("Movie", apiKey, userId, baseUri, true, startIndex, count, parentIdFilder);
} }
public async Task<EmbyItemContainer<EmbyEpisodes>> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) public async Task<EmbyItemContainer<EmbyEpisodes>> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri)
{ {
return await GetAll<EmbyEpisodes>("Episode", apiKey, userId, baseUri, false, startIndex, count); return await GetAll<EmbyEpisodes>("Episode", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder);
} }
public async Task<EmbyItemContainer<EmbySeries>> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) public async Task<EmbyItemContainer<EmbySeries>> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri)
{ {
return await GetAll<EmbySeries>("Series", apiKey, userId, baseUri, false, startIndex, count); return await GetAll<EmbySeries>("Series", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder);
} }
public async Task<SeriesInformation> GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) public async Task<SeriesInformation> GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl)
@ -167,7 +180,7 @@ namespace Ombi.Api.Emby
var obj = await Api.Request<EmbyItemContainer<T>>(request); var obj = await Api.Request<EmbyItemContainer<T>>(request);
return obj; return obj;
} }
private async Task<EmbyItemContainer<T>> GetAll<T>(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) private async Task<EmbyItemContainer<T>> GetAll<T>(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); var request = new Request($"emby/users/{userId}/items", baseUri, HttpMethod.Get);
@ -176,6 +189,10 @@ namespace Ombi.Api.Emby
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds"); request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
request.AddQueryString("startIndex", startIndex.ToString()); request.AddQueryString("startIndex", startIndex.ToString());
request.AddQueryString("limit", count.ToString()); request.AddQueryString("limit", count.ToString());
if (!string.IsNullOrEmpty(parentIdFilder))
{
request.AddQueryString("ParentId", parentIdFilder);
}
request.AddQueryString("IsVirtualItem", "False"); request.AddQueryString("IsVirtualItem", "False");

@ -13,13 +13,13 @@ namespace Ombi.Api.Emby
Task<List<EmbyUser>> GetUsers(string baseUri, string apiKey); Task<List<EmbyUser>> GetUsers(string baseUri, string apiKey);
Task<EmbyUser> LogIn(string username, string password, string apiKey, string baseUri); Task<EmbyUser> LogIn(string username, string password, string apiKey, string baseUri);
Task<EmbyItemContainer<EmbyMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId, Task<EmbyItemContainer<EmbyMovie>> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId,
string baseUri); string baseUri);
Task<EmbyItemContainer<EmbyEpisodes>> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, Task<EmbyItemContainer<EmbyEpisodes>> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId,
string baseUri); string baseUri);
Task<EmbyItemContainer<EmbySeries>> GetAllShows(string apiKey, int startIndex, int count, string userId, Task<EmbyItemContainer<EmbySeries>> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId,
string baseUri); string baseUri);
Task<EmbyItemContainer<EmbyMovie>> GetCollection(string mediaId, Task<EmbyItemContainer<EmbyMovie>> GetCollection(string mediaId,

@ -1,10 +1,13 @@
using System.Threading.Tasks; using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models;
using Ombi.Api.Emby.Models.Media;
namespace Ombi.Api.Emby namespace Ombi.Api.Emby
{ {
public interface IEmbyApi : IBaseEmbyApi public interface IEmbyApi : IBaseEmbyApi
{ {
Task<EmbyConnectUser> LoginConnectUser(string username, string password); Task<EmbyConnectUser> LoginConnectUser(string username, string password);
Task<List<LibraryVirtualFolders>> GetLibraries(string apiKey, string baseUrl);
} }
} }

@ -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<Typeoption> TypeOptions { get; set; } = new List<Typeoption>();
}
public class Typeoption
{
public string Type { get; set; }
}
}

@ -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; }
}
}

@ -13,13 +13,12 @@ namespace Ombi.Api.Jellyfin
Task<List<JellyfinUser>> GetUsers(string baseUri, string apiKey); Task<List<JellyfinUser>> GetUsers(string baseUri, string apiKey);
Task<JellyfinUser> LogIn(string username, string password, string apiKey, string baseUri); Task<JellyfinUser> LogIn(string username, string password, string apiKey, string baseUri);
Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId, Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri);
string baseUri);
Task<JellyfinItemContainer<JellyfinEpisodes>> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, Task<JellyfinItemContainer<JellyfinEpisodes>> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId,
string baseUri); string baseUri);
Task<JellyfinItemContainer<JellyfinSeries>> GetAllShows(string apiKey, int startIndex, int count, string userId, Task<JellyfinItemContainer<JellyfinSeries>> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId,
string baseUri); string baseUri);
Task<JellyfinItemContainer<JellyfinMovie>> GetCollection(string mediaId, Task<JellyfinItemContainer<JellyfinMovie>> GetCollection(string mediaId,

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System.Collections.Generic;
using System.Threading.Tasks;
using Ombi.Api.Jellyfin.Models; using Ombi.Api.Jellyfin.Models;
namespace Ombi.Api.Jellyfin namespace Ombi.Api.Jellyfin
@ -6,5 +7,6 @@ namespace Ombi.Api.Jellyfin
public interface IJellyfinApi : IBaseJellyfinApi public interface IJellyfinApi : IBaseJellyfinApi
{ {
Task<JellyfinConnectUser> LoginConnectUser(string username, string password); Task<JellyfinConnectUser> LoginConnectUser(string username, string password);
Task<List<LibraryVirtualFolders>> GetLibraries(string apiKey, string baseUrl);
} }
} }

@ -1,12 +1,12 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Internal;
using Newtonsoft.Json; using Newtonsoft.Json;
using Ombi.Api.Jellyfin.Models; using Ombi.Api.Jellyfin.Models;
using Ombi.Api.Jellyfin.Models.Media.Tv; using Ombi.Api.Jellyfin.Models.Media.Tv;
using Ombi.Api.Jellyfin.Models.Movie; using Ombi.Api.Jellyfin.Models.Movie;
using Ombi.Helpers;
namespace Ombi.Api.Jellyfin namespace Ombi.Api.Jellyfin
{ {
@ -87,19 +87,28 @@ namespace Ombi.Api.Jellyfin
return await Api.Request<JellyfinItemContainer<JellyfinMovie>>(request); return await Api.Request<JellyfinItemContainer<JellyfinMovie>>(request);
} }
public async Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId, string baseUri) public async Task<List<LibraryVirtualFolders>> GetLibraries(string apiKey, string baseUrl)
{ {
return await GetAll<JellyfinMovie>("Movie", apiKey, userId, baseUri, true, startIndex, count); var request = new Request("library/virtualfolders", baseUrl, HttpMethod.Get);
AddHeaders(request, apiKey);
var response = await Api.Request<List<LibraryVirtualFolders>>(request);
return response;
}
public async Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri)
{
return await GetAll<JellyfinMovie>("Movie", apiKey, userId, baseUri, true, startIndex, count, parentIdFilder);
} }
public async Task<JellyfinItemContainer<JellyfinEpisodes>> GetAllEpisodes(string apiKey, int startIndex, int count, string userId, string baseUri) public async Task<JellyfinItemContainer<JellyfinEpisodes>> GetAllEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri)
{ {
return await GetAll<JellyfinEpisodes>("Episode", apiKey, userId, baseUri, false, startIndex, count); return await GetAll<JellyfinEpisodes>("Episode", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder);
} }
public async Task<JellyfinItemContainer<JellyfinSeries>> GetAllShows(string apiKey, int startIndex, int count, string userId, string baseUri) public async Task<JellyfinItemContainer<JellyfinSeries>> GetAllShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri)
{ {
return await GetAll<JellyfinSeries>("Series", apiKey, userId, baseUri, false, startIndex, count); return await GetAll<JellyfinSeries>("Series", apiKey, userId, baseUri, false, startIndex, count, parentIdFilder);
} }
public async Task<SeriesInformation> GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl) public async Task<SeriesInformation> GetSeriesInformation(string mediaId, string apiKey, string userId, string baseUrl)
@ -142,15 +151,19 @@ namespace Ombi.Api.Jellyfin
var obj = await Api.Request<JellyfinItemContainer<T>>(request); var obj = await Api.Request<JellyfinItemContainer<T>>(request);
return obj; return obj;
} }
private async Task<JellyfinItemContainer<T>> GetAll<T>(string type, string apiKey, string userId, string baseUri, bool includeOverview, int startIndex, int count) private async Task<JellyfinItemContainer<T>> GetAll<T>(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); var request = new Request($"users/{userId}/items", baseUri, HttpMethod.Get);
request.AddQueryString("Recursive", true.ToString()); request.AddQueryString("Recursive", true.ToString());
request.AddQueryString("IncludeItemTypes", type); 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("startIndex", startIndex.ToString());
request.AddQueryString("limit", count.ToString()); request.AddQueryString("limit", count.ToString());
if(!string.IsNullOrEmpty(parentIdFilder))
{
request.AddQueryString("ParentId", parentIdFilder);
}
request.AddQueryString("IsVirtualItem", "False"); request.AddQueryString("IsVirtualItem", "False");

@ -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<Typeoption> TypeOptions { get; set; } = new List<Typeoption>();
}
public class Typeoption
{
public string Type { get; set; }
}
}

@ -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; }
}
}

@ -23,7 +23,7 @@ namespace Ombi.Api.Plex
Task<PlexFriends> GetUsers(string authToken); Task<PlexFriends> GetUsers(string authToken);
Task<PlexAccount> GetAccount(string authToken); Task<PlexAccount> GetAccount(string authToken);
Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId); Task<PlexMetadata> GetRecentlyAdded(string authToken, string uri, string sectionId);
Task<OAuthPin> GetPin(int pinId); Task<OAuthContainer> GetPin(int pinId);
Task<Uri> GetOAuthUrl(string code, string applicationUrl); Task<Uri> GetOAuthUrl(string code, string applicationUrl);
Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs); Task<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
} }

@ -1,7 +1,14 @@
using System; using System;
using System.Collections.Generic;
namespace Ombi.Api.Plex.Models.OAuth namespace Ombi.Api.Plex.Models.OAuth
{ {
public class OAuthContainer
{
public OAuthPin Result { get; set; }
public OAuthErrorsContainer Errors { get; set; }
}
public class OAuthPin public class OAuthPin
{ {
public int id { get; set; } public int id { get; set; }
@ -24,4 +31,15 @@ namespace Ombi.Api.Plex.Models.OAuth
public string coordinates { get; set; } public string coordinates { get; set; }
} }
public class OAuthErrorsContainer
{
public List<OAuthErrors> errors { get; set; }
}
public class OAuthErrors
{
public int code { get; set; }
public string message { get; set; }
}
} }

@ -1,7 +1,7 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends; using Ombi.Api.Plex.Models.Friends;
using Ombi.Api.Plex.Models.OAuth; using Ombi.Api.Plex.Models.OAuth;
@ -208,12 +208,28 @@ namespace Ombi.Api.Plex
return await Api.Request<PlexMetadata>(request); return await Api.Request<PlexMetadata>(request);
} }
public async Task<OAuthPin> GetPin(int pinId) public async Task<OAuthContainer> GetPin(int pinId)
{ {
var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get); var request = new Request($"api/v2/pins/{pinId}", "https://plex.tv/", HttpMethod.Get);
await AddHeaders(request); await AddHeaders(request);
return await Api.Request<OAuthPin>(request); var response = await Api.RequestContent(request);
if (response.Contains("errors"))
{
var errors = JsonConvert.DeserializeObject<OAuthErrorsContainer>(response, Ombi.Api.Api.Settings);
return new OAuthContainer
{
Errors = errors
};
}
var pinResult = JsonConvert.DeserializeObject<OAuthPin>(response, Ombi.Api.Api.Settings);
return new OAuthContainer
{
Result = pinResult
};
} }
public async Task<Uri> GetOAuthUrl(string code, string applicationUrl) public async Task<Uri> GetOAuthUrl(string code, string applicationUrl)

@ -73,7 +73,7 @@ namespace Ombi.Api
} }
// do something with the response // do something with the response
var receivedString = await httpResponseMessage.Content.ReadAsStringAsync(); var receivedString = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken);
LogDebugContent(receivedString); LogDebugContent(receivedString);
if (request.ContentType == ContentType.Json) if (request.ContentType == ContentType.Json)
{ {

@ -42,8 +42,9 @@ namespace Ombi.Core.Tests.Engine.V2
var cache = new Mock<ICacheService>(); var cache = new Mock<ICacheService>();
var ombiSettings = new Mock<ISettingsService<OmbiSettings>>(); var ombiSettings = new Mock<ISettingsService<OmbiSettings>>();
var requestSubs = new Mock<IRepository<RequestSubscription>>(); var requestSubs = new Mock<IRepository<RequestSubscription>>();
var mediaCache = new Mock<IMediaCacheService>();
_engine = new MovieRequestEngine(movieApi.Object, requestService.Object, user.Object, notificationHelper.Object, rules.Object, movieSender.Object, _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] [Test]

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex; using Ombi.Api.Plex;
using Ombi.Api.Plex.Models; using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.OAuth; using Ombi.Api.Plex.Models.OAuth;
@ -11,24 +13,37 @@ namespace Ombi.Core.Authentication
{ {
public class PlexOAuthManager : IPlexOAuthManager public class PlexOAuthManager : IPlexOAuthManager
{ {
public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings) public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings, ILogger<PlexOAuthManager> logger)
{ {
_api = api; _api = api;
_customizationSettingsService = settings; _customizationSettingsService = settings;
_logger = logger;
} }
private readonly IPlexApi _api; private readonly IPlexApi _api;
private readonly ISettingsService<CustomizationSettings> _customizationSettingsService; private readonly ISettingsService<CustomizationSettings> _customizationSettingsService;
private readonly ILogger _logger;
public async Task<string> GetAccessTokenFromPin(int pinId) public async Task<string> GetAccessTokenFromPin(int pinId)
{ {
var pin = await _api.GetPin(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<OAuthErrors>())
{
_logger.LogError($"Code: '{err.code}' : '{err.message}'");
}
return string.Empty;
}
if (pin.Result.expiresIn <= 0)
{
_logger.LogError("Pin has expired");
return string.Empty; return string.Empty;
} }
return pin.authToken; return pin.Result.authToken;
} }
public async Task<PlexAccount> GetAccount(string accessToken) public async Task<PlexAccount> GetAccount(string accessToken)

@ -127,7 +127,7 @@ namespace Ombi.Core.Engine
UserId = user.Id 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 var result = new HideResult
{ {
Hide = settings.HideRequestsUsers, Hide = settings.HideRequestsUsers,
@ -173,6 +173,10 @@ namespace Ombi.Core.Engine
return currentCode; return currentCode;
} }
var user = await GetUser(); var user = await GetUser();
if (user == null)
{
return "en";
}
if (string.IsNullOrEmpty(user.Language)) if (string.IsNullOrEmpty(user.Language))
{ {

@ -28,5 +28,7 @@ namespace Ombi.Core.Engine.Interfaces
Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted); Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken requestAborted);
Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken); Task<IEnumerable<StreamingData>> GetStreamInformation(int movieDbId, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken); Task<IEnumerable<SearchMovieViewModel>> RecentlyRequestedMovies(int currentlyLoaded, int toLoad, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken);
Task<IEnumerable<SearchMovieViewModel>> AdvancedSearch(DiscoverModel model, int currentlyLoaded, int toLoad, CancellationToken cancellationToken);
} }
} }

@ -14,5 +14,6 @@ namespace Ombi.Core
Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad, string langCustomCode = null); Task<IEnumerable<SearchTvShowViewModel>> Popular(int currentlyLoaded, int amountToLoad, string langCustomCode = null);
Task<IEnumerable<SearchTvShowViewModel>> Anticipated(int currentlyLoaded, int amountToLoad); Task<IEnumerable<SearchTvShowViewModel>> Anticipated(int currentlyLoaded, int amountToLoad);
Task<IEnumerable<SearchTvShowViewModel>> Trending(int currentlyLoaded, int amountToLoad); Task<IEnumerable<SearchTvShowViewModel>> Trending(int currentlyLoaded, int amountToLoad);
Task<IEnumerable<SearchFullInfoTvShowViewModel>> RecentlyRequestedShows(int currentlyLoaded, int toLoad, CancellationToken cancellationToken);
} }
} }

@ -30,7 +30,7 @@ namespace Ombi.Core.Engine
public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user, public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log, INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger<MovieRequestEngine> log,
OmbiUserManager manager, IRepository<RequestLog> rl, ICacheService cache, OmbiUserManager manager, IRepository<RequestLog> rl, ICacheService cache,
ISettingsService<OmbiSettings> ombiSettings, IRepository<RequestSubscription> sub) ISettingsService<OmbiSettings> ombiSettings, IRepository<RequestSubscription> sub, IMediaCacheService mediaCacheService)
: base(user, requestService, r, manager, cache, ombiSettings, sub) : base(user, requestService, r, manager, cache, ombiSettings, sub)
{ {
MovieApi = movieApi; MovieApi = movieApi;
@ -38,6 +38,7 @@ namespace Ombi.Core.Engine
Sender = sender; Sender = sender;
Logger = log; Logger = log;
_requestLog = rl; _requestLog = rl;
_mediaCacheService = mediaCacheService;
} }
private IMovieDbApi MovieApi { get; } private IMovieDbApi MovieApi { get; }
@ -45,6 +46,7 @@ namespace Ombi.Core.Engine
private IMovieSender Sender { get; } private IMovieSender Sender { get; }
private ILogger<MovieRequestEngine> Logger { get; } private ILogger<MovieRequestEngine> Logger { get; }
private readonly IRepository<RequestLog> _requestLog; private readonly IRepository<RequestLog> _requestLog;
private readonly IMediaCacheService _mediaCacheService;
/// <summary> /// <summary>
/// Requests the movie. /// Requests the movie.
@ -315,7 +317,7 @@ namespace Ombi.Core.Engine
// TODO fix this so we execute this on the server // TODO fix this so we execute this on the server
var requests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) 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(); : allRequests.ToList().OrderByDescending(x => prop.GetValue(x)).ToList();
var total = requests.Count(); var total = requests.Count();
requests = requests.Skip(position).Take(count).ToList(); requests = requests.Skip(position).Take(count).ToList();
@ -371,7 +373,6 @@ namespace Ombi.Core.Engine
}; };
} }
public async Task<RequestEngineResult> UpdateAdvancedOptions(MediaAdvancedOptions options) public async Task<RequestEngineResult> UpdateAdvancedOptions(MediaAdvancedOptions options)
{ {
var request = await MovieRepository.Find(options.RequestId); var request = await MovieRepository.Find(options.RequestId);
@ -527,6 +528,7 @@ namespace Ombi.Core.Engine
// We are denying a request // We are denying a request
await NotificationHelper.Notify(request, NotificationType.RequestDeclined); await NotificationHelper.Notify(request, NotificationType.RequestDeclined);
await MovieRepository.Update(request); await MovieRepository.Update(request);
await _mediaCacheService.Purge();
return new RequestEngineResult return new RequestEngineResult
{ {
@ -555,6 +557,7 @@ namespace Ombi.Core.Engine
{ {
await NotificationHelper.Notify(request, NotificationType.RequestApproved); await NotificationHelper.Notify(request, NotificationType.RequestApproved);
} }
await _mediaCacheService.Purge();
return await ProcessSendingMovie(request); return await ProcessSendingMovie(request);
} }
@ -562,8 +565,8 @@ namespace Ombi.Core.Engine
public async Task<RequestEngineResult> RequestCollection(int collectionId, CancellationToken cancellationToken) public async Task<RequestEngineResult> RequestCollection(int collectionId, CancellationToken cancellationToken)
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
var collections = await Cache.GetOrAdd($"GetCollection{collectionId}{langCode}", var collections = await Cache.GetOrAddAsync($"GetCollection{collectionId}{langCode}",
async () => await MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTime.Now.AddDays(1), cancellationToken); () => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1));
var results = new List<RequestEngineResult>(); var results = new List<RequestEngineResult>();
foreach (var collection in collections.parts) foreach (var collection in collections.parts)
@ -639,6 +642,7 @@ namespace Ombi.Core.Engine
results.RootPathOverride = request.RootPathOverride; results.RootPathOverride = request.RootPathOverride;
await MovieRepository.Update(results); await MovieRepository.Update(results);
await _mediaCacheService.Purge();
return results; return results;
} }
@ -651,12 +655,14 @@ namespace Ombi.Core.Engine
{ {
var request = await MovieRepository.GetAll().FirstOrDefaultAsync(x => x.Id == requestId); var request = await MovieRepository.GetAll().FirstOrDefaultAsync(x => x.Id == requestId);
await MovieRepository.Delete(request); await MovieRepository.Delete(request);
await _mediaCacheService.Purge();
} }
public async Task RemoveAllMovieRequests() public async Task RemoveAllMovieRequests()
{ {
var request = MovieRepository.GetAll(); var request = MovieRepository.GetAll();
await MovieRepository.DeleteRange(request); await MovieRepository.DeleteRange(request);
await _mediaCacheService.Purge();
} }
public async Task<bool> UserHasRequest(string userId) public async Task<bool> UserHasRequest(string userId)
@ -692,6 +698,7 @@ namespace Ombi.Core.Engine
request.Available = false; request.Available = false;
await MovieRepository.Update(request); await MovieRepository.Update(request);
await _mediaCacheService.Purge();
return new RequestEngineResult return new RequestEngineResult
{ {
@ -715,6 +722,7 @@ namespace Ombi.Core.Engine
request.MarkedAsAvailable = DateTime.Now; request.MarkedAsAvailable = DateTime.Now;
await NotificationHelper.Notify(request, NotificationType.RequestAvailable); await NotificationHelper.Notify(request, NotificationType.RequestAvailable);
await MovieRepository.Update(request); await MovieRepository.Update(request);
await _mediaCacheService.Purge();
return new RequestEngineResult return new RequestEngineResult
{ {
@ -733,6 +741,8 @@ namespace Ombi.Core.Engine
await NotificationHelper.NewRequest(model); await NotificationHelper.NewRequest(model);
} }
await _mediaCacheService.Purge();
await _requestLog.Add(new RequestLog await _requestLog.Add(new RequestLog
{ {
UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id, UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id,

@ -45,9 +45,9 @@ namespace Ombi.Core.Engine
public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null) public async Task<SearchMovieViewModel> LookupImdbInformation(int theMovieDbId, string langCode = null)
{ {
langCode = await DefaultLanguageCode(langCode); langCode = await DefaultLanguageCode(langCode);
var movieInfo = await Cache.GetOrAdd(nameof(LookupImdbInformation) + langCode + theMovieDbId, var movieInfo = await Cache.GetOrAddAsync(nameof(LookupImdbInformation) + langCode + theMovieDbId,
async () => await MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode), () => MovieApi.GetMovieInformationWithExtraInfo(theMovieDbId, langCode),
DateTime.Now.AddHours(12)); DateTimeOffset.Now.AddHours(12));
var viewMovie = Mapper.Map<SearchMovieViewModel>(movieInfo); var viewMovie = Mapper.Map<SearchMovieViewModel>(movieInfo);
return await ProcessSingleMovie(viewMovie, true); return await ProcessSingleMovie(viewMovie, true);
@ -121,11 +121,11 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies() public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.PopularMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.PopularMovies(langCode); return await MovieApi.PopularMovies(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API
@ -139,11 +139,11 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies() public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.TopRatedMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.TopRated(langCode); return await MovieApi.TopRated(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API
@ -157,11 +157,11 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies() public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode); return await MovieApi.Upcoming(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -176,11 +176,11 @@ namespace Ombi.Core.Engine
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies() public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.NowPlayingMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.NowPlaying(langCode); return await MovieApi.NowPlaying(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API return await TransformMovieResultsToResponse(result.Take(ResultLimit)); // Take x to stop us overloading the API

@ -35,7 +35,7 @@ namespace Ombi.Core.Engine
public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user, public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, IPrincipal user,
INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger<TvRequestEngine> logger, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger<TvRequestEngine> logger,
ITvSender sender, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache, ITvSender sender, IRepository<RequestLog> rl, ISettingsService<OmbiSettings> settings, ICacheService cache,
IRepository<RequestSubscription> sub) : base(user, requestService, rule, manager, cache, settings, sub) IRepository<RequestSubscription> sub, IMediaCacheService mediaCacheService) : base(user, requestService, rule, manager, cache, settings, sub)
{ {
TvApi = tvApi; TvApi = tvApi;
MovieDbApi = movApi; MovieDbApi = movApi;
@ -43,6 +43,7 @@ namespace Ombi.Core.Engine
_logger = logger; _logger = logger;
TvSender = sender; TvSender = sender;
_requestLog = rl; _requestLog = rl;
_mediaCacheService = mediaCacheService;
} }
private INotificationHelper NotificationHelper { get; } private INotificationHelper NotificationHelper { get; }
@ -52,6 +53,7 @@ namespace Ombi.Core.Engine
private readonly ILogger<TvRequestEngine> _logger; private readonly ILogger<TvRequestEngine> _logger;
private readonly IRepository<RequestLog> _requestLog; private readonly IRepository<RequestLog> _requestLog;
private readonly IMediaCacheService _mediaCacheService;
public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv) public async Task<RequestEngineResult> RequestTvShow(TvRequestViewModel tv)
{ {
@ -329,6 +331,7 @@ namespace Ombi.Core.Engine
Collection = allRequests Collection = allRequests
}; };
} }
public async Task<IEnumerable<TvRequests>> GetRequests() public async Task<IEnumerable<TvRequests>> GetRequests()
{ {
var shouldHide = await HideFromOtherUsers(); var shouldHide = await HideFromOtherUsers();
@ -348,7 +351,6 @@ namespace Ombi.Core.Engine
return allRequests; return allRequests;
} }
public async Task<RequestsViewModel<ChildRequests>> GetRequests(int count, int position, string sortProperty, string sortOrder) public async Task<RequestsViewModel<ChildRequests>> GetRequests(int count, int position, string sortProperty, string sortOrder)
{ {
var shouldHide = await HideFromOtherUsers(); var shouldHide = await HideFromOtherUsers();
@ -476,6 +478,7 @@ namespace Ombi.Core.Engine
Total = total, Total = total,
}; };
} }
public async Task<RequestsViewModel<ChildRequests>> GetUnavailableRequests(int count, int position, string sortProperty, string sortOrder) public async Task<RequestsViewModel<ChildRequests>> GetUnavailableRequests(int count, int position, string sortProperty, string sortOrder)
{ {
var shouldHide = await HideFromOtherUsers(); var shouldHide = await HideFromOtherUsers();
@ -529,7 +532,6 @@ namespace Ombi.Core.Engine
}; };
} }
public async Task<IEnumerable<TvRequests>> GetRequestsLite() public async Task<IEnumerable<TvRequests>> GetRequestsLite()
{ {
var shouldHide = await HideFromOtherUsers(); var shouldHide = await HideFromOtherUsers();
@ -699,6 +701,7 @@ namespace Ombi.Core.Engine
} }
await TvRepository.UpdateChild(request); await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
if (request.Approved) if (request.Approved)
{ {
@ -725,6 +728,7 @@ namespace Ombi.Core.Engine
request.Denied = true; request.Denied = true;
request.DeniedReason = reason; request.DeniedReason = reason;
await TvRepository.UpdateChild(request); await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
await NotificationHelper.Notify(request, NotificationType.RequestDeclined); await NotificationHelper.Notify(request, NotificationType.RequestDeclined);
return new RequestEngineResult return new RequestEngineResult
{ {
@ -735,6 +739,7 @@ namespace Ombi.Core.Engine
public async Task<ChildRequests> UpdateChildRequest(ChildRequests request) public async Task<ChildRequests> UpdateChildRequest(ChildRequests request)
{ {
await TvRepository.UpdateChild(request); await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
return request; return request;
} }
@ -754,12 +759,14 @@ namespace Ombi.Core.Engine
} }
await TvRepository.Db.SaveChangesAsync(); await TvRepository.Db.SaveChangesAsync();
await _mediaCacheService.Purge();
} }
public async Task RemoveTvRequest(int requestId) public async Task RemoveTvRequest(int requestId)
{ {
var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId); var request = await TvRepository.Get().FirstOrDefaultAsync(x => x.Id == requestId);
await TvRepository.Delete(request); await TvRepository.Delete(request);
await _mediaCacheService.Purge();
} }
public async Task<bool> UserHasRequest(string userId) public async Task<bool> UserHasRequest(string userId)
@ -786,6 +793,7 @@ namespace Ombi.Core.Engine
} }
} }
await TvRepository.UpdateChild(request); await TvRepository.UpdateChild(request);
await _mediaCacheService.Purge();
return new RequestEngineResult return new RequestEngineResult
{ {
Result = true, Result = true,
@ -814,6 +822,7 @@ namespace Ombi.Core.Engine
} }
await TvRepository.UpdateChild(request); await TvRepository.UpdateChild(request);
await NotificationHelper.Notify(request, NotificationType.RequestAvailable); await NotificationHelper.Notify(request, NotificationType.RequestAvailable);
await _mediaCacheService.Purge();
return new RequestEngineResult return new RequestEngineResult
{ {
Result = true, Result = true,
@ -888,19 +897,6 @@ namespace Ombi.Core.Engine
return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf); return await AfterRequest(model.ChildRequests.FirstOrDefault(), requestOnBehalf);
} }
private static List<ChildRequests> SortEpisodes(List<ChildRequests> 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<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken) public async Task<RequestEngineResult> ReProcessRequest(int requestId, CancellationToken cancellationToken)
{ {
var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId, cancellationToken); var request = await TvRepository.GetChild().FirstOrDefaultAsync(x => x.Id == requestId, cancellationToken);
@ -933,6 +929,7 @@ namespace Ombi.Core.Engine
RequestType = RequestType.TvShow, RequestType = RequestType.TvShow,
EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(), EpisodeCount = model.SeasonRequests.Select(m => m.Episodes.Count).Sum(),
}); });
await _mediaCacheService.Purge();
return await ProcessSendingShow(model); return await ProcessSendingShow(model);
} }

@ -77,16 +77,16 @@ namespace Ombi.Core.Engine
public async Task<SearchTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token) public async Task<SearchTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token)
{ {
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvdbid, var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + tvdbid,
async () => await TvMazeApi.ShowLookupByTheTvDbId(int.Parse(tvdbid)), DateTime.Now.AddHours(12)); () => TvMazeApi.ShowLookupByTheTvDbId(int.Parse(tvdbid)), DateTimeOffset.Now.AddHours(12));
if (show == null) if (show == null)
{ {
// We don't have enough information // We don't have enough information
return null; return null;
} }
var episodes = await Cache.GetOrAdd("TvMazeEpisodeLookup" + show.id, var episodes = await Cache.GetOrAddAsync("TvMazeEpisodeLookup" + show.id,
async () => await TvMazeApi.EpisodeLookup(show.id), DateTime.Now.AddHours(12)); () => TvMazeApi.EpisodeLookup(show.id), DateTimeOffset.Now.AddHours(12));
if (episodes == null || !episodes.Any()) if (episodes == null || !episodes.Any())
{ {
// We don't have enough information // We don't have enough information
@ -133,7 +133,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Popular() public async Task<IEnumerable<SearchTvShowViewModel>> 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); var processed = ProcessResults(result);
return await processed; return await processed;
} }
@ -146,8 +146,8 @@ namespace Ombi.Core.Engine
var results = new List<TraktShow>(); var results = new List<TraktShow>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(Popular) + langCode + pagesToLoad.Page, var apiResult = await Cache.GetOrAddAsync(nameof(Popular) + langCode + pagesToLoad.Page,
async () => await TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12)); () => TraktApi.GetPopularShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
@ -158,7 +158,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Anticipated() public async Task<IEnumerable<SearchTvShowViewModel>> 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); var processed = ProcessResults(result);
return await processed; return await processed;
} }
@ -171,8 +171,8 @@ namespace Ombi.Core.Engine
var results = new List<TraktShow>(); var results = new List<TraktShow>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(Anticipated) + langCode + pagesToLoad.Page, var apiResult = await Cache.GetOrAddAsync(nameof(Anticipated) + langCode + pagesToLoad.Page,
async () => await TraktApi.GetAnticipatedShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12)); () => TraktApi.GetAnticipatedShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
var processed = ProcessResults(results); var processed = ProcessResults(results);
@ -181,7 +181,7 @@ namespace Ombi.Core.Engine
public async Task<IEnumerable<SearchTvShowViewModel>> Trending() public async Task<IEnumerable<SearchTvShowViewModel>> 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); var processed = ProcessResults(result);
return await processed; return await processed;
} }
@ -195,8 +195,8 @@ namespace Ombi.Core.Engine
var results = new List<TraktShow>(); var results = new List<TraktShow>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(Trending) + langCode + pagesToLoad.Page, var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await TraktApi.GetTrendingShows(pagesToLoad.Page, ResultLimit), DateTime.Now.AddHours(12)); () => TraktApi.GetTrendingShows(pagesToLoad.Page, ResultLimit), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
var processed = ProcessResults(results); var processed = ProcessResults(results);

@ -19,6 +19,7 @@ using Ombi.Store.Repository;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Security.Principal; using System.Security.Principal;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -29,7 +30,7 @@ namespace Ombi.Core.Engine.V2
{ {
public MovieSearchEngineV2(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper, public MovieSearchEngineV2(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<MovieSearchEngineV2> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s, IRepository<RequestSubscription> sub, ILogger<MovieSearchEngineV2> logger, IRuleEvaluator r, OmbiUserManager um, ICacheService mem, ISettingsService<OmbiSettings> s, IRepository<RequestSubscription> sub,
ISettingsService<CustomizationSettings> customizationSettings, IMovieRequestEngine movieRequestEngine) ISettingsService<CustomizationSettings> customizationSettings, IMovieRequestEngine movieRequestEngine, IHttpClientFactory httpClientFactory)
: base(identity, service, r, um, mem, s, sub) : base(identity, service, r, um, mem, s, sub)
{ {
MovieApi = movApi; MovieApi = movApi;
@ -37,6 +38,7 @@ namespace Ombi.Core.Engine.V2
Logger = logger; Logger = logger;
_customizationSettings = customizationSettings; _customizationSettings = customizationSettings;
_movieRequestEngine = movieRequestEngine; _movieRequestEngine = movieRequestEngine;
_client = httpClientFactory.CreateClient();
} }
private IMovieDbApi MovieApi { get; } private IMovieDbApi MovieApi { get; }
@ -44,12 +46,13 @@ namespace Ombi.Core.Engine.V2
private ILogger Logger { get; } private ILogger Logger { get; }
private readonly ISettingsService<CustomizationSettings> _customizationSettings; private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly IMovieRequestEngine _movieRequestEngine; private readonly IMovieRequestEngine _movieRequestEngine;
private readonly HttpClient _client;
public async Task<MovieFullInfoViewModel> GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null) public async Task<MovieFullInfoViewModel> GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null)
{ {
langCode = await DefaultLanguageCode(langCode); langCode = await DefaultLanguageCode(langCode);
var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + theMovieDbId + langCode, var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + theMovieDbId + langCode,
async () => await MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken); () => MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo); return await ProcessSingleMovie(movieInfo);
} }
@ -58,8 +61,8 @@ namespace Ombi.Core.Engine.V2
{ {
langCode = await DefaultLanguageCode(langCode); langCode = await DefaultLanguageCode(langCode);
var request = await RequestService.MovieRequestService.Find(requestId); var request = await RequestService.MovieRequestService.Find(requestId);
var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode, var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode,
async () => await MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken); () => MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo); return await ProcessSingleMovie(movieInfo);
} }
@ -67,8 +70,8 @@ namespace Ombi.Core.Engine.V2
public async Task<MovieCollectionsViewModel> GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null) public async Task<MovieCollectionsViewModel> GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null)
{ {
langCode = await DefaultLanguageCode(langCode); langCode = await DefaultLanguageCode(langCode);
var collections = await Cache.GetOrAdd(nameof(GetCollection) + collectionId + langCode, var collections = await Cache.GetOrAddAsync(nameof(GetCollection) + collectionId + langCode,
async () => await MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTime.Now.AddDays(1), cancellationToken); () => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1));
var c = await ProcessCollection(collections); var c = await ProcessCollection(collections);
c.Collection = c.Collection.OrderBy(x => x.ReleaseDate).ToList(); c.Collection = c.Collection.OrderBy(x => x.ReleaseDate).ToList();
@ -105,11 +108,11 @@ namespace Ombi.Core.Engine.V2
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies() public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.PopularMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.PopularMovies(langCode); return await MovieApi.PopularMovies(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -133,24 +136,39 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(PopularMovies) + pagesToLoad.Page + langCode, var apiResult = await Cache.GetOrAddAsync(nameof(PopularMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTime.Now.AddHours(12), cancellationToken); () => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
return await TransformMovieResultsToResponse(results); return await TransformMovieResultsToResponse(results);
} }
public async Task<IEnumerable<SearchMovieViewModel>> 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<MovieDbSearchResult>();
//foreach (var pagesToLoad in pages)
//{
var apiResult = await MovieApi.AdvancedSearch(model, cancellationToken);
//results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
//}
return await TransformMovieResultsToResponse(apiResult);
}
/// <summary> /// <summary>
/// Gets top rated movies. /// Gets top rated movies.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies() public async Task<IEnumerable<SearchMovieViewModel>> TopRatedMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.TopRatedMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.TopRatedMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.TopRated(langCode); return await MovieApi.TopRated(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -167,8 +185,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(TopRatedMovies) + pagesToLoad.Page + langCode, var apiResult = await Cache.GetOrAddAsync(nameof(TopRatedMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.TopRated(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); () => MovieApi.TopRated(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
return await TransformMovieResultsToResponse(results); return await TransformMovieResultsToResponse(results);
@ -183,8 +201,32 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode, var apiResult = await Cache.GetOrAddAsync(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); () => MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
}
public async Task<IEnumerable<SearchMovieViewModel>> SeasonalList(int currentPosition, int amountToLoad, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var result = await _client.GetAsync("https://raw.githubusercontent.com/Ombi-app/Ombi.News/main/Seasonal.md");
var keyWordIds = await result.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(keyWordIds) || keyWordIds.Equals("\n"))
{
return new List<SearchMovieViewModel>();
}
var pages = PaginationHelper.GetNextPages(currentPosition, amountToLoad, _theMovieDbMaxPageItems);
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAddAsync(nameof(SeasonalList) + pagesToLoad.Page + langCode + keyWordIds,
() => MovieApi.GetMoviesViaKeywords(keyWordIds, langCode, cancellationToken, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
return await TransformMovieResultsToResponse(results); return await TransformMovieResultsToResponse(results);
@ -200,16 +242,16 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieResponseDto>(); var results = new List<MovieResponseDto>();
var requestResult = await Cache.GetOrAdd(nameof(RecentlyRequestedMovies) + "Requests" + toLoad + langCode, var requestResult = await Cache.GetOrAddAsync(nameof(RecentlyRequestedMovies) + "Requests" + toLoad + langCode,
async () => async () =>
{ {
return await _movieRequestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel return await _movieRequestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel
{ {
OrderType = OrderType.RequestedDateDesc 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 () => async () =>
{ {
var responses = new List<MovieResponseDto>(); var responses = new List<MovieResponseDto>();
@ -218,7 +260,7 @@ namespace Ombi.Core.Engine.V2
responses.Add(await MovieApi.GetMovieInformation(movie.TheMovieDbId)); responses.Add(await MovieApi.GetMovieInformation(movie.TheMovieDbId));
} }
return responses; return responses;
}, DateTime.Now.AddHours(12), cancellationToken); }, DateTimeOffset.Now.AddHours(12));
results.AddRange(movieDBResults); results.AddRange(movieDBResults);
@ -232,11 +274,11 @@ namespace Ombi.Core.Engine.V2
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies() public async Task<IEnumerable<SearchMovieViewModel>> UpcomingMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.UpcomingMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.UpcomingMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.Upcoming(langCode); return await MovieApi.Upcoming(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
Logger.LogDebug("Search Result: {result}", result); Logger.LogDebug("Search Result: {result}", result);
@ -254,8 +296,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(UpcomingMovies) + pagesToLoad.Page + langCode, var apiResult = await Cache.GetOrAddAsync(nameof(UpcomingMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); () => MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
return await TransformMovieResultsToResponse(results); return await TransformMovieResultsToResponse(results);
@ -267,11 +309,11 @@ namespace Ombi.Core.Engine.V2
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies() public async Task<IEnumerable<SearchMovieViewModel>> NowPlayingMovies()
{ {
var result = await Cache.GetOrAdd(CacheKeys.NowPlayingMovies, async () => var result = await Cache.GetOrAddAsync(CacheKeys.NowPlayingMovies, async () =>
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
return await MovieApi.NowPlaying(langCode); return await MovieApi.NowPlaying(langCode);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
if (result != null) if (result != null)
{ {
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -281,8 +323,8 @@ namespace Ombi.Core.Engine.V2
public async Task<ActorCredits> GetMoviesByActor(int actorId, string langCode) public async Task<ActorCredits> GetMoviesByActor(int actorId, string langCode)
{ {
var result = await Cache.GetOrAdd(nameof(GetMoviesByActor) + actorId + langCode, var result = await Cache.GetOrAddAsync(nameof(GetMoviesByActor) + actorId + langCode,
async () => await MovieApi.GetActorMovieCredits(actorId, langCode)); () => MovieApi.GetActorMovieCredits(actorId, langCode), DateTimeOffset.Now.AddHours(12));
// Later we run this through the rules engine // Later we run this through the rules engine
return result; return result;
} }
@ -339,6 +381,14 @@ namespace Ombi.Core.Engine.V2
private async Task<MovieFullInfoViewModel> ProcessSingleMovie(FullMovieInfo movie) private async Task<MovieFullInfoViewModel> ProcessSingleMovie(FullMovieInfo movie)
{ {
var viewMovie = Mapper.Map<SearchMovieViewModel>(movie); var viewMovie = Mapper.Map<SearchMovieViewModel>(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); await RunSearchRules(viewMovie);
// This requires the rules to be run first to populate the RequestId property // This requires the rules to be run first to populate the RequestId property
@ -354,6 +404,7 @@ namespace Ombi.Core.Engine.V2
mapped.JellyfinUrl = viewMovie.JellyfinUrl; mapped.JellyfinUrl = viewMovie.JellyfinUrl;
mapped.Subscribed = viewMovie.Subscribed; mapped.Subscribed = viewMovie.Subscribed;
mapped.ShowSubscribe = viewMovie.ShowSubscribe; mapped.ShowSubscribe = viewMovie.ShowSubscribe;
mapped.DigitalReleaseDate = viewMovie.DigitalReleaseDate;
return mapped; return mapped;
} }
@ -389,12 +440,21 @@ namespace Ombi.Core.Engine.V2
{ {
if (viewMovie.ImdbId.IsNullOrEmpty()) if (viewMovie.ImdbId.IsNullOrEmpty())
{ {
var showInfo = await Cache.GetOrAdd("GetMovieInformationWIthImdbId" + viewMovie.Id, var showInfo = await Cache.GetOrAddAsync("GetMovieInformationWIthImdbId" + viewMovie.Id,
async () => await MovieApi.GetMovieInformation(viewMovie.Id), DateTime.Now.AddHours(12)); () => MovieApi.GetMovieInformation(viewMovie.Id), DateTimeOffset.Now.AddHours(12));
viewMovie.Id = showInfo.Id; // TheMovieDbId viewMovie.Id = showInfo.Id; // TheMovieDbId
viewMovie.ImdbId = showInfo.ImdbId; 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(); viewMovie.TheMovieDbId = viewMovie.Id.ToString();
await RunSearchRules(viewMovie); await RunSearchRules(viewMovie);
@ -431,12 +491,12 @@ namespace Ombi.Core.Engine.V2
public async Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken cancellationToken) public async Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken cancellationToken)
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
var findResult = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + imdbId + langCode, var findResult = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + imdbId + langCode,
async () => await MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTime.Now.AddHours(12), cancellationToken); () => MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTimeOffset.Now.AddHours(12));
var movie = findResult.movie_results.FirstOrDefault(); var movie = findResult.movie_results.FirstOrDefault();
var movieInfo = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + movie.id + langCode, var movieInfo = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + movie.id + langCode,
async () => await MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken); () => MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo); return await ProcessSingleMovie(movieInfo);
} }

@ -23,6 +23,8 @@ using System.Threading;
using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TheMovieDb.Models;
using System.Diagnostics; using System.Diagnostics;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.UI;
namespace Ombi.Core.Engine.V2 namespace Ombi.Core.Engine.V2
{ {
@ -33,10 +35,11 @@ namespace Ombi.Core.Engine.V2
private readonly ITraktApi _traktApi; private readonly ITraktApi _traktApi;
private readonly IMovieDbApi _movieApi; private readonly IMovieDbApi _movieApi;
private readonly ISettingsService<CustomizationSettings> _customization; private readonly ISettingsService<CustomizationSettings> _customization;
private readonly ITvRequestEngine _requestEngine;
public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper, public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService<OmbiSettings> s, ITraktApi trakt, IRuleEvaluator r, OmbiUserManager um, ICacheService memCache, ISettingsService<OmbiSettings> s,
IRepository<RequestSubscription> sub, IMovieDbApi movieApi, ISettingsService<CustomizationSettings> customization) IRepository<RequestSubscription> sub, IMovieDbApi movieApi, ISettingsService<CustomizationSettings> customization, ITvRequestEngine requestEngine)
: base(identity, service, r, um, memCache, s, sub) : base(identity, service, r, um, memCache, s, sub)
{ {
_tvMaze = tvMaze; _tvMaze = tvMaze;
@ -44,6 +47,7 @@ namespace Ombi.Core.Engine.V2
_traktApi = trakt; _traktApi = trakt;
_movieApi = movieApi; _movieApi = movieApi;
_customization = customization; _customization = customization;
_requestEngine = requestEngine;
} }
@ -56,8 +60,8 @@ namespace Ombi.Core.Engine.V2
public async Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token) public async Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token)
{ {
var langCode = await DefaultLanguageCode(null); var langCode = await DefaultLanguageCode(null);
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + langCode + tvdbid, var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + langCode + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTime.Now.AddHours(12)); async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTimeOffset.Now.AddHours(12));
if (show == null || show.name == null) if (show == null || show.name == null)
{ {
// We don't have enough information // We don't have enough information
@ -68,8 +72,8 @@ namespace Ombi.Core.Engine.V2
{ {
// There's no regional assets for this, so // There's no regional assets for this, so
// lookup the en-us version to get them // lookup the en-us version to get them
var enShow = await Cache.GetOrAdd(nameof(GetShowInformation) + "en" + tvdbid, var enShow = await Cache.GetOrAddAsync(nameof(GetShowInformation) + "en" + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTime.Now.AddHours(12)); async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTimeOffset.Now.AddHours(12));
// For some of the more obsecure cases // For some of the more obsecure cases
if (!show.overview.HasValue()) if (!show.overview.HasValue())
@ -101,8 +105,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(Popular) + langCode + pagesToLoad.Page, var apiResult = await Cache.GetOrAddAsync(nameof(Popular) + langCode + pagesToLoad.Page,
async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
@ -118,8 +122,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(Anticipated) + langCode + pagesToLoad.Page, var apiResult = await Cache.GetOrAddAsync(nameof(Anticipated) + langCode + pagesToLoad.Page,
async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
var processed = ProcessResults(results); var processed = ProcessResults(results);
@ -134,8 +138,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>(); var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages) foreach (var pagesToLoad in pages)
{ {
var apiResult = await Cache.GetOrAdd(nameof(Trending) + langCode + pagesToLoad.Page, var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12)); async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take)); results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
} }
@ -164,6 +168,43 @@ namespace Ombi.Core.Engine.V2
return data; return data;
} }
public async Task<IEnumerable<SearchFullInfoTvShowViewModel>> RecentlyRequestedShows(int currentlyLoaded, int toLoad, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var results = new List<SearchFullInfoTvShowViewModel>();
var requestResult = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + "Requests" + toLoad + langCode,
async () =>
{
return await _requestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel
{
OrderType = OrderType.RequestedDateDesc
});
}, DateTimeOffset.Now.AddMinutes(15));
var movieDBResults = await Cache.GetOrAddAsync(nameof(RecentlyRequestedShows) + toLoad + langCode,
async () =>
{
var responses = new List<TvInfo>();
foreach (var movie in requestResult.Collection)
{
responses.Add(await _movieApi.GetTVInfo(movie.ExternalProviderId.ToString()));
}
return responses;
}, DateTimeOffset.Now.AddHours(12));
var mapped = _mapper.Map<List<SearchFullInfoTvShowViewModel>>(movieDBResults);
foreach(var map in mapped)
{
var processed = await ProcessResult(map);
results.Add(processed);
}
return results;
}
private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults(List<MovieDbSearchResult> items) private async Task<IEnumerable<SearchTvShowViewModel>> ProcessResults(List<MovieDbSearchResult> items)
{ {
var retVal = new List<SearchTvShowViewModel>(); var retVal = new List<SearchTvShowViewModel>();
@ -178,14 +219,14 @@ namespace Ombi.Core.Engine.V2
if (settings.HideAvailableFromDiscover) 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 // 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)); 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 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); return await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, CancellationToken.None);
}, DateTime.Now.AddHours(12)); }, DateTimeOffset.Now.AddHours(12));
MapSeasons(tvMazeSearch.SeasonRequests, tvSeason, seasonEpisodes); MapSeasons(tvMazeSearch.SeasonRequests, tvSeason, seasonEpisodes);
} }

@ -23,8 +23,8 @@ namespace Ombi.Core
public async Task<string> GetTvBackground(string tvdbId) public async Task<string> GetTvBackground(string tvdbId)
{ {
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await _configRepository.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), 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.GetOrAdd($"{CacheKeys.FanartTv}tv{tvdbId}", async () => await _fanartTvApi.GetTvImages(int.Parse(tvdbId), key.Value), DateTime.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) if (images == null)
{ {

@ -28,6 +28,7 @@ namespace Ombi.Core.Rule.Rules.Search
var useImdb = false; var useImdb = false;
var useTheMovieDb = false; var useTheMovieDb = false;
var useTvDb = false; var useTvDb = false;
var useId = false;
if (obj.ImdbId.HasValue()) if (obj.ImdbId.HasValue())
{ {
@ -39,6 +40,14 @@ namespace Ombi.Core.Rule.Rules.Search
} }
if (item == null) if (item == null)
{ {
if (obj.Id > 0)
{
item = await JellyfinContentRepository.GetByTheMovieDbId(obj.Id.ToString());
if (item != null)
{
useId = true;
}
}
if (obj.TheMovieDbId.HasValue()) if (obj.TheMovieDbId.HasValue())
{ {
item = await JellyfinContentRepository.GetByTheMovieDbId(obj.TheMovieDbId); item = await JellyfinContentRepository.GetByTheMovieDbId(obj.TheMovieDbId);
@ -63,6 +72,11 @@ namespace Ombi.Core.Rule.Rules.Search
if (item != null) if (item != null)
{ {
if (useId)
{
obj.TheMovieDbId = obj.Id.ToString();
useTheMovieDb = true;
}
obj.Available = true; obj.Available = true;
var s = await JellyfinSettings.GetSettingsAsync(); var s = await JellyfinSettings.GetSettingsAsync();
if (s.Enable) if (s.Enable)

@ -206,6 +206,7 @@ namespace Ombi.DependencyInjection
services.AddTransient<IEmailProvider, GenericEmailProvider>(); services.AddTransient<IEmailProvider, GenericEmailProvider>();
services.AddTransient<INotificationHelper, NotificationHelper>(); services.AddTransient<INotificationHelper, NotificationHelper>();
services.AddSingleton<ICacheService, CacheService>(); services.AddSingleton<ICacheService, CacheService>();
services.AddSingleton<IMediaCacheService, MediaCacheService>();
services.AddScoped<IImageService, ImageService>(); services.AddScoped<IImageService, ImageService>();
services.AddTransient<IDiscordNotification, DiscordNotification>(); services.AddTransient<IDiscordNotification, DiscordNotification>();

@ -1,48 +1,31 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using LazyCache;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Nito.AsyncEx;
namespace Ombi.Helpers namespace Ombi.Helpers
{ {
public class CacheService : ICacheService public class CacheService : ICacheService
{ {
private readonly IMemoryCache _memoryCache; protected readonly IMemoryCache _memoryCache;
private readonly AsyncLock _mutex = new AsyncLock();
public CacheService(IMemoryCache memoryCache) public CacheService(IMemoryCache memoryCache)
{ {
_memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); _memoryCache = memoryCache;
} }
public async Task<T> GetOrAdd<T>(string cacheKey, Func<Task<T>> factory, DateTime absoluteExpiration = default(DateTime), CancellationToken cancellationToken = default(CancellationToken)) public virtual async Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> 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<T>(cacheKey, out var result))
{
return result;
}
if (_memoryCache.TryGetValue(cacheKey, out result))
{
return result;
} }
if (cancellationToken.CanBeCanceled) return await _memoryCache.GetOrCreateAsync<T>(cacheKey, entry =>
{ {
cancellationToken.ThrowIfCancellationRequested(); entry.AbsoluteExpiration = absoluteExpiration;
} return factory();
});
result = await factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
} }
public void Remove(string key) public void Remove(string key)
@ -50,28 +33,14 @@ namespace Ombi.Helpers
_memoryCache.Remove(key); _memoryCache.Remove(key);
} }
public T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTimeOffset absoluteExpiration)
public T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTime absoluteExpiration)
{ {
// locks get and set internally // locks get and set internally
if (_memoryCache.TryGetValue<T>(cacheKey, out var result)) return _memoryCache.GetOrCreate<T>(cacheKey, entry =>
{
return result;
}
lock (TypeLock<T>.Lock)
{
if (_memoryCache.TryGetValue(cacheKey, out result))
{ {
return result; entry.AbsoluteExpiration = absoluteExpiration;
} return factory();
});
result = factory();
_memoryCache.Set(cacheKey, result, absoluteExpiration);
return result;
}
} }
private static class TypeLock<T> private static class TypeLock<T>

@ -6,8 +6,8 @@ namespace Ombi.Helpers
{ {
public interface ICacheService public interface ICacheService
{ {
Task<T> GetOrAdd<T>(string cacheKey, Func<Task<T>> factory, DateTime absoluteExpiration = default(DateTime), CancellationToken cancellationToken = default(CancellationToken)); Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default);
T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTime absoluteExpiration); T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTimeOffset absoluteExpiration);
void Remove(string key); void Remove(string key);
} }
} }

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LazyCache;
using Microsoft.Extensions.Caching.Memory;
namespace Ombi.Helpers
{
public interface IMediaCacheService
{
Task<T> GetOrAddAsync<T>(string cacheKey, System.Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default);
Task Purge();
}
public class MediaCacheService : CacheService, IMediaCacheService
{
private const string CacheKey = "MediaCacheServiceKeys";
public MediaCacheService(IMemoryCache memoryCache) : base(memoryCache)
{
}
public async override Task<T> GetOrAddAsync<T>(string cacheKey, System.Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default)
{
if (absoluteExpiration == default)
{
absoluteExpiration = DateTimeOffset.Now.AddHours(1);
}
if (_memoryCache.TryGetValue<T>($"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.GetOrCreateAsync<T>(cacheKey, entry =>
{
entry.AbsoluteExpiration = absoluteExpiration;
return factory();
});
}
private async Task UpdateLocalCache(string cacheKey)
{
var mediaServiceCache = _memoryCache.Get<List<string>>(CacheKey);
if (mediaServiceCache == null)
{
mediaServiceCache = new List<string>();
}
mediaServiceCache.Add(cacheKey);
_memoryCache.Remove(CacheKey);
_memoryCache.Set(CacheKey, mediaServiceCache);
}
public async Task Purge()
{
var keys = _memoryCache.Get<List<string>>(CacheKey);
if (keys == null)
{
return;
}
foreach (var key in keys)
{
base.Remove(key);
}
}
}
}

@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EasyCrypto" Version="3.3.2" /> <PackageReference Include="EasyCrypto" Version="3.3.2" />
<PackageReference Include="LazyCache.AspNetCore" Version="2.1.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

@ -35,6 +35,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly IEmbyApiFactory _apiFactory; private readonly IEmbyApiFactory _apiFactory;
private readonly IEmbyContentRepository _repo; private readonly IEmbyContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification; private readonly IHubContext<NotificationHub> _notification;
private IEmbyApi Api { get; set; } private IEmbyApi Api { get; set; }
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
@ -78,47 +79,46 @@ namespace Ombi.Schedule.Jobs.Emby
//await _repo.ExecuteSql("DELETE FROM EmbyEpisode"); //await _repo.ExecuteSql("DELETE FROM EmbyEpisode");
//await _repo.ExecuteSql("DELETE FROM EmbyContent"); //await _repo.ExecuteSql("DELETE FROM EmbyContent");
var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled))
var totalCount = movies.TotalRecordCount;
var processed = 1;
var mediaToAdd = new HashSet<EmbyContent>();
while (processed < totalCount)
{
foreach (var movie in movies.Items)
{ {
if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies");
foreach (var movieParentIdFilder in movieLibsToFilter)
{ {
var movieInfo = _logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'");
await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); await ProcessMovies(server, movieParentIdFilder.Key);
foreach (var item in movieInfo.Items) }
var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows");
foreach (var tvParentIdFilter in tvLibsToFilter)
{ {
await ProcessMovies(item, mediaToAdd, server); _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'");
await ProcessTv(server, tvParentIdFilter.Key);
} }
processed++;
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 else
{ {
processed++; await ProcessMovies(server);
// Regular movie await ProcessTv(server);
await ProcessMovies(movie, mediaToAdd, server);
} }
} }
// Get the next batch private async Task ProcessTv(EmbyServers server, string parentId = default)
movies = await Api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri); {
await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear();
}
// TV Time // TV Time
var tv = await Api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); var mediaToAdd = new HashSet<EmbyContent>();
var tv = await Api.GetAllShows(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri);
var totalTv = tv.TotalRecordCount; var totalTv = tv.TotalRecordCount;
processed = 1; var processed = 1;
while (processed < totalTv) while (processed < totalTv)
{ {
foreach (var tvShow in tv.Items) foreach (var tvShow in tv.Items)
@ -162,7 +162,7 @@ namespace Ombi.Schedule.Jobs.Emby
} }
} }
// Get the next batch // 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); await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear(); mediaToAdd.Clear();
} }
@ -171,6 +171,43 @@ namespace Ombi.Schedule.Jobs.Emby
await _repo.AddRange(mediaToAdd); 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<EmbyContent>();
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<EmbyContent> content, EmbyServers server) private async Task ProcessMovies(EmbyMovie movieInfo, ICollection<EmbyContent> content, EmbyServers server)
{ {
// Check if it exists // Check if it exists

@ -60,6 +60,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly ILogger<EmbyEpisodeSync> _logger; private readonly ILogger<EmbyEpisodeSync> _logger;
private readonly IEmbyContentRepository _repo; private readonly IEmbyContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification; private readonly IHubContext<NotificationHub> _notification;
private IEmbyApi Api { get; set; } private IEmbyApi Api { get; set; }
@ -72,7 +73,19 @@ namespace Ombi.Schedule.Jobs.Emby
.SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started"); .SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started");
foreach (var server in settings.Servers) 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) await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
@ -81,9 +94,9 @@ namespace Ombi.Schedule.Jobs.Emby
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); 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 total = allEpisodes.TotalRecordCount;
var processed = 1; var processed = 1;
var epToAdd = new HashSet<EmbyEpisode>(); var epToAdd = new HashSet<EmbyEpisode>();
@ -150,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Emby
await _repo.AddRange(epToAdd); await _repo.AddRange(epToAdd);
epToAdd.Clear(); 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()) if (epToAdd.Any())

@ -35,6 +35,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
private readonly IJellyfinApiFactory _apiFactory; private readonly IJellyfinApiFactory _apiFactory;
private readonly IJellyfinContentRepository _repo; private readonly IJellyfinContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification; private readonly IHubContext<NotificationHub> _notification;
private IJellyfinApi Api { get; set; } private IJellyfinApi Api { get; set; }
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
@ -52,7 +53,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
{ {
try try
{ {
await StartServerCache(server, jellyfinSettings); await StartServerCache(server);
} }
catch (Exception e) catch (Exception e)
{ {
@ -61,7 +62,6 @@ namespace Ombi.Schedule.Jobs.Jellyfin
_logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name); _logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name);
} }
} }
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Finished"); .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Finished");
// Episodes // Episodes
@ -70,55 +70,55 @@ namespace Ombi.Schedule.Jobs.Jellyfin
} }
private async Task StartServerCache(JellyfinServers server, JellyfinSettings settings) private async Task StartServerCache(JellyfinServers server)
{ {
if (!ValidateSettings(server)) if (!ValidateSettings(server))
{
return; return;
}
//await _repo.ExecuteSql("DELETE FROM JellyfinEpisode"); //await _repo.ExecuteSql("DELETE FROM JellyfinEpisode");
//await _repo.ExecuteSql("DELETE FROM JellyfinContent"); //await _repo.ExecuteSql("DELETE FROM JellyfinContent");
var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); if (server.JellyfinSelectedLibraries.Any() && server.JellyfinSelectedLibraries.Any(x => x.Enabled))
var totalCount = movies.TotalRecordCount;
var processed = 1;
var mediaToAdd = new HashSet<JellyfinContent>();
while (processed < totalCount)
{
foreach (var movie in movies.Items)
{ {
if (movie.Type.Equals("boxset", StringComparison.InvariantCultureIgnoreCase)) var movieLibsToFilter = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies");
foreach (var movieParentIdFilder in movieLibsToFilter)
{ {
var movieInfo = _logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'");
await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri); await ProcessMovies(server, movieParentIdFilder.Key);
foreach (var item in movieInfo.Items) }
var tvLibsToFilter = server.JellyfinSelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows");
foreach (var tvParentIdFilter in tvLibsToFilter)
{ {
await ProcessMovies(item, mediaToAdd, server); _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'");
await ProcessTv(server, tvParentIdFilter.Key);
} }
processed++; 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 else
{ {
processed++; await ProcessMovies(server);
// Regular movie await ProcessTv(server);
await ProcessMovies(movie, mediaToAdd, server);
}
} }
// Get the next batch
movies = await Api.GetAllMovies(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri);
await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear();
} }
private async Task ProcessTv(JellyfinServers server, string parentId = default)
{
// TV Time // TV Time
var tv = await Api.GetAllShows(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri); var mediaToAdd = new HashSet<JellyfinContent>();
var tv = await Api.GetAllShows(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri);
var totalTv = tv.TotalRecordCount; var totalTv = tv.TotalRecordCount;
processed = 1; var processed = 1;
while (processed < totalTv) while (processed < totalTv)
{ {
foreach (var tvShow in tv.Items) foreach (var tvShow in tv.Items)
@ -162,13 +162,52 @@ namespace Ombi.Schedule.Jobs.Jellyfin
} }
} }
// Get the next batch // 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); await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear(); mediaToAdd.Clear();
} }
if (mediaToAdd.Any()) 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<JellyfinContent>();
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); await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear();
}
} }
private async Task ProcessMovies(JellyfinMovie movieInfo, ICollection<JellyfinContent> content, JellyfinServers server) private async Task ProcessMovies(JellyfinMovie movieInfo, ICollection<JellyfinContent> content, JellyfinServers server)

@ -72,7 +72,20 @@ namespace Ombi.Schedule.Jobs.Jellyfin
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Started"); .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Started");
foreach (var server in settings.Servers) foreach (var server in settings.Servers)
{ {
await CacheEpisodes(server);
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)
{
_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) await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
@ -81,9 +94,9 @@ namespace Ombi.Schedule.Jobs.Jellyfin
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); 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 total = allEpisodes.TotalRecordCount;
var processed = 1; var processed = 1;
var epToAdd = new HashSet<JellyfinEpisode>(); var epToAdd = new HashSet<JellyfinEpisode>();
@ -150,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
await _repo.AddRange(epToAdd); await _repo.AddRange(epToAdd);
epToAdd.Clear(); 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()) if (epToAdd.Any())

@ -16,21 +16,26 @@ namespace Ombi.Schedule.Jobs.Ombi
public class MediaDatabaseRefresh : IMediaDatabaseRefresh public class MediaDatabaseRefresh : IMediaDatabaseRefresh
{ {
public MediaDatabaseRefresh(ISettingsService<PlexSettings> s, ILogger<MediaDatabaseRefresh> log, public MediaDatabaseRefresh(ISettingsService<PlexSettings> s, ILogger<MediaDatabaseRefresh> log,
IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo) IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo,
ISettingsService<EmbySettings> embySettings, ISettingsService<JellyfinSettings> jellyfinSettings)
{ {
_settings = s; _plexSettings = s;
_log = log; _log = log;
_plexRepo = plexRepo; _plexRepo = plexRepo;
_embyRepo = embyRepo; _embyRepo = embyRepo;
_jellyfinRepo = jellyfinRepo; _jellyfinRepo = jellyfinRepo;
_settings.ClearCache(); _embySettings = embySettings;
_jellyfinSettings = jellyfinSettings;
_plexSettings.ClearCache();
} }
private readonly ISettingsService<PlexSettings> _settings; private readonly ISettingsService<PlexSettings> _plexSettings;
private readonly ILogger _log; private readonly ILogger _log;
private readonly IPlexContentRepository _plexRepo; private readonly IPlexContentRepository _plexRepo;
private readonly IEmbyContentRepository _embyRepo; private readonly IEmbyContentRepository _embyRepo;
private readonly IJellyfinContentRepository _jellyfinRepo; private readonly IJellyfinContentRepository _jellyfinRepo;
private readonly ISettingsService<EmbySettings> _embySettings;
private readonly ISettingsService<JellyfinSettings> _jellyfinSettings;
public async Task Execute(IJobExecutionContext job) public async Task Execute(IJobExecutionContext job)
{ {
@ -51,7 +56,7 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
try try
{ {
var s = await _settings.GetSettingsAsync(); var s = await _embySettings.GetSettingsAsync();
if (!s.Enable) if (!s.Enable)
{ {
return; return;
@ -73,7 +78,7 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
try try
{ {
var s = await _settings.GetSettingsAsync(); var s = await _jellyfinSettings.GetSettingsAsync();
if (!s.Enable) if (!s.Enable)
{ {
return; return;
@ -95,7 +100,7 @@ namespace Ombi.Schedule.Jobs.Ombi
{ {
try try
{ {
var s = await _settings.GetSettingsAsync(); var s = await _plexSettings.GetSettingsAsync();
if (!s.Enable) if (!s.Enable)
{ {
return; return;

@ -130,12 +130,11 @@ namespace Ombi.Schedule.Jobs.Ombi
var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes).AsNoTracking(); var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes).AsNoTracking();
var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable); 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<int>(); HashSet<int> addedPlexMovieLogIds, addedEmbyMoviesLogIds, addedJellyfinMoviesLogIds;
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet(); HashSet<string> addedAlbumLogIds;
var addedJellyfinMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet(); GetRecentlyAddedMoviesData(addedLog, out addedPlexMovieLogIds, out addedEmbyMoviesLogIds, out addedJellyfinMoviesLogIds, out addedAlbumLogIds);
var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId).ToHashSet();
var addedPlexEpisodesLogIds = var addedPlexEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode); 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"); .SendAsync(NotificationHub.NotificationEvent, "Newsletter Finished");
} }
private void GetRecentlyAddedMoviesData(List<RecentlyAddedLog> addedLog, out HashSet<int> addedPlexMovieLogIds, out HashSet<int> addedEmbyMoviesLogIds, out HashSet<int> addedJellyfinMoviesLogIds, out HashSet<string> 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<int>()) : new HashSet<int>();
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<int>()) : new HashSet<int>();
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<int>()) : new HashSet<int>();
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<string>()) : new HashSet<string>();
}
public static string GenerateUnsubscribeLink(string applicationUrl, string id) public static string GenerateUnsubscribeLink(string applicationUrl, string id)
{ {
if (!applicationUrl.HasValue()) if (!applicationUrl.HasValue())
@ -487,7 +501,7 @@ namespace Ombi.Schedule.Jobs.Ombi
await Start(newsletterSettings, false); await Start(newsletterSettings, false);
} }
private HashSet<PlexEpisode> FilterPlexEpisodes(IEnumerable<PlexEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded) private HashSet<PlexEpisode> FilterPlexEpisodes(IEnumerable<PlexEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{ {
var itemsToReturn = new HashSet<PlexEpisode>(); var itemsToReturn = new HashSet<PlexEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb)) foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -504,7 +518,7 @@ namespace Ombi.Schedule.Jobs.Ombi
return itemsToReturn; return itemsToReturn;
} }
private HashSet<EmbyEpisode> FilterEmbyEpisodes(IEnumerable<EmbyEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded) private HashSet<EmbyEpisode> FilterEmbyEpisodes(IEnumerable<EmbyEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{ {
var itemsToReturn = new HashSet<EmbyEpisode>(); var itemsToReturn = new HashSet<EmbyEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb)) foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -521,7 +535,7 @@ namespace Ombi.Schedule.Jobs.Ombi
return itemsToReturn; return itemsToReturn;
} }
private HashSet<JellyfinEpisode> FilterJellyfinEpisodes(IEnumerable<JellyfinEpisode> source, IQueryable<RecentlyAddedLog> recentlyAdded) private HashSet<JellyfinEpisode> FilterJellyfinEpisodes(IEnumerable<JellyfinEpisode> source, IEnumerable<RecentlyAddedLog> recentlyAdded)
{ {
var itemsToReturn = new HashSet<JellyfinEpisode>(); var itemsToReturn = new HashSet<JellyfinEpisode>();
foreach (var ep in source.Where(x => x.Series.HasTvDb)) 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 plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie); var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
var jellyfinMovies = jellyfinContentToSend.Where(x => x.Type == JellyfinMediaType.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("<h1 style=\"text-align: center; max-width: 1042px;\">New Movies</h1><br /><br />"); sb.Append("<h1 style=\"text-align: center; max-width: 1042px;\">New Movies</h1><br /><br />");
sb.Append( sb.Append(
@ -589,7 +603,7 @@ namespace Ombi.Schedule.Jobs.Ombi
sb.Append("</table>"); sb.Append("</table>");
} }
if ((plexEpisodes.Any() || embyEp.Any()) || jellyfinEp.Any() && !settings.DisableTv) if ((plexEpisodes.Any() || embyEp.Any() || jellyfinEp.Any()) && !settings.DisableTv)
{ {
sb.Append("<br /><br /><h1 style=\"text-align: center; max-width: 1042px;\">New TV</h1><br /><br />"); sb.Append("<br /><br /><h1 style=\"text-align: center; max-width: 1042px;\">New TV</h1><br /><br />");
sb.Append( sb.Append(

@ -30,7 +30,7 @@ namespace Ombi.Schedule.Jobs.Ombi
IMovieDbApi movieApi, IMovieDbApi movieApi,
ISettingsService<EmbySettings> embySettings, IEmbyApiFactory embyApi, ISettingsService<EmbySettings> embySettings, IEmbyApiFactory embyApi,
ISettingsService<JellyfinSettings> jellyfinSettings, IJellyfinApiFactory jellyfinApi, ISettingsService<JellyfinSettings> jellyfinSettings, IJellyfinApiFactory jellyfinApi,
IHubContext<NotificationHub> notification) IHubContext<NotificationHub> notification, IMediaCacheService mediaCacheService)
{ {
_plexRepo = plexRepo; _plexRepo = plexRepo;
_embyRepo = embyRepo; _embyRepo = embyRepo;
@ -44,6 +44,7 @@ namespace Ombi.Schedule.Jobs.Ombi
_jellyfinSettings = jellyfinSettings; _jellyfinSettings = jellyfinSettings;
_jellyfinApiFactory = jellyfinApi; _jellyfinApiFactory = jellyfinApi;
_notification = notification; _notification = notification;
_mediaCacheService = mediaCacheService;
} }
private readonly IPlexContentRepository _plexRepo; private readonly IPlexContentRepository _plexRepo;
@ -58,6 +59,8 @@ namespace Ombi.Schedule.Jobs.Ombi
private readonly IEmbyApiFactory _embyApiFactory; private readonly IEmbyApiFactory _embyApiFactory;
private readonly IJellyfinApiFactory _jellyfinApiFactory; private readonly IJellyfinApiFactory _jellyfinApiFactory;
private readonly IHubContext<NotificationHub> _notification; private readonly IHubContext<NotificationHub> _notification;
private readonly IMediaCacheService _mediaCacheService;
private IEmbyApi EmbyApi { get; set; } private IEmbyApi EmbyApi { get; set; }
private IJellyfinApi JellyfinApi { get; set; } private IJellyfinApi JellyfinApi { get; set; }
@ -102,6 +105,8 @@ namespace Ombi.Schedule.Jobs.Ombi
return; return;
} }
await _mediaCacheService.Purge();
_log.LogInformation("Metadata refresh finished"); _log.LogInformation("Metadata refresh finished");
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Metadata Refresh Finished"); .SendAsync(NotificationHub.NotificationEvent, "Metadata Refresh Finished");

@ -52,8 +52,10 @@ namespace Ombi.Schedule.Jobs.Plex
public class PlexContentSync : IPlexContentSync public class PlexContentSync : IPlexContentSync
{ {
private readonly IMovieDbApi _movieApi; private readonly IMovieDbApi _movieApi;
private readonly IMediaCacheService _mediaCacheService;
public PlexContentSync(ISettingsService<PlexSettings> plex, IPlexApi plexApi, ILogger<PlexContentSync> logger, IPlexContentRepository repo, public PlexContentSync(ISettingsService<PlexSettings> plex, IPlexApi plexApi, ILogger<PlexContentSync> logger, IPlexContentRepository repo,
IPlexEpisodeSync epsiodeSync, IHubContext<NotificationHub> hub, IMovieDbApi movieDbApi) IPlexEpisodeSync epsiodeSync, IHubContext<NotificationHub> hub, IMovieDbApi movieDbApi, IMediaCacheService mediaCacheService)
{ {
Plex = plex; Plex = plex;
PlexApi = plexApi; PlexApi = plexApi;
@ -62,6 +64,7 @@ namespace Ombi.Schedule.Jobs.Plex
EpisodeSync = epsiodeSync; EpisodeSync = epsiodeSync;
Notification = hub; Notification = hub;
_movieApi = movieDbApi; _movieApi = movieDbApi;
_mediaCacheService = mediaCacheService;
Plex.ClearCache(); Plex.ClearCache();
} }
@ -121,6 +124,7 @@ namespace Ombi.Schedule.Jobs.Plex
{ {
await NotifyClient("Plex Sync - Checking if any requests are now available"); await NotifyClient("Plex Sync - Checking if any requests are now available");
Logger.LogInformation("Kicking off Plex Availability Checker"); Logger.LogInformation("Kicking off Plex Availability Checker");
await _mediaCacheService.Purge();
await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex"); await OmbiQuartz.TriggerJob(nameof(IPlexAvailabilityChecker), "Plex");
} }
var processedCont = processedContent?.Content?.Count() ?? 0; var processedCont = processedContent?.Content?.Count() ?? 0;
@ -175,7 +179,7 @@ namespace Ombi.Schedule.Jobs.Plex
var allEps = Repo.GetAllEpisodes(); 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"); Logger.LogDebug($"Got type '{content.viewGroup}' to process");
if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.InvariantCultureIgnoreCase)) if (content.viewGroup.Equals(PlexMediaType.Episode.ToString(), StringComparison.InvariantCultureIgnoreCase))

@ -17,5 +17,14 @@ namespace Ombi.Core.Settings.Models.External
public string AdministratorId { get; set; } public string AdministratorId { get; set; }
public string ServerHostname { get; set; } public string ServerHostname { get; set; }
public bool EnableEpisodeSearching { get; set; } public bool EnableEpisodeSearching { get; set; }
public List<EmbySelectedLibraries> EmbySelectedLibraries { get; set; } = new List<EmbySelectedLibraries>();
}
public class EmbySelectedLibraries
{
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; }
} }
} }

@ -17,5 +17,14 @@ namespace Ombi.Core.Settings.Models.External
public string AdministratorId { get; set; } public string AdministratorId { get; set; }
public string ServerHostname { get; set; } public string ServerHostname { get; set; }
public bool EnableEpisodeSearching { get; set; } public bool EnableEpisodeSearching { get; set; }
public List<JellyfinSelectedLibraries> JellyfinSelectedLibraries { get; set; } = new List<JellyfinSelectedLibraries>();
}
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; }
} }
} }

@ -41,12 +41,12 @@ namespace Ombi.Settings.Settings
var model = obj; var model = obj;
return model; return model;
}, DateTime.Now.AddHours(2)); }, DateTimeOffset.Now.AddHours(2));
} }
public async Task<T> GetSettingsAsync() public async Task<T> GetSettingsAsync()
{ {
return await _cache.GetOrAdd(CacheName, async () => return await _cache.GetOrAddAsync(CacheName, async () =>
{ {
var result = await Repo.GetAsync(EntityName); var result = await Repo.GetAsync(EntityName);
if (result == null) if (result == null)
@ -61,7 +61,7 @@ namespace Ombi.Settings.Settings
var model = obj; var model = obj;
return model; return model;
}, DateTime.Now.AddHours(5)); }, DateTimeOffset.Now.AddHours(5));
} }
public bool SaveSettings(T model) public bool SaveSettings(T model)

@ -18,6 +18,7 @@ namespace Ombi.Api.TheMovieDb
Task<List<MovieDbSearchResult>> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task<List<MovieDbSearchResult>> PopularMovies(string languageCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken));
Task<List<MovieDbSearchResult>> PopularTv(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken)); Task<List<MovieDbSearchResult>> PopularTv(string langCode, int? page = null, CancellationToken cancellationToken = default(CancellationToken));
Task<List<MovieDbSearchResult>> SearchMovie(string searchTerm, int? year, string languageCode); Task<List<MovieDbSearchResult>> SearchMovie(string searchTerm, int? year, string languageCode);
Task<List<MovieDbSearchResult>> GetMoviesViaKeywords(string keywordId, string langCode, CancellationToken cancellationToken, int? page = null);
Task<List<TvSearchResult>> SearchTv(string searchTerm, string year = default); Task<List<TvSearchResult>> SearchTv(string searchTerm, string year = default);
Task<List<MovieDbSearchResult>> TopRated(string languageCode, int? page = null); Task<List<MovieDbSearchResult>> TopRated(string languageCode, int? page = null);
Task<List<MovieDbSearchResult>> Upcoming(string languageCode, int? page = null); Task<List<MovieDbSearchResult>> Upcoming(string languageCode, int? page = null);
@ -34,10 +35,12 @@ namespace Ombi.Api.TheMovieDb
Task<TheMovieDbContainer<DiscoverMovies>> DiscoverMovies(string langCode, int keywordId); Task<TheMovieDbContainer<DiscoverMovies>> DiscoverMovies(string langCode, int keywordId);
Task<FullMovieInfo> GetFullMovieInfo(int movieId, CancellationToken cancellationToken, string langCode); Task<FullMovieInfo> GetFullMovieInfo(int movieId, CancellationToken cancellationToken, string langCode);
Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken); Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken);
Task<List<Keyword>> SearchKeyword(string searchTerm); Task<List<TheMovidDbKeyValue>> SearchKeyword(string searchTerm);
Task<Keyword> GetKeyword(int keywordId); Task<TheMovidDbKeyValue> GetKeyword(int keywordId);
Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token); Task<WatchProviders> GetMovieWatchProviders(int theMoviedbId, CancellationToken token);
Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token); Task<WatchProviders> GetTvWatchProviders(int theMoviedbId, CancellationToken token);
Task<List<Genre>> GetGenres(string media); Task<List<Genre>> GetGenres(string media, CancellationToken cancellationToken);
Task<List<WatchProvidersResults>> SearchWatchProviders(string media, string searchTerm, CancellationToken cancellationToken);
Task<List<MovieDbSearchResult>> AdvancedSearch(DiscoverModel model, CancellationToken cancellationToken);
} }
} }

@ -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<int> GenreIds { get; set; } = new List<int>();
public List<int> KeywordIds { get; set; } = new List<int>();
public List<int> WatchProviders { get; set; } = new List<int>();
public List<int> Companies { get; set; } = new List<int>();
}
}

@ -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; }
}
}

@ -2,7 +2,7 @@
namespace Ombi.Api.TheMovieDb.Models namespace Ombi.Api.TheMovieDb.Models
{ {
public sealed class Keyword public sealed class TheMovidDbKeyValue
{ {
[DataMember(Name = "id")] [DataMember(Name = "id")]
public int Id { get; set; } public int Id { get; set; }

@ -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; }
}
}

@ -68,6 +68,34 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<TheMovieDbContainer<DiscoverMovies>>(request); return await Api.Request<TheMovieDbContainer<DiscoverMovies>>(request);
} }
public async Task<List<MovieDbSearchResult>> 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<TheMovieDbContainer<SearchResult>>(request, cancellationToken);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
}
public async Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken) public async Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken)
{ {
// https://developers.themoviedb.org/3/discover/movie-discover // https://developers.themoviedb.org/3/discover/movie-discover
@ -331,34 +359,71 @@ namespace Ombi.Api.TheMovieDb
return await Api.Request<SeasonDetails>(request, token); return await Api.Request<SeasonDetails>(request, token);
} }
public async Task<List<Keyword>> SearchKeyword(string searchTerm) public async Task<List<MovieDbSearchResult>> 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<TheMovieDbContainer<SearchResult>>(request, cancellationToken);
return Mapper.Map<List<MovieDbSearchResult>>(result.results);
}
public async Task<List<TheMovidDbKeyValue>> SearchKeyword(string searchTerm)
{ {
var request = new Request("search/keyword", BaseUri, HttpMethod.Get); var request = new Request("search/keyword", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken); request.AddQueryString("api_key", ApiToken);
request.AddQueryString("query", searchTerm); request.AddQueryString("query", searchTerm);
AddRetry(request); AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<Keyword>>(request); var result = await Api.Request<TheMovieDbContainer<TheMovidDbKeyValue>>(request);
return result.results ?? new List<Keyword>(); return result.results ?? new List<TheMovidDbKeyValue>();
}
public async Task<List<WatchProvidersResults>> 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<TheMovieDbContainer<WatchProvidersResults>>(request, cancellationToken);
return result.results ?? new List<WatchProvidersResults>();
} }
public async Task<Keyword> GetKeyword(int keywordId) public async Task<TheMovidDbKeyValue> GetKeyword(int keywordId)
{ {
var request = new Request($"keyword/{keywordId}", BaseUri, HttpMethod.Get); var request = new Request($"keyword/{keywordId}", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken); request.AddQueryString("api_key", ApiToken);
AddRetry(request); AddRetry(request);
var keyword = await Api.Request<Keyword>(request); var keyword = await Api.Request<TheMovidDbKeyValue>(request);
return keyword == null || keyword.Id == 0 ? null : keyword; return keyword == null || keyword.Id == 0 ? null : keyword;
} }
public async Task<List<Genre>> GetGenres(string media) public async Task<List<Genre>> GetGenres(string media, CancellationToken cancellationToken)
{ {
var request = new Request($"genre/{media}/list", BaseUri, HttpMethod.Get); var request = new Request($"genre/{media}/list", BaseUri, HttpMethod.Get);
request.AddQueryString("api_key", ApiToken); request.AddQueryString("api_key", ApiToken);
AddRetry(request); AddRetry(request);
var result = await Api.Request<GenreContainer<Genre>>(request); var result = await Api.Request<GenreContainer<Genre>>(request, cancellationToken);
return result.genres ?? new List<Genre>(); return result.genres ?? new List<Genre>();
} }

@ -28,7 +28,6 @@ export class AppComponent implements OnInit {
public showNav: boolean; public showNav: boolean;
public updateAvailable: boolean; public updateAvailable: boolean;
public currentUrl: string; public currentUrl: string;
public userAccessToken: string;
public voteEnabled = false; public voteEnabled = false;
public applicationName: string = "Ombi" public applicationName: string = "Ombi"
public isAdmin: boolean; public isAdmin: boolean;
@ -51,7 +50,7 @@ export class AppComponent implements OnInit {
private readonly identity: IdentityService, private readonly identity: IdentityService,
@Inject(DOCUMENT) private document: HTMLDocument) { @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()) { if (this.authService.loggedIn()) {
this.user = this.authService.claims(); this.user = this.authService.claims();
@ -79,7 +78,7 @@ export class AppComponent implements OnInit {
// See if we can match the supported langs with the current browser lang // See if we can match the supported langs with the current browser lang
const browserLang: string = translate.getBrowserLang(); 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");
} }

@ -1,73 +1,98 @@
import { CommonModule, PlatformLocation, APP_BASE_HREF } from "@angular/common"; import { APP_BASE_HREF, CommonModule, PlatformLocation } from "@angular/common";
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; import { CardsFreeModule, MDBBootstrapModule, NavbarModule } from "angular-bootstrap-md";
import { NgModule } from "@angular/core"; import { CustomPageService, ImageService, RequestService, SettingsService } from "./services";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser"; import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { IdentityService, IssuesService, JobService, MessageService, PlexTvService, SearchService, StatusService } from "./services";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { JwtModule } from "@auth0/angular-jwt";
import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; 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 { ButtonModule } from "primeng/button";
import { ConfirmDialogModule } from "primeng/confirmdialog"; 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 { DataViewModule } from "primeng/dataview";
import { DialogModule } from "primeng/dialog"; import { DialogModule } from "primeng/dialog";
import { OverlayPanelModule } from "primeng/overlaypanel"; import { FilterService } from "./discover/services/filter-service";
import { TooltipModule } from "primeng/tooltip"; import { JwtModule } from "@auth0/angular-jwt";
import { SidebarModule } from "primeng/sidebar"; 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 { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from "@angular/material/card";
import { MatCheckboxModule } from '@angular/material/checkbox'; 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 { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from "@angular/material/input";
import { MatListModule } from '@angular/material/list'; 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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav'; 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 { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTabsModule } from "@angular/material/tabs"; import { MatTabsModule } from "@angular/material/tabs";
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from "@angular/material/tooltip"; 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 // 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 = [ const routes: Routes = [
{ path: "*", component: PageNotFoundComponent }, { path: "*", component: PageNotFoundComponent },
@ -135,6 +160,8 @@ export function JwtTokenGetter() {
MatMenuModule, MatMenuModule,
MatInputModule, MatInputModule,
MatTabsModule, MatTabsModule,
MatChipsModule,
MatDialogModule,
ReactiveFormsModule, ReactiveFormsModule,
MatAutocompleteModule, MatAutocompleteModule,
TooltipModule, TooltipModule,
@ -146,7 +173,6 @@ export function JwtTokenGetter() {
MatCheckboxModule, MatCheckboxModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
MDBBootstrapModule.forRoot(), MDBBootstrapModule.forRoot(),
// NbThemeModule.forRoot({ name: 'dark'}),
JwtModule.forRoot({ JwtModule.forRoot({
config: { config: {
tokenGetter: JwtTokenGetter, tokenGetter: JwtTokenGetter,

@ -1,4 +1,4 @@
<div class="right"> <div class="right" *ngIf="discoverType !== DiscoverType.Seasonal">
<mat-button-toggle-group name="discoverMode" (change)="toggleChanged($event)" value="{{discoverOptions}}" class="discover-filter-buttons-group"> <mat-button-toggle-group name="discoverMode" (change)="toggleChanged($event)" value="{{discoverOptions}}" class="discover-filter-buttons-group">
<mat-button-toggle id="{{id}}Combined" [ngClass]="{'button-active': discoverOptions === DiscoverOption.Combined}" value="{{DiscoverOption.Combined}}" class="discover-filter-button">{{'Discovery.Combined' | translate}}</mat-button-toggle> <mat-button-toggle id="{{id}}Combined" [ngClass]="{'button-active': discoverOptions === DiscoverOption.Combined}" value="{{DiscoverOption.Combined}}" class="discover-filter-button">{{'Discovery.Combined' | translate}}</mat-button-toggle>
<mat-button-toggle id="{{id}}Movie" [ngClass]="{'button-active': discoverOptions === DiscoverOption.Movie}" value="{{DiscoverOption.Movie}}" class="discover-filter-button">{{'Discovery.Movies' | translate}}</mat-button-toggle> <mat-button-toggle id="{{id}}Movie" [ngClass]="{'button-active': discoverOptions === DiscoverOption.Movie}" value="{{DiscoverOption.Movie}}" class="discover-filter-button">{{'Discovery.Movies' | translate}}</mat-button-toggle>

@ -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 { DiscoverOption, IDiscoverCardResult } from "../../interfaces";
import { ISearchMovieResult, ISearchTvResult, RequestType } from "../../../interfaces"; import { ISearchMovieResult, ISearchTvResult, RequestType } from "../../../interfaces";
import { SearchV2Service } from "../../../services"; import { SearchV2Service } from "../../../services";
@ -11,6 +11,7 @@ export enum DiscoverType {
Trending, Trending,
Popular, Popular,
RecentlyRequested, RecentlyRequested,
Seasonal,
} }
@Component({ @Component({
@ -23,6 +24,7 @@ export class CarouselListComponent implements OnInit {
@Input() public discoverType: DiscoverType; @Input() public discoverType: DiscoverType;
@Input() public id: string; @Input() public id: string;
@Input() public isAdmin: boolean; @Input() public isAdmin: boolean;
@Output() public movieCount: EventEmitter<number> = new EventEmitter();
@ViewChild('carousel', {static: false}) carousel: Carousel; @ViewChild('carousel', {static: false}) carousel: Carousel;
public DiscoverOption = DiscoverOption; public DiscoverOption = DiscoverOption;
@ -33,6 +35,7 @@ export class CarouselListComponent implements OnInit {
public responsiveOptions: any; public responsiveOptions: any;
public RequestType = RequestType; public RequestType = RequestType;
public loadingFlag: boolean; public loadingFlag: boolean;
public DiscoverType = DiscoverType;
get mediaTypeStorageKey() { get mediaTypeStorageKey() {
return "DiscoverOptions" + this.discoverType.toString(); return "DiscoverOptions" + this.discoverType.toString();
@ -220,7 +223,12 @@ export class CarouselListComponent implements OnInit {
break break
case DiscoverType.RecentlyRequested: case DiscoverType.RecentlyRequested:
this.movies = await this.searchService.recentlyRequestedMoviesByPage(this.currentlyLoaded, this.amountToLoad); 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; this.currentlyLoaded += this.amountToLoad;
} }
@ -235,6 +243,9 @@ export class CarouselListComponent implements OnInit {
case DiscoverType.Upcoming: case DiscoverType.Upcoming:
this.tvShows = await this.searchService.anticipatedTvByPage(this.currentlyLoaded, this.amountToLoad); this.tvShows = await this.searchService.anticipatedTvByPage(this.currentlyLoaded, this.amountToLoad);
break 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; this.currentlyLoaded += this.amountToLoad;
} }

@ -1,4 +1,12 @@
<div class="small-middle-container"> <div class="small-middle-container">
<div class="section" [hidden]="!showSeasonal">
<h2>{{'Discovery.SeasonalTab' | translate}}</h2>
<div>
<carousel-list [id]="'seasonal'" [isAdmin]="isAdmin" [discoverType]="DiscoverType.Seasonal" (movieCount)="setSeasonalMovieCount($event)"></carousel-list>
</div>
</div>
<div class="section"> <div class="section">
<h2>{{'Discovery.PopularTab' | translate}}</h2> <h2>{{'Discovery.PopularTab' | translate}}</h2>
<div> <div>

@ -1,4 +1,5 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../../auth/auth.service"; import { AuthService } from "../../../auth/auth.service";
import { DiscoverType } from "../carousel-list/carousel-list.component"; import { DiscoverType } from "../carousel-list/carousel-list.component";
@ -11,6 +12,7 @@ export class DiscoverComponent implements OnInit {
public DiscoverType = DiscoverType; public DiscoverType = DiscoverType;
public isAdmin: boolean; public isAdmin: boolean;
public showSeasonal: boolean;
constructor(private authService: AuthService) { } constructor(private authService: AuthService) { }
@ -18,4 +20,9 @@ export class DiscoverComponent implements OnInit {
this.isAdmin = this.authService.isAdmin(); this.isAdmin = this.authService.isAdmin();
} }
public setSeasonalMovieCount(count: number) {
if (count > 0) {
this.showSeasonal = true;
}
}
} }

@ -1,15 +1,15 @@
import { DiscoverComponent } from "./discover/discover.component"; import { RadarrService, RequestService, SearchService, SonarrService } from "../../services";
import { DiscoverCollectionsComponent } from "./collections/discover-collections.component";
import { AuthGuard } from "../../auth/auth.guard";
import { CarouselListComponent } from "./carousel-list/carousel-list.component";
import { DiscoverActorComponent } from "./actor/discover-actor.component"; import { DiscoverActorComponent } from "./actor/discover-actor.component";
import { DiscoverCardComponent } from "./card/discover-card.component"; import { DiscoverCardComponent } from "./card/discover-card.component";
import { Routes } from "@angular/router"; import { DiscoverCollectionsComponent } from "./collections/discover-collections.component";
import { AuthGuard } from "../../auth/auth.guard"; import { DiscoverComponent } from "./discover/discover.component";
import { SearchService, RequestService, SonarrService, RadarrService } from "../../services";
import { MatDialog } from "@angular/material/dialog";
import { DiscoverSearchResultsComponent } from "./search-results/search-results.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 { RequestServiceV2 } from "../../services/requestV2.service";
import { Routes } from "@angular/router";
export const components: any[] = [ export const components: any[] = [
DiscoverComponent, DiscoverComponent,
@ -34,4 +34,5 @@ export const routes: Routes = [
{ path: "collection/:collectionId", component: DiscoverCollectionsComponent, canActivate: [AuthGuard] }, { path: "collection/:collectionId", component: DiscoverCollectionsComponent, canActivate: [AuthGuard] },
{ path: "actor/:actorId", component: DiscoverActorComponent, canActivate: [AuthGuard] }, { path: "actor/:actorId", component: DiscoverActorComponent, canActivate: [AuthGuard] },
{ path: ":searchTerm", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] }, { path: ":searchTerm", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] },
{ path: "advanced/search", component: DiscoverSearchResultsComponent, canActivate: [AuthGuard] },
]; ];

@ -1,4 +1,5 @@
<div class="small-middle-container" > <div class="small-middle-container" >
<div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner"> <div *ngIf="loadingFlag" class="row justify-content-md-center top-spacing loading-spinner">
<mat-spinner [color]="'accent'"></mat-spinner> <mat-spinner [color]="'accent'"></mat-spinner>
</div> </div>

@ -1,14 +1,15 @@
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { IMultiSearchResult, ISearchMovieResult, RequestType } from "../../../interfaces";
import { SearchV2Service } from "../../../services";
import { IDiscoverCardResult } from "../../interfaces"; import { AdvancedSearchDialogDataService } from "../../../shared/advanced-search-dialog/advanced-search-dialog-data.service";
import { IMultiSearchResult, RequestType } from "../../../interfaces"; import { AuthService } from "../../../auth/auth.service";
import { FilterService } from "../../services/filter-service"; import { FilterService } from "../../services/filter-service";
import { IDiscoverCardResult } from "../../interfaces";
import { SearchFilter } from "../../../my-nav/SearchFilter"; import { SearchFilter } from "../../../my-nav/SearchFilter";
import { SearchV2Service } from "../../../services";
import { StorageService } from "../../../shared/storage/storage-service"; import { StorageService } from "../../../shared/storage/storage-service";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { AuthService } from "../../../auth/auth.service";
@Component({ @Component({
templateUrl: "./search-results.component.html", templateUrl: "./search-results.component.html",
@ -25,22 +26,41 @@ export class DiscoverSearchResultsComponent implements OnInit {
public filter: SearchFilter; public filter: SearchFilter;
private isAdvancedSearch: boolean;
constructor(private searchService: SearchV2Service, constructor(private searchService: SearchV2Service,
private route: ActivatedRoute, private route: ActivatedRoute,
private filterService: FilterService, private filterService: FilterService,
private router: Router,
private advancedDataService: AdvancedSearchDialogDataService,
private store: StorageService, private store: StorageService,
private authService: AuthService) { private authService: AuthService) {
this.route.params.subscribe((params: any) => { 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.searchTerm = params.searchTerm;
this.clear(); this.clear();
this.init(); this.init();
}); });
this.advancedDataService.onDataChange.subscribe(() => {
this.clear();
this.loadAdvancedData();
});
} }
public async ngOnInit() { public async ngOnInit() {
this.loadingFlag = true;
this.isAdmin = this.authService.isAdmin(); this.isAdmin = this.authService.isAdmin();
if (this.advancedDataService) {
return;
}
this.loadingFlag = true;
this.filterService.onFilterChange.subscribe(async x => { this.filterService.onFilterChange.subscribe(async x => {
if (!isEqual(this.filter, x)) { if (!isEqual(this.filter, x)) {
this.filter = { ...x }; this.filter = { ...x };
@ -115,6 +135,48 @@ export class DiscoverSearchResultsComponent implements OnInit {
this.discoverResults = []; 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() { private async search() {
this.clear(); this.clear();
this.results = await this.searchService this.results = await this.searchService

@ -1,16 +1,14 @@
import { NgModule } from "@angular/core"; import * as fromComponents from './components';
import { RouterModule } from "@angular/router";
import { CarouselModule } from 'primeng/carousel';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {MatButtonToggleModule} from '@angular/material/button-toggle';
import { NgModule } from "@angular/core";
import { SharedModule } from "../shared/shared.module";
import { PipeModule } from "../pipes/pipe.module"; 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 { SkeletonModule } from 'primeng/skeleton';
import * as fromComponents from './components';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild(fromComponents.routes), RouterModule.forChild(fromComponents.routes),

@ -2,3 +2,18 @@
id: number; id: number;
name: string; 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[];
}

@ -45,6 +45,7 @@ export interface IEmbyServer extends IExternalSettings {
administratorId: string; administratorId: string;
enableEpisodeSearching: boolean; enableEpisodeSearching: boolean;
serverHostname: string; serverHostname: string;
embySelectedLibraries: IEmbyLibrariesSettings[];
} }
export interface IPublicInfo { export interface IPublicInfo {
@ -64,6 +65,37 @@ export interface IJellyfinServer extends IExternalSettings {
administratorId: string; administratorId: string;
enableEpisodeSearching: boolean; enableEpisodeSearching: boolean;
serverHostname: string; serverHostname: string;
jellyfinSelectedLibraries: IJellyfinLibrariesSettings[];
}
export interface IJellyfinLibrariesSettings {
key: string;
title: string;
enabled: boolean;
collectionType: string;
}
export interface IEmbyLibrariesSettings {
key: string;
title: string;
enabled: boolean;
collectionType: string;
}
export interface IMediaServerMediaContainer<T> {
items: T[];
totalRecordCount: number;
}
export interface IJellyfinLibrary {
name: string;
serverId: string;
id: string;
collectionType: string;
}
export interface IEmbyLibrary {
name: string;
serverId: string;
id: string;
collectionType: string;
} }
export interface IPublicInfo { export interface IPublicInfo {

@ -23,7 +23,6 @@ import { MatSnackBar } from "@angular/material/snack-bar";
styleUrls: ["./login.component.scss"], styleUrls: ["./login.component.scss"],
}) })
export class LoginComponent implements OnDestroy, OnInit { export class LoginComponent implements OnDestroy, OnInit {
public form: FormGroup; public form: FormGroup;
public customizationSettings: ICustomizationSettings; public customizationSettings: ICustomizationSettings;
public authenticationSettings: IAuthenticationSettings; public authenticationSettings: IAuthenticationSettings;
@ -53,18 +52,28 @@ export class LoginComponent implements OnDestroy, OnInit {
private errorValidation: string; private errorValidation: string;
private href: string; private href: string;
private oAuthWindow: Window|null; private oAuthWindow: Window | null;
constructor(private authService: AuthService, private router: Router, private status: StatusService, constructor(
private fb: FormBuilder, private settingsService: SettingsService, private images: ImageService, private sanitizer: DomSanitizer, private authService: AuthService,
private route: ActivatedRoute, @Inject(APP_BASE_HREF) href:string, private translate: TranslateService, private plexTv: PlexTvService, private router: Router,
private store: StorageService, private readonly notify: MatSnackBar) { 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.href = href;
this.route.params this.route.params.subscribe((params: any) => {
.subscribe((params: any) => {
this.landingFlag = params.landing; this.landingFlag = params.landing;
if (!this.landingFlag) { if (!this.landingFlag) {
this.settingsService.getLandingPage().subscribe(x => { this.settingsService.getLandingPage().subscribe((x) => {
if (x.enabled && !this.landingFlag) { if (x.enabled && !this.landingFlag) {
this.router.navigate(["landingpage"]); this.router.navigate(["landingpage"]);
} }
@ -78,7 +87,7 @@ export class LoginComponent implements OnDestroy, OnInit {
rememberMe: [false], rememberMe: [false],
}); });
this.status.getWizardStatus().subscribe(x => { this.status.getWizardStatus().subscribe((x) => {
if (!x.result) { if (!x.result) {
this.router.navigate(["Wizard"]); this.router.navigate(["Wizard"]);
} }
@ -90,11 +99,17 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.settingsService.getAuthentication().subscribe(x => this.authenticationSettings = x); this.settingsService
this.settingsService.getClientId().subscribe(x => this.clientId = x); .getAuthentication()
this.settingsService.getCustomization().subscribe(x => this.customizationSettings = x); .subscribe((x) => (this.authenticationSettings = x));
this.images.getRandomBackground().subscribe(x => { this.settingsService.getClientId().subscribe((x) => (this.clientId = x));
this.background = this.sanitizer.bypassSecurityTrustStyle("url(" + x.url + ")"); 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.timer = setInterval(() => {
this.cycleBackground(); this.cycleBackground();
@ -105,27 +120,37 @@ export class LoginComponent implements OnDestroy, OnInit {
this.baseUrl = base; this.baseUrl = base;
} }
this.translate.get("Login.Errors.IncorrectCredentials").subscribe(x => this.errorBody = x); this.translate
this.translate.get("Common.Errors.Validation").subscribe(x => this.errorValidation = x); .get("Login.Errors.IncorrectCredentials")
.subscribe((x) => (this.errorBody = x));
this.translate
.get("Common.Errors.Validation")
.subscribe((x) => (this.errorValidation = x));
} }
public onSubmit(form: FormGroup) { public onSubmit(form: FormGroup) {
if (form.invalid) { if (form.invalid) {
this.notify.open(this.errorValidation, "OK", { this.notify.open(this.errorValidation, "OK", {
duration: 300000 duration: 300000,
}); });
return; return;
} }
const value = form.value; const value = form.value;
const user = { password: value.password, username: value.username, rememberMe: value.rememberMe, usePlexOAuth: false, plexTvPin: { id: 0, code: "" } }; const user = {
this.authService.requiresPassword(user).subscribe(x => { 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) { if (x && this.authenticationSettings.allowNoPassword) {
// Looks like this user requires a password // Looks like this user requires a password
this.authenticationSettings.allowNoPassword = false; this.authenticationSettings.allowNoPassword = false;
return; return;
} }
this.authService.login(user) this.authService.login(user).subscribe(
.subscribe(x => { (x) => {
this.store.save("id_token", x.access_token); this.store.save("id_token", x.access_token);
if (this.authService.loggedIn()) { if (this.authService.loggedIn()) {
@ -133,15 +158,16 @@ export class LoginComponent implements OnDestroy, OnInit {
this.router.navigate(["/"]); this.router.navigate(["/"]);
} else { } else {
this.notify.open(this.errorBody, "OK", { this.notify.open(this.errorBody, "OK", {
duration: 3000 duration: 3000,
}); });
} }
},
}, err => { (err) => {
this.notify.open(this.errorBody, "OK", { this.notify.open(this.errorBody, "OK", {
duration: 3000000 duration: 3000000,
})
}); });
}
);
}); });
} }
@ -149,17 +175,28 @@ export class LoginComponent implements OnDestroy, OnInit {
if (this.oAuthWindow) { if (this.oAuthWindow) {
this.oAuthWindow.close(); this.oAuthWindow.close();
} }
this.oAuthWindow = window.open(window.location.toString(), "_blank", `toolbar=0, this.oAuthWindow = window.open(
window.location.toString(),
"_blank",
`toolbar=0,
location=0, location=0,
status=0, status=0,
menubar=0, menubar=0,
scrollbars=1, scrollbars=1,
resizable=1, resizable=1,
width=500, width=500,
height=500`); height=500`
);
this.plexTv.GetPin(this.clientId, this.appName).subscribe((pin: any) => { this.plexTv.GetPin(this.clientId, this.appName).subscribe((pin: any) => {
this.authService
this.authService.login({ usePlexOAuth: true, password: "", rememberMe: true, username: "", plexTvPin: pin }).subscribe(x => { .login({
usePlexOAuth: true,
password: "",
rememberMe: true,
username: "",
plexTvPin: pin,
})
.subscribe((x) => {
this.oAuthWindow!.location.replace(x.url); this.oAuthWindow!.location.replace(x.url);
if (this.pinTimer) { if (this.pinTimer) {
@ -167,19 +204,18 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
this.pinTimer = setInterval(() => { this.pinTimer = setInterval(() => {
if(this.oAuthWindow.closed) {
this.oauthLoading = true; this.oauthLoading = true;
this.getPinResult(x.pinId); this.getPinResult(x.pinId);
}
}, 1000); }, 1000);
}); });
}); });
} }
public getPinResult(pinId: number) { public getPinResult(pinId: number) {
this.authService.oAuth(pinId).subscribe(
(x) => {
if (x.access_token) {
clearInterval(this.pinTimer); clearInterval(this.pinTimer);
this.authService.oAuth(pinId).subscribe(x => {
if(x.access_token) {
this.store.save("id_token", x.access_token); this.store.save("id_token", x.access_token);
if (this.authService.loggedIn()) { if (this.authService.loggedIn()) {
@ -193,19 +229,22 @@ export class LoginComponent implements OnDestroy, OnInit {
return; return;
} }
} }
this.notify.open("Could not log you in!", "OK", { // if (notifyUser) {
duration: 3000 // this.notify.open("Could not log you in!", "OK", {
}); // duration: 3000,
// });
// }
this.oauthLoading = false; this.oauthLoading = false;
},
}, err => { (err) => {
console.log(err); console.log(err);
this.notify.open(err.body, "OK", { this.notify.open("You are not authenticated with Ombi", "OK", {
duration: 3000 duration: 3000,
}); });
this.router.navigate(["login"]); this.router.navigate(["login"]);
}); }
);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -214,12 +253,13 @@ export class LoginComponent implements OnDestroy, OnInit {
} }
private cycleBackground() { private cycleBackground() {
this.images.getRandomBackground().subscribe(x => { this.images.getRandomBackground().subscribe((x) => {
this.background = ""; this.background = "";
}); });
this.images.getRandomBackground().subscribe(x => { this.images.getRandomBackground().subscribe((x) => {
this.background = this.sanitizer this.background = this.sanitizer.bypassSecurityTrustStyle(
.bypassSecurityTrustStyle("url(" + x.url + ")"); "url(" + x.url + ")"
);
}); });
} }
} }

@ -82,6 +82,11 @@
<i class="fas fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }} <i class="fas fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</button> </button>
<button id="markUnavailableBtn" *ngIf="movie.available" (click)="markUnavailable()" mat-raised-button class="btn-spacing"
color="accent">
<i class="fas fa-minus"></i> {{ 'Requests.MarkUnavailable' | translate }}
</button>
<button id="denyBtn" *ngIf="movieRequest && !movieRequest.denied" mat-raised-button class="btn-spacing" color="warn" (click)="deny()"> <button id="denyBtn" *ngIf="movieRequest && !movieRequest.denied" mat-raised-button class="btn-spacing" color="warn" (click)="deny()">
<i class="fas fa-times"></i> {{'Requests.Deny' | translate }} <i class="fas fa-times"></i> {{'Requests.Deny' | translate }}
</button> </button>

@ -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) { public setAdvancedOptions(data: IAdvancedData) {
this.advancedOptions = data; this.advancedOptions = data;
if (data.rootFolderId) { if (data.rootFolderId) {

@ -44,7 +44,7 @@
</div> </div>
<div *ngIf="request"> <div *ngIf="request">
<span class="label">{{'Requests.RequestedBy' | translate }}:</span> <span class="label">{{'Requests.RequestedBy' | translate }}: </span>
<span id="requestedByInfo">{{request.requestedUser.userAlias}}</span> <span id="requestedByInfo">{{request.requestedUser.userAlias}}</span>
</div> </div>

@ -9,7 +9,8 @@
<div class="row"> <div class="row">
<div <div
class="mobile-top-text"> class="mobile-top-text">
<h1 id="mediaTitle" class="large-text">{{title}} <span *ngIf="releaseDate" class="grey-text"> <h1 id="mediaTitle" class="large-text">{{title}}
<span *ngIf="releaseDateFormat" class="grey-text">
({{releaseDate | amLocal | amDateFormat: 'YYYY'}})</span> ({{releaseDate | amLocal | amDateFormat: 'YYYY'}})</span>
</h1> </h1>

@ -13,6 +13,12 @@ export class TopBannerComponent {
@Input() available: boolean; @Input() available: boolean;
@Input() background: any; @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){ } constructor(private sanitizer:DomSanitizer){ }

@ -78,6 +78,7 @@
<mat-slide-toggle id="filterMusic" class="mat-menu-item slide-menu" [checked]="searchFilter.music" <mat-slide-toggle id="filterMusic" class="mat-menu-item slide-menu" [checked]="searchFilter.music"
(click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.Music)"> (click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.Music)">
{{ 'NavigationBar.Filter.Music' | translate}}</mat-slide-toggle> {{ 'NavigationBar.Filter.Music' | translate}}</mat-slide-toggle>
<button class="advanced-search" mat-raised-button color="accent" (click)="openAdvancedSearch()">Advanced Search</button>
<!-- <mat-slide-toggle class="mat-menu-item slide-menu" [checked]="searchFilter.people" <!-- <mat-slide-toggle class="mat-menu-item slide-menu" [checked]="searchFilter.people"
(click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.People)"> (click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.People)">
{{ 'NavigationBar.Filter.People' | translate}}</mat-slide-toggle> --> {{ 'NavigationBar.Filter.People' | translate}}</mat-slide-toggle> -->

@ -231,3 +231,17 @@
padding:0; padding:0;
margin: 0 4em 0 0.5em; margin: 0 4em 0 0.5em;
} }
.advanced-search {
margin-left: 10px;
}
::ng-deep .dialog-responsive {
width: 40%;
}
@media only screen and (max-width: 760px) {
::ng-deep .dialog-responsive {
width: 100%;
}
}

@ -3,12 +3,15 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { IUser, RequestType, UserType } from '../interfaces'; import { IUser, RequestType, UserType } from '../interfaces';
import { SettingsService, SettingsStateService } from '../services'; import { SettingsService, SettingsStateService } from '../services';
import { AdvancedSearchDialogComponent } from '../shared/advanced-search-dialog/advanced-search-dialog.component';
import { FilterService } from '../discover/services/filter-service'; import { FilterService } from '../discover/services/filter-service';
import { ILocalUser } from '../auth/IUserLogin'; import { ILocalUser } from '../auth/IUserLogin';
import { INavBar } from '../interfaces/ICommon'; import { INavBar } from '../interfaces/ICommon';
import { MatDialog } from '@angular/material/dialog';
import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Md5 } from 'ts-md5/dist/md5'; import { Md5 } from 'ts-md5/dist/md5';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Router } from '@angular/router';
import { SearchFilter } from './SearchFilter'; import { SearchFilter } from './SearchFilter';
import { StorageService } from '../shared/storage/storage-service'; import { StorageService } from '../shared/storage/storage-service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -54,7 +57,9 @@ export class MyNavComponent implements OnInit {
private settingsService: SettingsService, private settingsService: SettingsService,
private store: StorageService, private store: StorageService,
private filterService: FilterService, private filterService: FilterService,
private readonly settingState: SettingsStateService) { private dialogService: MatDialog,
private readonly settingState: SettingsStateService,
private router: Router) {
} }
public async ngOnInit() { public async ngOnInit() {
@ -121,6 +126,18 @@ export class MyNavComponent implements OnInit {
this.store.save("searchFilter", JSON.stringify(this.searchFilter)); this.store.save("searchFilter", JSON.stringify(this.searchFilter));
} }
public openAdvancedSearch() {
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 { public getUserImage(): string {
var fallback = this.applicationLogo ? this.applicationLogo : 'https://raw.githubusercontent.com/Ombi-app/Ombi/gh-pages/img/android-chrome-512x512.png'; 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}`; return `https://www.gravatar.com/avatar/${this.emailHash}?d=${fallback}`;

@ -5,7 +5,7 @@ import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";
import { IEmbyServer, IEmbySettings, IPublicInfo, IUsersModel } from "../../interfaces"; import { IEmbyLibrary, IEmbyServer, IEmbySettings, IMediaServerMediaContainer, IPublicInfo, IUsersModel } from "../../interfaces";
@Injectable() @Injectable()
export class EmbyService extends ServiceHelpers { export class EmbyService extends ServiceHelpers {
@ -25,4 +25,8 @@ export class EmbyService extends ServiceHelpers {
return this.http.post<IPublicInfo>(`${this.url}info`, JSON.stringify(server), {headers: this.headers}); return this.http.post<IPublicInfo>(`${this.url}info`, JSON.stringify(server), {headers: this.headers});
} }
public getLibraries(settings: IEmbyServer): Observable<IMediaServerMediaContainer<IEmbyLibrary>> {
return this.http.post<IMediaServerMediaContainer<IEmbyLibrary>>(`${this.url}Library`, JSON.stringify(settings), {headers: this.headers});
}
} }

@ -5,7 +5,7 @@ import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";
import { IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces"; import { IEmbyServer, IMediaServerMediaContainer, IJellyfinLibrary, IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces";
@Injectable() @Injectable()
export class JellyfinService extends ServiceHelpers { export class JellyfinService extends ServiceHelpers {
@ -25,4 +25,7 @@ export class JellyfinService extends ServiceHelpers {
return this.http.post<IPublicInfo>(`${this.url}info`, JSON.stringify(server), {headers: this.headers}); return this.http.post<IPublicInfo>(`${this.url}info`, JSON.stringify(server), {headers: this.headers});
} }
public getLibraries(settings: IJellyfinServer): Observable<IMediaServerMediaContainer<IJellyfinLibrary>> {
return this.http.post<IMediaServerMediaContainer<IJellyfinLibrary>>(`${this.url}Library`, JSON.stringify(settings), {headers: this.headers});
}
} }

@ -20,6 +20,7 @@ export class PlexTvService {
"X-Plex-Device": "Ombi (Web)", "X-Plex-Device": "Ombi (Web)",
"X-Plex-Platform": "Web", "X-Plex-Platform": "Web",
"Accept": "application/json", "Accept": "application/json",
'X-Plex-Model': 'Plex OAuth',
}); });
return this.http.post<IPlexPin>("https://plex.tv/api/v2/pins?strong=true", null, {headers}); return this.http.post<IPlexPin>("https://plex.tv/api/v2/pins?strong=true", null, {headers});
} }

@ -4,10 +4,12 @@ import { Injectable, Inject } from "@angular/core";
import { empty, Observable, throwError } from "rxjs"; import { empty, Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators"; import { catchError } from "rxjs/operators";
import { IMovieDbKeyword } from "../../interfaces"; import { IMovieDbKeyword, IWatchProvidersResults } from "../../interfaces";
import { ServiceHelpers } from "../service.helpers"; import { ServiceHelpers } from "../service.helpers";
@Injectable() @Injectable({
providedIn: 'root',
})
export class TheMovieDbService extends ServiceHelpers { export class TheMovieDbService extends ServiceHelpers {
constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) { constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) {
super(http, "/api/v1/TheMovieDb", href); super(http, "/api/v1/TheMovieDb", href);
@ -26,4 +28,8 @@ export class TheMovieDbService extends ServiceHelpers {
public getGenres(media: string): Observable<IMovieDbKeyword[]> { public getGenres(media: string): Observable<IMovieDbKeyword[]> {
return this.http.get<IMovieDbKeyword[]>(`${this.url}/Genres/${media}`, { headers: this.headers }) return this.http.get<IMovieDbKeyword[]>(`${this.url}/Genres/${media}`, { headers: this.headers })
} }
public getWatchProviders(media: string): Observable<IWatchProvidersResults[]> {
return this.http.get<IWatchProvidersResults[]>(`${this.url}/WatchProviders/${media}`, {headers: this.headers});
}
} }

@ -4,7 +4,7 @@ import { Injectable, Inject } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces"; import { IDiscoverModel, IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces";
import { ServiceHelpers } from "./service.helpers"; import { ServiceHelpers } from "./service.helpers";
import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2"; import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2";
@ -51,6 +51,10 @@ export class SearchV2Service extends ServiceHelpers {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/Popular/${currentlyLoaded}/${toLoad}`).toPromise(); return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/Popular/${currentlyLoaded}/${toLoad}`).toPromise();
} }
public advancedSearch(model: IDiscoverModel, currentlyLoaded: number, toLoad: number): Promise<ISearchMovieResult[]> {
return this.http.post<ISearchMovieResult[]>(`${this.url}/advancedSearch/Movie/${currentlyLoaded}/${toLoad}`, model).toPromise();
}
public upcomingMovies(): Observable<ISearchMovieResult[]> { public upcomingMovies(): Observable<ISearchMovieResult[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/upcoming`); return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/upcoming`);
} }
@ -63,6 +67,14 @@ export class SearchV2Service extends ServiceHelpers {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/requested/${currentlyLoaded}/${toLoad}`).toPromise(); return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/requested/${currentlyLoaded}/${toLoad}`).toPromise();
} }
public recentlyRequestedTvByPage(currentlyLoaded: number, toLoad: number): Promise<ISearchTvResultV2[]> {
return this.http.get<ISearchTvResultV2[]>(`${this.url}/tv/requested/${currentlyLoaded}/${toLoad}`).toPromise();
}
public seasonalMoviesByPage(currentlyLoaded: number, toLoad: number): Promise<ISearchMovieResult[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/seasonal/${currentlyLoaded}/${toLoad}`).toPromise();
}
public nowPlayingMovies(): Observable<ISearchMovieResult[]> { public nowPlayingMovies(): Observable<ISearchMovieResult[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/nowplaying`); return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/nowplaying`);
} }

@ -1,4 +1,4 @@
<settings-menu> <settings-menu>
</settings-menu> </settings-menu>
<div class="small-middle-container"> <div class="small-middle-container">
<div *ngIf="form"> <div *ngIf="form">
@ -55,7 +55,7 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Quality Profiles</mat-label> <mat-label>Quality Profiles</mat-label>
<mat-select formControlName="defaultProfileId"> <mat-select formControlName="defaultProfileId">
<mat-option *ngFor="let profile of profiles" value="{{profile._id}}">{{profile.label}}</mat-option> <mat-option *ngFor="let profile of profiles?.list" value="{{profile._id}}">{{profile.label}}</mat-option>
</mat-select> </mat-select>
<mat-error>A Default Quality Profile is required</mat-error> <mat-error>A Default Quality Profile is required</mat-error>
</mat-form-field> </mat-form-field>

@ -9,12 +9,10 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-6 col-sm-6"> <div class="col-md-6 col-6 col-sm-6">
<div style="float:right;text-align:left;">
<div class="md-form-field"> <div class="md-form-field">
<mat-slide-toggle [(ngModel)]="settings.enable" (change)="toggle()" [checked]="settings.enable">Enable</mat-slide-toggle> <mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable
</div> </mat-slide-toggle>
</div> </div> </div>
</div>
</div> </div>
<mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)" (selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="display:block;"> <mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)" (selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="display:block;">
<mat-tab *ngFor="let server of settings.servers" [label]="server.name"> <mat-tab *ngFor="let server of settings.servers" [label]="server.name">
@ -74,7 +72,28 @@
<span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/item/item.html?id=1</span> <span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/item/item.html?id=1</span>
</small> </small>
</div> </div>
<label>Please select the libraries you want Ombi to look in for content</label>
<br />
<small>Note: if nothing is selected, we will monitor all libraries</small>
<div class="md-form-field">
<div>
<button mat-raised-button (click)="loadLibraries(server)"
class="mat-focus-indicator mat-stroked-button mat-button-base">Load Libraries
<i class="fas fa-film"></i>
</button>
</div>
</div>
<br />
<div *ngIf="server.embySelectedLibraries">
<div *ngFor="let lib of server.embySelectedLibraries">
<div class="md-form-field">
<div class="checkbox">
<mat-slide-toggle [(ngModel)]="lib.enabled" [checked]="lib.enabled"
for="{{lib.title}}">{{lib.title}}</mat-slide-toggle>
</div>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div> <div>

@ -1,9 +1,9 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { IEmbyServer, IEmbySettings } from "../../interfaces";
import { EmbyService, JobService, NotificationService, SettingsService, TesterService } from "../../services"; 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 {FormControl} from '@angular/forms';
import { MatTabChangeEvent } from "@angular/material/tabs";
@Component({ @Component({
templateUrl: "./emby.component.html", 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); });
}
} }

@ -9,11 +9,11 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-6 col-sm-6"> <div class="col-md-6 col-6 col-sm-6">
<div style="float:right;text-align:left;">
<div class="md-form-field"> <div class="md-form-field">
<mat-slide-toggle [(ngModel)]="settings.enable" (change)="toggle()" [checked]="settings.enable">Enable</mat-slide-toggle> <mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable
</div> </mat-slide-toggle>
</div> </div>
</div> </div>
</div> </div>
<mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)" (selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="display:block;"> <mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)" (selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="display:block;">
@ -75,7 +75,28 @@
</small> </small>
</div> </div>
<label>Please select the libraries you want Ombi to look in for content</label>
<br />
<small>Note: if nothing is selected, we will monitor all libraries</small>
<div class="md-form-field">
<div>
<button mat-raised-button (click)="loadLibraries(server)"
class="mat-focus-indicator mat-stroked-button mat-button-base">Load Libraries
<i class="fas fa-film"></i>
</button>
</div>
</div>
<br />
<div *ngIf="server.jellyfinSelectedLibraries">
<div *ngFor="let lib of server.jellyfinSelectedLibraries">
<div class="md-form-field">
<div class="checkbox">
<mat-slide-toggle [(ngModel)]="lib.enabled" [checked]="lib.enabled"
for="{{lib.title}}">{{lib.title}}</mat-slide-toggle>
</div>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div> <div>
<button mat-raised-button id="testJellyfin" type="button" (click)="test(server)" class="mat-focus-indicator mat-stroked-button mat-button-base">Test Connectivity <div id="spinner"></div></button> <button mat-raised-button id="testJellyfin" type="button" (click)="test(server)" class="mat-focus-indicator mat-stroked-button mat-button-base">Test Connectivity <div id="spinner"></div></button>

@ -1,9 +1,9 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { IEmbyServer, IJellyfinLibrariesSettings, IJellyfinServer, IJellyfinSettings } from "../../interfaces";
import { IJellyfinServer, IJellyfinSettings } from "../../interfaces";
import { JellyfinService, JobService, NotificationService, SettingsService, TesterService } from "../../services"; import { JellyfinService, JobService, NotificationService, SettingsService, TesterService } from "../../services";
import { MatTabChangeEvent } from "@angular/material/tabs";
import {FormControl} from '@angular/forms'; import {FormControl} from '@angular/forms';
import { MatTabChangeEvent } from "@angular/material/tabs";
@Component({ @Component({
templateUrl: "./jellyfin.component.html", 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); });
}
} }

@ -1,13 +1,13 @@
import {COMMA, ENTER} from "@angular/cdk/keycodes"; import {COMMA, ENTER} from "@angular/cdk/keycodes";
import { Component, OnInit, ElementRef, ViewChild } from "@angular/core"; import { Component, ElementRef, OnInit, ViewChild } from "@angular/core";
import { MatAutocomplete } from "@angular/material/autocomplete"; 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 { NotificationService } from "../../services";
import { SettingsService } from "../../services"; import { SettingsService } from "../../services";
import { TheMovieDbService } from "../../services"; import { TheMovieDbService } from "../../services";
import { FormBuilder, FormGroup } from "@angular/forms";
import { debounceTime, switchMap } from "rxjs/operators";
interface IKeywordTag { interface IKeywordTag {
id: number; id: number;
@ -30,8 +30,6 @@ export class TheMovieDbComponent implements OnInit {
public filteredMovieGenres: IMovieDbKeyword[]; public filteredMovieGenres: IMovieDbKeyword[];
public filteredTvGenres: IMovieDbKeyword[]; public filteredTvGenres: IMovieDbKeyword[];
@ViewChild('fruitInput') public fruitInput: ElementRef<HTMLInputElement>;
constructor(private settingsService: SettingsService, constructor(private settingsService: SettingsService,
private notificationService: NotificationService, private notificationService: NotificationService,
private tmdbService: TheMovieDbService, private tmdbService: TheMovieDbService,

@ -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<any>();
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;
}
}

@ -0,0 +1,68 @@
<form [formGroup]="form" (ngSubmit)="onSubmit()" *ngIf="form">
<h1 id="advancedOptionsTitle">
<i class="fas fa-sliders-h"></i> Advanced Search
</h1>
<hr />
<div class="alert alert-info" role="alert">
<i class="fas fa-x7 fa-search glyphicon"></i>
<span>{{ "Search.AdvancedSearch" | translate }}</span>
</div>
<div style="max-width: 0; max-height: 0; overflow: hidden">
<input autofocus="true" />
</div>
<div class="row">
<div style="margin: 2%;">
<span>Please choose what type of media you are searching for:</span>
</div>
<div class="col-md-12">
<div class="md-form-field">
<mat-radio-group formControlName="type" aria-label="Select an option">
<mat-radio-button value="movie">Movies </mat-radio-button>
<mat-radio-button style="padding-left: 5px;" value="tv">TV Shows </mat-radio-button>
</mat-radio-group>
</div>
</div>
<div class="col-md-12" style="margin-top:1%">
<mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Year of Release</mat-label>
<input matInput id="releaseYear" name="releaseYear" formControlName="releaseYear">
</mat-form-field>
</div>
<div class="col-md-12">
<genre-select [form]="form" [mediaType]="form.controls.type.value"></genre-select>
</div>
<div class="col-md-12">
<watch-providers-select [form]="form" [mediaType]="form.controls.type.value"></watch-providers-select>
</div>
<div class="col-md-12">
<span style="margin: 1%;">Please note that Keyword Searching is very hit and miss due to the inconsistent data in TheMovieDb</span>
<keyword-search [form]="form"></keyword-search>
</div>
</div>
<div mat-dialog-actions class="right-buttons">
<button
mat-raised-button
id="cancelButton"
[mat-dialog-close]=""
color="warn"
>
<i class="fas fa-times"></i> {{ "Common.Cancel" | translate }}
</button>
<button
mat-raised-button
id="requestButton"
color="accent"
type="submit"
>
<i class="fas fa-plus"></i> {{ "Common.Search" | translate }}
</button>
</div>
</form>

@ -0,0 +1,8 @@
@import "~styles/variables.scss";
.alert-info {
background: $accent;
border-color: $ombi-background-primary;
color:white;
}

@ -0,0 +1,61 @@
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
import { RequestType } from "../../interfaces";
import { SearchV2Service } from "../../services";
import { AdvancedSearchDialogDataService } from "./advanced-search-dialog-data.service";
@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<AdvancedSearchDialogComponent, boolean>,
private fb: FormBuilder,
private searchService: SearchV2Service,
private advancedSearchDialogService: AdvancedSearchDialogDataService
) {}
public form: FormGroup;
public async ngOnInit() {
this.form = this.fb.group({
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 formData = this.form.value;
const watchProviderIds = <number[]>formData.watchProviders.map(x => x.provider_id);
const genres = <number[]>formData.genreIds.map(x => x.id);
const keywords = <number[]>formData.keywordIds.map(x => x.id);
const data = await this.searchService.advancedSearch({
watchProviders: watchProviderIds,
genreIds: genres,
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);
}
}

@ -0,0 +1,25 @@
<mat-form-field appearance="outline" floatLabel=auto class="example-chip-list">
<mat-label>Genres</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.genreIds.value"
[removable]="true"
(removed)="remove(word)">
{{word.name}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Search Keyword"
#keywordInput
[formControl]="control"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"/>
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let word of filteredKeywords | async" [value]="word">
{{word.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

@ -0,0 +1,77 @@
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 {
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<IMovieDbKeyword[]>;
@ViewChild('keywordInput') input: ElementRef<HTMLInputElement>;
remove(word: IMovieDbKeyword): void {
const exisiting = this.form.controls.genreIds.value;
const index = exisiting.indexOf(word);
if (index >= 0) {
exisiting.splice(index, 1);
this.form.controls.genreIds.setValue(exisiting);
}
}
selected(event: MatAutocompleteSelectedEvent): void {
const val = event.option.value;
const exisiting = this.form.controls.genreIds.value;
if(exisiting.indexOf(val) < 0) {
exisiting.push(val);
}
this.form.controls.genreIds.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;
}
}

@ -0,0 +1,24 @@
<mat-form-field class="example-chip-list" appearance="outline" floatLabel=auto>
<mat-label>Keywords</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.keywordIds.value"
[removable]="true"
(removed)="remove(word)">
{{word.name}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Search Keyword"
#keywordInput
[formControl]="control"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"/>
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let word of filteredKeywords | async" [value]="word">
{{word.name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

@ -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<IMovieDbKeyword[]>;
@ViewChild('keywordInput') input: ElementRef<HTMLInputElement>;
ngOnInit(): void {
this.filteredKeywords = this.control.valueChanges.pipe(
startWith(''),
debounceTime(400),
distinctUntilChanged(),
switchMap(val => {
return this.filter(val || '')
})
);
}
filter(val: string): Observable<any[]> {
return this.tmdbService.getKeywords(val);
};
remove(word: IMovieDbKeyword): void {
const exisiting = this.form.controls.keywordIds.value;
const index = exisiting.indexOf(word);
if (index >= 0) {
exisiting.splice(index, 1);
this.form.controls.keywordIds.setValue(exisiting);
}
}
selected(event: MatAutocompleteSelectedEvent): void {
const val = event.option.value;
const exisiting = this.form.controls.keywordIds.value;
if (exisiting.indexOf(val) < 0) {
exisiting.push(val);
}
this.form.controls.keywordIds.setValue(exisiting);
this.input.nativeElement.value = '';
this.control.setValue(null);
}
}

@ -0,0 +1,24 @@
<mat-form-field class="example-chip-list" appearance="outline" floatLabel=auto>
<mat-label>Watch Providers</mat-label>
<mat-chip-list #chipList aria-label="Fruit selection">
<mat-chip
*ngFor="let word of form.controls.watchProviders.value"
[removable]="true"
(removed)="remove(word)">
{{word.provider_name}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
placeholder="Search Keyword"
#keywordInput
[formControl]="control"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"/>
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let word of filteredList | async" [value]="word">
{{word.provider_name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

@ -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<IWatchProvidersResults[]>;
@ViewChild('keywordInput') input: ElementRef<HTMLInputElement>;
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;
}
}

@ -1,43 +1,47 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 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 { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component";
import { AdvancedSearchDialogComponent } from "./advanced-search-dialog/advanced-search-dialog.component";
import { SidebarModule } from "primeng/sidebar"; 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 { 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 { 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 { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from "@angular/material/input";
import { MatListModule } from '@angular/material/list'; 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 { 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 { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatStepperModule } from '@angular/material/stepper'; import { MatStepperModule } from '@angular/material/stepper';
import { MatTableModule } from '@angular/material/table'; 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 { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree'; import { MatTreeModule } from '@angular/material/tree';
import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MomentModule } from "ngx-moment";
import { MatCardModule } from "@angular/material/card"; import { NgModule } from "@angular/core";
import { MatCheckboxModule } from "@angular/material/checkbox"; import { SidebarModule } from "primeng/sidebar";
import { MatChipsModule } from "@angular/material/chips"; import { TheMovieDbService } from "../services";
import { MatDialogModule } from "@angular/material/dialog"; import { TranslateModule } from "@ngx-translate/core";
import { MatExpansionModule } from "@angular/material/expansion"; import { TruncateModule } from "@yellowspot/ng-truncate";
import { MatInputModule } from "@angular/material/input"; import { WatchProvidersSelectComponent } from "./components/watch-providers-select/watch-providers-select.component";
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";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -45,6 +49,10 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques
EpisodeRequestComponent, EpisodeRequestComponent,
DetailsGroupComponent, DetailsGroupComponent,
AdminRequestDialogComponent, AdminRequestDialogComponent,
AdvancedSearchDialogComponent,
KeywordSearchComponent,
GenreSelectComponent,
WatchProvidersSelectComponent,
], ],
imports: [ imports: [
SidebarModule, SidebarModule,
@ -59,6 +67,7 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques
MatAutocompleteModule, MatAutocompleteModule,
MatInputModule, MatInputModule,
MatTabsModule, MatTabsModule,
MatRadioModule,
MatButtonModule, MatButtonModule,
MatNativeDateModule, MatNativeDateModule,
MatChipsModule, MatChipsModule,
@ -89,6 +98,10 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques
IssuesReportComponent, IssuesReportComponent,
EpisodeRequestComponent, EpisodeRequestComponent,
AdminRequestDialogComponent, AdminRequestDialogComponent,
AdvancedSearchDialogComponent,
GenreSelectComponent,
KeywordSearchComponent,
WatchProvidersSelectComponent,
DetailsGroupComponent, DetailsGroupComponent,
TruncateModule, TruncateModule,
InputSwitchModule, InputSwitchModule,

@ -1,9 +1,8 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { EmbyService } from "../../services"; import { EmbyService } from "../../services";
import { NotificationService } from "../../services";
import { IEmbySettings } from "../../interfaces"; import { IEmbySettings } from "../../interfaces";
import { NotificationService } from "../../services";
@Component({ @Component({
selector: "wizard-emby", selector: "wizard-emby",
@ -35,7 +34,8 @@ export class EmbyComponent implements OnInit {
ssl: false, ssl: false,
subDir: "", subDir: "",
serverHostname: "", serverHostname: "",
serverId: undefined serverId: undefined,
embySelectedLibraries: []
}); });
} }

@ -1,10 +1,9 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { IJellyfinSettings } from "../../interfaces";
import { JellyfinService } from "../../services"; import { JellyfinService } from "../../services";
import { NotificationService } from "../../services"; import { NotificationService } from "../../services";
import { IJellyfinSettings } from "../../interfaces";
@Component({ @Component({
selector: "wizard-jellyfin", selector: "wizard-jellyfin",
templateUrl: "./jellyfin.component.html", templateUrl: "./jellyfin.component.html",
@ -35,7 +34,8 @@ export class JellyfinComponent implements OnInit {
ssl: false, ssl: false,
subDir: "", subDir: "",
serverHostname: "", serverHostname: "",
serverId: undefined serverId: undefined,
jellyfinSelectedLibraries: []
}); });
} }

@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Ombi.Api.Emby; using Ombi.Api.Emby;
using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models;
using Ombi.Api.Plex; using Ombi.Api.Emby.Models.Media;
using Ombi.Attributes; using Ombi.Attributes;
using Ombi.Core.Settings; using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External; using Ombi.Core.Settings.Models.External;
@ -92,5 +92,51 @@ namespace Ombi.Controllers.V1.External
// Filter out any dupes // Filter out any dupes
return vm.DistinctBy(x => x.Id); return vm.DistinctBy(x => x.Id);
} }
[HttpPost("Library")]
public async Task<EmbyItemContainer<MediaFolders>> GetLibaries([FromBody] EmbyServers server)
{
var client = await EmbyApi.CreateClient();
var result = await client.GetLibraries(server.ApiKey, server.FullUri);
var mediaFolders = new EmbyItemContainer<MediaFolders>
{
TotalRecordCount = result.Count,
Items = new List<MediaFolders>()
};
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;
}
} }
} }

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -66,6 +67,52 @@ namespace Ombi.Controllers.V1.External
return result; return result;
} }
[HttpPost("Library")]
public async Task<JellyfinItemContainer<MediaFolders>> GetLibaries([FromBody] JellyfinServers server)
{
var client = await JellyfinApi.CreateClient();
var result = await client.GetLibraries(server.ApiKey, server.FullUri);
var mediaFolders = new JellyfinItemContainer<MediaFolders>
{
TotalRecordCount = result.Count,
Items = new List<MediaFolders>()
};
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;
}
/// <summary> /// <summary>
/// Gets the jellyfin users. /// Gets the jellyfin users.
/// </summary> /// </summary>

@ -69,7 +69,7 @@ namespace Ombi.Controllers.V1.External
[HttpGet("Profiles")] [HttpGet("Profiles")]
public async Task<IEnumerable<LidarrProfile>> GetProfiles() public async Task<IEnumerable<LidarrProfile>> GetProfiles()
{ {
return await Cache.GetOrAdd(CacheKeys.LidarrQualityProfiles, async () => return await Cache.GetOrAddAsync(CacheKeys.LidarrQualityProfiles, async () =>
{ {
var settings = await _lidarrSettings.GetSettingsAsync(); var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled) if (settings.Enabled)
@ -77,7 +77,7 @@ namespace Ombi.Controllers.V1.External
return await _lidarrApi.GetProfiles(settings.ApiKey, settings.FullUri); return await _lidarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
} }
return null; return null;
}, DateTime.Now.AddHours(1)); }, DateTimeOffset.Now.AddHours(1));
} }
/// <summary> /// <summary>
@ -88,7 +88,7 @@ namespace Ombi.Controllers.V1.External
[HttpGet("RootFolders")] [HttpGet("RootFolders")]
public async Task<IEnumerable<LidarrRootFolder>> GetRootFolders() public async Task<IEnumerable<LidarrRootFolder>> GetRootFolders()
{ {
return await Cache.GetOrAdd(CacheKeys.LidarrRootFolders, async () => return await Cache.GetOrAddAsync(CacheKeys.LidarrRootFolders, async () =>
{ {
var settings = await _lidarrSettings.GetSettingsAsync(); var settings = await _lidarrSettings.GetSettingsAsync();
if (settings.Enabled) if (settings.Enabled)
@ -96,7 +96,7 @@ namespace Ombi.Controllers.V1.External
return await _lidarrApi.GetRootFolders(settings.ApiKey, settings.FullUri); return await _lidarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
} }
return null; return null;
}, DateTime.Now.AddHours(1)); }, DateTimeOffset.Now.AddHours(1));
} }
} }
} }

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.TheMovieDb; using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models; using Ombi.Api.TheMovieDb.Models;
using Ombi.Attributes;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,10 +11,10 @@ using Genre = Ombi.TheMovieDbApi.Models.Genre;
namespace Ombi.Controllers.External namespace Ombi.Controllers.External
{ {
[Admin]
[ApiV1] [ApiV1]
[Authorize]
[Produces("application/json")] [Produces("application/json")]
public sealed class TheMovieDbController : Controller public sealed class TheMovieDbController : ControllerBase
{ {
public TheMovieDbController(IMovieDbApi tmdbApi) => TmdbApi = tmdbApi; public TheMovieDbController(IMovieDbApi tmdbApi) => TmdbApi = tmdbApi;
@ -25,7 +25,7 @@ namespace Ombi.Controllers.External
/// </summary> /// </summary>
/// <param name="searchTerm">The search term.</param> /// <param name="searchTerm">The search term.</param>
[HttpGet("Keywords")] [HttpGet("Keywords")]
public async Task<IEnumerable<Keyword>> GetKeywords([FromQuery]string searchTerm) => public async Task<IEnumerable<TheMovidDbKeyValue>> GetKeywords([FromQuery]string searchTerm) =>
await TmdbApi.SearchKeyword(searchTerm); await TmdbApi.SearchKeyword(searchTerm);
/// <summary> /// <summary>
@ -36,7 +36,7 @@ namespace Ombi.Controllers.External
public async Task<IActionResult> GetKeywords(int keywordId) public async Task<IActionResult> GetKeywords(int keywordId)
{ {
var keyword = await TmdbApi.GetKeyword(keywordId); var keyword = await TmdbApi.GetKeyword(keywordId);
return keyword == null ? NotFound() : (IActionResult)Ok(keyword); return keyword == null ? NotFound() : Ok(keyword);
} }
/// <summary> /// <summary>
@ -45,6 +45,24 @@ namespace Ombi.Controllers.External
/// <param name="media">Either `tv` or `movie`.</param> /// <param name="media">Either `tv` or `movie`.</param>
[HttpGet("Genres/{media}")] [HttpGet("Genres/{media}")]
public async Task<IEnumerable<Genre>> GetGenres(string media) => public async Task<IEnumerable<Genre>> GetGenres(string media) =>
await TmdbApi.GetGenres(media); await TmdbApi.GetGenres(media, HttpContext.RequestAborted);
/// <summary>
/// Searches for the watch providers matching the specified term.
/// </summary>
/// <param name="searchTerm">The search term.</param>
[HttpGet("WatchProviders/movie")]
public async Task<IEnumerable<WatchProvidersResults>> GetWatchProvidersMovies([FromQuery] string searchTerm) =>
await TmdbApi.SearchWatchProviders("movie", searchTerm, HttpContext.RequestAborted);
/// <summary>
/// Searches for the watch providers matching the specified term.
/// </summary>
/// <param name="searchTerm">The search term.</param>
[HttpGet("WatchProviders/tv")]
public async Task<IEnumerable<WatchProvidersResults>> GetWatchProvidersTv([FromQuery] string searchTerm) =>
await TmdbApi.SearchWatchProviders("tv", searchTerm, HttpContext.RequestAborted);
} }
} }

@ -293,8 +293,8 @@ namespace Ombi.Controllers.V1
[PowerUser] [PowerUser]
public async Task<IEnumerable<UserViewModelDropdown>> GetAllUsersDropdown() public async Task<IEnumerable<UserViewModelDropdown>> GetAllUsersDropdown()
{ {
var users = await _cacheService.GetOrAdd(CacheKeys.UsersDropdown, var users = await _cacheService.GetOrAddAsync(CacheKeys.UsersDropdown,
async () => await UserManager.Users.Where(x => x.UserType != UserType.SystemUser).ToListAsync()); () => UserManager.Users.Where(x => x.UserType != UserType.SystemUser).ToListAsync());
var model = new List<UserViewModelDropdown>(); var model = new List<UserViewModelDropdown>();

@ -45,9 +45,9 @@ namespace Ombi.Controllers.V1
{ {
return string.Empty; 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) if (images == null)
{ {
return string.Empty; return string.Empty;
@ -70,7 +70,7 @@ namespace Ombi.Controllers.V1
[HttpGet("poster")] [HttpGet("poster")]
public async Task<string> GetRandomPoster() public async Task<string> 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 rand = new Random();
var val = rand.Next(1, 3); var val = rand.Next(1, 3);
if (val == 1) if (val == 1)
@ -79,7 +79,7 @@ namespace Ombi.Controllers.V1
var selectedMovieIndex = rand.Next(movies.Count()); var selectedMovieIndex = rand.Next(movies.Count());
var movie = movies[selectedMovieIndex]; 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) if (images == null)
{ {
return string.Empty; return string.Empty;
@ -114,9 +114,9 @@ namespace Ombi.Controllers.V1
[HttpGet("poster/movie/{movieDbId}")] [HttpGet("poster/movie/{movieDbId}")]
public async Task<string> GetMoviePoster(string movieDbId) public async Task<string> 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) if (images == null)
{ {
@ -148,9 +148,9 @@ namespace Ombi.Controllers.V1
{ {
return string.Empty; 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) if (images == null)
{ {
@ -178,9 +178,9 @@ namespace Ombi.Controllers.V1
[HttpGet("background/movie/{movieDbId}")] [HttpGet("background/movie/{movieDbId}")]
public async Task<string> GetMovieBackground(string movieDbId) public async Task<string> 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) if (images == null)
{ {
@ -203,9 +203,9 @@ namespace Ombi.Controllers.V1
[HttpGet("banner/movie/{movieDbId}")] [HttpGet("banner/movie/{movieDbId}")]
public async Task<string> GetMovieBanner(string movieDbId) public async Task<string> 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) if (images == null)
{ {
@ -246,17 +246,17 @@ namespace Ombi.Controllers.V1
var movieUrl = string.Empty; var movieUrl = string.Empty;
var tvUrl = 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) if (moviesArray.Length > 0)
{ {
var item = rand.Next(moviesArray.Length); 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) while (!result.moviebackground?.Any() ?? true)
{ {
item = rand.Next(moviesArray.Length); 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) if (tvArray.Length > 0)
{ {
var item = rand.Next(tvArray.Length); 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) while (!result.showbackground?.Any() ?? true)
{ {
item = rand.Next(tvArray.Length); 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 otherRand = new Random();
var res = otherRand.Next(result.showbackground.Length); var res = otherRand.Next(result.showbackground.Length);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save