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.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Internal;
using Newtonsoft.Json;
using Ombi.Api.Emby.Models;
using Ombi.Api.Emby.Models.Media;
using Ombi.Api.Emby.Models.Media.Tv;
using Ombi.Api.Emby.Models.Movie;
using Ombi.Helpers;
@ -112,19 +115,29 @@ namespace Ombi.Api.Emby
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)
@ -167,7 +180,7 @@ namespace Ombi.Api.Emby
var obj = await Api.Request<EmbyItemContainer<T>>(request);
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);
@ -176,6 +189,10 @@ namespace Ombi.Api.Emby
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
request.AddQueryString("startIndex", startIndex.ToString());
request.AddQueryString("limit", count.ToString());
if (!string.IsNullOrEmpty(parentIdFilder))
{
request.AddQueryString("ParentId", parentIdFilder);
}
request.AddQueryString("IsVirtualItem", "False");

@ -13,13 +13,13 @@ namespace Ombi.Api.Emby
Task<List<EmbyUser>> GetUsers(string baseUri, string apiKey);
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);
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);
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);
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.Media;
namespace Ombi.Api.Emby
{
public interface IEmbyApi : IBaseEmbyApi
{
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<JellyfinUser> LogIn(string username, string password, string apiKey, string baseUri);
Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, int startIndex, int count, string userId,
string baseUri);
Task<JellyfinItemContainer<JellyfinMovie>> GetAllMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, 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);
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);
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;
namespace Ombi.Api.Jellyfin
@ -6,5 +7,6 @@ namespace Ombi.Api.Jellyfin
public interface IJellyfinApi : IBaseJellyfinApi
{
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.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Internal;
using Newtonsoft.Json;
using Ombi.Api.Jellyfin.Models;
using Ombi.Api.Jellyfin.Models.Media.Tv;
using Ombi.Api.Jellyfin.Models.Movie;
using Ombi.Helpers;
namespace Ombi.Api.Jellyfin
{
@ -87,19 +87,28 @@ namespace Ombi.Api.Jellyfin
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)
@ -142,15 +151,19 @@ namespace Ombi.Api.Jellyfin
var obj = await Api.Request<JellyfinItemContainer<T>>(request);
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);
request.AddQueryString("Recursive", true.ToString());
request.AddQueryString("IncludeItemTypes", type);
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview" : "ProviderIds");
request.AddQueryString("Fields", includeOverview ? "ProviderIds,Overview,ParentId" : "ProviderIds,ParentId");
request.AddQueryString("startIndex", startIndex.ToString());
request.AddQueryString("limit", count.ToString());
if(!string.IsNullOrEmpty(parentIdFilder))
{
request.AddQueryString("ParentId", parentIdFilder);
}
request.AddQueryString("IsVirtualItem", "False");

@ -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<PlexAccount> GetAccount(string authToken);
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<PlexAddWrapper> AddUser(string emailAddress, string serverId, string authToken, int[] libs);
}

@ -1,7 +1,14 @@
using System;
using System.Collections.Generic;
namespace Ombi.Api.Plex.Models.OAuth
{
public class OAuthContainer
{
public OAuthPin Result { get; set; }
public OAuthErrorsContainer Errors { get; set; }
}
public class OAuthPin
{
public int id { get; set; }
@ -24,4 +31,15 @@ namespace Ombi.Api.Plex.Models.OAuth
public string coordinates { get; set; }
}
public class OAuthErrorsContainer
{
public List<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.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.Friends;
using Ombi.Api.Plex.Models.OAuth;
@ -208,12 +208,28 @@ namespace Ombi.Api.Plex
return await Api.Request<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);
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)

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

