From 9cfb10bb1ee69067a6f47bd2c8a72d4e6834350e Mon Sep 17 00:00:00 2001 From: sephrat <34862846+sephrat@users.noreply.github.com> Date: Sun, 7 May 2023 22:09:25 +0200 Subject: [PATCH] feat(emby): Show watched status for Movie requests * First step towards played sync * Change TMDB id format to integer This will better integrate with TMDB id type in the request model * Display played state in the requests list * Fix played status filter * Run played sync job after content sync instead of on its own * Add a toggle to activate played sync * Hoovering * FIx played sync job not being triggered * Expose played state according to hide requests setting * Fix tests * Fix tests for real * Add MySql migrations [skip ci] --- src/Ombi.Api.Emby/EmbyApi.cs | 32 + src/Ombi.Api.Emby/IBaseEmbyApi.cs | 2 + .../Engine/MovieRequestEngineTests.cs | 1 + .../Engine/V2/MovieRequestEngineTests.cs | 3 +- src/Ombi.Core/Engine/MovieRequestEngine.cs | 43 +- src/Ombi.DependencyInjection/IocExtensions.cs | 2 + .../Jobs/Emby/EmbyContentSync.cs | 139 +---- .../Jobs/Emby/EmbyLibrarySync.cs | 146 +++++ src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs | 110 ++++ .../Jobs/Emby/IEmbyPlayedSync.cs | 6 + .../Jobs/Ombi/MediaDatabaseRefresh.cs | 29 +- src/Ombi.Schedule/OmbiScheduler.cs | 1 + .../Settings/Models/FeatureSettings.cs | 1 + src/Ombi.Store/Context/ExternalContext.cs | 1 + .../Entities/Requests/MovieRequests.cs | 5 + src/Ombi.Store/Entities/UserPlayedMovie.cs | 8 + ...20230406152218_MovieUserPlayed.Designer.cs | 566 ++++++++++++++++++ .../20230406152218_MovieUserPlayed.cs | 35 ++ .../ExternalMySqlContextModelSnapshot.cs | 19 +- ...20230310130339_MovieUserPlayed.Designer.cs | 564 +++++++++++++++++ .../20230310130339_MovieUserPlayed.cs | 32 + .../ExternalSqliteContextModelSnapshot.cs | 19 +- .../Repository/IUserPlayedMovieRepository.cs | 13 + .../Repository/UserPlayedMovieRepository.cs | 27 + .../src/app/interfaces/IRequestModel.ts | 4 +- .../movies-grid/movies-grid.component.html | 18 + .../movies-grid/movies-grid.component.ts | 26 +- .../src/app/state/features/features.facade.ts | 4 +- .../app/state/features/features.selectors.ts | 7 +- src/Ombi/wwwroot/translations/en.json | 3 + 30 files changed, 1727 insertions(+), 139 deletions(-) create mode 100644 src/Ombi.Schedule/Jobs/Emby/EmbyLibrarySync.cs create mode 100644 src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs create mode 100644 src/Ombi.Schedule/Jobs/Emby/IEmbyPlayedSync.cs create mode 100644 src/Ombi.Store/Entities/UserPlayedMovie.cs create mode 100644 src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.Designer.cs create mode 100644 src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.cs create mode 100644 src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.Designer.cs create mode 100644 src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.cs create mode 100644 src/Ombi.Store/Repository/IUserPlayedMovieRepository.cs create mode 100644 src/Ombi.Store/Repository/UserPlayedMovieRepository.cs diff --git a/src/Ombi.Api.Emby/EmbyApi.cs b/src/Ombi.Api.Emby/EmbyApi.cs index e9e5f0fca..d8691d984 100644 --- a/src/Ombi.Api.Emby/EmbyApi.cs +++ b/src/Ombi.Api.Emby/EmbyApi.cs @@ -248,5 +248,37 @@ namespace Ombi.Api.Emby req.AddContentHeader("Content-Type", "application/json"); req.AddHeader("Device", "Ombi"); } + + public async Task> GetMoviesPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) + { + return await GetPlayed("Movie", apiKey, userId, baseUri, startIndex, count, parentIdFilder); + } + + private async Task> GetPlayed(string type, string apiKey, string userId, string baseUri, int startIndex, int count, string parentIdFilder = default) + { + var request = new Request($"emby/items", baseUri, HttpMethod.Get); + + request.AddQueryString("Recursive", true.ToString()); + request.AddQueryString("IncludeItemTypes", type); + request.AddQueryString("Fields", "ProviderIds"); + request.AddQueryString("UserId", userId); + request.AddQueryString("isPlayed", true.ToString()); + + // paginate and display recently played items first + request.AddQueryString("sortBy", "DatePlayed"); + request.AddQueryString("SortOrder", "Descending"); + request.AddQueryString("startIndex", startIndex.ToString()); + request.AddQueryString("limit", count.ToString()); + + if (!string.IsNullOrEmpty(parentIdFilder)) + { + request.AddQueryString("ParentId", parentIdFilder); + } + + AddHeaders(request, apiKey); + + var obj = await Api.Request>(request); + return obj; + } } } diff --git a/src/Ombi.Api.Emby/IBaseEmbyApi.cs b/src/Ombi.Api.Emby/IBaseEmbyApi.cs index 248c0a88f..582eac0c9 100644 --- a/src/Ombi.Api.Emby/IBaseEmbyApi.cs +++ b/src/Ombi.Api.Emby/IBaseEmbyApi.cs @@ -32,5 +32,7 @@ namespace Ombi.Api.Emby Task> RecentlyAddedMovies(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); Task> RecentlyAddedEpisodes(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); Task> RecentlyAddedShows(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); + + Task> GetMoviesPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); } } \ No newline at end of file diff --git a/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs b/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs index 8418cddbc..c55d76e8f 100644 --- a/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs +++ b/src/Ombi.Core.Tests/Engine/MovieRequestEngineTests.cs @@ -52,6 +52,7 @@ namespace Ombi.Core.Tests.Engine _subject = _mocker.CreateInstance(); var list = DbHelper.GetQueryableMockDbSet(new RequestSubscription()); _mocker.Setup, IQueryable>(x => x.GetAll()).Returns(new List().AsQueryable().BuildMock()); + _mocker.Setup>(x => x.GetAll()).Returns(new List().AsQueryable().BuildMock()); } [Test] diff --git a/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs b/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs index 16b7cb811..c483d81ba 100644 --- a/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs +++ b/src/Ombi.Core.Tests/Engine/V2/MovieRequestEngineTests.cs @@ -46,8 +46,9 @@ namespace Ombi.Core.Tests.Engine.V2 var requestSubs = new Mock>(); var mediaCache = new Mock(); var featureService = new Mock(); + var userPlayedMovieRepository = new Mock(); _engine = new MovieRequestEngine(movieApi.Object, requestService.Object, user.Object, notificationHelper.Object, rules.Object, movieSender.Object, - logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object, mediaCache.Object, featureService.Object); + logger.Object, userManager.Object, requestLogRepo.Object, cache.Object, ombiSettings.Object, requestSubs.Object, mediaCache.Object, featureService.Object, userPlayedMovieRepository.Object); } [Test] diff --git a/src/Ombi.Core/Engine/MovieRequestEngine.cs b/src/Ombi.Core/Engine/MovieRequestEngine.cs index 109460ff9..7863aed25 100644 --- a/src/Ombi.Core/Engine/MovieRequestEngine.cs +++ b/src/Ombi.Core/Engine/MovieRequestEngine.cs @@ -33,7 +33,8 @@ namespace Ombi.Core.Engine INotificationHelper helper, IRuleEvaluator r, IMovieSender sender, ILogger log, OmbiUserManager manager, IRepository rl, ICacheService cache, ISettingsService ombiSettings, IRepository sub, IMediaCacheService mediaCacheService, - IFeatureService featureService) + IFeatureService featureService, + IUserPlayedMovieRepository userPlayedMovieRepository) : base(user, requestService, r, manager, cache, ombiSettings, sub) { MovieApi = movieApi; @@ -43,6 +44,7 @@ namespace Ombi.Core.Engine _requestLog = rl; _mediaCacheService = mediaCacheService; _featureService = featureService; + _userPlayedMovieRepository = userPlayedMovieRepository; } private IMovieDbApi MovieApi { get; } @@ -52,6 +54,7 @@ namespace Ombi.Core.Engine private readonly IRepository _requestLog; private readonly IMediaCacheService _mediaCacheService; private readonly IFeatureService _featureService; + protected readonly IUserPlayedMovieRepository _userPlayedMovieRepository; /// /// Requests the movie. @@ -252,7 +255,7 @@ namespace Ombi.Core.Engine var requests = await (OrderMovies(allRequests, orderFilter.OrderType)).Skip(position).Take(count) .ToListAsync(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -296,7 +299,7 @@ namespace Ombi.Core.Engine var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -381,7 +384,7 @@ namespace Ombi.Core.Engine // TODO fix this so we execute this on the server requests = requests.Skip(position).Take(count).ToList(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -424,7 +427,7 @@ namespace Ombi.Core.Engine var total = requests.Count(); requests = requests.Skip(position).Take(count).ToList(); - await CheckForSubscription(shouldHide.UserId, requests); + await FillAdditionalFields(shouldHide, requests); return new RequestsViewModel { Collection = requests, @@ -506,18 +509,25 @@ namespace Ombi.Core.Engine allRequests = await MovieRepository.GetWithUser().ToListAsync(); } - await CheckForSubscription(shouldHide.UserId, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } public async Task GetRequest(int requestId) { + var shouldHide = await HideFromOtherUsers(); + // TODO: this query should return the request only if the user is allowed to see it (see shouldHide implementations) var request = await MovieRepository.GetWithUser().Where(x => x.Id == requestId).FirstOrDefaultAsync(); - await CheckForSubscription((await GetUser()).Id, new List { request }); + 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) { @@ -543,6 +553,23 @@ namespace Ombi.Core.Engine } } } + + 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. @@ -563,7 +590,7 @@ namespace Ombi.Core.Engine } var results = allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToList(); - await CheckForSubscription(shouldHide.UserId, results); + await FillAdditionalFields(shouldHide, results); return results; } diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 09d99d4b7..c9bcc13d3 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -197,6 +197,7 @@ namespace Ombi.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -244,6 +245,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs index 443c1c3da..7b301d4ed 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyContentSync.cs @@ -8,10 +8,12 @@ using Ombi.Api.Emby; using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models.Media.Tv; using Ombi.Api.Emby.Models.Movie; +using Ombi.Core.Services; using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.Hubs; +using Ombi.Settings.Settings.Models; using Ombi.Store.Entities; using Ombi.Store.Repository; using Quartz; @@ -19,108 +21,43 @@ using MediaType = Ombi.Store.Entities.MediaType; namespace Ombi.Schedule.Jobs.Emby { - public class EmbyContentSync : IEmbyContentSync + public class EmbyContentSync : EmbyLibrarySync, IEmbyContentSync { - public EmbyContentSync(ISettingsService settings, IEmbyApiFactory api, ILogger logger, - IEmbyContentRepository repo, INotificationHubService notification) + public EmbyContentSync( + ISettingsService settings, + IEmbyApiFactory api, + ILogger logger, + IEmbyContentRepository repo, + INotificationHubService notification, + IFeatureService feature): + base(settings, api, logger, notification) { - _logger = logger; - _settings = settings; - _apiFactory = api; _repo = repo; - _notification = notification; + _feature = feature; } - private readonly ILogger _logger; - private readonly ISettingsService _settings; - private readonly IEmbyApiFactory _apiFactory; private readonly IEmbyContentRepository _repo; - private readonly INotificationHubService _notification; + private readonly IFeatureService _feature; - private const int AmountToTake = 100; - private IEmbyApi Api { get; set; } - - public async Task Execute(IJobExecutionContext context) + public async override Task Execute(IJobExecutionContext context) { - JobDataMap dataMap = context.JobDetail.JobDataMap; - var recentlyAddedSearch = false; - if (dataMap.TryGetValue(JobDataKeys.EmbyRecentlyAddedSearch, out var recentlyAddedObj)) - { - recentlyAddedSearch = Convert.ToBoolean(recentlyAddedObj); - } - - var embySettings = await _settings.GetSettingsAsync(); - if (!embySettings.Enable) - return; - - Api = _apiFactory.CreateClient(embySettings); - - await _notification.SendNotificationToAdmins(recentlyAddedSearch ? "Emby Recently Added Started" : "Emby Content Sync Started"); - foreach (var server in embySettings.Servers) - { - try - { - await StartServerCache(server, recentlyAddedSearch); - } - catch (Exception e) - { - await _notification.SendNotificationToAdmins("Emby Content Sync Failed"); - _logger.LogError(e, "Exception when caching Emby for server {0}", server.Name); - } - } + await base.Execute(context); - await _notification.SendNotificationToAdmins("Emby Content Sync Finished"); // Episodes + await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyEpisodeSync), "Emby"), new JobDataMap(new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAdded.ToString() } })); - - await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyEpisodeSync), "Emby"), new JobDataMap(new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAddedSearch.ToString() } })); - } - - - private async Task StartServerCache(EmbyServers server, bool recentlyAdded) - { - if (!ValidateSettings(server)) + // Played state + var isPlayedSyncEnabled = await _feature.FeatureEnabled(FeatureNames.PlayedSync); + if(isPlayedSyncEnabled) { - return; - } - - - if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) - { - var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies"); - - foreach (var movieParentIdFilder in movieLibsToFilter) - { - _logger.LogInformation($"Scanning Lib '{movieParentIdFilder.Title}'"); - await ProcessMovies(server, recentlyAdded, movieParentIdFilder.Key); - } - - var tvLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "tvshows"); - foreach (var tvParentIdFilter in tvLibsToFilter) - { - _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); - await ProcessTv(server, recentlyAdded, tvParentIdFilter.Key); - } - - - var mixedLibs = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed"); - foreach (var m in mixedLibs) - { - _logger.LogInformation($"Scanning Lib '{m.Title}'"); - await ProcessTv(server, recentlyAdded, m.Key); - await ProcessMovies(server, recentlyAdded, m.Key); - } - } - else - { - await ProcessMovies(server, recentlyAdded); - await ProcessTv(server, recentlyAdded); + await OmbiQuartz.Scheduler.TriggerJob(new JobKey(nameof(IEmbyPlayedSync), "Emby"), new JobDataMap(new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, recentlyAdded.ToString() } })); } } - private async Task ProcessTv(EmbyServers server, bool recentlyAdded, string parentId = default) + + protected async override Task ProcessTv(EmbyServers server, string parentId = default) { // TV Time var mediaToAdd = new HashSet(); @@ -196,7 +133,7 @@ namespace Ombi.Schedule.Jobs.Emby await _repo.AddRange(mediaToAdd); } - private async Task ProcessMovies(EmbyServers server, bool recentlyAdded, string parentId = default) + protected override async Task ProcessMovies(EmbyServers server, string parentId = default) { EmbyItemContainer movies; if (recentlyAdded) @@ -324,36 +261,6 @@ namespace Ombi.Schedule.Jobs.Emby content.Quality = has4K ? null : quality; content.Has4K = has4K; } - - private bool ValidateSettings(EmbyServers server) - { - if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) - { - _logger.LogInformation(LoggingEvents.EmbyContentCacher, $"Server {server?.Name} is not configured correctly"); - return false; - } - - return true; - } - - private bool _disposed; - protected virtual void Dispose(bool disposing) - { - if (_disposed) - return; - - if (disposing) - { - //_settings?.Dispose(); - } - _disposed = true; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } } diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyLibrarySync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyLibrarySync.cs new file mode 100644 index 000000000..9695530de --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyLibrarySync.cs @@ -0,0 +1,146 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Ombi.Api.Emby; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Hubs; +using Quartz; + +namespace Ombi.Schedule.Jobs.Emby +{ + public abstract class EmbyLibrarySync + { + public EmbyLibrarySync(ISettingsService settings, IEmbyApiFactory api, ILogger logger, + INotificationHubService notification) + { + _logger = logger; + _settings = settings; + _apiFactory = api; + _notification = notification; + } + + protected readonly ILogger _logger; + protected readonly ISettingsService _settings; + protected readonly IEmbyApiFactory _apiFactory; + protected bool recentlyAdded; + protected readonly INotificationHubService _notification; + + protected const int AmountToTake = 100; + + protected IEmbyApi Api { get; set; } + + public virtual async Task Execute(IJobExecutionContext context) + { + + JobDataMap dataMap = context.JobDetail.JobDataMap; + if (dataMap.TryGetValue(JobDataKeys.EmbyRecentlyAddedSearch, out var recentlyAddedObj)) + { + recentlyAdded = Convert.ToBoolean(recentlyAddedObj); + } + + await _notification.SendNotificationToAdmins(recentlyAdded ? "Emby Recently Added Started" : "Emby Content Sync Started"); + + + var embySettings = await _settings.GetSettingsAsync(); + if (!embySettings.Enable) + return; + + Api = _apiFactory.CreateClient(embySettings); + + foreach (var server in embySettings.Servers) + { + try + { + await StartServerCache(server); + } + catch (Exception e) + { + await _notification.SendNotificationToAdmins("Emby Content Sync Failed"); + _logger.LogError(e, "Exception when caching Emby for server {0}", server.Name); + } + } + + await _notification.SendNotificationToAdmins("Emby Content Sync Finished"); + } + + + private async Task StartServerCache(EmbyServers server) + { + if (!ValidateSettings(server)) + { + return; + } + + + if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) + { + var movieLibsToFilter = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "movies"); + + foreach (var movieParentIdFilder in movieLibsToFilter) + { + _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) + { + _logger.LogInformation($"Scanning Lib '{tvParentIdFilter.Title}'"); + await ProcessTv(server, tvParentIdFilter.Key); + } + + + var mixedLibs = server.EmbySelectedLibraries.Where(x => x.Enabled && x.CollectionType == "mixed"); + foreach (var m in mixedLibs) + { + _logger.LogInformation($"Scanning Lib '{m.Title}'"); + await ProcessTv(server, m.Key); + await ProcessMovies(server, m.Key); + } + } + else + { + await ProcessMovies(server); + await ProcessTv(server); + } + } + + protected abstract Task ProcessTv(EmbyServers server, string parentId = default); + + protected abstract Task ProcessMovies(EmbyServers server, string parentId = default); + + private bool ValidateSettings(EmbyServers server) + { + if (server?.Ip == null || string.IsNullOrEmpty(server?.ApiKey)) + { + _logger.LogInformation(LoggingEvents.EmbyContentCacher, $"Server {server?.Name} is not configured correctly"); + return false; + } + + return true; + } + + private bool _disposed; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + //_settings?.Dispose(); + } + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } + +} diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs new file mode 100644 index 000000000..5af5a9756 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Ombi.Api.Emby; +using Ombi.Api.Emby.Models; +using Ombi.Api.Emby.Models.Movie; +using Ombi.Core.Authentication; +using Ombi.Core.Settings; +using Ombi.Core.Settings.Models.External; +using Ombi.Helpers; +using Ombi.Hubs; +using Ombi.Store.Entities; +using Ombi.Store.Repository; + +namespace Ombi.Schedule.Jobs.Emby +{ + public class EmbyPlayedSync : EmbyLibrarySync, IEmbyPlayedSync + { + public EmbyPlayedSync(ISettingsService settings, IEmbyApiFactory api, ILogger logger, + IUserPlayedMovieRepository repo, INotificationHubService notification, OmbiUserManager user) : base(settings, api, logger, notification) + { + _userManager = user; + _repo = repo; + } + private OmbiUserManager _userManager { get; } + + private readonly IUserPlayedMovieRepository _repo; + + protected override Task ProcessTv(EmbyServers server, string parentId = default) + { + // TODO + return Task.CompletedTask; + } + + protected async override Task ProcessMovies(EmbyServers server, string parentId = default) + { + + var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync(); + foreach (var user in allUsers) + { + await ProcessMoviesUser(server, user, parentId); + } + } + + + private async Task ProcessMoviesUser(EmbyServers server, OmbiUser user, string parentId = default) + { + EmbyItemContainer movies; + if (recentlyAdded) + { + var recentlyAddedAmountToTake = 5; // to be adjusted? + movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, 0, recentlyAddedAmountToTake, user.ProviderUserId, server.FullUri); + // Setting this so we don't attempt to grab more than we need + if (movies.TotalRecordCount > recentlyAddedAmountToTake) + { + movies.TotalRecordCount = recentlyAddedAmountToTake; + } + } + else + { + movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, 0, AmountToTake, user.ProviderUserId, server.FullUri); + } + var totalCount = movies.TotalRecordCount; + var processed = 0; + var mediaToAdd = new HashSet(); + + while (processed < totalCount) + { + foreach (var movie in movies.Items) + { + await ProcessMovie(movie, user, mediaToAdd, server); + processed++; + } + + // Get the next batch + // Recently Added should never be checked as the TotalRecords should equal the amount to take + if (!recentlyAdded) + { + movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, processed, AmountToTake, user.ProviderUserId, server.FullUri); + } + await _repo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + } + } + + private async Task ProcessMovie(EmbyMovie movieInfo, OmbiUser user, ICollection content, EmbyServers server) + { + if (movieInfo.ProviderIds.Tmdb.IsNullOrEmpty()) + { + _logger.LogWarning($"Movie {movieInfo.Name} has no relevant metadata. Skipping."); + return; + } + var userPlayedMovie = new UserPlayedMovie() + { + TheMovieDbId = int.Parse(movieInfo.ProviderIds.Tmdb), + UserId = user.Id + }; + // Check if it exists + var existingMovie = await _repo.Get(userPlayedMovie.TheMovieDbId, userPlayedMovie.UserId); + var alreadyGoingToAdd = content.Any(x => x.TheMovieDbId == userPlayedMovie.TheMovieDbId && x.UserId == userPlayedMovie.UserId); + if (existingMovie == null && !alreadyGoingToAdd) + { + content.Add(userPlayedMovie); + } + } + } + +} diff --git a/src/Ombi.Schedule/Jobs/Emby/IEmbyPlayedSync.cs b/src/Ombi.Schedule/Jobs/Emby/IEmbyPlayedSync.cs new file mode 100644 index 000000000..80434bddb --- /dev/null +++ b/src/Ombi.Schedule/Jobs/Emby/IEmbyPlayedSync.cs @@ -0,0 +1,6 @@ +namespace Ombi.Schedule.Jobs.Emby +{ + public interface IEmbyPlayedSync : IBaseJob + { + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs index 46f3a7e56..e3573e6c8 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs @@ -15,15 +15,22 @@ namespace Ombi.Schedule.Jobs.Ombi { public class MediaDatabaseRefresh : IMediaDatabaseRefresh { - public MediaDatabaseRefresh(ISettingsService s, ILogger log, - IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo, - ISettingsService embySettings, ISettingsService jellyfinSettings) + public MediaDatabaseRefresh( + ISettingsService s, + ILogger log, + IPlexContentRepository plexRepo, + IEmbyContentRepository embyRepo, + IJellyfinContentRepository jellyfinRepo, + IUserPlayedMovieRepository userPlayedRepo, + ISettingsService embySettings, + ISettingsService jellyfinSettings) { _plexSettings = s; _log = log; _plexRepo = plexRepo; _embyRepo = embyRepo; _jellyfinRepo = jellyfinRepo; + _userPlayedRepo = userPlayedRepo; _embySettings = embySettings; _jellyfinSettings = jellyfinSettings; _plexSettings.ClearCache(); @@ -34,6 +41,7 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly IPlexContentRepository _plexRepo; private readonly IEmbyContentRepository _embyRepo; private readonly IJellyfinContentRepository _jellyfinRepo; + private readonly IUserPlayedMovieRepository _userPlayedRepo; private readonly ISettingsService _embySettings; private readonly ISettingsService _jellyfinSettings; @@ -41,6 +49,7 @@ namespace Ombi.Schedule.Jobs.Ombi { try { + await RemovePlayedData(); await RemovePlexData(); await RemoveEmbyData(); await RemoveJellyfinData(); @@ -52,6 +61,20 @@ namespace Ombi.Schedule.Jobs.Ombi } + private async Task RemovePlayedData() + { + try + { + const string movieSql = "DELETE FROM UserPlayedMovie"; + await _userPlayedRepo.ExecuteSql(movieSql); + } + catch (Exception e) + { + _log.LogError(LoggingEvents.MediaReferesh, e, "Refreshing Played Data Failed"); + } + } + + private async Task RemoveEmbyData() { try diff --git a/src/Ombi.Schedule/OmbiScheduler.cs b/src/Ombi.Schedule/OmbiScheduler.cs index 41602f641..3b4f6316f 100644 --- a/src/Ombi.Schedule/OmbiScheduler.cs +++ b/src/Ombi.Schedule/OmbiScheduler.cs @@ -99,6 +99,7 @@ namespace Ombi.Schedule await OmbiQuartz.Instance.AddJob(nameof(IEmbyContentSync), "Emby", JobSettingsHelper.EmbyContent(s)); await OmbiQuartz.Instance.AddJob(nameof(IEmbyContentSync) + "RecentlyAdded", "Emby", JobSettingsHelper.EmbyRecentlyAddedSync(s), new Dictionary { { JobDataKeys.EmbyRecentlyAddedSearch, "true" } }); await OmbiQuartz.Instance.AddJob(nameof(IEmbyEpisodeSync), "Emby", null); + await OmbiQuartz.Instance.AddJob(nameof(IEmbyPlayedSync), "Emby", null); await OmbiQuartz.Instance.AddJob(nameof(IEmbyAvaliabilityChecker), "Emby", null); await OmbiQuartz.Instance.AddJob(nameof(IEmbyUserImporter), "Emby", JobSettingsHelper.UserImporter(s)); } diff --git a/src/Ombi.Settings/Settings/Models/FeatureSettings.cs b/src/Ombi.Settings/Settings/Models/FeatureSettings.cs index 9d0149e5d..f541d1e0d 100644 --- a/src/Ombi.Settings/Settings/Models/FeatureSettings.cs +++ b/src/Ombi.Settings/Settings/Models/FeatureSettings.cs @@ -21,5 +21,6 @@ namespace Ombi.Settings.Settings.Models { public const string Movie4KRequests = nameof(Movie4KRequests); public const string OldTrendingSource = nameof(OldTrendingSource); + public const string PlayedSync = nameof(PlayedSync); } } diff --git a/src/Ombi.Store/Context/ExternalContext.cs b/src/Ombi.Store/Context/ExternalContext.cs index f13c1e74f..c54c39beb 100644 --- a/src/Ombi.Store/Context/ExternalContext.cs +++ b/src/Ombi.Store/Context/ExternalContext.cs @@ -41,6 +41,7 @@ namespace Ombi.Store.Context public DbSet SonarrEpisodeCache { get; set; } public DbSet SickRageCache { get; set; } public DbSet SickRageEpisodeCache { get; set; } + public DbSet UserPlayedMovie { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Ombi.Store/Entities/Requests/MovieRequests.cs b/src/Ombi.Store/Entities/Requests/MovieRequests.cs index 415efded2..3c3c75893 100644 --- a/src/Ombi.Store/Entities/Requests/MovieRequests.cs +++ b/src/Ombi.Store/Entities/Requests/MovieRequests.cs @@ -84,5 +84,10 @@ namespace Ombi.Store.Entities.Requests [NotMapped] public override bool CanApprove => !Approved && !Available || !Approved4K && !Available4K; + + [NotMapped] + public bool WatchedByRequestedUser { get; set; } + [NotMapped] + public int PlayedByUsersCount { get; set; } } } diff --git a/src/Ombi.Store/Entities/UserPlayedMovie.cs b/src/Ombi.Store/Entities/UserPlayedMovie.cs new file mode 100644 index 000000000..7f28e9d99 --- /dev/null +++ b/src/Ombi.Store/Entities/UserPlayedMovie.cs @@ -0,0 +1,8 @@ +namespace Ombi.Store.Entities +{ + public class UserPlayedMovie : Entity + { + public int TheMovieDbId { get; set; } + public string UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.Designer.cs new file mode 100644 index 000000000..0e2e290b7 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.Designer.cs @@ -0,0 +1,566 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.MySql; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalMySql +{ + [DbContext(typeof(ExternalMySqlContext))] + [Migration("20230406152218_MovieUserPlayed")] + partial class MovieUserPlayed + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmbyId") + .HasColumnType("longtext"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("JellyfinId") + .HasColumnType("longtext"); + + b.Property("ParentId") + .HasColumnType("varchar(255)"); + + b.Property("ProviderId") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ForeignAlbumId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.Property("PercentOfTracks") + .HasColumnType("decimal(65,30)"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TrackCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("ArtistName") + .HasColumnType("longtext"); + + b.Property("ForeignArtistId") + .HasColumnType("longtext"); + + b.Property("Monitored") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("GrandparentKey") + .HasColumnType("varchar(255)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("ParentKey") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ParentKey") + .HasColumnType("longtext"); + + b.Property("PlexContentId") + .HasColumnType("longtext"); + + b.Property("PlexServerContentId") + .HasColumnType("int"); + + b.Property("SeasonKey") + .HasColumnType("longtext"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AddedAt") + .HasColumnType("datetime(6)"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("ImdbId") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Quality") + .HasColumnType("longtext"); + + b.Property("ReleaseYear") + .HasColumnType("longtext"); + + b.Property("RequestId") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TvDbId") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TmdbId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexWatchlistHistory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Has4K") + .HasColumnType("tinyint(1)"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("HasRegular") + .HasColumnType("tinyint(1)"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("HasFile") + .HasColumnType("tinyint(1)"); + + b.Property("MovieDbId") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TvDbId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.cs new file mode 100644 index 000000000..48336a03d --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230406152218_MovieUserPlayed.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalMySql +{ + public partial class MovieUserPlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedMovie", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TheMovieDbId = table.Column(type: "int", nullable: false), + UserId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedMovie", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedMovie"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs index 1e86ddf7b..0121a99ba 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs @@ -16,7 +16,7 @@ namespace Ombi.Store.Migrations.ExternalMySql { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.0") + .HasAnnotation("ProductVersion", "6.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => @@ -488,6 +488,23 @@ namespace Ombi.Store.Migrations.ExternalMySql b.ToTable("SonarrEpisodeCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => { b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.Designer.cs new file mode 100644 index 000000000..f1162e20f --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.Designer.cs @@ -0,0 +1,564 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Ombi.Store.Context.Sqlite; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + [DbContext(typeof(ExternalSqliteContext))] + [Migration("20230310130339_MovieUserPlayed")] + partial class MovieUserPlayed + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); + + modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("CouchPotatoCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmbyContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EmbyId") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("EmbyEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("JellyfinId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("JellyfinEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrAlbumCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ForeignAlbumId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.Property("PercentOfTracks") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TrackCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrAlbumCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.LidarrArtistCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("ArtistName") + .HasColumnType("TEXT"); + + b.Property("ForeignArtistId") + .HasColumnType("TEXT"); + + b.Property("Monitored") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("LidarrArtistCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("GrandparentKey") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ParentKey") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GrandparentKey"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ParentKey") + .HasColumnType("TEXT"); + + b.Property("PlexContentId") + .HasColumnType("TEXT"); + + b.Property("PlexServerContentId") + .HasColumnType("INTEGER"); + + b.Property("SeasonKey") + .HasColumnType("TEXT"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlexServerContentId"); + + b.ToTable("PlexSeasonsContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddedAt") + .HasColumnType("TEXT"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("ImdbId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("TEXT"); + + b.Property("ReleaseYear") + .HasColumnType("TEXT"); + + b.Property("RequestId") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TvDbId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexServerContent"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexWatchlistHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TmdbId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexWatchlistHistory"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.RadarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Has4K") + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("HasRegular") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RadarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SickRageEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SickRageEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.SonarrEpisodeCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("HasFile") + .HasColumnType("INTEGER"); + + b.Property("MovieDbId") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TvDbId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SonarrEpisodeCache"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => + { + b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("EmbyId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinEpisode", b => + { + b.HasOne("Ombi.Store.Entities.JellyfinContent", "Series") + .WithMany("Episodes") + .HasForeignKey("ParentId") + .HasPrincipalKey("JellyfinId"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexEpisode", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", "Series") + .WithMany("Episodes") + .HasForeignKey("GrandparentKey") + .HasPrincipalKey("Key"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexSeasonsContent", b => + { + b.HasOne("Ombi.Store.Entities.PlexServerContent", null) + .WithMany("Seasons") + .HasForeignKey("PlexServerContentId"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.EmbyContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.JellyfinContent", b => + { + b.Navigation("Episodes"); + }); + + modelBuilder.Entity("Ombi.Store.Entities.PlexServerContent", b => + { + b.Navigation("Episodes"); + + b.Navigation("Seasons"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.cs new file mode 100644 index 000000000..23345e7a1 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230310130339_MovieUserPlayed.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + public partial class MovieUserPlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedMovie", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TheMovieDbId = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedMovie", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedMovie"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs index 2f5de3382..857259ab1 100644 --- a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Ombi.Store.Migrations.ExternalSqlite protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); modelBuilder.Entity("Ombi.Store.Entities.CouchPotatoCache", b => { @@ -486,6 +486,23 @@ namespace Ombi.Store.Migrations.ExternalSqlite b.ToTable("SonarrEpisodeCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedMovie"); + }); + modelBuilder.Entity("Ombi.Store.Entities.EmbyEpisode", b => { b.HasOne("Ombi.Store.Entities.EmbyContent", "Series") diff --git a/src/Ombi.Store/Repository/IUserPlayedMovieRepository.cs b/src/Ombi.Store/Repository/IUserPlayedMovieRepository.cs new file mode 100644 index 000000000..966171b3a --- /dev/null +++ b/src/Ombi.Store/Repository/IUserPlayedMovieRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public interface IUserPlayedMovieRepository : IExternalRepository + { + Task Get(int theMovieDbId, string userId); + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Repository/UserPlayedMovieRepository.cs b/src/Ombi.Store/Repository/UserPlayedMovieRepository.cs new file mode 100644 index 000000000..aaff5f2b1 --- /dev/null +++ b/src/Ombi.Store/Repository/UserPlayedMovieRepository.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Ombi.Store.Context; +using Ombi.Store.Entities; + +namespace Ombi.Store.Repository +{ + public class UserPlayedMovieRepository : ExternalRepository, IUserPlayedMovieRepository + { + protected ExternalContext Db { get; } + public UserPlayedMovieRepository(ExternalContext db) : base(db) + { + Db = db; + } + + public async Task Get(int theMovieDbId, string userId) + { + return await Db.UserPlayedMovie.FirstOrDefaultAsync(x => x.TheMovieDbId == theMovieDbId && x.UserId == userId); + + } + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts index 36b75adb6..6031e9796 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts @@ -23,6 +23,8 @@ export interface IMovieRequests extends IFullBaseRequest { deniedReason4K: string; requestedDate4k: Date; requestedDate: Date; + watchedByRequestedUser: boolean; + playedByUsersCount: number; // For the UI rootPathOverrideTitle: string; @@ -212,4 +214,4 @@ export class BaseRequestOptions { requestOnBehalf: string | undefined; rootFolderOverride: number | undefined; qualityPathOverride: number | undefined; -} \ No newline at end of file +} diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html index d50cd62b5..4b91306ed 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.html @@ -80,6 +80,24 @@ {{element.requestStatus | translate}} + + + {{ 'Requests.Watched' | translate}} + + + + + + + diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts index ca36a5d62..1db29de12 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/movies-grid/movies-grid.component.ts @@ -24,10 +24,11 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { public dataSource: MatTableDataSource; public resultsLength: number; public isLoadingResults = true; - public displayedColumns: string[] = ['title', 'requestedUser.requestedBy', 'status', 'requestStatus','requestedDate', 'actions']; + public displayedColumns: string[] = ['title', 'requestedUser.requestedBy', 'status', 'requestStatus','requestedDate']; public gridCount: string = "15"; public isAdmin: boolean; public is4kEnabled = false; + public isPlayedSyncEnabled = false; public manageOwnRequests: boolean; public defaultSort: string = "requestedDate"; public defaultOrder: string = "desc"; @@ -65,10 +66,9 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { } this.is4kEnabled = this.featureFacade.is4kEnabled(); - if ((this.isAdmin || this.auth.hasRole("Request4KMovie")) - && this.is4kEnabled) { - this.displayedColumns.splice(4, 0, 'has4kRequest'); - } + this.isPlayedSyncEnabled = this.featureFacade.isPlayedSyncEnabled(); + + this.addDynamicColumns(); const defaultCount = this.storageService.get(this.storageKeyGridCount); const defaultSort = this.storageService.get(this.storageKey); @@ -88,6 +88,20 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { } } + addDynamicColumns() { + if ((this.isAdmin || this.auth.hasRole("Request4KMovie")) + && this.is4kEnabled) { + this.displayedColumns.splice(4, 0, 'has4kRequest'); + } + + if (this.isPlayedSyncEnabled) { + this.displayedColumns.push('watchedByRequestedUser'); + } + + // always put the actions column at the end + this.displayedColumns.push('actions'); + } + public async ngAfterViewInit() { this.storageService.save(this.storageKeyGridCount, this.gridCount); @@ -263,4 +277,4 @@ export class MoviesGridComponent implements OnInit, AfterViewInit { } return request.requestedDate; } -} \ No newline at end of file +} diff --git a/src/Ombi/ClientApp/src/app/state/features/features.facade.ts b/src/Ombi/ClientApp/src/app/state/features/features.facade.ts index 10e229eba..9b5091cba 100644 --- a/src/Ombi/ClientApp/src/app/state/features/features.facade.ts +++ b/src/Ombi/ClientApp/src/app/state/features/features.facade.ts @@ -23,4 +23,6 @@ export class FeaturesFacade { public is4kEnabled = (): boolean => this.store.selectSnapshot(FeaturesSelectors.is4kEnabled); -} \ No newline at end of file + public isPlayedSyncEnabled = (): boolean => this.store.selectSnapshot(FeaturesSelectors.isPlayedSyncEnabled); + +} diff --git a/src/Ombi/ClientApp/src/app/state/features/features.selectors.ts b/src/Ombi/ClientApp/src/app/state/features/features.selectors.ts index 143dfb875..bbea921e5 100644 --- a/src/Ombi/ClientApp/src/app/state/features/features.selectors.ts +++ b/src/Ombi/ClientApp/src/app/state/features/features.selectors.ts @@ -15,4 +15,9 @@ export class FeaturesSelectors { return features.filter(x => x.name === "Movie4KRequests")[0].enabled; } -} \ No newline at end of file + @Selector([FeaturesSelectors.features]) + public static isPlayedSyncEnabled(features: IFeatureEnablement[]): boolean { + return features.filter(x => x.name === "PlayedSync")[0].enabled; + } + +} diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index 4d7fdf28b..ed09c1b64 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -159,6 +159,9 @@ "RequestedBy": "Requested By", "Status": "Status", "RequestStatus": "Request status", + "Watched": "Watched", + "WatchedTooltip": "The user who made the request has watched it", + "WatchedByUsersCount": "{{count}} users have watched this.", "Denied": " Denied:", "TheatricalRelease": "Theatrical Release: {{date}}", "ReleaseDate": "Released: {{date}}",