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
MetadataKind = MetadataKind.Fallback, MetadataKind = MetadataKind.Fallback,
Title = fileName ?? path, Title = fileName ?? path,
DateAdded = DateTime.UtcNow, 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 return fileName != null
? GetEpisodeMetadata(fileName, baseMetadata) ? GetEpisodeMetadata(fileName, baseMetadata)
@ -120,7 +128,14 @@ namespace ErsatzTV.Core.Metadata
EpisodeNumber = episodeNumber, EpisodeNumber = episodeNumber,
DateAdded = baseMetadata.DateAdded, DateAdded = baseMetadata.DateAdded,
DateUpdated = 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); result.Add(metadata);

88
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -8,6 +8,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
using LanguageExt; using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
@ -375,8 +376,9 @@ namespace ErsatzTV.Core.Plex
// TODO: figure out how to rebuild playlists // TODO: figure out how to rebuild playlists
Either<BaseError, PlexEpisode> maybeEpisode = await _televisionRepository Either<BaseError, PlexEpisode> maybeEpisode = await _televisionRepository
.GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming) .GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming)
.BindT(existing => UpdateMetadata(existing, incoming))
.BindT( .BindT(
existing => UpdateMetadataAndStatistics( existing => UpdateStatistics(
existing, existing,
incoming, incoming,
plexMediaSourceLibrary, plexMediaSourceLibrary,
@ -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 existing,
PlexEpisode incoming, PlexEpisode incoming,
PlexLibrary library, PlexLibrary library,
@ -441,22 +472,25 @@ namespace ErsatzTV.Core.Plex
{ {
(EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple;
EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head(); Option<EpisodeMetadata> maybeExisting = existing.EpisodeMetadata
.Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber);
foreach (MetadataGuid guid in existingMetadata.Guids foreach (EpisodeMetadata existingMetadata in maybeExisting)
.Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList())
{ {
existingMetadata.Guids.Remove(guid); foreach (MetadataGuid guid in existingMetadata.Guids
await _metadataRepository.RemoveGuid(guid); .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 foreach (MetadataGuid guid in incomingMetadata.Guids
.Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid))
.ToList()) .ToList())
{ {
existingMetadata.Guids.Add(guid); existingMetadata.Guids.Add(guid);
await _metadataRepository.AddGuid(existingMetadata, guid); await _metadataRepository.AddGuid(existingMetadata, guid);
}
} }
existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio;
@ -471,17 +505,21 @@ namespace ErsatzTV.Core.Plex
return Right<BaseError, PlexEpisode>(existing); return Right<BaseError, PlexEpisode>(existing);
} }
private async Task<Either<BaseError, PlexEpisode>> UpdateArtwork( private async Task<Either<BaseError, PlexEpisode>> UpdateArtwork(PlexEpisode existing, PlexEpisode incoming)
PlexEpisode existing,
PlexEpisode incoming)
{ {
EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head(); foreach (EpisodeMetadata incomingMetadata in incoming.EpisodeMetadata)
EpisodeMetadata incomingMetadata = incoming.EpisodeMetadata.Head();
if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated)
{ {
await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Thumbnail); Option<EpisodeMetadata> maybeExistingMetadata = existing.EpisodeMetadata
await _metadataRepository.MarkAsUpdated(existingMetadata, incomingMetadata.DateUpdated); .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; return existing;

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

@ -59,6 +59,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(em => em.Directors) .ThenInclude(em => em.Directors)
.Include(mi => (mi as Episode).EpisodeMetadata) .Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Writers) .ThenInclude(em => em.Writers)
.Include(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Guids)
.Include(mi => (mi as Episode).MediaVersions) .Include(mi => (mi as Episode).MediaVersions)
.ThenInclude(em => em.Streams) .ThenInclude(em => em.Streams)
.Include(mi => (mi as Episode).Season) .Include(mi => (mi as Episode).Season)

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

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

24
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -110,7 +110,8 @@ namespace ErsatzTV.Infrastructure.Plex
IPlexServerApi service = XmlServiceFor(connection.Uri); IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetSeasonChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken) 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(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) catch (Exception ex)
{ {
@ -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( public async Task<Either<BaseError, MovieMetadata>> GetMovieMetadata(
PlexLibrary library, PlexLibrary library,
string key, string key,

16
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -586,13 +586,21 @@ namespace ErsatzTV.Infrastructure.Search
private void UpdateEpisode(Episode episode) private void UpdateEpisode(Episode episode)
{ {
Option<EpisodeMetadata> maybeMetadata = episode.EpisodeMetadata.HeadOrNone(); foreach (EpisodeMetadata metadata in episode.EpisodeMetadata)
if (maybeMetadata.IsSome)
{ {
EpisodeMetadata metadata = maybeMetadata.ValueUnsafe();
try 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(); var doc = new Document();
doc.Add(new StringField(IdField, episode.Id.ToString(), Field.Store.YES)); doc.Add(new StringField(IdField, episode.Id.ToString(), Field.Store.YES));
doc.Add(new StringField(TypeField, EpisodeType, Field.Store.NO)); doc.Add(new StringField(TypeField, EpisodeType, Field.Store.NO));

Loading…
Cancel
Save