@ -42,8 +42,9 @@ namespace Ombi.Core.Tests.Engine.V2
var cache = new Mock<ICacheService>();
var ombiSettings = new Mock<ISettingsService<OmbiSettings>>();
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,
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]

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Ombi.Api.Plex;
using Ombi.Api.Plex.Models;
using Ombi.Api.Plex.Models.OAuth;
@ -11,24 +13,37 @@ namespace Ombi.Core.Authentication
{
public class PlexOAuthManager : IPlexOAuthManager
{
public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings)
public PlexOAuthManager(IPlexApi api, ISettingsService<CustomizationSettings> settings, ILogger<PlexOAuthManager> logger)
{
_api = api;
_customizationSettingsService = settings;
_logger = logger;
}
private readonly IPlexApi _api;
private readonly ISettingsService<CustomizationSettings> _customizationSettingsService;
private readonly ILogger _logger;
public async Task<string> GetAccessTokenFromPin(int pinId)
{
var pin = await _api.GetPin(pinId);
if (pin.expiresAt < DateTime.UtcNow)
if (pin.Errors != null)
{
foreach (var err in pin.Errors?.errors ?? new List<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 pin.authToken;
return pin.Result.authToken;
}
public async Task<PlexAccount> GetAccount(string accessToken)

@ -127,7 +127,7 @@ namespace Ombi.Core.Engine
UserId = user.Id
};
}
var settings = await Cache.GetOrAdd(CacheKeys.OmbiSettings, async () => await OmbiSettings.GetSettingsAsync());
var settings = await Cache.GetOrAddAsync(CacheKeys.OmbiSettings, () => OmbiSettings.GetSettingsAsync());
var result = new HideResult
{
Hide = settings.HideRequestsUsers,
@ -173,6 +173,10 @@ namespace Ombi.Core.Engine
return currentCode;
}
var user = await GetUser();
if (user == null)
{
return "en";
}
if (string.IsNullOrEmpty(user.Language))
{

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

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

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

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

@ -19,6 +19,7 @@ using Ombi.Store.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
@ -29,7 +30,7 @@ namespace Ombi.Core.Engine.V2
{
public MovieSearchEngineV2(IPrincipal identity, IRequestServiceMain service, IMovieDbApi movApi, IMapper mapper,
ILogger<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)
{
MovieApi = movApi;
@ -37,6 +38,7 @@ namespace Ombi.Core.Engine.V2
Logger = logger;
_customizationSettings = customizationSettings;
_movieRequestEngine = movieRequestEngine;
_client = httpClientFactory.CreateClient();
}
private IMovieDbApi MovieApi { get; }
@ -44,12 +46,13 @@ namespace Ombi.Core.Engine.V2
private ILogger Logger { get; }
private readonly ISettingsService<CustomizationSettings> _customizationSettings;
private readonly IMovieRequestEngine _movieRequestEngine;
private readonly HttpClient _client;
public async Task<MovieFullInfoViewModel> GetFullMovieInformation(int theMovieDbId, CancellationToken cancellationToken, string langCode = null)
{
langCode = await DefaultLanguageCode(langCode);
var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + theMovieDbId + langCode,
async () => await MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken);
var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + theMovieDbId + langCode,
() => MovieApi.GetFullMovieInfo(theMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo);
}
@ -58,8 +61,8 @@ namespace Ombi.Core.Engine.V2
{
langCode = await DefaultLanguageCode(langCode);
var request = await RequestService.MovieRequestService.Find(requestId);
var movieInfo = await Cache.GetOrAdd(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode,
async () => await MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken);
var movieInfo = await Cache.GetOrAddAsync(nameof(GetFullMovieInformation) + request.TheMovieDbId + langCode,
() => MovieApi.GetFullMovieInfo(request.TheMovieDbId, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo);
}
@ -67,8 +70,8 @@ namespace Ombi.Core.Engine.V2
public async Task<MovieCollectionsViewModel> GetCollection(int collectionId, CancellationToken cancellationToken, string langCode = null)
{
langCode = await DefaultLanguageCode(langCode);
var collections = await Cache.GetOrAdd(nameof(GetCollection) + collectionId + langCode,
async () => await MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTime.Now.AddDays(1), cancellationToken);
var collections = await Cache.GetOrAddAsync(nameof(GetCollection) + collectionId + langCode,
() => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1));
var c = await ProcessCollection(collections);
c.Collection = c.Collection.OrderBy(x => x.ReleaseDate).ToList();
@ -105,11 +108,11 @@ namespace Ombi.Core.Engine.V2
public async Task<IEnumerable<SearchMovieViewModel>> PopularMovies()
{
var result = await Cache.GetOrAdd(CacheKeys.PopularMovies, async () =>
var result = await Cache.GetOrAddAsync(CacheKeys.PopularMovies, async () =>
{
var langCode = await DefaultLanguageCode(null);
return await MovieApi.PopularMovies(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -133,24 +136,39 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(PopularMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTime.Now.AddHours(12), cancellationToken);
var apiResult = await Cache.GetOrAddAsync(nameof(PopularMovies) + pagesToLoad.Page + langCode,
() => MovieApi.PopularMovies(langCode, pagesToLoad.Page, cancellationToken), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
}
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>
/// Gets top rated movies.
/// </summary>
/// <returns></returns>
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);
return await MovieApi.TopRated(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -167,8 +185,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(TopRatedMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.TopRated(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(TopRatedMovies) + pagesToLoad.Page + langCode,
() => MovieApi.TopRated(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -183,8 +201,32 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(NowPlayingMovies) + pagesToLoad.Page + langCode,
() => MovieApi.NowPlaying(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
}
public async Task<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));
}
return await TransformMovieResultsToResponse(results);
@ -200,16 +242,16 @@ namespace Ombi.Core.Engine.V2
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 () =>
{
return await _movieRequestEngine.GetRequests(toLoad, currentlyLoaded, new Models.UI.OrderFilterModel
{
OrderType = OrderType.RequestedDateDesc
});
}, DateTime.Now.AddMinutes(15), cancellationToken);
}, DateTimeOffset.Now.AddMinutes(15));
var movieDBResults = await Cache.GetOrAdd(nameof(RecentlyRequestedMovies) + toLoad + langCode,
var movieDBResults = await Cache.GetOrAddAsync(nameof(RecentlyRequestedMovies) + toLoad + langCode,
async () =>
{
var responses = new List<MovieResponseDto>();
@ -218,7 +260,7 @@ namespace Ombi.Core.Engine.V2
responses.Add(await MovieApi.GetMovieInformation(movie.TheMovieDbId));
}
return responses;
}, DateTime.Now.AddHours(12), cancellationToken);
}, DateTimeOffset.Now.AddHours(12));
results.AddRange(movieDBResults);
@ -232,11 +274,11 @@ namespace Ombi.Core.Engine.V2
/// <returns></returns>
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);
return await MovieApi.Upcoming(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
Logger.LogDebug("Search Result: {result}", result);
@ -254,8 +296,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(UpcomingMovies) + pagesToLoad.Page + langCode,
async () => await MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(UpcomingMovies) + pagesToLoad.Page + langCode,
() => MovieApi.Upcoming(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
return await TransformMovieResultsToResponse(results);
@ -267,11 +309,11 @@ namespace Ombi.Core.Engine.V2
/// <returns></returns>
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);
return await MovieApi.NowPlaying(langCode);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
if (result != null)
{
return await TransformMovieResultsToResponse(result.Shuffle().Take(ResultLimit)); // Take x to stop us overloading the API
@ -281,8 +323,8 @@ namespace Ombi.Core.Engine.V2
public async Task<ActorCredits> GetMoviesByActor(int actorId, string langCode)
{
var result = await Cache.GetOrAdd(nameof(GetMoviesByActor) + actorId + langCode,
async () => await MovieApi.GetActorMovieCredits(actorId, langCode));
var result = await Cache.GetOrAddAsync(nameof(GetMoviesByActor) + actorId + langCode,
() => MovieApi.GetActorMovieCredits(actorId, langCode), DateTimeOffset.Now.AddHours(12));
// Later we run this through the rules engine
return result;
}
@ -339,6 +381,14 @@ namespace Ombi.Core.Engine.V2
private async Task<MovieFullInfoViewModel> ProcessSingleMovie(FullMovieInfo 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);
// 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.Subscribed = viewMovie.Subscribed;
mapped.ShowSubscribe = viewMovie.ShowSubscribe;
mapped.DigitalReleaseDate = viewMovie.DigitalReleaseDate;
return mapped;
}
@ -389,12 +440,21 @@ namespace Ombi.Core.Engine.V2
{
if (viewMovie.ImdbId.IsNullOrEmpty())
{
var showInfo = await Cache.GetOrAdd("GetMovieInformationWIthImdbId" + viewMovie.Id,
async () => await MovieApi.GetMovieInformation(viewMovie.Id), DateTime.Now.AddHours(12));
var showInfo = await Cache.GetOrAddAsync("GetMovieInformationWIthImdbId" + viewMovie.Id,
() => MovieApi.GetMovieInformation(viewMovie.Id), DateTimeOffset.Now.AddHours(12));
viewMovie.Id = showInfo.Id; // TheMovieDbId
viewMovie.ImdbId = showInfo.ImdbId;
}
var user = await GetUser();
var digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == user.StreamingCountry);
if (digitalReleaseDate == null)
{
digitalReleaseDate = viewMovie.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US");
}
viewMovie.DigitalReleaseDate = digitalReleaseDate?.ReleaseDate?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate;
viewMovie.TheMovieDbId = viewMovie.Id.ToString();
await RunSearchRules(viewMovie);
@ -431,12 +491,12 @@ namespace Ombi.Core.Engine.V2
public async Task<MovieFullInfoViewModel> GetMovieInfoByImdbId(string imdbId, CancellationToken cancellationToken)
{
var langCode = await DefaultLanguageCode(null);
var findResult = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + imdbId + langCode,
async () => await MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTime.Now.AddHours(12), cancellationToken);
var findResult = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + imdbId + langCode,
() => MovieApi.Find(imdbId, ExternalSource.imdb_id), DateTimeOffset.Now.AddHours(12));
var movie = findResult.movie_results.FirstOrDefault();
var movieInfo = await Cache.GetOrAdd(nameof(GetMovieInfoByImdbId) + movie.id + langCode,
async () => await MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTime.Now.AddHours(12), cancellationToken);
var movieInfo = await Cache.GetOrAddAsync(nameof(GetMovieInfoByImdbId) + movie.id + langCode,
() => MovieApi.GetFullMovieInfo(movie.id, cancellationToken, langCode), DateTimeOffset.Now.AddHours(12));
return await ProcessSingleMovie(movieInfo);
}

@ -23,6 +23,8 @@ using System.Threading;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using System.Diagnostics;
using Ombi.Core.Engine.Interfaces;
using Ombi.Core.Models.UI;
namespace Ombi.Core.Engine.V2
{
@ -33,10 +35,11 @@ namespace Ombi.Core.Engine.V2
private readonly ITraktApi _traktApi;
private readonly IMovieDbApi _movieApi;
private readonly ISettingsService<CustomizationSettings> _customization;
private readonly ITvRequestEngine _requestEngine;
public TvSearchEngineV2(IPrincipal identity, IRequestServiceMain service, ITvMazeApi tvMaze, IMapper mapper,
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)
{
_tvMaze = tvMaze;
@ -44,6 +47,7 @@ namespace Ombi.Core.Engine.V2
_traktApi = trakt;
_movieApi = movieApi;
_customization = customization;
_requestEngine = requestEngine;
}
@ -56,8 +60,8 @@ namespace Ombi.Core.Engine.V2
public async Task<SearchFullInfoTvShowViewModel> GetShowInformation(string tvdbid, CancellationToken token)
{
var langCode = await DefaultLanguageCode(null);
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + langCode + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTime.Now.AddHours(12));
var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + langCode + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, langCode), DateTimeOffset.Now.AddHours(12));
if (show == null || show.name == null)
{
// We don't have enough information
@ -68,8 +72,8 @@ namespace Ombi.Core.Engine.V2
{
// There's no regional assets for this, so
// lookup the en-us version to get them
var enShow = await Cache.GetOrAdd(nameof(GetShowInformation) + "en" + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTime.Now.AddHours(12));
var enShow = await Cache.GetOrAddAsync(nameof(GetShowInformation) + "en" + tvdbid,
async () => await _movieApi.GetTVInfo(tvdbid, "en"), DateTimeOffset.Now.AddHours(12));
// For some of the more obsecure cases
if (!show.overview.HasValue())
@ -101,8 +105,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Popular) + langCode + pagesToLoad.Page,
async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Popular) + langCode + pagesToLoad.Page,
async () => await _movieApi.PopularTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
@ -118,8 +122,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Anticipated) + langCode + pagesToLoad.Page,
async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Anticipated) + langCode + pagesToLoad.Page,
async () => await _movieApi.UpcomingTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
var processed = ProcessResults(results);
@ -134,8 +138,8 @@ namespace Ombi.Core.Engine.V2
var results = new List<MovieDbSearchResult>();
foreach (var pagesToLoad in pages)
{
var apiResult = await Cache.GetOrAdd(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTime.Now.AddHours(12));
var apiResult = await Cache.GetOrAddAsync(nameof(Trending) + langCode + pagesToLoad.Page,
async () => await _movieApi.TopRatedTv(langCode, pagesToLoad.Page), DateTimeOffset.Now.AddHours(12));
results.AddRange(apiResult.Skip(pagesToLoad.Skip).Take(pagesToLoad.Take));
}
@ -164,6 +168,43 @@ namespace Ombi.Core.Engine.V2
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)
{
var retVal = new List<SearchTvShowViewModel>();
@ -178,14 +219,14 @@ namespace Ombi.Core.Engine.V2
if (settings.HideAvailableFromDiscover)
{
// To hide, we need to know if it's fully available, the only way to do this is to lookup it's episodes to check if we have every episode
var show = await Cache.GetOrAdd(nameof(GetShowInformation) + tvMazeSearch.Id.ToString(),
var show = await Cache.GetOrAddAsync(nameof(GetShowInformation) + tvMazeSearch.Id.ToString(),
async () => await _movieApi.GetTVInfo(tvMazeSearch.Id.ToString()), DateTime.Now.AddHours(12));
foreach (var tvSeason in show.seasons.Where(x => x.season_number != 0)) // skip the first season
{
var seasonEpisodes = await Cache.GetOrAdd("SeasonEpisodes" + show.id + tvSeason.season_number, async () =>
var seasonEpisodes = await Cache.GetOrAddAsync("SeasonEpisodes" + show.id + tvSeason.season_number, async () =>
{
return await _movieApi.GetSeasonEpisodes(show.id, tvSeason.season_number, CancellationToken.None);
}, DateTime.Now.AddHours(12));
}, DateTimeOffset.Now.AddHours(12));
MapSeasons(tvMazeSearch.SeasonRequests, tvSeason, seasonEpisodes);
}

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

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

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

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

@ -6,8 +6,8 @@ namespace Ombi.Helpers
{
public interface ICacheService
{
Task<T> GetOrAdd<T>(string cacheKey, Func<Task<T>> factory, DateTime absoluteExpiration = default(DateTime), CancellationToken cancellationToken = default(CancellationToken));
T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTime absoluteExpiration);
Task<T> GetOrAddAsync<T>(string cacheKey, Func<Task<T>> factory, DateTimeOffset absoluteExpiration = default);
T GetOrAdd<T>(string cacheKey, Func<T> factory, DateTimeOffset absoluteExpiration);
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>
<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.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

@ -35,6 +35,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly IEmbyApiFactory _apiFactory;
private readonly IEmbyContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification;
private IEmbyApi Api { get; set; }
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 EmbyContent");
var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri);
var totalCount = movies.TotalRecordCount;
var processed = 1;
var mediaToAdd = new HashSet<EmbyContent>();
while (processed < totalCount)
{
foreach (var movie in movies.Items)
if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled))
{
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 =
await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri);
foreach (var item in movieInfo.Items)
_logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'");
await ProcessMovies(server, movieParentIdFilder.Key);
}
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
{
processed++;
// Regular movie
await ProcessMovies(movie, mediaToAdd, server);
await ProcessMovies(server);
await ProcessTv(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(EmbyServers server, string parentId = default)
{
// 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;
processed = 1;
var processed = 1;
while (processed < totalTv)
{
foreach (var tvShow in tv.Items)
@ -162,7 +162,7 @@ namespace Ombi.Schedule.Jobs.Emby
}
}
// Get the next batch
tv = await Api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri);
tv = await Api.GetAllShows(server.ApiKey, parentId, processed, 200, server.AdministratorId, server.FullUri);
await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear();
}
@ -171,6 +171,43 @@ namespace Ombi.Schedule.Jobs.Emby
await _repo.AddRange(mediaToAdd);
}
private async Task ProcessMovies(EmbyServers server, string parentId = default)
{
var movies = await Api.GetAllMovies(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri);
var totalCount = movies.TotalRecordCount;
var processed = 1;
var mediaToAdd = new HashSet<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)
{
// Check if it exists

@ -60,6 +60,7 @@ namespace Ombi.Schedule.Jobs.Emby
private readonly ILogger<EmbyEpisodeSync> _logger;
private readonly IEmbyContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification;
private IEmbyApi Api { get; set; }
@ -72,7 +73,19 @@ namespace Ombi.Schedule.Jobs.Emby
.SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started");
foreach (var server in settings.Servers)
{
await CacheEpisodes(server);
if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled))
{
var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows");
foreach (var tvParentIdFilter in tvLibsToFilter)
{
_logger.LogInformation($"Scanning Lib for episodes '{tvParentIdFilter.Title}'");
await CacheEpisodes(server, tvParentIdFilter.Key);
}
}
else
{
await CacheEpisodes(server, string.Empty);
}
}
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
@ -81,9 +94,9 @@ namespace Ombi.Schedule.Jobs.Emby
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System");
}
private async Task CacheEpisodes(EmbyServers server)
private async Task CacheEpisodes(EmbyServers server, string parentIdFilter)
{
var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri);
var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, 0, 200, server.AdministratorId, server.FullUri);
var total = allEpisodes.TotalRecordCount;
var processed = 1;
var epToAdd = new HashSet<EmbyEpisode>();
@ -150,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Emby
await _repo.AddRange(epToAdd);
epToAdd.Clear();
allEpisodes = await Api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri);
allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, 200, server.AdministratorId, server.FullUri);
}
if (epToAdd.Any())

