using Ombi.Api.External.ExternalApis.TheMovieDb; using Ombi.Core.Models.Requests; using Ombi.Helpers; using Ombi.Store.Entities; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Ombi.Api.External.ExternalApis.TheMovieDb.Models; using Ombi.Core.Authentication; using Ombi.Core.Engine.Interfaces; using Ombi.Core.Models.UI; using Ombi.Core.Rule.Interfaces; using Ombi.Core.Settings; using Ombi.Settings.Settings.Models; using Ombi.Store.Entities.Requests; using Ombi.Store.Repository; using Ombi.Core.Models; using System.Threading; using Ombi.Core.Services; using Ombi.Core.Helpers; using Ombi.Notifications.Models; namespace Ombi.Core.Engine { public class MovieRequestEngine : BaseMediaEngine, IMovieRequestEngine { public MovieRequestEngine(IMovieDbApi movieApi, IRequestServiceMain requestService, ICurrentUser user, INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger log, OmbiUserManager manager, IRepository rl, ICacheService cache, ISettingsService ombiSettings, IRepository sub, IMediaCacheService mediaCacheService, IFeatureService featureService, IUserPlayedMovieRepository userPlayedMovieRepository) : base(user, requestService, r, manager, cache, ombiSettings, sub) { MovieApi = movieApi; NotificationHelper = helper; Sender = sender; Logger = log; _requestLog = rl; _mediaCacheService = mediaCacheService; _featureService = featureService; _userPlayedMovieRepository = userPlayedMovieRepository; } private IMovieDbApi MovieApi { get; } private INotificationHelper NotificationHelper { get; } private IMovieSender Sender { get; } private ILogger Logger { get; } private readonly IRepository _requestLog; private readonly IMediaCacheService _mediaCacheService; private readonly IFeatureService _featureService; protected readonly IUserPlayedMovieRepository _userPlayedMovieRepository; /// /// Requests the movie. /// /// The model. /// public async Task RequestMovie(MovieRequestViewModel model) { var movieInfo = await MovieApi.GetMovieInformationWithExtraInfo(model.TheMovieDbId, model.LanguageCode); if (movieInfo == null || movieInfo.Id == 0) { return new RequestEngineResult { Result = false, Message = "There was an issue adding this movie!", ErrorMessage = $"Please try again later" }; } var fullMovieName = $"{movieInfo.Title}{(!string.IsNullOrEmpty(movieInfo.ReleaseDate) ? $" ({DateTime.Parse(movieInfo.ReleaseDate).Year})" : string.Empty)}"; var userDetails = await GetUser(); var canRequestOnBehalf = model.RequestOnBehalf.HasValue(); var isAdmin = Username.Equals("API", StringComparison.CurrentCultureIgnoreCase) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.PowerUser) || await UserManager.IsInRoleAsync(userDetails, OmbiRoles.Admin); if (canRequestOnBehalf && !isAdmin) { return new RequestEngineResult { Result = false, Message = "You do not have the correct permissions to request on behalf of users!", ErrorMessage = $"You do not have the correct permissions to request on behalf of users!" }; } if ((model.RootFolderOverride.HasValue || model.QualityPathOverride.HasValue) && !isAdmin) { return new RequestEngineResult { Result = false, Message = "You do not have the correct permissions!", ErrorMessage = $"You do not have the correct permissions!" }; } var is4kFeatureEnabled = await _featureService.FeatureEnabled(FeatureNames.Movie4KRequests); var is4kRequest = is4kFeatureEnabled && model.Is4kRequest; MovieRequests requestModel; bool isExisting = false; // Do we already have a request? 4k or non 4k var existingRequest = await MovieRepository.GetRequestAsync(movieInfo.Id); if (existingRequest != null && is4kFeatureEnabled) { if (model.Is4kRequest) { existingRequest.Is4kRequest = true; existingRequest.RequestedDate4k = DateTime.UtcNow; } else { existingRequest.RequestedDate = DateTime.UtcNow; } isExisting = true; requestModel = existingRequest; } else { requestModel = new MovieRequests { TheMovieDbId = movieInfo.Id, RequestType = RequestType.Movie, Overview = movieInfo.Overview, ImdbId = movieInfo.ImdbId, PosterPath = PosterPathHelper.FixPosterPath(movieInfo.PosterPath), Title = movieInfo.Title, ReleaseDate = !string.IsNullOrEmpty(movieInfo.ReleaseDate) ? DateTime.Parse(movieInfo.ReleaseDate) : DateTime.MinValue, Status = movieInfo.Status, RequestedDate = model.Is4kRequest ? DateTime.MinValue : DateTime.UtcNow, Approved = false, Approved4K = false, RequestedUserId = canRequestOnBehalf ? model.RequestOnBehalf : userDetails.Id, Background = movieInfo.BackdropPath, LangCode = model.LanguageCode, RequestedByAlias = model.RequestedByAlias, RootPathOverride = model.RootFolderOverride.GetValueOrDefault(), QualityOverride = model.QualityPathOverride.GetValueOrDefault(), RequestedDate4k = model.Is4kRequest ? DateTime.UtcNow : DateTime.MinValue, Is4kRequest = model.Is4kRequest, Source = model.Source }; } var usDates = movieInfo.ReleaseDates?.Results?.FirstOrDefault(x => x.IsoCode == "US"); requestModel.DigitalReleaseDate = usDates?.ReleaseDate ?.FirstOrDefault(x => x.Type == ReleaseDateType.Digital)?.ReleaseDate; var ruleResults = (await RunRequestRules(requestModel)).ToList(); var ruleResultInError = ruleResults.Find(x => !x.Success); if (ruleResultInError != null) { return new RequestEngineResult { ErrorMessage = ruleResultInError.Message, ErrorCode = ruleResultInError.ErrorCode }; } if (requestModel.Approved || requestModel.Approved4K) // The rules have auto approved this { var requestEngineResult = await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf, isExisting, is4kRequest); if (requestEngineResult.Result) { var result = await ApproveMovie(requestModel, model.Is4kRequest); if (result.IsError) { Logger.LogWarning("Tried auto sending movie but failed. Message: {0}", result.Message); return new RequestEngineResult { Message = result.Message, ErrorMessage = result.Message, Result = false }; } return requestEngineResult; } // If there are no providers then it's successful but movie has not been sent } return await AddMovieRequest(requestModel, fullMovieName, model.RequestOnBehalf, isExisting, is4kRequest); } /// /// Gets the requests. /// /// The count. /// The position. /// The order/filter type. /// public async Task> GetRequests(int count, int position, OrderFilterModel orderFilter) { var shouldHide = await HideFromOtherUsers(); IQueryable allRequests; if (shouldHide.Hide) { allRequests = MovieRepository.GetWithUser(shouldHide .UserId); //.Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync(); } else { allRequests = MovieRepository .GetWithUser(); //.Skip(position).Take(count).OrderByDescending(x => x.ReleaseDate).ToListAsync(); } switch (orderFilter.AvailabilityFilter) { case FilterType.None: break; case FilterType.Available: allRequests = allRequests.Where(x => x.Available); break; case FilterType.NotAvailable: allRequests = allRequests.Where(x => !x.Available); break; default: throw new ArgumentOutOfRangeException(); } switch (orderFilter.StatusFilter) { case FilterType.None: break; case FilterType.Approved: allRequests = allRequests.Where(x => x.Approved); break; case FilterType.Processing: allRequests = allRequests.Where(x => x.Approved && !x.Available); break; case FilterType.PendingApproval: allRequests = allRequests.Where(x => !x.Approved && !x.Available && !(x.Denied ?? false)); break; default: throw new ArgumentOutOfRangeException(); } var total = allRequests.Count(); var requests = await (OrderMovies(allRequests, orderFilter.OrderType)).Skip(position).Take(count) .ToListAsync(); await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, Total = total }; } public async Task> GetRequests(int count, int position, string sortProperty, string sortOrder, string requestedByUserId = null) { var shouldHide = await HideFromOtherUsers(); IQueryable allRequests; if (shouldHide.Hide) { allRequests = MovieRepository.GetWithUser(shouldHide .UserId); } else { allRequests = MovieRepository .GetWithUser(); } allRequests = FilterByRequestedUser(allRequests, requestedByUserId, shouldHide.IsAdmin); var total = await allRequests.CountAsync(); var requests = await ApplySortMovies(allRequests, sortProperty, sortOrder) .Skip(position).Take(count).ToListAsync(); await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, Total = total }; } public async Task> GetRequestsByStatus(int count, int position, string sortProperty, string sortOrder, RequestStatus status, string requestedByUserId = null) { var shouldHide = await HideFromOtherUsers(); IQueryable allRequests; if (shouldHide.Hide) { allRequests = MovieRepository.GetWithUser(shouldHide .UserId); } else { allRequests = MovieRepository .GetWithUser(); } allRequests = FilterByRequestedUser(allRequests, requestedByUserId, shouldHide.IsAdmin); switch (status) { case RequestStatus.PendingApproval: allRequests = allRequests.Where(x => (x.RequestedDate != DateTime.MinValue && !x.Approved && !x.Available && (!x.Denied.HasValue || !x.Denied.Value)) || (x.Has4KRequest && !x.Approved4K && !x.Available4K && (!x.Denied4K.HasValue || !x.Denied4K.Value)) ); break; case RequestStatus.ProcessingRequest: allRequests = allRequests.Where(x => (x.RequestedDate != DateTime.MinValue && x.Approved && !x.Available && (!x.Denied.HasValue || !x.Denied.Value)) || (x.Has4KRequest && x.Approved4K && !x.Available4K && (!x.Denied4K.HasValue || !x.Denied4K.Value)) ); break; case RequestStatus.Available: allRequests = allRequests.Where(x => x.Available || x.Available4K); break; case RequestStatus.Denied: allRequests = allRequests.Where(x => (x.Denied.HasValue && x.Denied.Value && !x.Available) || (x.Has4KRequest && x.Denied4K.HasValue && x.Denied4K.Value && !x.Available4K) ); break; default: break; } var total = await allRequests.CountAsync(); if (total == 0) { return new RequestsViewModel { Collection = Enumerable.Empty(), Total = total }; } var requests = await ApplySortMovies(allRequests, sortProperty, sortOrder) .Skip(position).Take(count).ToListAsync(); await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, Total = total }; } public async Task> GetUnavailableRequests(int count, int position, string sortProperty, string sortOrder, string requestedByUserId = null) { var shouldHide = await HideFromOtherUsers(); IQueryable allRequests; if (shouldHide.Hide) { allRequests = MovieRepository.GetWithUser(shouldHide .UserId).Where(x => !x.Available && x.Approved); } else { allRequests = MovieRepository .GetWithUser().Where(x => !x.Available && x.Approved); } allRequests = FilterByRequestedUser(allRequests, requestedByUserId, shouldHide.IsAdmin); var total = await allRequests.CountAsync(); var requests = await ApplySortMovies(allRequests, sortProperty, sortOrder) .Skip(position).Take(count).ToListAsync(); await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, Total = total }; } public async Task UpdateAdvancedOptions(MediaAdvancedOptions options) { var request = await MovieRepository.Find(options.RequestId); if (request == null) { return new RequestEngineResult { Result = false, ErrorMessage = "Request does not exist" }; } request.QualityOverride = options.QualityOverride; request.RootPathOverride = options.RootPathOverride; await MovieRepository.Update(request); return new RequestEngineResult { Result = true }; } private static IQueryable ApplySortMovies(IQueryable query, string sortProperty, string sortOrder) { var asc = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase); return sortProperty.ToLowerInvariant() switch { "id" => asc ? query.OrderBy(x => x.Id) : query.OrderByDescending(x => x.Id), "title" => asc ? query.OrderBy(x => x.Title) : query.OrderByDescending(x => x.Title), "releasedate" => asc ? query.OrderBy(x => x.ReleaseDate) : query.OrderByDescending(x => x.ReleaseDate), _ => asc ? query.OrderBy(x => x.RequestedDate) : query.OrderByDescending(x => x.RequestedDate) }; } private IQueryable OrderMovies(IQueryable allRequests, OrderType type) { switch (type) { case OrderType.RequestedDateAsc: return allRequests.OrderBy(x => x.RequestedDate); case OrderType.RequestedDateDesc: return allRequests.OrderByDescending(x => x.RequestedDate); case OrderType.TitleAsc: return allRequests.OrderBy(x => x.Title); case OrderType.TitleDesc: return allRequests.OrderByDescending(x => x.Title); case OrderType.StatusAsc: return allRequests.OrderBy(x => x.Status); case OrderType.StatusDesc: return allRequests.OrderByDescending(x => x.Status); default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } public async Task GetTotal() { var shouldHide = await HideFromOtherUsers(); if (shouldHide.Hide) { return await MovieRepository.GetWithUser(shouldHide.UserId).CountAsync(); } else { return await MovieRepository.GetWithUser().CountAsync(); } } /// /// Gets the requests. /// /// public async Task> GetRequests() { var shouldHide = await HideFromOtherUsers(); List allRequests; if (shouldHide.Hide) { allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).ToListAsync(); } else { allRequests = await MovieRepository.GetWithUser().ToListAsync(); } await FillAdditionalFields(shouldHide, allRequests); return allRequests; } public async Task GetRequest(int requestId) { var shouldHide = await HideFromOtherUsers(); var query = shouldHide.Hide ? MovieRepository.GetWithUser(shouldHide.UserId) : MovieRepository.GetWithUser(); var request = await query.Where(x => x.Id == requestId).FirstOrDefaultAsync(); if (request == null) { return null; } await FillAdditionalFields(shouldHide, new List { request }); return request; } private async Task FillAdditionalFields(HideResult shouldHide, List requests) { await CheckForSubscription(shouldHide.UserId, requests); await CheckForPlayed(shouldHide, requests); } private async Task CheckForSubscription(string UserId, List movieRequests) { var requestIds = movieRequests.Select(x => x.Id); var sub = await _subscriptionRepository.GetAll().Where(s => s.UserId == UserId && requestIds.Contains(s.RequestId) && s.RequestType == RequestType.Movie) .ToListAsync(); foreach (var x in movieRequests) { x.PosterPath = PosterPathHelper.FixPosterPath(x.PosterPath); if (UserId == x.RequestedUserId) { x.ShowSubscribe = false; } else { if (!x.Available && !x.Available4K && (!x.Denied ?? true) && (!x.Denied4K ?? true)) { x.ShowSubscribe = true; } var hasSub = sub.FirstOrDefault(r => r.RequestId == x.Id); x.Subscribed = hasSub != null; } } } private async Task CheckForPlayed(HideResult shouldHide, List movieRequests) { var theMovieDbIds = movieRequests.Select(x => x.TheMovieDbId); var plays = await _userPlayedMovieRepository.GetAll().Where(x => theMovieDbIds.Contains(x.TheMovieDbId)) .ToListAsync(); foreach (var request in movieRequests) { request.WatchedByRequestedUser = plays.Exists(x => x.TheMovieDbId == request.TheMovieDbId && x.UserId == request.RequestedUserId); if (!shouldHide.Hide) { request.PlayedByUsersCount = plays.Count(x => x.TheMovieDbId == request.TheMovieDbId); } } } /// /// Searches the movie request. /// /// The search. /// public async Task> SearchMovieRequest(string search) { var shouldHide = await HideFromOtherUsers(); List allRequests; if (shouldHide.Hide) { allRequests = await MovieRepository.GetWithUser(shouldHide.UserId).ToListAsync(); } else { allRequests = await MovieRepository.GetWithUser().ToListAsync(); } var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList(); await FillAdditionalFields(shouldHide, results); return results; } public async Task ApproveMovieById(int requestId, bool is4K) { var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId); return await ApproveMovie(request, is4K); } public async Task DenyMovieById(int modelId, string denyReason, bool is4K) { var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId); if (request == null) { return new RequestEngineResult { ErrorMessage = "Request does not exist" }; } if (is4K) { request.Denied4K = true; request.DeniedReason4K = denyReason; } else { request.Denied = true; request.DeniedReason = denyReason; } await MovieRepository.Update(request); await _mediaCacheService.Purge(); // We are denying a request await NotificationHelper.Notify(request, NotificationType.RequestDeclined); return new RequestEngineResult { Result = true, Message = "Request successfully deleted", }; } public async Task ApproveMovie(MovieRequests request, bool is4K) { if (request == null) { return new RequestEngineResult { ErrorMessage = "Request does not exist" }; } if (is4K) { request.MarkedAsApproved4K = DateTime.Now; request.Approved4K = true; request.Denied4K = false; } else { request.MarkedAsApproved = DateTime.Now; request.Approved = true; request.Denied = false; } await MovieRepository.Update(request); var canNotify = await RunSpecificRule(request, SpecificRules.CanSendNotification, string.Empty); if (canNotify.Success) { await NotificationHelper.Notify(request, NotificationType.RequestApproved); } await _mediaCacheService.Purge(); return await ProcessSendingMovie(request, is4K); } public async Task RequestCollection(int collectionId, CancellationToken cancellationToken) { var langCode = await DefaultLanguageCode(null); var collections = await Cache.GetOrAddAsync($"GetCollection{collectionId}{langCode}", () => MovieApi.GetCollection(langCode, collectionId, cancellationToken), DateTimeOffset.Now.AddDays(1)); var results = new List(); foreach (var collection in collections.parts) { results.Add(await RequestMovie(new MovieRequestViewModel { TheMovieDbId = collection.id, LanguageCode = langCode })); } if (results.All(x => x.IsError)) { new RequestEngineResult { Result = false, ErrorMessage = $"The whole collection {collections.name} Is already monitored or requested!" }; } return new RequestEngineResult { Result = true, Message = $"The collection {collections.name} has been successfully added!", RequestId = results.FirstOrDefault().RequestId }; } private async Task ProcessSendingMovie(MovieRequests request, bool is4K) { if (is4K ? request.Approved4K : request.Approved) { var result = await Sender.Send(request, is4K); if (result.Success && result.Sent) { return new RequestEngineResult { Result = true }; } if (!result.Success) { Logger.LogWarning("Tried auto sending movie but failed. Message: {0}", result.Message); return new RequestEngineResult { Message = result.Message, ErrorMessage = result.Message, Result = false }; } // If there are no providers then it's successful but movie has not been sent } return new RequestEngineResult { Result = true }; } /// /// Updates the movie request. /// /// The request. /// public async Task UpdateMovieRequest(MovieRequests request) { var allRequests = await MovieRepository.GetWithUser().ToListAsync(); var results = allRequests.FirstOrDefault(x => x.Id == request.Id); results.Approved = request.Approved; results.Available = request.Available; results.Denied = request.Denied; results.DeniedReason = request.DeniedReason; results.ImdbId = request.ImdbId; results.IssueId = request.IssueId; results.Issues = request.Issues; results.Overview = request.Overview; results.PosterPath = PosterPathHelper.FixPosterPath(request.PosterPath); results.QualityOverride = request.QualityOverride; results.RootPathOverride = request.RootPathOverride; await MovieRepository.Update(results); await _mediaCacheService.Purge(); return results; } /// /// Removes the movie request. /// /// The request identifier. /// public async Task RemoveMovieRequest(int requestId) { var request = await MovieRepository.GetAll().FirstOrDefaultAsync(x => x.Id == requestId); var result = await CheckCanManageRequest(request); if (result.IsError) return result; await NotificationHelper.Notify(new NotificationOptions { RequestId = 0, DateTime = DateTime.Now, NotificationType = NotificationType.RequestDeleted, RequestType = request.RequestType, Recipient = request.RequestedUser?.Email ?? string.Empty, UserId = request.RequestedUserId, Substitutes = new Dictionary { { NotificationSubstitues.Title, request.Title }, { NotificationSubstitues.RequestType, request.RequestType.ToString() }, } }); await MovieRepository.Delete(request); await _mediaCacheService.Purge(); return new RequestEngineResult { Result = true, }; } public async Task RemoveAllMovieRequests() { var request = MovieRepository.GetAll(); await MovieRepository.DeleteRange(request); await _mediaCacheService.Purge(); } public async Task UserHasRequest(string userId) { return await MovieRepository.GetAll().AnyAsync(x => x.RequestedUserId == userId); } public async Task ReProcessRequest(int requestId, bool is4K, CancellationToken cancellationToken) { var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == requestId); if (request == null) { return new RequestEngineResult { Result = false, ErrorMessage = "Request does not exist" }; } return await ProcessSendingMovie(request, is4K); } public async Task MarkUnavailable(int modelId, bool is4K) { var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId); if (request == null) { return new RequestEngineResult { ErrorMessage = "Request does not exist" }; } if (is4K) { request.Available4K = false; } else { request.Available = false; } await MovieRepository.Update(request); await _mediaCacheService.Purge(); return new RequestEngineResult { Message = "Request is now unavailable", Result = true }; } public async Task MarkAvailable(int modelId, bool is4K) { var request = await MovieRepository.GetWithUser().FirstOrDefaultAsync(x => x.Id == modelId); if (request == null) { return new RequestEngineResult { ErrorMessage = "Request does not exist" }; } if (!is4K) { request.Available = true; request.MarkedAsAvailable = DateTime.Now; } else { request.Available4K = true; request.MarkedAsAvailable4K = DateTime.Now; } await NotificationHelper.Notify(request, NotificationType.RequestAvailable); await MovieRepository.Update(request); await _mediaCacheService.Purge(); return new RequestEngineResult { Message = "Request is now available", Result = true }; } private async Task AddMovieRequest(MovieRequests model, string movieName, string requestOnBehalf, bool isExisting, bool is4k) { if (is4k) { model.Has4KRequest = true; } if (!isExisting) { await MovieRepository.Add(model); } else { await MovieRepository.Update(model); } var result = await RunSpecificRule(model, SpecificRules.CanSendNotification, requestOnBehalf); if (result.Success) { await NotificationHelper.NewRequest(model); } await _mediaCacheService.Purge(); await _requestLog.Add(new RequestLog { UserId = requestOnBehalf.HasValue() ? requestOnBehalf : (await GetUser()).Id, RequestDate = DateTime.UtcNow, RequestId = model.Id, RequestType = RequestType.Movie, }); return new RequestEngineResult { Result = true, Message = $"{movieName} has been successfully added!", RequestId = model.Id }; } } }