Browse Source

support multi-episode files from plex (#243)

* minor fallback metadata bug fixes

* support multi-episode files from plex
pull/244/head
Jason Dove 4 years ago committed by GitHub
parent
commit
6c867d0d51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  2. 88
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  3. 2
      ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs
  4. 16
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  5. 24
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  6. 16
      ErsatzTV.Infrastructure/Search/SearchIndex.cs

19
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -35,7 +35,15 @@ namespace ErsatzTV.Core.Metadata @@ -35,7 +35,15 @@ namespace ErsatzTV.Core.Metadata
MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path,
DateAdded = DateTime.UtcNow,
EpisodeNumber = 0
EpisodeNumber = 0,
Actors = new List<Actor>(),
Artwork = new List<Artwork>(),
Directors = new List<Director>(),
Genres = new List<Genre>(),
Guids = new List<MetadataGuid>(),
Studios = new List<Studio>(),
Tags = new List<Tag>(),
Writers = new List<Writer>()
};
return fileName != null
? GetEpisodeMetadata(fileName, baseMetadata)
@ -120,7 +128,14 @@ namespace ErsatzTV.Core.Metadata @@ -120,7 +128,14 @@ namespace ErsatzTV.Core.Metadata
EpisodeNumber = episodeNumber,
DateAdded = baseMetadata.DateAdded,
DateUpdated = baseMetadata.DateAdded,
Actors = new List<Actor>()
Actors = new List<Actor>(),
Artwork = new List<Artwork>(),
Directors = new List<Director>(),
Genres = new List<Genre>(),
Guids = new List<MetadataGuid>(),
Studios = new List<Studio>(),
Tags = new List<Tag>(),
Writers = new List<Writer>()
};
result.Add(metadata);

88
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -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 @@ -375,8 +376,9 @@ namespace ErsatzTV.Core.Plex
// TODO: figure out how to rebuild playlists
Either<BaseError, PlexEpisode> 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 @@ -417,7 +419,36 @@ namespace ErsatzTV.Core.Plex
});
}
private async Task<Either<BaseError, PlexEpisode>> UpdateMetadataAndStatistics(
private async Task<Either<BaseError, PlexEpisode>> 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<Either<BaseError, PlexEpisode>> UpdateStatistics(
PlexEpisode existing,
PlexEpisode incoming,
PlexLibrary library,
@ -441,22 +472,25 @@ namespace ErsatzTV.Core.Plex @@ -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<EpisodeMetadata> 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 @@ -471,17 +505,21 @@ namespace ErsatzTV.Core.Plex
return Right<BaseError, PlexEpisode>(existing);
}
private async Task<Either<BaseError, PlexEpisode>> UpdateArtwork(
PlexEpisode existing,
PlexEpisode incoming)
private async Task<Either<BaseError, PlexEpisode>> 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<EpisodeMetadata> 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;

2
ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs

@ -59,6 +59,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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)

16
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -806,13 +806,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -806,13 +806,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
}
item.LibraryPathId = library.Paths.Head().Id;
EpisodeMetadata metadata = item.EpisodeMetadata.Head();
metadata.Genres ??= new List<Genre>();
metadata.Tags ??= new List<Tag>();
metadata.Studios ??= new List<Studio>();
metadata.Actors ??= new List<Actor>();
metadata.Directors ??= new List<Director>();
metadata.Writers ??= new List<Writer>();
foreach (EpisodeMetadata metadata in item.EpisodeMetadata)
{
metadata.Genres ??= new List<Genre>();
metadata.Tags ??= new List<Tag>();
metadata.Studios ??= new List<Studio>();
metadata.Actors ??= new List<Actor>();
metadata.Directors ??= new List<Director>();
metadata.Writers ??= new List<Writer>();
}
await dbContext.PlexEpisodes.AddAsync(item);
await dbContext.SaveChangesAsync();

24
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -110,7 +110,8 @@ namespace ErsatzTV.Infrastructure.Plex @@ -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 @@ -118,6 +119,27 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
private List<PlexEpisode> ProcessMultiEpisodeFiles(IEnumerable<PlexEpisode> 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<string, PlexEpisode>();
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<Either<BaseError, MovieMetadata>> GetMovieMetadata(
PlexLibrary library,
string key,

16
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -586,13 +586,21 @@ namespace ErsatzTV.Infrastructure.Search @@ -586,13 +586,21 @@ namespace ErsatzTV.Infrastructure.Search
private void UpdateEpisode(Episode episode)
{
Option<EpisodeMetadata> 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));

Loading…
Cancel
Save