From 6c867d0d513bdf6bcfcbcfe294492b56ee325050 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Fri, 4 Jun 2021 15:06:19 -0500 Subject: [PATCH] support multi-episode files from plex (#243) * minor fallback metadata bug fixes * support multi-episode files from plex --- .../Metadata/FallbackMetadataProvider.cs | 19 +++- .../Plex/PlexTelevisionLibraryScanner.cs | 88 +++++++++++++------ .../Data/Repositories/SearchRepository.cs | 2 + .../Data/Repositories/TelevisionRepository.cs | 16 ++-- .../Plex/PlexServerApiClient.cs | 24 ++++- ErsatzTV.Infrastructure/Search/SearchIndex.cs | 16 +++- 6 files changed, 126 insertions(+), 39 deletions(-) diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index c623342a3..b9aafd5fa 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -35,7 +35,15 @@ namespace ErsatzTV.Core.Metadata MetadataKind = MetadataKind.Fallback, Title = fileName ?? path, DateAdded = DateTime.UtcNow, - EpisodeNumber = 0 + EpisodeNumber = 0, + Actors = new List(), + Artwork = new List(), + Directors = new List(), + Genres = new List(), + Guids = new List(), + Studios = new List(), + Tags = new List(), + Writers = new List() }; return fileName != null ? GetEpisodeMetadata(fileName, baseMetadata) @@ -120,7 +128,14 @@ namespace ErsatzTV.Core.Metadata EpisodeNumber = episodeNumber, DateAdded = baseMetadata.DateAdded, DateUpdated = baseMetadata.DateAdded, - Actors = new List() + Actors = new List(), + Artwork = new List(), + Directors = new List(), + Genres = new List(), + Guids = new List(), + Studios = new List(), + Tags = new List(), + Writers = new List() }; result.Add(metadata); diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index 39989bd50..0e1799698 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using LanguageExt; +using LanguageExt.UnsafeValueAccess; using MediatR; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; @@ -375,8 +376,9 @@ namespace ErsatzTV.Core.Plex // TODO: figure out how to rebuild playlists Either maybeEpisode = await _televisionRepository .GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming) + .BindT(existing => UpdateMetadata(existing, incoming)) .BindT( - existing => UpdateMetadataAndStatistics( + existing => UpdateStatistics( existing, incoming, plexMediaSourceLibrary, @@ -417,7 +419,36 @@ namespace ErsatzTV.Core.Plex }); } - private async Task> UpdateMetadataAndStatistics( + private async Task> UpdateMetadata(PlexEpisode existing, PlexEpisode incoming) + { + var toUpdate = existing.EpisodeMetadata + .Where(em => incoming.EpisodeMetadata.Any(em2 => em2.EpisodeNumber == em.EpisodeNumber)) + .ToList(); + var toRemove = existing.EpisodeMetadata.Except(toUpdate).ToList(); + var toAdd = incoming.EpisodeMetadata + .Where(em => existing.EpisodeMetadata.All(em2 => em2.EpisodeNumber != em.EpisodeNumber)) + .ToList(); + + foreach (EpisodeMetadata metadata in toRemove) + { + await _televisionRepository.RemoveMetadata(existing, metadata); + } + + foreach (EpisodeMetadata metadata in toAdd) + { + metadata.EpisodeId = existing.Id; + metadata.Episode = existing; + existing.EpisodeMetadata.Add(metadata); + + await _metadataRepository.Add(metadata); + } + + // TODO: update existing metadata + + return existing; + } + + private async Task> UpdateStatistics( PlexEpisode existing, PlexEpisode incoming, PlexLibrary library, @@ -441,22 +472,25 @@ namespace ErsatzTV.Core.Plex { (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; - EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head(); - - foreach (MetadataGuid guid in existingMetadata.Guids - .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) - .ToList()) + Option maybeExisting = existing.EpisodeMetadata + .Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber); + foreach (EpisodeMetadata existingMetadata in maybeExisting) { - existingMetadata.Guids.Remove(guid); - await _metadataRepository.RemoveGuid(guid); - } + foreach (MetadataGuid guid in existingMetadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Remove(guid); + await _metadataRepository.RemoveGuid(guid); + } - foreach (MetadataGuid guid in incomingMetadata.Guids - .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) - .ToList()) - { - existingMetadata.Guids.Add(guid); - await _metadataRepository.AddGuid(existingMetadata, guid); + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Add(guid); + await _metadataRepository.AddGuid(existingMetadata, guid); + } } existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; @@ -471,17 +505,21 @@ namespace ErsatzTV.Core.Plex return Right(existing); } - private async Task> UpdateArtwork( - PlexEpisode existing, - PlexEpisode incoming) + private async Task> UpdateArtwork(PlexEpisode existing, PlexEpisode incoming) { - EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head(); - EpisodeMetadata incomingMetadata = incoming.EpisodeMetadata.Head(); - - if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) + foreach (EpisodeMetadata incomingMetadata in incoming.EpisodeMetadata) { - await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Thumbnail); - await _metadataRepository.MarkAsUpdated(existingMetadata, incomingMetadata.DateUpdated); + Option maybeExistingMetadata = existing.EpisodeMetadata + .Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber); + if (maybeExistingMetadata.IsSome) + { + EpisodeMetadata existingMetadata = maybeExistingMetadata.ValueUnsafe(); + if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) + { + await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Thumbnail); + await _metadataRepository.MarkAsUpdated(existingMetadata, incomingMetadata.DateUpdated); + } + } } return existing; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs index 65f8dba12..8c4f0ad4d 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs @@ -59,6 +59,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(em => em.Directors) .Include(mi => (mi as Episode).EpisodeMetadata) .ThenInclude(em => em.Writers) + .Include(mi => (mi as Episode).EpisodeMetadata) + .ThenInclude(em => em.Guids) .Include(mi => (mi as Episode).MediaVersions) .ThenInclude(em => em.Streams) .Include(mi => (mi as Episode).Season) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index 48a7e83f4..21642169a 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -806,13 +806,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories } item.LibraryPathId = library.Paths.Head().Id; - EpisodeMetadata metadata = item.EpisodeMetadata.Head(); - metadata.Genres ??= new List(); - metadata.Tags ??= new List(); - metadata.Studios ??= new List(); - metadata.Actors ??= new List(); - metadata.Directors ??= new List(); - metadata.Writers ??= new List(); + foreach (EpisodeMetadata metadata in item.EpisodeMetadata) + { + metadata.Genres ??= new List(); + metadata.Tags ??= new List(); + metadata.Studios ??= new List(); + metadata.Actors ??= new List(); + metadata.Directors ??= new List(); + metadata.Writers ??= new List(); + } await dbContext.PlexEpisodes.AddAsync(item); await dbContext.SaveChangesAsync(); diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index b92e2694e..7e2cec44a 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -110,7 +110,8 @@ namespace ErsatzTV.Infrastructure.Plex IPlexServerApi service = XmlServiceFor(connection.Uri); return await service.GetSeasonChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken) .Map(r => r.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)) - .Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId)).ToList()); + .Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId))) + .Map(ProcessMultiEpisodeFiles); } catch (Exception ex) { @@ -118,6 +119,27 @@ namespace ErsatzTV.Infrastructure.Plex } } + private List ProcessMultiEpisodeFiles(IEnumerable episodes) + { + // add all metadata from duplicate paths to first entry with given path + // i.e. s1e1 episode will add s1e2 metadata if s1e1 and s1e2 have same physical path + var result = new Dictionary(); + foreach (PlexEpisode episode in episodes.OrderBy(e => e.EpisodeMetadata.Head().EpisodeNumber)) + { + string path = episode.MediaVersions.Head().MediaFiles.Head().Path; + if (result.TryGetValue(path, out PlexEpisode existing)) + { + existing.EpisodeMetadata.Add(episode.EpisodeMetadata.Head()); + } + else + { + result.Add(path, episode); + } + } + + return result.Values.ToList(); + } + public async Task> GetMovieMetadata( PlexLibrary library, string key, diff --git a/ErsatzTV.Infrastructure/Search/SearchIndex.cs b/ErsatzTV.Infrastructure/Search/SearchIndex.cs index a18f04b00..4d961d12a 100644 --- a/ErsatzTV.Infrastructure/Search/SearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/SearchIndex.cs @@ -586,13 +586,21 @@ namespace ErsatzTV.Infrastructure.Search private void UpdateEpisode(Episode episode) { - Option maybeMetadata = episode.EpisodeMetadata.HeadOrNone(); - if (maybeMetadata.IsSome) + foreach (EpisodeMetadata metadata in episode.EpisodeMetadata) { - EpisodeMetadata metadata = maybeMetadata.ValueUnsafe(); - try { + if (string.IsNullOrWhiteSpace(metadata.Title)) + { + _logger.LogWarning( + "Unable to index episode without title {Show} s{Season}e{Episode}", + metadata.Episode.Season?.Show?.ShowMetadata.Head().Title, + metadata.Episode.Season?.SeasonNumber, + metadata.EpisodeNumber); + + continue; + } + var doc = new Document(); doc.Add(new StringField(IdField, episode.Id.ToString(), Field.Store.YES)); doc.Add(new StringField(TypeField, EpisodeType, Field.Store.NO));