@ -35,6 +35,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
private readonly IJellyfinApiFactory _apiFactory;
private readonly IJellyfinContentRepository _repo;
private readonly IHubContext<NotificationHub> _notification;
private IJellyfinApi Api { get; set; }
public async Task Execute(IJobExecutionContext job)
@ -52,7 +53,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
{
try
{
await StartServerCache(server, jellyfinSettings);
await StartServerCache(server);
}
catch (Exception e)
{
@ -61,7 +62,6 @@ namespace Ombi.Schedule.Jobs.Jellyfin
_logger.LogError(e, "Exception when caching Jellyfin for server {0}", server.Name);
}
}
await _notification.Clients.Clients(NotificationHub.AdminConnectionIds)
.SendAsync(NotificationHub.NotificationEvent, "Jellyfin Content Sync Finished");
// Episodes
@ -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))
{
return;
}
//await _repo.ExecuteSql("DELETE FROM JellyfinEpisode");
//await _repo.ExecuteSql("DELETE FROM JellyfinContent");
var movies = await Api.GetAllMovies(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri);
var totalCount = movies.TotalRecordCount;
var processed = 1;
var mediaToAdd = new HashSet<JellyfinContent>();
while (processed < totalCount)
{
foreach (var movie in movies.Items)
if (server.JellyfinSelectedLibraries.Any() && server.JellyfinSelectedLibraries.Any(x => x.Enabled))
{
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 =
await Api.GetCollection(movie.Id, server.ApiKey, server.AdministratorId, server.FullUri);
foreach (var item in movieInfo.Items)
_logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'");
await ProcessMovies(server, movieParentIdFilder.Key);
}
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
{
processed++;
// Regular movie
await ProcessMovies(movie, mediaToAdd, server);
}
await ProcessMovies(server);
await ProcessTv(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
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;
processed = 1;
var processed = 1;
while (processed < totalTv)
{
foreach (var tvShow in tv.Items)
@ -162,13 +162,52 @@ namespace Ombi.Schedule.Jobs.Jellyfin
}
}
// Get the next batch
tv = await Api.GetAllShows(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri);
tv = await Api.GetAllShows(server.ApiKey, parentId, processed, 200, server.AdministratorId, server.FullUri);
await _repo.AddRange(mediaToAdd);
mediaToAdd.Clear();
}
if (mediaToAdd.Any())
{
await _repo.AddRange(mediaToAdd);
}
}
private async Task ProcessMovies(JellyfinServers server, string parentId = default)
{
var movies = await Api.GetAllMovies(server.ApiKey, parentId, 0, 200, server.AdministratorId, server.FullUri);
var totalCount = movies.TotalRecordCount;
var processed = 1;
var mediaToAdd = new HashSet<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);
mediaToAdd.Clear();
}
}
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");
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)
@ -81,9 +94,9 @@ namespace Ombi.Schedule.Jobs.Jellyfin
await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System");
}
private async Task CacheEpisodes(JellyfinServers server)
private async Task CacheEpisodes(JellyfinServers server, string parentIdFilter)
{
var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, 0, 200, server.AdministratorId, server.FullUri);
var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, 0, 200, server.AdministratorId, server.FullUri);
var total = allEpisodes.TotalRecordCount;
var processed = 1;
var epToAdd = new HashSet<JellyfinEpisode>();
@ -150,7 +163,7 @@ namespace Ombi.Schedule.Jobs.Jellyfin
await _repo.AddRange(epToAdd);
epToAdd.Clear();
allEpisodes = await Api.GetAllEpisodes(server.ApiKey, processed, 200, server.AdministratorId, server.FullUri);
allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, 200, server.AdministratorId, server.FullUri);
}
if (epToAdd.Any())

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

