From 1f37de08888812b6d130d92bb664a89e89149105 Mon Sep 17 00:00:00 2001 From: sephrat <34862846+sephrat@users.noreply.github.com> Date: Sat, 3 Jun 2023 00:07:09 +0200 Subject: [PATCH] feat(emby): Show watched status for TV requests * feat(emby): Show watched status for TV requests * Consider only requested episodes in played progress * Clarify tv watched progress tooltip * Fix unrespected code guidelines --- src/Ombi.Api.Emby/EmbyApi.cs | 26 +- src/Ombi.Api.Emby/IBaseEmbyApi.cs | 1 + src/Ombi.Core/Engine/TvRequestEngine.cs | 81 ++- src/Ombi.DependencyInjection/IocExtensions.cs | 1 + src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs | 139 ++++- .../Jobs/Ombi/MediaDatabaseRefresh.cs | 14 +- src/Ombi.Store/Context/ExternalContext.cs | 1 + .../Entities/Requests/ChildRequests.cs | 3 + src/Ombi.Store/Entities/UserPlayedEpisode.cs | 10 + ...30515182204_MovieEpisodePlayed.Designer.cs | 589 ++++++++++++++++++ .../20230515182204_MovieEpisodePlayed.cs | 37 ++ .../ExternalMySqlContextModelSnapshot.cs | 23 + ...230515161757_EpisodeUserPlayed.Designer.cs | 587 +++++++++++++++++ .../20230515161757_EpisodeUserPlayed.cs | 34 + .../ExternalSqliteContextModelSnapshot.cs | 23 + .../IUserPlayedEpisodeRepository.cs | 13 + .../Repository/UserPlayedEpisodeRepository.cs | 26 + src/Ombi/ClientApp/src/app/app.module.ts | 2 + .../src/app/interfaces/IRequestModel.ts | 1 + .../components/tv-grid/tv-grid.component.html | 13 + .../components/tv-grid/tv-grid.component.scss | 6 + .../components/tv-grid/tv-grid.component.ts | 32 +- .../ClientApp/src/app/shared/shared.module.ts | 3 + src/Ombi/wwwroot/translations/en.json | 1 + 24 files changed, 1624 insertions(+), 42 deletions(-) create mode 100644 src/Ombi.Store/Entities/UserPlayedEpisode.cs create mode 100644 src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.Designer.cs create mode 100644 src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.cs create mode 100644 src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.Designer.cs create mode 100644 src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.cs create mode 100644 src/Ombi.Store/Repository/IUserPlayedEpisodeRepository.cs create mode 100644 src/Ombi.Store/Repository/UserPlayedEpisodeRepository.cs create mode 100644 src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.scss diff --git a/src/Ombi.Api.Emby/EmbyApi.cs b/src/Ombi.Api.Emby/EmbyApi.cs index ca9d140ce..5d941b083 100644 --- a/src/Ombi.Api.Emby/EmbyApi.cs +++ b/src/Ombi.Api.Emby/EmbyApi.cs @@ -254,18 +254,30 @@ namespace Ombi.Api.Emby 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) + public async Task> GetMoviesPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) => + await GetPlayed("Movie", apiKey, userId, baseUri, startIndex, count, parentIdFilder, "ProviderIds"); + + public async Task> GetTvPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri) => + await GetPlayed("Episode", 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, + string fields = default) { var request = new Request($"emby/items", baseUri, HttpMethod.Get); request.AddQueryString("Recursive", true.ToString()); request.AddQueryString("IncludeItemTypes", type); - request.AddQueryString("Fields", "ProviderIds"); + if (!string.IsNullOrEmpty(fields)) + { + request.AddQueryString("Fields", fields); + } request.AddQueryString("UserId", userId); request.AddQueryString("isPlayed", true.ToString()); diff --git a/src/Ombi.Api.Emby/IBaseEmbyApi.cs b/src/Ombi.Api.Emby/IBaseEmbyApi.cs index c9b0275f6..e2ebc513c 100644 --- a/src/Ombi.Api.Emby/IBaseEmbyApi.cs +++ b/src/Ombi.Api.Emby/IBaseEmbyApi.cs @@ -34,5 +34,6 @@ namespace Ombi.Api.Emby 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); + Task> GetTvPlayed(string apiKey, string parentIdFilder, int startIndex, int count, string userId, string baseUri); } } \ No newline at end of file diff --git a/src/Ombi.Core/Engine/TvRequestEngine.cs b/src/Ombi.Core/Engine/TvRequestEngine.cs index 5ebce9586..16866d0dd 100644 --- a/src/Ombi.Core/Engine/TvRequestEngine.cs +++ b/src/Ombi.Core/Engine/TvRequestEngine.cs @@ -35,7 +35,8 @@ namespace Ombi.Core.Engine public TvRequestEngine(ITvMazeApi tvApi, IMovieDbApi movApi, IRequestServiceMain requestService, ICurrentUser user, INotificationHelper helper, IRuleEvaluator rule, OmbiUserManager manager, ILogger logger, ITvSender sender, IRepository rl, ISettingsService settings, ICacheService cache, - IRepository sub, IMediaCacheService mediaCacheService) : base(user, requestService, rule, manager, cache, settings, sub) + IRepository sub, IMediaCacheService mediaCacheService, + IUserPlayedEpisodeRepository userPlayedEpisodeRepository) : base(user, requestService, rule, manager, cache, settings, sub) { TvApi = tvApi; MovieDbApi = movApi; @@ -44,6 +45,7 @@ namespace Ombi.Core.Engine TvSender = sender; _requestLog = rl; _mediaCacheService = mediaCacheService; + _userPlayedEpisodeRepository = userPlayedEpisodeRepository; } private INotificationHelper NotificationHelper { get; } @@ -54,6 +56,7 @@ namespace Ombi.Core.Engine private readonly ILogger _logger; private readonly IRepository _requestLog; private readonly IMediaCacheService _mediaCacheService; + private readonly IUserPlayedEpisodeRepository _userPlayedEpisodeRepository; public async Task RequestTvShow(TvRequestViewModel tv) { @@ -292,7 +295,7 @@ namespace Ombi.Core.Engine .Skip(position).Take(count).ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return new RequestsViewModel { @@ -328,7 +331,7 @@ namespace Ombi.Core.Engine return new RequestsViewModel(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return new RequestsViewModel { @@ -351,7 +354,7 @@ namespace Ombi.Core.Engine allRequests = await TvRepository.Get().ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } @@ -396,7 +399,7 @@ namespace Ombi.Core.Engine ? allRequests.OrderBy(x => prop.GetValue(x)).ToList() : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList(); - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); // Make sure we do not show duplicate child requests allRequests = allRequests.DistinctBy(x => x.ParentRequest.Title).ToList(); @@ -469,7 +472,7 @@ namespace Ombi.Core.Engine ? allRequests.OrderBy(x => prop.GetValue(x)).ToList() : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList(); - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); // Make sure we do not show duplicate child requests allRequests = allRequests.DistinctBy(x => x.ParentRequest.Title).ToList(); @@ -523,7 +526,7 @@ namespace Ombi.Core.Engine allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase) ? allRequests.OrderBy(x => prop.GetValue(x)).ToList() : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList(); - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); // Make sure we do not show duplicate child requests allRequests = allRequests.DistinctBy(x => x.ParentRequest.Title).ToList(); @@ -551,7 +554,7 @@ namespace Ombi.Core.Engine allRequests = await TvRepository.GetLite().ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } @@ -570,7 +573,7 @@ namespace Ombi.Core.Engine request = await TvRepository.Get().Where(x => x.Id == requestId).FirstOrDefaultAsync(); } - await CheckForSubscription(shouldHide, new List{request}); + await FillAdditionalFields(shouldHide, new List{request}); return request; } @@ -624,7 +627,7 @@ namespace Ombi.Core.Engine allRequests = await TvRepository.GetChild().Include(x => x.SeasonRequests).Where(x => x.ParentRequestId == tvId).ToListAsync(); } - await CheckForSubscription(shouldHide, allRequests); + await FillAdditionalFields(shouldHide, allRequests); return allRequests; } @@ -643,7 +646,7 @@ namespace Ombi.Core.Engine } var results = await allRequests.Where(x => x.Title.Contains(search, CompareOptions.IgnoreCase)).ToListAsync(); - await CheckForSubscription(shouldHide, results); + await FillAdditionalFields(shouldHide, results); return results; } @@ -864,14 +867,20 @@ namespace Ombi.Core.Engine } } - private async Task CheckForSubscription(HideResult shouldHide, List x) + private async Task FillAdditionalFields(HideResult shouldHide, List x) { foreach (var tvRequest in x) { - await CheckForSubscription(shouldHide, tvRequest.ChildRequests); + await FillAdditionalFields(shouldHide, tvRequest.ChildRequests); } } + private async Task FillAdditionalFields(HideResult shouldHide, List childRequests) + { + await CheckForSubscription(shouldHide, childRequests); + CheckForPlayed(shouldHide, childRequests); + } + private async Task CheckForSubscription(HideResult shouldHide, List childRequests) { var sub = _subscriptionRepository.GetAll(); @@ -896,6 +905,52 @@ namespace Ombi.Core.Engine } } + private class EpisodeKey + { + public int SeasonNumber; + public int EpisodeNumber; + } + + private void CheckForPlayed(HideResult shouldHide, List childRequests) + { + var theMovieDbIds = childRequests.Select(x => x.Id); + foreach (var request in childRequests) + { + var requestedEpisodes = GetEpisodesKeys(request); + + var playedEpisodes = _userPlayedEpisodeRepository + .GetAll() + .Where(x => x.TheMovieDbId == request.Id && x.UserId == request.RequestedUserId) + .AsEnumerable() + .Join(requestedEpisodes, + played => new { played.SeasonNumber, played.EpisodeNumber }, + requested => new { requested.SeasonNumber, requested.EpisodeNumber }, + (played, requested) => new { played }); + + var playedCount = playedEpisodes.Count(); + var toWatchCount = requestedEpisodes.Count(); + request.RequestedUserPlayedProgress = 100 * playedCount / toWatchCount; + + } + } + + private List GetEpisodesKeys(ChildRequests request) + { + List result = new List(); + foreach(var season in request.SeasonRequests) + { + foreach(var episode in season.Episodes) + { + result.Add(new EpisodeKey + { + SeasonNumber = season.SeasonNumber, + EpisodeNumber = episode.EpisodeNumber + }); + } + } + return result; + } + private async Task AddExistingRequest(ChildRequests newRequest, TvRequests existingRequest, string requestOnBehalf, int rootFolder, int qualityProfile) { // Add the child diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index c9bcc13d3..8a5509963 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -198,6 +198,7 @@ namespace Ombi.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs index 5af5a9756..36ed0ae7a 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyPlayedSync.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; 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.Authentication; using Ombi.Core.Settings; @@ -18,20 +19,34 @@ 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) + public EmbyPlayedSync( + ISettingsService settings, + IEmbyApiFactory api, + ILogger logger, + IUserPlayedMovieRepository movieRepo, + IUserPlayedEpisodeRepository episodeRepo, + IEmbyContentRepository contentRepo, + INotificationHubService notification, + OmbiUserManager user) : base(settings, api, logger, notification) { _userManager = user; - _repo = repo; + _movieRepo = movieRepo; + _contentRepo = contentRepo; + _episodeRepo = episodeRepo; } private OmbiUserManager _userManager { get; } - private readonly IUserPlayedMovieRepository _repo; + private readonly IUserPlayedMovieRepository _movieRepo; + private readonly IUserPlayedEpisodeRepository _episodeRepo; + private readonly IEmbyContentRepository _contentRepo; - protected override Task ProcessTv(EmbyServers server, string parentId = default) + protected async override Task ProcessTv(EmbyServers server, string parentId = default) { - // TODO - return Task.CompletedTask; + var allUsers = await _userManager.Users.Where(x => x.UserType == UserType.EmbyUser || x.UserType == UserType.EmbyConnectUser).ToListAsync(); + foreach (var user in allUsers) + { + await ProcessTvUser(server, user, parentId); + } } protected async override Task ProcessMovies(EmbyServers server, string parentId = default) @@ -65,7 +80,7 @@ namespace Ombi.Schedule.Jobs.Emby var totalCount = movies.TotalRecordCount; var processed = 0; var mediaToAdd = new HashSet(); - + while (processed < totalCount) { foreach (var movie in movies.Items) @@ -80,7 +95,7 @@ namespace Ombi.Schedule.Jobs.Emby { movies = await Api.GetMoviesPlayed(server.ApiKey, parentId, processed, AmountToTake, user.ProviderUserId, server.FullUri); } - await _repo.AddRange(mediaToAdd); + await _movieRepo.AddRange(mediaToAdd); mediaToAdd.Clear(); } } @@ -98,13 +113,117 @@ namespace Ombi.Schedule.Jobs.Emby UserId = user.Id }; // Check if it exists - var existingMovie = await _repo.Get(userPlayedMovie.TheMovieDbId, userPlayedMovie.UserId); + var existingMovie = await _movieRepo.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); } } + + private async Task ProcessTvUser(EmbyServers server, OmbiUser user, string parentId = default) + { + EmbyItemContainer episodes; + if (recentlyAdded) + { + var recentlyAddedAmountToTake = 10; // to be adjusted? + episodes = await Api.GetTvPlayed(server.ApiKey, parentId, 0, recentlyAddedAmountToTake, user.ProviderUserId, server.FullUri); + // Setting this so we don't attempt to grab more than we need + if (episodes.TotalRecordCount > recentlyAddedAmountToTake) + { + episodes.TotalRecordCount = recentlyAddedAmountToTake; + } + } + else + { + episodes = await Api.GetTvPlayed(server.ApiKey, parentId, 0, AmountToTake, user.ProviderUserId, server.FullUri); + } + var totalCount = episodes.TotalRecordCount; + var processed = 0; + var mediaToAdd = new HashSet(); + + while (processed < totalCount) + { + foreach (var episode in episodes.Items) + { + await ProcessTv(episode, 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) + { + episodes = await Api.GetTvPlayed(server.ApiKey, parentId, processed, AmountToTake, user.ProviderUserId, server.FullUri); + } + await _episodeRepo.AddRange(mediaToAdd); + mediaToAdd.Clear(); + } + } + + + private async Task ProcessTv(EmbyEpisodes episode, OmbiUser user, ICollection content, EmbyServers server) + { + + var parent = await _contentRepo.GetByEmbyId(episode.SeriesId); + if (parent == null) + { + _logger.LogInformation("The episode {0} does not relate to a series, so we cannot save this", + episode.Name); + return; + } + if (parent.TheMovieDbId.IsNullOrEmpty()) + { + _logger.LogWarning($"Episode {episode.Name} is not linked to a TMDB series. Skipping."); + return; + } + + await AddToContent(content, new UserPlayedEpisode() + { + TheMovieDbId = int.Parse(parent.TheMovieDbId), + SeasonNumber = episode.ParentIndexNumber, + EpisodeNumber = episode.IndexNumber, + UserId = user.Id + }); + + if (episode.IndexNumberEnd.HasValue && episode.IndexNumberEnd.Value != episode.IndexNumber) + { + int episodeNumber = episode.IndexNumber; + do + { + _logger.LogDebug($"Multiple-episode file detected. Adding episode ${episodeNumber}"); + episodeNumber++; + + await AddToContent(content, new UserPlayedEpisode() + { + TheMovieDbId = int.Parse(parent.TheMovieDbId), + SeasonNumber = episode.ParentIndexNumber, + EpisodeNumber = episodeNumber, + UserId = user.Id + }); + + + } while (episodeNumber < episode.IndexNumberEnd.Value); + + } + } + + private async Task AddToContent(ICollection content, UserPlayedEpisode episode) + { + + // Check if it exists + var existingEpisode = await _episodeRepo.Get(episode.TheMovieDbId, episode.SeasonNumber, episode.EpisodeNumber, episode.UserId); + var alreadyGoingToAdd = content.Any(x => + x.TheMovieDbId == episode.TheMovieDbId + && x.SeasonNumber == episode.SeasonNumber + && x.EpisodeNumber == episode.EpisodeNumber + && x.UserId == episode.UserId); + if (existingEpisode == null && !alreadyGoingToAdd) + { + content.Add(episode); + } + } } + } diff --git a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs index e3573e6c8..d1363a455 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/MediaDatabaseRefresh.cs @@ -21,7 +21,8 @@ namespace Ombi.Schedule.Jobs.Ombi IPlexContentRepository plexRepo, IEmbyContentRepository embyRepo, IJellyfinContentRepository jellyfinRepo, - IUserPlayedMovieRepository userPlayedRepo, + IUserPlayedMovieRepository userPlayedMovieRepo, + IUserPlayedEpisodeRepository userPlayedEpisodeRepo, ISettingsService embySettings, ISettingsService jellyfinSettings) { @@ -30,7 +31,8 @@ namespace Ombi.Schedule.Jobs.Ombi _plexRepo = plexRepo; _embyRepo = embyRepo; _jellyfinRepo = jellyfinRepo; - _userPlayedRepo = userPlayedRepo; + _userPlayedMovieRepo = userPlayedMovieRepo; + _userPlayedEpisodeRepo = userPlayedEpisodeRepo; _embySettings = embySettings; _jellyfinSettings = jellyfinSettings; _plexSettings.ClearCache(); @@ -41,7 +43,8 @@ namespace Ombi.Schedule.Jobs.Ombi private readonly IPlexContentRepository _plexRepo; private readonly IEmbyContentRepository _embyRepo; private readonly IJellyfinContentRepository _jellyfinRepo; - private readonly IUserPlayedMovieRepository _userPlayedRepo; + private readonly IUserPlayedMovieRepository _userPlayedMovieRepo; + private readonly IUserPlayedEpisodeRepository _userPlayedEpisodeRepo; private readonly ISettingsService _embySettings; private readonly ISettingsService _jellyfinSettings; @@ -66,7 +69,10 @@ namespace Ombi.Schedule.Jobs.Ombi try { const string movieSql = "DELETE FROM UserPlayedMovie"; - await _userPlayedRepo.ExecuteSql(movieSql); + await _userPlayedMovieRepo.ExecuteSql(movieSql); + + const string episodeSql = "DELETE FROM UserPlayedEpisode"; + await _userPlayedEpisodeRepo.ExecuteSql(episodeSql); } catch (Exception e) { diff --git a/src/Ombi.Store/Context/ExternalContext.cs b/src/Ombi.Store/Context/ExternalContext.cs index c54c39beb..da6bcce71 100644 --- a/src/Ombi.Store/Context/ExternalContext.cs +++ b/src/Ombi.Store/Context/ExternalContext.cs @@ -42,6 +42,7 @@ namespace Ombi.Store.Context public DbSet SickRageCache { get; set; } public DbSet SickRageEpisodeCache { get; set; } public DbSet UserPlayedMovie { get; set; } + public DbSet UserPlayedEpisode { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/src/Ombi.Store/Entities/Requests/ChildRequests.cs b/src/Ombi.Store/Entities/Requests/ChildRequests.cs index 9d4376452..dca501456 100644 --- a/src/Ombi.Store/Entities/Requests/ChildRequests.cs +++ b/src/Ombi.Store/Entities/Requests/ChildRequests.cs @@ -59,6 +59,9 @@ namespace Ombi.Store.Entities.Requests return string.Empty; } } + + [NotMapped] + public int RequestedUserPlayedProgress { get; set; } } public enum SeriesType diff --git a/src/Ombi.Store/Entities/UserPlayedEpisode.cs b/src/Ombi.Store/Entities/UserPlayedEpisode.cs new file mode 100644 index 000000000..2966f4892 --- /dev/null +++ b/src/Ombi.Store/Entities/UserPlayedEpisode.cs @@ -0,0 +1,10 @@ +namespace Ombi.Store.Entities +{ + public class UserPlayedEpisode : Entity + { + public int TheMovieDbId { get; set; } + public int SeasonNumber { get; set; } + public int EpisodeNumber { get; set; } + public string UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.Designer.cs new file mode 100644 index 000000000..96cb5c176 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.Designer.cs @@ -0,0 +1,589 @@ +// +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("20230515182204_MovieEpisodePlayed")] + partial class MovieEpisodePlayed + { + 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.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + + 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/20230515182204_MovieEpisodePlayed.cs b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.cs new file mode 100644 index 000000000..0e0cd54a3 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalMySql/20230515182204_MovieEpisodePlayed.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalMySql +{ + public partial class MovieEpisodePlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedEpisode", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TheMovieDbId = table.Column(type: "int", nullable: false), + SeasonNumber = table.Column(type: "int", nullable: false), + EpisodeNumber = table.Column(type: "int", nullable: false), + UserId = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedEpisode", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedEpisode"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs index 0121a99ba..c4de7028e 100644 --- a/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalMySql/ExternalMySqlContextModelSnapshot.cs @@ -488,6 +488,29 @@ namespace Ombi.Store.Migrations.ExternalMySql b.ToTable("SonarrEpisodeCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("TheMovieDbId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => { b.Property("Id") diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.Designer.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.Designer.cs new file mode 100644 index 000000000..adf675d22 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.Designer.cs @@ -0,0 +1,587 @@ +// +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("20230515161757_EpisodeUserPlayed")] + partial class EpisodeUserPlayed + { + 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.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + + 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/20230515161757_EpisodeUserPlayed.cs b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.cs new file mode 100644 index 000000000..b4125f905 --- /dev/null +++ b/src/Ombi.Store/Migrations/ExternalSqlite/20230515161757_EpisodeUserPlayed.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Ombi.Store.Migrations.ExternalSqlite +{ + public partial class EpisodeUserPlayed : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPlayedEpisode", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TheMovieDbId = table.Column(type: "INTEGER", nullable: false), + SeasonNumber = table.Column(type: "INTEGER", nullable: false), + EpisodeNumber = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserPlayedEpisode", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPlayedEpisode"); + } + } +} diff --git a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs index 857259ab1..e53ed4363 100644 --- a/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs +++ b/src/Ombi.Store/Migrations/ExternalSqlite/ExternalSqliteContextModelSnapshot.cs @@ -486,6 +486,29 @@ namespace Ombi.Store.Migrations.ExternalSqlite b.ToTable("SonarrEpisodeCache"); }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedEpisode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("TheMovieDbId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("UserPlayedEpisode"); + }); + modelBuilder.Entity("Ombi.Store.Entities.UserPlayedMovie", b => { b.Property("Id") diff --git a/src/Ombi.Store/Repository/IUserPlayedEpisodeRepository.cs b/src/Ombi.Store/Repository/IUserPlayedEpisodeRepository.cs new file mode 100644 index 000000000..8e292cb8a --- /dev/null +++ b/src/Ombi.Store/Repository/IUserPlayedEpisodeRepository.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 IUserPlayedEpisodeRepository : IExternalRepository + { + Task Get(int theMovieDbId, int seasonNumber, int episodeNumber, string userId); + } +} \ No newline at end of file diff --git a/src/Ombi.Store/Repository/UserPlayedEpisodeRepository.cs b/src/Ombi.Store/Repository/UserPlayedEpisodeRepository.cs new file mode 100644 index 000000000..c5dda0ef6 --- /dev/null +++ b/src/Ombi.Store/Repository/UserPlayedEpisodeRepository.cs @@ -0,0 +1,26 @@ +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 UserPlayedEpisodeRepository : ExternalRepository, IUserPlayedEpisodeRepository + { + protected ExternalContext Db { get; } + public UserPlayedEpisodeRepository(ExternalContext db) : base(db) + { + Db = db; + } + + public async Task Get(int theMovieDbId, int seasonNumber, int episodeNumber, string userId) + { + return await Db.UserPlayedEpisode.FirstOrDefaultAsync(x => x.TheMovieDbId == theMovieDbId && x.SeasonNumber == seasonNumber && x.EpisodeNumber == episodeNumber && x.UserId == userId); + } + } +} \ No newline at end of file diff --git a/src/Ombi/ClientApp/src/app/app.module.ts b/src/Ombi/ClientApp/src/app/app.module.ts index c4547ffe2..3240dd80c 100644 --- a/src/Ombi/ClientApp/src/app/app.module.ts +++ b/src/Ombi/ClientApp/src/app/app.module.ts @@ -46,6 +46,7 @@ import { MatNativeDateModule } from '@angular/material/core'; import { MatPaginatorI18n } from "./localization/MatPaginatorI18n"; import { MatPaginatorIntl } from "@angular/material/paginator"; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { MatSnackBarModule } from '@angular/material/snack-bar'; @@ -150,6 +151,7 @@ export function JwtTokenGetter() { OverlayModule, MatCheckboxModule, MatProgressSpinnerModule, + MatProgressBarModule, JwtModule.forRoot({ config: { tokenGetter: JwtTokenGetter, diff --git a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts index 6031e9796..7270e125b 100644 --- a/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts +++ b/src/Ombi/ClientApp/src/app/interfaces/IRequestModel.ts @@ -132,6 +132,7 @@ export interface ITvRequests { background: any; totalSeasons: number; tvDbId: number; // NO LONGER USED + requestedUserPlayedProgress: number; open: boolean; // THIS IS FOR THE UI diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html index 3c03e06ad..7ee45dcae 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.html @@ -60,6 +60,19 @@ + + + {{ 'Requests.Watched' | translate}} + + + + + + diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.scss b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.scss new file mode 100644 index 000000000..cc8664954 --- /dev/null +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.scss @@ -0,0 +1,6 @@ +@import "./styles/variables.scss"; + +.played-progress { + width: 5rem; + height: 1rem; +} diff --git a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts index 25c552924..e4b80967d 100644 --- a/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts +++ b/src/Ombi/ClientApp/src/app/requests-list/components/tv-grid/tv-grid.component.ts @@ -4,6 +4,7 @@ import { Observable, merge, of as observableOf } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { AuthService } from "../../../auth/auth.service"; +import { FeaturesFacade } from "../../../state/features/features.facade"; import { MatPaginator } from "@angular/material/paginator"; import { MatSort } from "@angular/material/sort"; import { RequestFilterType } from "../../models/RequestFilterType"; @@ -13,15 +14,16 @@ import { StorageService } from "../../../shared/storage/storage-service"; @Component({ templateUrl: "./tv-grid.component.html", selector: "tv-grid", - styleUrls: ["../requests-list.component.scss"] + styleUrls: ["../requests-list.component.scss", "tv-grid.component.scss"] }) export class TvGridComponent implements OnInit, AfterViewInit { public dataSource: IChildRequests[] = []; public resultsLength: number; public isLoadingResults = true; - public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate','actions']; + public displayedColumns: string[] = ['series', 'requestedBy', 'status', 'requestStatus', 'requestedDate']; public gridCount: string = "15"; public isAdmin: boolean; + public isPlayedSyncEnabled = false; public defaultSort: string = "requestedDate"; public defaultOrder: string = "desc"; public currentFilter: RequestFilterType = RequestFilterType.All; @@ -40,12 +42,17 @@ export class TvGridComponent implements OnInit, AfterViewInit { @ViewChild(MatSort) sort: MatSort; constructor(private requestService: RequestServiceV2, private auth: AuthService, - private ref: ChangeDetectorRef, private storageService: StorageService) { + private ref: ChangeDetectorRef, private storageService: StorageService, + private featureFacade: FeaturesFacade) { } - public ngOnInit() { - this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + public ngOnInit() { + this.isAdmin = this.auth.hasRole("admin") || this.auth.hasRole("poweruser"); + this.isPlayedSyncEnabled = this.featureFacade.isPlayedSyncEnabled(); + + this.addDynamicColumns(); + const defaultCount = this.storageService.get(this.storageKeyGridCount); const defaultSort = this.storageService.get(this.storageKey); const defaultOrder = this.storageService.get(this.storageKeyOrder); @@ -64,9 +71,18 @@ export class TvGridComponent implements OnInit, AfterViewInit { } } + addDynamicColumns() { + 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); + this.storageService.save(this.storageKeyGridCount, this.gridCount); this.storageService.save(this.storageKeyCurrentFilter, (+this.currentFilter).toString()); this.paginator.showFirstLastButtons = true; @@ -78,7 +94,7 @@ export class TvGridComponent implements OnInit, AfterViewInit { startWith({}), switchMap((value: any) => { this.isLoadingResults = true; - + if (value.active || value.direction) { this.storageService.save(this.storageKey, value.active); this.storageService.save(this.storageKeyOrder, value.direction); @@ -103,7 +119,7 @@ export class TvGridComponent implements OnInit, AfterViewInit { const filter = () => { this.dataSource = this.dataSource.filter((req) => { return req.id !== request.id; })}; - + const onChange = () => { this.ref.detectChanges(); }; diff --git a/src/Ombi/ClientApp/src/app/shared/shared.module.ts b/src/Ombi/ClientApp/src/app/shared/shared.module.ts index 87ea0051c..14b2e97ad 100644 --- a/src/Ombi/ClientApp/src/app/shared/shared.module.ts +++ b/src/Ombi/ClientApp/src/app/shared/shared.module.ts @@ -23,6 +23,7 @@ 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 { MatProgressBarModule } from "@angular/material/progress-bar"; import {MatRadioModule} from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; @@ -66,6 +67,7 @@ import { WatchProvidersSelectComponent } from "./components/watch-providers-sele MomentModule, MatCardModule, MatProgressSpinnerModule, + MatProgressBarModule, MatAutocompleteModule, MatInputModule, MatTabsModule, @@ -99,6 +101,7 @@ import { WatchProvidersSelectComponent } from "./components/watch-providers-sele TranslateModule, SidebarModule, MatProgressSpinnerModule, + MatProgressBarModule, IssuesReportComponent, EpisodeRequestComponent, AdminRequestDialogComponent, diff --git a/src/Ombi/wwwroot/translations/en.json b/src/Ombi/wwwroot/translations/en.json index ed09c1b64..0607e7c13 100644 --- a/src/Ombi/wwwroot/translations/en.json +++ b/src/Ombi/wwwroot/translations/en.json @@ -161,6 +161,7 @@ "RequestStatus": "Request status", "Watched": "Watched", "WatchedTooltip": "The user who made the request has watched it", + "WatchedProgressTooltip": "Shows how much the user who made the request has watched it", "WatchedByUsersCount": "{{count}} users have watched this.", "Denied": " Denied:", "TheatricalRelease": "Theatrical Release: {{date}}",