From 66abf92311586292bf40ae0308bc00aa279862da Mon Sep 17 00:00:00 2001 From: Florian Dupret <34862846+sephrat@users.noreply.github.com> Date: Tue, 22 Mar 2022 13:42:24 +0100 Subject: [PATCH] First attempt at episode sync refactor --- .../Models/Media/Tv/EmbyEpisodes.cs | 4 +- .../Models/Media/Tv/JellyfinEpisodes.cs | 4 +- .../Models/Tv/IMediaServerEpisode.cs | 8 + .../Ombi.Api.MediaServer.csproj | 1 + src/Ombi.Api.Plex/Models/Metadata.cs | 5 +- src/Ombi.Api.Plex/Ombi.Api.Plex.csproj | 1 + .../Jobs/Emby/EmbyEpisodeSync.cs | 156 ++++++------- .../Jobs/Jellyfin/JellyfinEpisodeSync.cs | 165 +++++++------ .../MediaServer/IMediaServerEpisodeSync.cs | 14 ++ .../MediaServer/MediaServerEpisodeSync.cs | 105 +++++++++ .../Jobs/Plex/Interfaces/IPlexEpisodeSync.cs | 7 +- .../Jobs/Plex/PlexContentSync.cs | 4 +- .../Jobs/Plex/PlexEpisodeSync.cs | 216 +++++++----------- 13 files changed, 371 insertions(+), 319 deletions(-) create mode 100644 src/Ombi.Api.MediaServer/Models/Tv/IMediaServerEpisode.cs create mode 100644 src/Ombi.Schedule/Jobs/MediaServer/IMediaServerEpisodeSync.cs create mode 100644 src/Ombi.Schedule/Jobs/MediaServer/MediaServerEpisodeSync.cs diff --git a/src/Ombi.Api.Emby/Models/Media/Tv/EmbyEpisodes.cs b/src/Ombi.Api.Emby/Models/Media/Tv/EmbyEpisodes.cs index 83d64ed15..bb5accd45 100644 --- a/src/Ombi.Api.Emby/Models/Media/Tv/EmbyEpisodes.cs +++ b/src/Ombi.Api.Emby/Models/Media/Tv/EmbyEpisodes.cs @@ -1,9 +1,10 @@ using Ombi.Api.Emby.Models.Movie; +using Ombi.Api.MediaServer.Models.Media.Tv; using System; namespace Ombi.Api.Emby.Models.Media.Tv { - public class EmbyEpisodes + public class EmbyEpisodes : IMediaServerEpisodes { public string Name { get; set; } public string ServerId { get; set; } @@ -41,5 +42,6 @@ namespace Ombi.Api.Emby.Models.Media.Tv public string MediaType { get; set; } public bool HasSubtitles { get; set; } public EmbyProviderids ProviderIds { get; set; } + int IMediaServerEpisodes.EpisodeNumber => IndexNumber; } } \ No newline at end of file diff --git a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs index a2769138e..66a818a38 100644 --- a/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs +++ b/src/Ombi.Api.Jellyfin/Models/Media/Tv/JellyfinEpisodes.cs @@ -1,9 +1,10 @@ using Ombi.Api.Jellyfin.Models.Movie; +using Ombi.Api.MediaServer.Models.Media.Tv; using System; namespace Ombi.Api.Jellyfin.Models.Media.Tv { - public class JellyfinEpisodes + public class JellyfinEpisodes : IMediaServerEpisodes { public string Name { get; set; } public string ServerId { get; set; } @@ -41,5 +42,6 @@ namespace Ombi.Api.Jellyfin.Models.Media.Tv public string MediaType { get; set; } public bool HasSubtitles { get; set; } public JellyfinProviderids ProviderIds { get; set; } + int IMediaServerEpisodes.EpisodeNumber => IndexNumber; } } \ No newline at end of file diff --git a/src/Ombi.Api.MediaServer/Models/Tv/IMediaServerEpisode.cs b/src/Ombi.Api.MediaServer/Models/Tv/IMediaServerEpisode.cs new file mode 100644 index 000000000..e4dacd6e1 --- /dev/null +++ b/src/Ombi.Api.MediaServer/Models/Tv/IMediaServerEpisode.cs @@ -0,0 +1,8 @@ +namespace Ombi.Api.MediaServer.Models.Media.Tv +{ + public interface IMediaServerEpisodes + { + public int EpisodeNumber { get; } + public string Name { get; } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.MediaServer/Ombi.Api.MediaServer.csproj b/src/Ombi.Api.MediaServer/Ombi.Api.MediaServer.csproj index f167146af..25e4efaff 100644 --- a/src/Ombi.Api.MediaServer/Ombi.Api.MediaServer.csproj +++ b/src/Ombi.Api.MediaServer/Ombi.Api.MediaServer.csproj @@ -13,6 +13,7 @@ + \ No newline at end of file diff --git a/src/Ombi.Api.Plex/Models/Metadata.cs b/src/Ombi.Api.Plex/Models/Metadata.cs index 65cb21e95..732aeca63 100644 --- a/src/Ombi.Api.Plex/Models/Metadata.cs +++ b/src/Ombi.Api.Plex/Models/Metadata.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using Ombi.Api.MediaServer.Models.Media.Tv; namespace Ombi.Api.Plex.Models { - public class Metadata + public class Metadata : IMediaServerEpisodes { public int ratingKey { get; set; } public string key { get; set; } @@ -39,6 +40,8 @@ namespace Ombi.Api.Plex.Models public string chapterSource { get; set; } public Medium[] Media { get; set; } public List Guid { get; set; } = new List(); + int IMediaServerEpisodes.EpisodeNumber => index; + public string Name => title; } public class PlexGuids diff --git a/src/Ombi.Api.Plex/Ombi.Api.Plex.csproj b/src/Ombi.Api.Plex/Ombi.Api.Plex.csproj index 4945a2fb2..1fc3dc5a0 100644 --- a/src/Ombi.Api.Plex/Ombi.Api.Plex.csproj +++ b/src/Ombi.Api.Plex/Ombi.Api.Plex.csproj @@ -12,6 +12,7 @@ + \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs index b3c1ffbab..3d28f7a64 100644 --- a/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Emby/EmbyEpisodeSync.cs @@ -42,46 +42,51 @@ using Quartz; using Ombi.Schedule.Jobs.Ombi; using Ombi.Api.Emby.Models; using Ombi.Api.Emby.Models.Media.Tv; +using Ombi.Schedule.Jobs.MediaServer; namespace Ombi.Schedule.Jobs.Emby { - public class EmbyEpisodeSync : IEmbyEpisodeSync + public class EmbyEpisodeSync : MediaServerEpisodeSync, IEmbyEpisodeSync { public EmbyEpisodeSync(ISettingsService s, IEmbyApiFactory api, ILogger l, IEmbyContentRepository repo - , IHubContext notification) + , IHubContext notification) : base(l, repo, notification) { _apiFactory = api; - _logger = l; _settings = s; - _repo = repo; - _notification = notification; } private readonly ISettingsService _settings; private readonly IEmbyApiFactory _apiFactory; - private readonly ILogger _logger; - private readonly IEmbyContentRepository _repo; - private readonly IHubContext _notification; + private bool _recentlyAddedSearch = false; private const int AmountToTake = 100; private IEmbyApi Api { get; set; } - public async Task Execute(IJobExecutionContext context) + public override async Task Execute(IJobExecutionContext context) { JobDataMap dataMap = context.MergedJobDataMap; - var recentlyAddedSearch = false; if (dataMap.TryGetValue(JobDataKeys.EmbyRecentlyAddedSearch, out var recentlyAddedObj)) { - recentlyAddedSearch = Convert.ToBoolean(recentlyAddedObj); + _recentlyAddedSearch = Convert.ToBoolean(recentlyAddedObj); } + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started"); + await CacheEpisodes(); + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Finished"); + _logger.LogInformation("Emby Episode Sync Finished - Triggering Metadata refresh"); + await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); + } + + protected async override IAsyncEnumerable GetMediaServerEpisodes() + { var settings = await _settings.GetSettingsAsync(); Api = _apiFactory.CreateClient(settings); - await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) - .SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Started"); foreach (var server in settings.Servers) { if (server.EmbySelectedLibraries.Any() && server.EmbySelectedLibraries.Any(x => x.Enabled)) @@ -90,25 +95,26 @@ namespace Ombi.Schedule.Jobs.Emby foreach (var tvParentIdFilter in tvLibsToFilter) { _logger.LogInformation($"Scanning Lib for episodes '{tvParentIdFilter.Title}'"); - await CacheEpisodes(server, recentlyAddedSearch, tvParentIdFilter.Key); + await foreach (var ep in GetEpisodesFromLibrary(server, tvParentIdFilter.Key)) + { + yield return ep; + } } } else { - await CacheEpisodes(server, recentlyAddedSearch, string.Empty); + await foreach (var ep in GetEpisodesFromLibrary(server, string.Empty)) + { + yield return ep; + } } } - await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) - .SendAsync(NotificationHub.NotificationEvent, "Emby Episode Sync Finished"); - _logger.LogInformation("Emby Episode Sync Finished - Triggering Metadata refresh"); - await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); } - - private async Task CacheEpisodes(EmbyServers server, bool recentlyAdded, string parentIdFilter) + private async IAsyncEnumerable GetEpisodesFromLibrary(EmbyServers server, string parentIdFilter) { EmbyItemContainer allEpisodes; - if (recentlyAdded) + if (_recentlyAddedSearch) { var recentlyAddedAmountToTake = AmountToTake; allEpisodes = await Api.RecentlyAddedEpisodes(server.ApiKey, parentIdFilter, 0, recentlyAddedAmountToTake, server.AdministratorId, server.FullUri); @@ -123,7 +129,6 @@ namespace Ombi.Schedule.Jobs.Emby } var total = allEpisodes.TotalRecordCount; var processed = 0; - var epToAdd = new HashSet(); while (processed < total) { foreach (var ep in allEpisodes.Items) @@ -145,84 +150,55 @@ namespace Ombi.Schedule.Jobs.Emby ep.Name); continue; } - - var existingEpisode = await _repo.GetEpisodeByEmbyId(ep.Id); - // Make sure it's not in the hashset too - var existingInList = epToAdd.Any(x => x.EmbyId == ep.Id); - - if (existingEpisode == null && !existingInList) - { - // Sanity checks - if (ep.IndexNumber == 0) - { - _logger.LogWarning($"Episode {ep.Name} has no episode number. Skipping."); - continue; - } - - _logger.LogDebug("Adding new episode {0} to parent {1}", ep.Name, ep.SeriesName); - // add it - epToAdd.Add(new EmbyEpisode - { - EmbyId = ep.Id, - EpisodeNumber = ep.IndexNumber, - SeasonNumber = ep.ParentIndexNumber, - ParentId = ep.SeriesId, - TvDbId = ep.ProviderIds.Tvdb, - TheMovieDbId = ep.ProviderIds.Tmdb, - ImdbId = ep.ProviderIds.Imdb, - Title = ep.Name, - AddedAt = DateTime.UtcNow - }); - - if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber) - { - epToAdd.Add(new EmbyEpisode - { - EmbyId = ep.Id, - EpisodeNumber = ep.IndexNumberEnd.Value, - SeasonNumber = ep.ParentIndexNumber, - ParentId = ep.SeriesId, - TvDbId = ep.ProviderIds.Tvdb, - TheMovieDbId = ep.ProviderIds.Tmdb, - ImdbId = ep.ProviderIds.Imdb, - Title = ep.Name, - AddedAt = DateTime.UtcNow - }); - } - } + yield return ep; } - - await _repo.AddRange(epToAdd); - epToAdd.Clear(); - if (!recentlyAdded) + if (!_recentlyAddedSearch) { allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, AmountToTake, server.AdministratorId, server.FullUri); } } - - if (epToAdd.Any()) - { - await _repo.AddRange(epToAdd); - } } - private bool _disposed; - protected virtual void Dispose(bool disposing) + protected override async Task GetExistingEpisode(EmbyEpisodes ep) { - if (_disposed) - return; - - if (disposing) - { - //_settings?.Dispose(); - } - _disposed = true; + return await _repo.GetEpisodeByEmbyId(ep.Id); } - public void Dispose() + protected override bool IsIn(EmbyEpisodes ep, ICollection list) { - Dispose(true); - GC.SuppressFinalize(this); + return list.Any(x => x.EmbyId == ep.Id); + } + + protected override void addEpisode(EmbyEpisodes ep, ICollection epToAdd) + { + epToAdd.Add(new EmbyEpisode + { + EmbyId = ep.Id, + EpisodeNumber = ep.IndexNumber, + SeasonNumber = ep.ParentIndexNumber, + ParentId = ep.SeriesId, + TvDbId = ep.ProviderIds.Tvdb, + TheMovieDbId = ep.ProviderIds.Tmdb, + ImdbId = ep.ProviderIds.Imdb, + Title = ep.Name, + AddedAt = DateTime.UtcNow + }); + + if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber) + { + epToAdd.Add(new EmbyEpisode + { + EmbyId = ep.Id, + EpisodeNumber = ep.IndexNumberEnd.Value, + SeasonNumber = ep.ParentIndexNumber, + ParentId = ep.SeriesId, + TvDbId = ep.ProviderIds.Tvdb, + TheMovieDbId = ep.ProviderIds.Tmdb, + ImdbId = ep.ProviderIds.Imdb, + Title = ep.Name, + AddedAt = DateTime.UtcNow + }); + } } } } diff --git a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs index 6e2daa7b3..394ca51a8 100644 --- a/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Jellyfin/JellyfinEpisodeSync.cs @@ -40,36 +40,51 @@ using Ombi.Store.Entities; using Ombi.Store.Repository; using Quartz; using Ombi.Schedule.Jobs.Ombi; +using Ombi.Schedule.Jobs.MediaServer; +using Ombi.Api.Jellyfin.Models.Media.Tv; namespace Ombi.Schedule.Jobs.Jellyfin { - public class JellyfinEpisodeSync : IJellyfinEpisodeSync + public class JellyfinEpisodeSync : MediaServerEpisodeSync, IJellyfinEpisodeSync { - public JellyfinEpisodeSync(ISettingsService s, IJellyfinApiFactory api, ILogger l, IJellyfinContentRepository repo - , IHubContext notification) + + + public JellyfinEpisodeSync( + ISettingsService s, + IJellyfinApiFactory api, + ILogger> l, + IJellyfinContentRepository repo, + IHubContext notification) : base(l, repo, notification) { _apiFactory = api; - _logger = l; _settings = s; - _repo = repo; - _notification = notification; } - private readonly ISettingsService _settings; private readonly IJellyfinApiFactory _apiFactory; - private readonly ILogger _logger; - private readonly IJellyfinContentRepository _repo; - private readonly IHubContext _notification; private IJellyfinApi Api { get; set; } - - public async Task Execute(IJobExecutionContext job) + public override async Task Execute(IJobExecutionContext job) { var settings = await _settings.GetSettingsAsync(); Api = _apiFactory.CreateClient(settings); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Started"); + + await CacheEpisodes(); + + await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) + .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Finished"); + _logger.LogInformation("Jellyfin Episode Sync Finished - Triggering Metadata refresh"); + await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); + } + + private int packageSize = 200; + protected override async IAsyncEnumerable GetMediaServerEpisodes() + { + var settings = await _settings.GetSettingsAsync(); + Api = _apiFactory.CreateClient(settings); + foreach (var server in settings.Servers) { @@ -79,27 +94,27 @@ namespace Ombi.Schedule.Jobs.Jellyfin foreach (var tvParentIdFilter in tvLibsToFilter) { _logger.LogInformation($"Scanning Lib for episodes '{tvParentIdFilter.Title}'"); - await CacheEpisodes(server, tvParentIdFilter.Key); + await foreach (var ep in GetEpisodesFromLibrary(server, tvParentIdFilter.Key)) + { + yield return ep; + } } } else { - await CacheEpisodes(server, string.Empty); + await foreach (var ep in GetEpisodesFromLibrary(server, string.Empty)) + { + yield return ep; + } } } - - await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) - .SendAsync(NotificationHub.NotificationEvent, "Jellyfin Episode Sync Finished"); - _logger.LogInformation("Jellyfin Episode Sync Finished - Triggering Metadata refresh"); - await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); } - - private async Task CacheEpisodes(JellyfinServers server, string parentIdFilter) + private async IAsyncEnumerable GetEpisodesFromLibrary(JellyfinServers server, string parentIdFilter) { - var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, 0, 200, server.AdministratorId, server.FullUri); - var total = allEpisodes.TotalRecordCount; var processed = 0; - var epToAdd = new HashSet(); + var allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, packageSize, server.AdministratorId, server.FullUri); + + var total = allEpisodes.TotalRecordCount; while (processed < total) { foreach (var ep in allEpisodes.Items) @@ -121,81 +136,55 @@ namespace Ombi.Schedule.Jobs.Jellyfin ep.Name); continue; } - - var existingEpisode = await _repo.GetEpisodeByJellyfinId(ep.Id); - // Make sure it's not in the hashset too - var existingInList = epToAdd.Any(x => x.JellyfinId == ep.Id); - - if (existingEpisode == null && !existingInList) - { - // Sanity checks - if (ep.IndexNumber == 0) // no check on season number, Season 0 can be Specials - { - _logger.LogWarning($"Episode {ep.Name} has no episode number. Skipping."); - continue; - } - - _logger.LogDebug("Adding new episode {0} to parent {1}", ep.Name, ep.SeriesName); - // add it - epToAdd.Add(new JellyfinEpisode - { - JellyfinId = ep.Id, - EpisodeNumber = ep.IndexNumber, - SeasonNumber = ep.ParentIndexNumber, - ParentId = ep.SeriesId, - TvDbId = ep.ProviderIds.Tvdb, - TheMovieDbId = ep.ProviderIds.Tmdb, - ImdbId = ep.ProviderIds.Imdb, - Title = ep.Name, - AddedAt = DateTime.UtcNow - }); - - if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber) - { - epToAdd.Add(new JellyfinEpisode - { - JellyfinId = ep.Id, - EpisodeNumber = ep.IndexNumberEnd.Value, - SeasonNumber = ep.ParentIndexNumber, - ParentId = ep.SeriesId, - TvDbId = ep.ProviderIds.Tvdb, - TheMovieDbId = ep.ProviderIds.Tmdb, - ImdbId = ep.ProviderIds.Imdb, - Title = ep.Name, - AddedAt = DateTime.UtcNow - }); - } - } + yield return ep; } - - await _repo.AddRange(epToAdd); - epToAdd.Clear(); - allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, 200, server.AdministratorId, server.FullUri); - } - - if (epToAdd.Any()) - { - await _repo.AddRange(epToAdd); + allEpisodes = await Api.GetAllEpisodes(server.ApiKey, parentIdFilter, processed, packageSize, server.AdministratorId, server.FullUri); } } - private bool _disposed; - protected virtual void Dispose(bool disposing) + protected override void addEpisode(JellyfinEpisodes ep, ICollection epToAdd) { - if (_disposed) - return; - if (disposing) + _logger.LogDebug("Adding new episode {0} to parent {1}", ep.Name, ep.SeriesName); + // add it + epToAdd.Add(new JellyfinEpisode { - //_settings?.Dispose(); + JellyfinId = ep.Id, + EpisodeNumber = ep.IndexNumber, + SeasonNumber = ep.ParentIndexNumber, + ParentId = ep.SeriesId, + TvDbId = ep.ProviderIds.Tvdb, + TheMovieDbId = ep.ProviderIds.Tmdb, + ImdbId = ep.ProviderIds.Imdb, + Title = ep.Name, + AddedAt = DateTime.UtcNow + }); + + if (ep.IndexNumberEnd.HasValue && ep.IndexNumberEnd.Value != ep.IndexNumber) + { + epToAdd.Add(new JellyfinEpisode + { + JellyfinId = ep.Id, + EpisodeNumber = ep.IndexNumberEnd.Value, + SeasonNumber = ep.ParentIndexNumber, + ParentId = ep.SeriesId, + TvDbId = ep.ProviderIds.Tvdb, + TheMovieDbId = ep.ProviderIds.Tmdb, + ImdbId = ep.ProviderIds.Imdb, + Title = ep.Name, + AddedAt = DateTime.UtcNow + }); } - _disposed = true; } - public void Dispose() + protected override async Task GetExistingEpisode(JellyfinEpisodes ep) { - Dispose(true); - GC.SuppressFinalize(this); + return await _repo.GetEpisodeByJellyfinId(ep.Id); + } + + protected override bool IsIn(JellyfinEpisodes ep, ICollection list) + { + return list.Any(x => x.JellyfinId == ep.Id); } } } diff --git a/src/Ombi.Schedule/Jobs/MediaServer/IMediaServerEpisodeSync.cs b/src/Ombi.Schedule/Jobs/MediaServer/IMediaServerEpisodeSync.cs new file mode 100644 index 000000000..bf8b53162 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/MediaServer/IMediaServerEpisodeSync.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ombi.Api.MediaServer.Models.Media.Tv; +using Ombi.Store.Entities; + +namespace Ombi.Schedule.Jobs.MediaServer +{ + public interface IMediaServerEpisodeSync : IBaseJob + where T: IMediaServerEpisode + where U: IMediaServerEpisodes + { + Task> ProcessEpisodes(IAsyncEnumerable serverEpisodes, ICollection currentEpisodes); + } +} \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/MediaServer/MediaServerEpisodeSync.cs b/src/Ombi.Schedule/Jobs/MediaServer/MediaServerEpisodeSync.cs new file mode 100644 index 000000000..ac773ac53 --- /dev/null +++ b/src/Ombi.Schedule/Jobs/MediaServer/MediaServerEpisodeSync.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Ombi.Hubs; +using Ombi.Store.Entities; +using Ombi.Store.Repository; +using Quartz; +using Ombi.Api.MediaServer.Models.Media.Tv; + +namespace Ombi.Schedule.Jobs.MediaServer +{ + public abstract class MediaServerEpisodeSync : IMediaServerEpisodeSync + where T : IMediaServerEpisodes + where U : IMediaServerEpisode + where V : IMediaServerContentRepository + where W : IMediaServerContent + { + public MediaServerEpisodeSync( + ILogger l, + V repo, + IHubContext notification) + { + _logger = l; + _repo = repo; + _notification = notification; + } + + protected readonly IHubContext _notification; + protected readonly ILogger _logger; + protected readonly V _repo; + protected abstract IAsyncEnumerable GetMediaServerEpisodes(); + protected abstract Task GetExistingEpisode(T ep); + protected abstract bool IsIn(T ep, ICollection list); + + protected async Task CacheEpisodes() + { + var epToAdd = new HashSet(); + await ProcessEpisodes(GetMediaServerEpisodes(), epToAdd); + } + + protected abstract void addEpisode(T ep, ICollection epToAdd); + + 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); + } + + public abstract Task Execute(IJobExecutionContext context); + + public async Task> ProcessEpisodes(IAsyncEnumerable serverEpisodes, ICollection episodesAlreadyAdded) + { + var episodesBeingAdded = new HashSet(); + + await foreach (var ep in GetMediaServerEpisodes()) + { + if (IsIn(ep, episodesAlreadyAdded) || IsIn(ep, episodesBeingAdded)) + { + _logger.LogWarning($"Episode {ep.Name} already processed in this update."); + continue; + } + // Sanity checks + if (ep.EpisodeNumber == 0) // no check on season number, Season 0 can be Specials + { + _logger.LogWarning($"Episode {ep.Name} has no episode number. Skipping."); + continue; + } + + var existingEpisode = await GetExistingEpisode(ep); + if (existingEpisode == null) + { + addEpisode(ep, episodesBeingAdded); + } + else + { + _logger.LogWarning($"Episode {ep.Name} already exists in database."); + // TODO: update if something changed on the media server + } + } + + + if (episodesBeingAdded.Any()) + { + await _repo.AddRange((IEnumerable)episodesBeingAdded); + } + return episodesBeingAdded; + } + } +} diff --git a/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexEpisodeSync.cs index 8eed35066..ad229c1c0 100644 --- a/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/Interfaces/IPlexEpisodeSync.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Ombi.Api.Plex.Models; +using Ombi.Schedule.Jobs.MediaServer; using Ombi.Store.Entities; namespace Ombi.Schedule.Jobs.Plex.Interfaces { - public interface IPlexEpisodeSync : IBaseJob + public interface IPlexEpisodeSync : IMediaServerEpisodeSync, IBaseJob { - Task> ProcessEpsiodes(Metadata[] episodes, IQueryable currentEpisodes); } } \ No newline at end of file diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs index 823766bd2..13083ad46 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexContentSync.cs @@ -226,7 +226,7 @@ namespace Ombi.Schedule.Jobs.Plex await Repo.SaveChangesAsync(); if (content.Metadata != null) { - var episodesAdded = await EpisodeSync.ProcessEpsiodes(content.Metadata, (IQueryable)allEps); + var episodesAdded = await EpisodeSync.ProcessEpisodes(content.Metadata.ToAsyncEnumerable(), (ICollection)allEps.ToHashSet()); episodesProcessed.AddRange(episodesAdded.Select(x => x.Id)); } } @@ -326,7 +326,7 @@ namespace Ombi.Schedule.Jobs.Plex Logger.LogDebug($"We already have movie {movie.title}, But found a 4K version!"); existing.Has4K = true; await Repo.Update(existing); - } + } else { qualitySaved = true; diff --git a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs index c17af088c..7c29a7ce9 100644 --- a/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs +++ b/src/Ombi.Schedule/Jobs/Plex/PlexEpisodeSync.cs @@ -11,6 +11,7 @@ using Ombi.Core.Settings; using Ombi.Core.Settings.Models.External; using Ombi.Helpers; using Ombi.Hubs; +using Ombi.Schedule.Jobs.MediaServer; using Ombi.Schedule.Jobs.Ombi; using Ombi.Schedule.Jobs.Plex.Interfaces; using Ombi.Store.Entities; @@ -19,111 +20,55 @@ using Quartz; namespace Ombi.Schedule.Jobs.Plex { - public class PlexEpisodeSync : IPlexEpisodeSync + public class PlexEpisodeSync : MediaServerEpisodeSync, IPlexEpisodeSync { public PlexEpisodeSync(ISettingsService s, ILogger log, IPlexApi plexApi, - IPlexContentRepository repo, IHubContext hub) + IPlexContentRepository repo, IHubContext hub) : base(log, repo, hub) { _settings = s; - _log = log; _api = plexApi; - _repo = repo; - _notification = hub; _settings.ClearCache(); } private readonly ISettingsService _settings; - private readonly ILogger _log; private readonly IPlexApi _api; - private readonly IPlexContentRepository _repo; - private readonly IHubContext _notification; - public async Task Execute(IJobExecutionContext job) + public override async Task Execute(IJobExecutionContext job) { try { - var s = await _settings.GetSettingsAsync(); - if (!s.Enable) - { - return; - } await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Plex Episode Sync Started"); - foreach (var server in s.Servers) - { - await Cache(server); - } + await CacheEpisodes(); + + _logger.LogInformation(LoggingEvents.PlexEpisodeCacher, "We have finished caching the episodes."); + await _repo.SaveChangesAsync(); } catch (Exception e) { await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Plex Episode Sync Failed"); - _log.LogError(LoggingEvents.Cacher, e, "Caching Episodes Failed"); + _logger.LogError(LoggingEvents.Cacher, e, "Caching Episodes Failed"); } - _log.LogInformation("Plex Episode Sync Finished - Triggering Metadata refresh"); + _logger.LogInformation("Plex Episode Sync Finished - Triggering Metadata refresh"); await OmbiQuartz.TriggerJob(nameof(IRefreshMetadata), "System"); await _notification.Clients.Clients(NotificationHub.AdminConnectionIds) .SendAsync(NotificationHub.NotificationEvent, "Plex Episode Sync Finished"); } - private async Task Cache(PlexServers settings) - { - if (!Validate(settings)) - { - _log.LogWarning("Validation failed"); - return; - } - - // Get the librarys and then get the tv section - var sections = await _api.GetLibrarySections(settings.PlexAuthToken, settings.FullUri); - - // Filter the libSections - var tvSections = sections.MediaContainer.Directory.Where(x => x.type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)); - - foreach (var section in tvSections) - { - if (settings.PlexSelectedLibraries.Any()) - { - // Are any enabled? - if (settings.PlexSelectedLibraries.Any(x => x.Enabled)) - { - // Make sure we have enabled this - var keys = settings.PlexSelectedLibraries.Where(x => x.Enabled).Select(x => x.Key.ToString()) - .ToList(); - if (!keys.Contains(section.key)) - { - // We are not monitoring this lib - continue; - } - } - } - - // Get the episodes - await GetEpisodes(settings, section); - } - - } - - private async Task GetEpisodes(PlexServers settings, Directory section) + private async IAsyncEnumerable GetEpisodes(PlexServers settings, Directory section) { var currentPosition = 0; var resultCount = settings.EpisodeBatchSize == 0 ? 150 : settings.EpisodeBatchSize; var currentEpisodes = _repo.GetAllEpisodes().Cast(); var episodes = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, resultCount); - _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}"); + _logger.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Total Epsiodes found for {episodes.MediaContainer.librarySectionTitle} = {episodes.MediaContainer.totalSize}"); - // Delete all the episodes because we cannot uniquly match an episode to series every time, - // see comment below. - - // 12.03.2017 - I think we should be able to match them now - //await _repo.ExecuteSql("DELETE FROM PlexEpisode"); - - await ProcessEpsiodes(episodes?.MediaContainer?.Metadata ?? new Metadata[] { }, currentEpisodes); currentPosition += resultCount; while (currentPosition < episodes.MediaContainer.totalSize) @@ -131,35 +76,8 @@ namespace Ombi.Schedule.Jobs.Plex var ep = await _api.GetAllEpisodes(settings.PlexAuthToken, settings.FullUri, section.key, currentPosition, resultCount); - await ProcessEpsiodes(ep?.MediaContainer?.Metadata ?? new Metadata[] { }, currentEpisodes); - _log.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Processed {resultCount} more episodes. Total Remaining {episodes.MediaContainer.totalSize - currentPosition}"); - currentPosition += resultCount; - } - - // we have now finished. - _log.LogInformation(LoggingEvents.PlexEpisodeCacher, "We have finished caching the episodes."); - await _repo.SaveChangesAsync(); - } - - public async Task> ProcessEpsiodes(Metadata[] episodes, IQueryable currentEpisodes) - { - var ep = new HashSet(); - try - { - foreach (var episode in episodes) + foreach (var episode in ep.MediaContainer.Metadata) { - // I don't think we need to get the metadata, we only need to get the metadata if we need the provider id (TheTvDbid). Why do we need it for episodes? - // We have the parent and grandparent rating keys to link up to the season and series - //var metadata = _api.GetEpisodeMetaData(server.PlexAuthToken, server.FullUri, episode.ratingKey); - - // This does seem to work, it looks like we can somehow get different rating, grandparent and parent keys with episodes. Not sure how. - var epExists = currentEpisodes.Any(x => episode.ratingKey == x.Key && - episode.grandparentRatingKey == x.GrandparentKey); - if (epExists) - { - continue; - } - // Let's check if we have the parent var seriesExists = await _repo.GetByKey(episode.grandparentRatingKey); if (seriesExists == null) @@ -169,7 +87,7 @@ namespace Ombi.Schedule.Jobs.Plex x.Title == episode.grandparentTitle); if (seriesExists == null) { - _log.LogWarning( + _logger.LogWarning( "The episode title {0} we cannot find the parent series. The episode grandparentKey = {1}, grandparentTitle = {2}", episode.title, episode.grandparentRatingKey, episode.grandparentTitle); continue; @@ -178,35 +96,16 @@ namespace Ombi.Schedule.Jobs.Plex // Set the rating key to the correct one episode.grandparentRatingKey = seriesExists.Key; } + yield return episode; - // Sanity checks - if (episode.index == 0) - { - _log.LogWarning($"Episode {episode.title} has no episode number. Skipping."); - continue; - } - - ep.Add(new PlexEpisode - { - EpisodeNumber = episode.index, - SeasonNumber = episode.parentIndex, - GrandparentKey = episode.grandparentRatingKey, - ParentKey = episode.parentRatingKey, - Key = episode.ratingKey, - Title = episode.title - }); } + _logger.LogInformation(LoggingEvents.PlexEpisodeCacher, $"Processed {resultCount} more episodes. Total Remaining {episodes.MediaContainer.totalSize - currentPosition}"); + currentPosition += resultCount; + } - await _repo.AddRange(ep); - return ep; - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } } + private bool Validate(PlexServers settings) { if (string.IsNullOrEmpty(settings.PlexAuthToken)) @@ -217,23 +116,76 @@ namespace Ombi.Schedule.Jobs.Plex return true; } - private bool _disposed; - protected virtual void Dispose(bool disposing) + protected override async IAsyncEnumerable GetMediaServerEpisodes() { - if (_disposed) - return; - - if (disposing) + var s = await _settings.GetSettingsAsync(); + if (!s.Enable) { - //_settings?.Dispose(); + yield break; + } + foreach (var server in s.Servers) + { + if (!Validate(server)) + { + _logger.LogWarning("Validation failed"); + continue; + } + + // Get the librarys and then get the tv section + var sections = await _api.GetLibrarySections(server.PlexAuthToken, server.FullUri); + + // Filter the libSections + var tvSections = sections.MediaContainer.Directory.Where(x => x.type.Equals(PlexMediaType.Show.ToString(), StringComparison.CurrentCultureIgnoreCase)); + + foreach (var section in tvSections) + { + if (server.PlexSelectedLibraries.Any()) + { + // Are any enabled? + if (server.PlexSelectedLibraries.Any(x => x.Enabled)) + { + // Make sure we have enabled this + var keys = server.PlexSelectedLibraries.Where(x => x.Enabled).Select(x => x.Key.ToString()) + .ToList(); + if (!keys.Contains(section.key)) + { + // We are not monitoring this lib + continue; + } + } + } + + // Get the episodes + await foreach (var ep in GetEpisodes(server, section)) + { + yield return ep; + } + } } - _disposed = true; } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + protected override Task GetExistingEpisode(Metadata ep) + { + return _repo.GetEpisodeByKey(ep.ratingKey); } + + protected override bool IsIn(Metadata ep, ICollection list) + { + return false; // That check was never needed in Plex before refactoring + } + + protected override void addEpisode(Metadata ep, ICollection epToAdd) + { + epToAdd.Add(new PlexEpisode + { + EpisodeNumber = ep.index, + SeasonNumber = ep.parentIndex, + GrandparentKey = ep.grandparentRatingKey, + ParentKey = ep.parentRatingKey, + Key = ep.ratingKey, + Title = ep.title + }); + } + } }