@ -130,12 +130,11 @@ namespace Ombi.Schedule.Jobs.Ombi
var jellyfinContent = _jellyfin.GetAll().Include(x => x.Episodes).AsNoTracking();
var lidarrContent = _lidarrAlbumRepository.GetAll().AsNoTracking().ToList().Where(x => x.FullyAvailable);
var addedLog = _recentlyAddedLog.GetAll();
var addedLog = _recentlyAddedLog.GetAll().ToList();
var addedPlexMovieLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Parent)?.Select(x => x.ContentId)?.ToHashSet() ?? new HashSet<int>();
var addedEmbyMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Emby && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedJellyfinMoviesLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Jellyfin && x.ContentType == ContentType.Parent).Select(x => x.ContentId).ToHashSet();
var addedAlbumLogIds = addedLog.Where(x => x.Type == RecentlyAddedType.Lidarr && x.ContentType == ContentType.Album).Select(x => x.AlbumId).ToHashSet();
HashSet<int> addedPlexMovieLogIds, addedEmbyMoviesLogIds, addedJellyfinMoviesLogIds;
HashSet<string> addedAlbumLogIds;
GetRecentlyAddedMoviesData(addedLog, out addedPlexMovieLogIds, out addedEmbyMoviesLogIds, out addedJellyfinMoviesLogIds, out addedAlbumLogIds);
var addedPlexEpisodesLogIds =
addedLog.Where(x => x.Type == RecentlyAddedType.Plex && x.ContentType == ContentType.Episode);
@ -375,6 +374,21 @@ namespace Ombi.Schedule.Jobs.Ombi
.SendAsync(NotificationHub.NotificationEvent, "Newsletter Finished");
}
private void GetRecentlyAddedMoviesData(List<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)
{
if (!applicationUrl.HasValue())
@ -487,7 +501,7 @@ namespace Ombi.Schedule.Jobs.Ombi
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>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -504,7 +518,7 @@ namespace Ombi.Schedule.Jobs.Ombi
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>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -521,7 +535,7 @@ namespace Ombi.Schedule.Jobs.Ombi
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>();
foreach (var ep in source.Where(x => x.Series.HasTvDb))
@ -558,7 +572,7 @@ namespace Ombi.Schedule.Jobs.Ombi
var plexMovies = plexContentToSend.Where(x => x.Type == PlexMediaTypeEntity.Movie);
var embyMovies = embyContentToSend.Where(x => x.Type == EmbyMediaType.Movie);
var jellyfinMovies = jellyfinContentToSend.Where(x => x.Type == JellyfinMediaType.Movie);
if ((plexMovies.Any() || embyMovies.Any()) || jellyfinMovies.Any() && !settings.DisableMovies)
if ((plexMovies.Any() || embyMovies.Any() || jellyfinMovies.Any()) && !settings.DisableMovies)
{
sb.Append("<h1 style=\"text-align: center; max-width: 1042px;\">New Movies</h1><br /><br />");
sb.Append(
@ -589,7 +603,7 @@ namespace Ombi.Schedule.Jobs.Ombi
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(

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

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

@ -17,5 +17,14 @@ namespace Ombi.Core.Settings.Models.External
public string AdministratorId { get; set; }
public string ServerHostname { get; set; }
public bool EnableEpisodeSearching { get; set; }
public List<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 ServerHostname { 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;
return model;
}, DateTime.Now.AddHours(2));
}, DateTimeOffset.Now.AddHours(2));
}
public async Task<T> GetSettingsAsync()
{
return await _cache.GetOrAdd(CacheName, async () =>
return await _cache.GetOrAddAsync(CacheName, async () =>
{
var result = await Repo.GetAsync(EntityName);
if (result == null)
@ -61,7 +61,7 @@ namespace Ombi.Settings.Settings
var model = obj;
return model;
}, DateTime.Now.AddHours(5));
}, DateTimeOffset.Now.AddHours(5));
}
public bool SaveSettings(T model)

@ -18,6 +18,7 @@ namespace Ombi.Api.TheMovieDb
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>> 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<MovieDbSearchResult>> TopRated(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<FullMovieInfo> GetFullMovieInfo(int movieId, CancellationToken cancellationToken, string langCode);
Task<Collections> GetCollection(string langCode, int collectionId, CancellationToken cancellationToken);
Task<List<Keyword>> SearchKeyword(string searchTerm);
Task<Keyword> GetKeyword(int keywordId);
Task<List<TheMovidDbKeyValue>> SearchKeyword(string searchTerm);
Task<TheMovidDbKeyValue> GetKeyword(int keywordId);
Task<WatchProviders> GetMovieWatchProviders(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
{
public sealed class Keyword
public sealed class TheMovidDbKeyValue
{
[DataMember(Name = "id")]
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);
}
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)
{
// https://developers.themoviedb.org/3/discover/movie-discover
@ -331,34 +359,71 @@ namespace Ombi.Api.TheMovieDb
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);
request.AddQueryString("api_key", ApiToken);
request.AddQueryString("query", searchTerm);
AddRetry(request);
var result = await Api.Request<TheMovieDbContainer<Keyword>>(request);
return result.results ?? new List<Keyword>();
var result = await Api.Request<TheMovieDbContainer<TheMovidDbKeyValue>>(request);
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);
request.AddQueryString("api_key", ApiToken);
AddRetry(request);
var keyword = await Api.Request<Keyword>(request);
var keyword = await Api.Request<TheMovidDbKeyValue>(request);
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);
request.AddQueryString("api_key", ApiToken);
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>();
}

@ -28,7 +28,6 @@ export class AppComponent implements OnInit {
public showNav: boolean;
public updateAvailable: boolean;
public currentUrl: string;
public userAccessToken: string;
public voteEnabled = false;
public applicationName: string = "Ombi"
public isAdmin: boolean;
@ -51,7 +50,7 @@ export class AppComponent implements OnInit {
private readonly identity: IdentityService,
@Inject(DOCUMENT) private document: HTMLDocument) {
this.translate.addLangs(["en", "de", "fr", "da", "es", "it", "nl", "sk", "sv", "no", "pl", "pt"]);
this.translate.addLangs(["en", "de", "fr", "da", "es", "it", "nl", "sk", "sv", "no", "pl", "ru", "pt"]);
if (this.authService.loggedIn()) {
this.user = this.authService.claims();
@ -79,7 +78,7 @@ export class AppComponent implements OnInit {
// See if we can match the supported langs with the current browser lang
const browserLang: string = translate.getBrowserLang();
this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|sk|sv|no|pl|pt/) ? browserLang : "en");
this.translate.use(browserLang.match(/en|fr|da|de|es|it|nl|ru|sk|sv|no|pl|pt/) ? browserLang : "en");
}

@ -1,73 +1,98 @@
import { CommonModule, PlatformLocation, APP_BASE_HREF } from "@angular/common";
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
import { NgModule } from "@angular/core";
import { APP_BASE_HREF, CommonModule, PlatformLocation } from "@angular/common";
import { CardsFreeModule, MDBBootstrapModule, NavbarModule } from "angular-bootstrap-md";
import { CustomPageService, ImageService, RequestService, SettingsService } from "./services";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from "@angular/common/http";
import { IdentityService, IssuesService, JobService, MessageService, PlexTvService, SearchService, StatusService } from "./services";
import { RouterModule, Routes } from "@angular/router";
import { JwtModule } from "@auth0/angular-jwt";
import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { CookieService } from "ng2-cookies";
import { AppComponent } from "./app.component";
import { AuthGuard } from "./auth/auth.guard";
import { AuthService } from "./auth/auth.service";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { BrowserModule } from "@angular/platform-browser";
import { ButtonModule } from "primeng/button";
import { ConfirmDialogModule } from "primeng/confirmdialog";
import { CookieComponent } from "./auth/cookie.component";
import { CookieService } from "ng2-cookies";
import { CustomPageComponent } from "./custompage/custompage.component";
import { DataViewModule } from "primeng/dataview";
import { DialogModule } from "primeng/dialog";
import { OverlayPanelModule } from "primeng/overlaypanel";
import { TooltipModule } from "primeng/tooltip";
import { SidebarModule } from "primeng/sidebar";
import { FilterService } from "./discover/services/filter-service";
import { JwtModule } from "@auth0/angular-jwt";
import { LandingPageComponent } from "./landingpage/landingpage.component";
import { LandingPageService } from "./services";
import { LayoutModule } from '@angular/cdk/layout';
import { LoginComponent } from "./login/login.component";
import { LoginOAuthComponent } from "./login/loginoauth.component";
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from "@angular/material/card";
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatNativeDateModule } from '@angular/material/core';
import { MatChipsModule } from "@angular/material/chips";
import { MatDialogModule } from "@angular/material/dialog";
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from "@angular/material/input";
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from "@angular/material/menu";
import { MatNativeDateModule } from '@angular/material/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCardModule } from "@angular/material/card";
import { MatInputModule } from "@angular/material/input";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTabsModule } from "@angular/material/tabs";
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from "@angular/material/tooltip";
import { MyNavComponent } from './my-nav/my-nav.component';
import { NavSearchComponent } from "./my-nav/nav-search.component";
import { NgModule } from "@angular/core";
import { NotificationService } from "./services";
import { OverlayModule } from "@angular/cdk/overlay";
import { OverlayPanelModule } from "primeng/overlaypanel";
import { PageNotFoundComponent } from "./errors/not-found.component";
import { RemainingRequestsComponent } from "./shared/remaining-requests/remaining-requests.component";
import { ResetPasswordComponent } from "./login/resetpassword.component";
import { SearchV2Service } from "./services/searchV2.service";
import { SidebarModule } from "primeng/sidebar";
import { SignalRNotificationService } from "./services/signlarnotification.service";
import { StorageService } from "./shared/storage/storage-service";
import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component";
import { TooltipModule } from "primeng/tooltip";
import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor";
// Components
import { MDBBootstrapModule, CardsFreeModule, NavbarModule } from "angular-bootstrap-md";
// Components
import { AppComponent } from "./app.component";
import { CookieComponent } from "./auth/cookie.component";
import { CustomPageComponent } from "./custompage/custompage.component";
import { PageNotFoundComponent } from "./errors/not-found.component";
import { LandingPageComponent } from "./landingpage/landingpage.component";
import { LoginComponent } from "./login/login.component";
import { LoginOAuthComponent } from "./login/loginoauth.component";
import { ResetPasswordComponent } from "./login/resetpassword.component";
import { TokenResetPasswordComponent } from "./login/tokenresetpassword.component";
// Services
import { AuthGuard } from "./auth/auth.guard";
import { AuthService } from "./auth/auth.service";
import { ImageService, SettingsService, CustomPageService, RequestService } from "./services";
import { LandingPageService } from "./services";
import { NotificationService } from "./services";
import { IssuesService, JobService, PlexTvService, StatusService, SearchService, IdentityService, MessageService } from "./services";
import { MyNavComponent } from './my-nav/my-nav.component';
import { LayoutModule } from '@angular/cdk/layout';
import { SearchV2Service } from "./services/searchV2.service";
import { NavSearchComponent } from "./my-nav/nav-search.component";
import { OverlayModule } from "@angular/cdk/overlay";
import { StorageService } from "./shared/storage/storage-service";
import { SignalRNotificationService } from "./services/signlarnotification.service";
import { MatMenuModule } from "@angular/material/menu";
import { RemainingRequestsComponent } from "./shared/remaining-requests/remaining-requests.component";
import { UnauthorizedInterceptor } from "./auth/unauthorized.interceptor";
import { FilterService } from "./discover/services/filter-service";
const routes: Routes = [
{ path: "*", component: PageNotFoundComponent },
@ -135,6 +160,8 @@ export function JwtTokenGetter() {
MatMenuModule,
MatInputModule,
MatTabsModule,
MatChipsModule,
MatDialogModule,
ReactiveFormsModule,
MatAutocompleteModule,
TooltipModule,
@ -146,7 +173,6 @@ export function JwtTokenGetter() {
MatCheckboxModule,
MatProgressSpinnerModule,
MDBBootstrapModule.forRoot(),
// NbThemeModule.forRoot({ name: 'dark'}),
JwtModule.forRoot({
config: {
tokenGetter: JwtTokenGetter,

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

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

@ -1,4 +1,12 @@
<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">
<h2>{{'Discovery.PopularTab' | translate}}</h2>
<div>

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

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

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

@ -1,14 +1,15 @@
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { SearchV2Service } from "../../../services";
import { IDiscoverCardResult } from "../../interfaces";
import { IMultiSearchResult, RequestType } from "../../../interfaces";
import { IMultiSearchResult, ISearchMovieResult, RequestType } from "../../../interfaces";
import { AdvancedSearchDialogDataService } from "../../../shared/advanced-search-dialog/advanced-search-dialog-data.service";
import { AuthService } from "../../../auth/auth.service";
import { FilterService } from "../../services/filter-service";
import { IDiscoverCardResult } from "../../interfaces";
import { SearchFilter } from "../../../my-nav/SearchFilter";
import { SearchV2Service } from "../../../services";
import { StorageService } from "../../../shared/storage/storage-service";
import { isEqual } from "lodash";
import { AuthService } from "../../../auth/auth.service";
@Component({
templateUrl: "./search-results.component.html",
@ -25,22 +26,41 @@ export class DiscoverSearchResultsComponent implements OnInit {
public filter: SearchFilter;
private isAdvancedSearch: boolean;
constructor(private searchService: SearchV2Service,
private route: ActivatedRoute,
private filterService: FilterService,
private router: Router,
private advancedDataService: AdvancedSearchDialogDataService,
private store: StorageService,
private authService: AuthService) {
this.route.params.subscribe((params: any) => {
this.isAdvancedSearch = this.router.url === '/discover/advanced/search';
if (this.isAdvancedSearch) {
this.loadAdvancedData();
return;
}
this.searchTerm = params.searchTerm;
this.clear();
this.init();
});
this.advancedDataService.onDataChange.subscribe(() => {
this.clear();
this.loadAdvancedData();
});
}
public async ngOnInit() {
this.loadingFlag = true;
this.isAdmin = this.authService.isAdmin();
if (this.advancedDataService) {
return;
}
this.loadingFlag = true;
this.filterService.onFilterChange.subscribe(async x => {
if (!isEqual(this.filter, x)) {
this.filter = { ...x };
@ -115,6 +135,48 @@ export class DiscoverSearchResultsComponent implements OnInit {
this.discoverResults = [];
}
private loadAdvancedData() {
const advancedData = this.advancedDataService.getData();
this.mapAdvancedData(advancedData);
return;
}
public mapAdvancedData(advancedData: ISearchMovieResult[]) {
this.finishLoading();
const type = this.advancedDataService.getType();
advancedData.forEach(m => {
let mediaType = type;
let poster = `https://image.tmdb.org/t/p/w300/${m.posterPath}`;
if (!m.posterPath) {
if (mediaType === RequestType.movie) {
poster = "images/default_movie_poster.png"
}
if (mediaType === RequestType.tvShow) {
poster = "images/default_tv_poster.png"
}
}
this.discoverResults.push({
posterPath: poster,
requested: false,
title: m.title,
type: mediaType,
id: m.id,
url: "",
rating: 0,
overview: m.overview,
approved: false,
imdbid: "",
denied: false,
background: "",
available: false,
tvMovieDb: false
});
});
}
private async search() {
this.clear();
this.results = await this.searchService

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

@ -2,3 +2,18 @@
id: number;
name: string;
}
export interface IWatchProvidersResults {
provider_id: number;
logo_path: string;
provider_name: string;
}
export interface IDiscoverModel {
type: string;
releaseYear?: number|undefined;
genreIds?: number[];
keywordIds?: number[];
watchProviders?: number[];
companies?: number[];
}

@ -45,6 +45,7 @@ export interface IEmbyServer extends IExternalSettings {
administratorId: string;
enableEpisodeSearching: boolean;
serverHostname: string;
embySelectedLibraries: IEmbyLibrariesSettings[];
}
export interface IPublicInfo {
@ -64,6 +65,37 @@ export interface IJellyfinServer extends IExternalSettings {
administratorId: string;
enableEpisodeSearching: boolean;
serverHostname: string;
jellyfinSelectedLibraries: IJellyfinLibrariesSettings[];
}
export interface IJellyfinLibrariesSettings {
key: string;
title: string;
enabled: boolean;
collectionType: string;
}
export interface 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 {

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

@ -82,6 +82,11 @@
<i class="fas fa-plus"></i> {{ 'Requests.MarkAvailable' | translate }}
</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()">
<i class="fas fa-times"></i> {{'Requests.Deny' | translate }}
</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) {
this.advancedOptions = data;
if (data.rootFolderId) {

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

@ -9,7 +9,8 @@
<div class="row">
<div
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>
</h1>

@ -13,6 +13,12 @@ export class TopBannerComponent {
@Input() available: boolean;
@Input() background: any;
get releaseDateFormat(): Date|null {
if (this.releaseDate && this.releaseDate instanceof Date && this.releaseDate.getFullYear() !== 1) {
return this.releaseDate;
}
return null;
}
constructor(private sanitizer:DomSanitizer){ }

@ -78,6 +78,7 @@
<mat-slide-toggle id="filterMusic" class="mat-menu-item slide-menu" [checked]="searchFilter.music"
(click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.Music)">
{{ '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"
(click)="$event.stopPropagation()" (change)="changeFilter($event,SearchFilterType.People)">
{{ 'NavigationBar.Filter.People' | translate}}</mat-slide-toggle> -->

@ -231,3 +231,17 @@
padding:0;
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 { SettingsService, SettingsStateService } from '../services';
import { AdvancedSearchDialogComponent } from '../shared/advanced-search-dialog/advanced-search-dialog.component';
import { FilterService } from '../discover/services/filter-service';
import { ILocalUser } from '../auth/IUserLogin';
import { INavBar } from '../interfaces/ICommon';
import { MatDialog } from '@angular/material/dialog';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Md5 } from 'ts-md5/dist/md5';
import { Observable } from 'rxjs';
import { Router } from '@angular/router';
import { SearchFilter } from './SearchFilter';
import { StorageService } from '../shared/storage/storage-service';
import { map } from 'rxjs/operators';
@ -54,7 +57,9 @@ export class MyNavComponent implements OnInit {
private settingsService: SettingsService,
private store: StorageService,
private filterService: FilterService,
private readonly settingState: SettingsStateService) {
private dialogService: MatDialog,
private readonly settingState: SettingsStateService,
private router: Router) {
}
public async ngOnInit() {
@ -121,6 +126,18 @@ export class MyNavComponent implements OnInit {
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 {
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}`;

@ -5,7 +5,7 @@ import { Observable } from "rxjs";
import { ServiceHelpers } from "../service.helpers";
import { IEmbyServer, IEmbySettings, IPublicInfo, IUsersModel } from "../../interfaces";
import { IEmbyLibrary, IEmbyServer, IEmbySettings, IMediaServerMediaContainer, IPublicInfo, IUsersModel } from "../../interfaces";
@Injectable()
export class EmbyService extends ServiceHelpers {
@ -25,4 +25,8 @@ export class EmbyService extends ServiceHelpers {
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 { IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces";
import { IEmbyServer, IMediaServerMediaContainer, IJellyfinLibrary, IJellyfinServer, IJellyfinSettings, IPublicInfo, IUsersModel } from "../../interfaces";
@Injectable()
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});
}
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-Platform": "Web",
"Accept": "application/json",
'X-Plex-Model': 'Plex OAuth',
});
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 { catchError } from "rxjs/operators";
import { IMovieDbKeyword } from "../../interfaces";
import { IMovieDbKeyword, IWatchProvidersResults } from "../../interfaces";
import { ServiceHelpers } from "../service.helpers";
@Injectable()
@Injectable({
providedIn: 'root',
})
export class TheMovieDbService extends ServiceHelpers {
constructor(http: HttpClient, @Inject(APP_BASE_HREF) href:string) {
super(http, "/api/v1/TheMovieDb", href);
@ -26,4 +28,8 @@ export class TheMovieDbService extends ServiceHelpers {
public getGenres(media: string): Observable<IMovieDbKeyword[]> {
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 { Observable } from "rxjs";
import { IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces";
import { IDiscoverModel, IMultiSearchResult, ISearchMovieResult, ISearchTvResult } from "../interfaces";
import { ServiceHelpers } from "./service.helpers";
import { ISearchMovieResultV2 } from "../interfaces/ISearchMovieResultV2";
@ -51,6 +51,10 @@ export class SearchV2Service extends ServiceHelpers {
return this.http.get<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[]> {
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();
}
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[]> {
return this.http.get<ISearchMovieResult[]>(`${this.url}/Movie/nowplaying`);
}

@ -1,4 +1,4 @@
<settings-menu>
<settings-menu>
</settings-menu>
<div class="small-middle-container">
<div *ngIf="form">
@ -55,7 +55,7 @@
<mat-form-field appearance="outline">
<mat-label>Quality Profiles</mat-label>
<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-error>A Default Quality Profile is required</mat-error>
</mat-form-field>

@ -9,12 +9,10 @@
<div class="row">
<div class="col-md-6 col-6 col-sm-6">
<div style="float:right;text-align:left;">
<div class="md-form-field">
<mat-slide-toggle [(ngModel)]="settings.enable" (change)="toggle()" [checked]="settings.enable">Enable</mat-slide-toggle>
</div>
</div>
</div>
<mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable
</mat-slide-toggle>
</div> </div>
</div>
<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">
@ -74,7 +72,28 @@
<span *ngIf="!server.serverHostname">Current URL: "https://app.emby.media/#!/item/item.html?id=1</span>
</small>
</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>

@ -1,9 +1,9 @@
import { Component, OnInit } from "@angular/core";
import { IEmbyServer, IEmbySettings } from "../../interfaces";
import { EmbyService, JobService, NotificationService, SettingsService, TesterService } from "../../services";
import { MatTabChangeEvent } from "@angular/material/tabs";
import { IEmbyLibrariesSettings, IEmbyServer, IEmbySettings } from "../../interfaces";
import {FormControl} from '@angular/forms';
import { MatTabChangeEvent } from "@angular/material/tabs";
@Component({
templateUrl: "./emby.component.html",
@ -100,4 +100,28 @@ export class EmbyComponent implements OnInit {
}
});
}
public loadLibraries(server: IEmbyServer) {
if (server.ip == null) {
this.notificationService.error("Emby is not yet configured correctly");
return;
}
this.embyService.getLibraries(server).subscribe(x => {
server.embySelectedLibraries = [];
if (x.totalRecordCount > 0) {
x.items.forEach((item) => {
const lib: IEmbyLibrariesSettings = {
key: item.id,
title: item.name,
enabled: false,
collectionType: item.collectionType
};
server.embySelectedLibraries.push(lib);
});
} else {
this.notificationService.error("Couldn't find any libraries");
}
},
err => { this.notificationService.error(err); });
}
}

@ -9,11 +9,11 @@
<div class="row">
<div class="col-md-6 col-6 col-sm-6">
<div style="float:right;text-align:left;">
<div class="md-form-field">
<mat-slide-toggle [(ngModel)]="settings.enable" (change)="toggle()" [checked]="settings.enable">Enable</mat-slide-toggle>
</div>
<mat-slide-toggle [(ngModel)]="settings.enable" [checked]="settings.enable">Enable
</mat-slide-toggle>
</div>
</div>
</div>
<mat-tab-group #tabGroup [selectedIndex]="selected.value" (selectedTabChange)="addTab($event)" (selectedIndexChange)="selected.setValue($event)" animationDuration="0ms" style="display:block;">
@ -75,7 +75,28 @@
</small>
</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>
<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 { IJellyfinServer, IJellyfinSettings } from "../../interfaces";
import { IEmbyServer, IJellyfinLibrariesSettings, IJellyfinServer, IJellyfinSettings } from "../../interfaces";
import { JellyfinService, JobService, NotificationService, SettingsService, TesterService } from "../../services";
import { MatTabChangeEvent } from "@angular/material/tabs";
import {FormControl} from '@angular/forms';
import { MatTabChangeEvent } from "@angular/material/tabs";
@Component({
templateUrl: "./jellyfin.component.html",
@ -101,4 +101,28 @@ export class JellyfinComponent implements OnInit {
}
});
}
public loadLibraries(server: IJellyfinServer) {
if (server.ip == null) {
this.notificationService.error("Jellyfin is not yet configured correctly");
return;
}
this.jellyfinService.getLibraries(server).subscribe(x => {
server.jellyfinSelectedLibraries = [];
if (x.totalRecordCount > 0) {
x.items.forEach((item) => {
const lib: IJellyfinLibrariesSettings = {
key: item.id,
title: item.name,
enabled: false,
collectionType: item.collectionType
};
server.jellyfinSelectedLibraries.push(lib);
});
} else {
this.notificationService.error("Couldn't find any libraries");
}
},
err => { this.notificationService.error(err); });
}
}

@ -1,13 +1,13 @@
import {COMMA, ENTER} from "@angular/cdk/keycodes";
import { Component, OnInit, ElementRef, ViewChild } from "@angular/core";
import { MatAutocomplete } from "@angular/material/autocomplete";
import { Component, ElementRef, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { IMovieDbKeyword, ITheMovieDbSettings } from "../../interfaces";
import { debounceTime, switchMap } from "rxjs/operators";
import { ITheMovieDbSettings, IMovieDbKeyword } from "../../interfaces";
import { MatAutocomplete } from "@angular/material/autocomplete";
import { NotificationService } from "../../services";
import { SettingsService } from "../../services";
import { TheMovieDbService } from "../../services";
import { FormBuilder, FormGroup } from "@angular/forms";
import { debounceTime, switchMap } from "rxjs/operators";
interface IKeywordTag {
id: number;
@ -30,8 +30,6 @@ export class TheMovieDbComponent implements OnInit {
public filteredMovieGenres: IMovieDbKeyword[];
public filteredTvGenres: IMovieDbKeyword[];
@ViewChild('fruitInput') public fruitInput: ElementRef<HTMLInputElement>;
constructor(private settingsService: SettingsService,
private notificationService: NotificationService,
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 { TranslateModule } from "@ngx-translate/core";
import { TruncateModule } from "@yellowspot/ng-truncate";
import { MomentModule } from "ngx-moment";
import { IssuesReportComponent } from "./issues-report.component";
import { SidebarModule } from "primeng/sidebar";
import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component";
import { AdvancedSearchDialogComponent } from "./advanced-search-dialog/advanced-search-dialog.component";
import { CommonModule } from "@angular/common";
import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component";
import { EpisodeRequestComponent } from "./episode-request/episode-request.component";
import { GenreSelectComponent } from "./components/genre-select/genre-select.component";
import { InputSwitchModule } from "primeng/inputswitch";
import { IssuesReportComponent } from "./issues-report.component";
import { KeywordSearchComponent } from "./components/keyword-search/keyword-search.component";
import { MatAutocompleteModule } from "@angular/material/autocomplete";
import { MatButtonModule } from '@angular/material/button';
import { MatNativeDateModule } from '@angular/material/core';
import { MatCardModule } from "@angular/material/card";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatChipsModule } from "@angular/material/chips";
import { MatDialogModule } from "@angular/material/dialog";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from "@angular/material/input";
import { MatListModule } from '@angular/material/list';
import {MatMenuModule} from '@angular/material/menu';
import { MatNativeDateModule } from '@angular/material/core';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import {MatRadioModule} from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTableModule } from '@angular/material/table';
import {MatMenuModule} from '@angular/material/menu';
import { MatTabsModule } from "@angular/material/tabs";
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
import { MatAutocompleteModule } from "@angular/material/autocomplete";
import { MatCardModule } from "@angular/material/card";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatChipsModule } from "@angular/material/chips";
import { MatDialogModule } from "@angular/material/dialog";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatInputModule } from "@angular/material/input";
import { MatProgressSpinnerModule } from "@angular/material/progress-spinner";
import { MatSlideToggleModule } from "@angular/material/slide-toggle";
import { MatTabsModule } from "@angular/material/tabs";
import { EpisodeRequestComponent } from "./episode-request/episode-request.component";
import { DetailsGroupComponent } from "../issues/components/details-group/details-group.component";
import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-request-dialog.component";
import { MomentModule } from "ngx-moment";
import { NgModule } from "@angular/core";
import { SidebarModule } from "primeng/sidebar";
import { TheMovieDbService } from "../services";
import { TranslateModule } from "@ngx-translate/core";
import { TruncateModule } from "@yellowspot/ng-truncate";
import { WatchProvidersSelectComponent } from "./components/watch-providers-select/watch-providers-select.component";
@NgModule({
declarations: [
@ -45,6 +49,10 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques
EpisodeRequestComponent,
DetailsGroupComponent,
AdminRequestDialogComponent,
AdvancedSearchDialogComponent,
KeywordSearchComponent,
GenreSelectComponent,
WatchProvidersSelectComponent,
],
imports: [
SidebarModule,
@ -59,6 +67,7 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques
MatAutocompleteModule,
MatInputModule,
MatTabsModule,
MatRadioModule,
MatButtonModule,
MatNativeDateModule,
MatChipsModule,
@ -89,6 +98,10 @@ import { AdminRequestDialogComponent } from "./admin-request-dialog/admin-reques
IssuesReportComponent,
EpisodeRequestComponent,
AdminRequestDialogComponent,
AdvancedSearchDialogComponent,
GenreSelectComponent,
KeywordSearchComponent,
WatchProvidersSelectComponent,
DetailsGroupComponent,
TruncateModule,
InputSwitchModule,

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

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

@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.Emby;
using Ombi.Api.Emby.Models;
using Ombi.Api.Plex;
using Ombi.Api.Emby.Models.Media;
using Ombi.Attributes;
using Ombi.Core.Settings;
using Ombi.Core.Settings.Models.External;
@ -92,5 +92,51 @@ namespace Ombi.Controllers.V1.External
// Filter out any dupes
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.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
@ -66,6 +67,52 @@ namespace Ombi.Controllers.V1.External
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>
/// Gets the jellyfin users.
/// </summary>

@ -69,7 +69,7 @@ namespace Ombi.Controllers.V1.External
[HttpGet("Profiles")]
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();
if (settings.Enabled)
@ -77,7 +77,7 @@ namespace Ombi.Controllers.V1.External
return await _lidarrApi.GetProfiles(settings.ApiKey, settings.FullUri);
}
return null;
}, DateTime.Now.AddHours(1));
}, DateTimeOffset.Now.AddHours(1));
}
/// <summary>
@ -88,7 +88,7 @@ namespace Ombi.Controllers.V1.External
[HttpGet("RootFolders")]
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();
if (settings.Enabled)
@ -96,7 +96,7 @@ namespace Ombi.Controllers.V1.External
return await _lidarrApi.GetRootFolders(settings.ApiKey, settings.FullUri);
}
return null;
}, DateTime.Now.AddHours(1));
}, DateTimeOffset.Now.AddHours(1));
}
}
}

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ombi.Api.TheMovieDb;
using Ombi.Api.TheMovieDb.Models;
using Ombi.Attributes;
using System.Collections.Generic;
using System.Threading.Tasks;
@ -11,10 +11,10 @@ using Genre = Ombi.TheMovieDbApi.Models.Genre;
namespace Ombi.Controllers.External
{
[Admin]
[ApiV1]
[Authorize]
[Produces("application/json")]
public sealed class TheMovieDbController : Controller
public sealed class TheMovieDbController : ControllerBase
{
public TheMovieDbController(IMovieDbApi tmdbApi) => TmdbApi = tmdbApi;
@ -25,7 +25,7 @@ namespace Ombi.Controllers.External
/// </summary>
/// <param name="searchTerm">The search term.</param>
[HttpGet("Keywords")]
public async Task<IEnumerable<Keyword>> GetKeywords([FromQuery]string searchTerm) =>
public async Task<IEnumerable<TheMovidDbKeyValue>> GetKeywords([FromQuery]string searchTerm) =>
await TmdbApi.SearchKeyword(searchTerm);
/// <summary>
@ -36,7 +36,7 @@ namespace Ombi.Controllers.External
public async Task<IActionResult> GetKeywords(int keywordId)
{
var keyword = await TmdbApi.GetKeyword(keywordId);
return keyword == null ? NotFound() : (IActionResult)Ok(keyword);
return keyword == null ? NotFound() : Ok(keyword);
}
/// <summary>
@ -45,6 +45,24 @@ namespace Ombi.Controllers.External
/// <param name="media">Either `tv` or `movie`.</param>
[HttpGet("Genres/{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]
public async Task<IEnumerable<UserViewModelDropdown>> GetAllUsersDropdown()
{
var users = await _cacheService.GetOrAdd(CacheKeys.UsersDropdown,
async () => await UserManager.Users.Where(x => x.UserType != UserType.SystemUser).ToListAsync());
var users = await _cacheService.GetOrAddAsync(CacheKeys.UsersDropdown,
() => UserManager.Users.Where(x => x.UserType != UserType.SystemUser).ToListAsync());
var model = new List<UserViewModelDropdown>();

@ -45,9 +45,9 @@ namespace Ombi.Controllers.V1
{
return string.Empty;
}
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1));
var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvdbid}", async () => await FanartTvApi.GetTvImages(tvdbid, key.Value), DateTime.Now.AddDays(1));
var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvdbid}", () => FanartTvApi.GetTvImages(tvdbid, key.Value), DateTimeOffset.Now.AddDays(1));
if (images == null)
{
return string.Empty;
@ -70,7 +70,7 @@ namespace Ombi.Controllers.V1
[HttpGet("poster")]
public async Task<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 val = rand.Next(1, 3);
if (val == 1)
@ -79,7 +79,7 @@ namespace Ombi.Controllers.V1
var selectedMovieIndex = rand.Next(movies.Count());
var movie = movies[selectedMovieIndex];
var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{movie.Id}", async () => await FanartTvApi.GetMovieImages(movie.Id.ToString(), key.Value), DateTime.Now.AddDays(1));
var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{movie.Id}", () => FanartTvApi.GetMovieImages(movie.Id.ToString(), key.Value), DateTimeOffset.Now.AddDays(1));
if (images == null)
{
return string.Empty;
@ -114,9 +114,9 @@ namespace Ombi.Controllers.V1
[HttpGet("poster/movie/{movieDbId}")]
public async Task<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)
{
@ -148,9 +148,9 @@ namespace Ombi.Controllers.V1
{
return string.Empty;
}
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1));
var images = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvdbid}", async () => await FanartTvApi.GetTvImages(tvdbid, key.Value), DateTime.Now.AddDays(1));
var images = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvdbid}", () => FanartTvApi.GetTvImages(tvdbid, key.Value), DateTimeOffset.Now.AddDays(1));
if (images == null)
{
@ -178,9 +178,9 @@ namespace Ombi.Controllers.V1
[HttpGet("background/movie/{movieDbId}")]
public async Task<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)
{
@ -203,9 +203,9 @@ namespace Ombi.Controllers.V1
[HttpGet("banner/movie/{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)
{
@ -246,17 +246,17 @@ namespace Ombi.Controllers.V1
var movieUrl = string.Empty;
var tvUrl = string.Empty;
var key = await _cache.GetOrAdd(CacheKeys.FanartTv, async () => await Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTime.Now.AddDays(1));
var key = await _cache.GetOrAddAsync(CacheKeys.FanartTv, () => Config.GetAsync(Store.Entities.ConfigurationTypes.FanartTv), DateTimeOffset.Now.AddDays(1));
if (moviesArray.Length > 0)
{
var item = rand.Next(moviesArray.Length);
var result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{moviesArray[item]}", async () => await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTime.Now.AddDays(1));
var result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{moviesArray[item]}", () => FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTimeOffset.Now.AddDays(1));
while (!result.moviebackground?.Any() ?? true)
{
item = rand.Next(moviesArray.Length);
result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}movie{moviesArray[item]}", async () => await FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTime.Now.AddDays(1));
result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}movie{moviesArray[item]}", () => FanartTvApi.GetMovieImages(moviesArray[item].ToString(), key.Value), DateTimeOffset.Now.AddDays(1));
}
@ -268,12 +268,12 @@ namespace Ombi.Controllers.V1
if (tvArray.Length > 0)
{
var item = rand.Next(tvArray.Length);
var result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvArray[item]}", async () => await FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTime.Now.AddDays(1));
var result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvArray[item]}", () => FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTimeOffset.Now.AddDays(1));
while (!result.showbackground?.Any() ?? true)
{
item = rand.Next(tvArray.Length);
result = await _cache.GetOrAdd($"{CacheKeys.FanartTv}tv{tvArray[item]}", async () => await FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTime.Now.AddDays(1));
result = await _cache.GetOrAddAsync($"{CacheKeys.FanartTv}tv{tvArray[item]}", () => FanartTvApi.GetTvImages(tvArray[item], key.Value), DateTimeOffset.Now.AddDays(1));
}
var otherRand = new Random();
var res = otherRand.Next(result.showbackground.Length);

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

Loading…
Cancel
Save