using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; namespace ErsatzTV.Core.Plex { public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner { private readonly ILogger _logger; private readonly IMetadataRepository _metadataRepository; private readonly IPlexServerApiClient _plexServerApiClient; private readonly ITelevisionRepository _televisionRepository; public PlexTelevisionLibraryScanner( IPlexServerApiClient plexServerApiClient, ITelevisionRepository televisionRepository, IMetadataRepository metadataRepository, ILogger logger) : base(metadataRepository) { _plexServerApiClient = plexServerApiClient; _televisionRepository = televisionRepository; _metadataRepository = metadataRepository; _logger = logger; } public async Task> ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary plexMediaSourceLibrary) { Either> entries = await _plexServerApiClient.GetShowLibraryContents( plexMediaSourceLibrary, connection, token); return await entries.Match>>( async showEntries => { foreach (PlexShow incoming in showEntries) { // TODO: figure out how to rebuild playlists Either maybeShow = await _televisionRepository .GetOrAddPlexShow(plexMediaSourceLibrary, incoming) .BindT(existing => UpdateMetadata(existing, incoming)) .BindT(existing => UpdateArtwork(existing, incoming)); await maybeShow.Match( async show => await ScanSeasons(plexMediaSourceLibrary, show, connection, token), error => { _logger.LogWarning( "Error processing plex show at {Key}: {Error}", incoming.Key, error.Value); return Task.CompletedTask; }); } var showKeys = showEntries.Map(s => s.Key).ToList(); await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys); return Unit.Default; }, error => { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", plexMediaSourceLibrary.Name, error.Value); return Left(error).AsTask(); }); } private Task> UpdateMetadata(PlexShow existing, PlexShow incoming) { ShowMetadata existingMetadata = existing.ShowMetadata.Head(); ShowMetadata incomingMetadata = incoming.ShowMetadata.Head(); // TODO: this probably doesn't work // plex doesn't seem to update genres returned by the main library call if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) { foreach (Genre genre in existingMetadata.Genres .Filter(g => incomingMetadata.Genres.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Genres.Remove(genre); _metadataRepository.RemoveGenre(genre); } foreach (Genre genre in incomingMetadata.Genres .Filter(g => existingMetadata.Genres.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Genres.Add(genre); _televisionRepository.AddGenre(existingMetadata, genre); } } return Right(existing).AsTask(); } private async Task> UpdateArtwork(PlexShow existing, PlexShow incoming) { ShowMetadata existingMetadata = existing.ShowMetadata.Head(); ShowMetadata incomingMetadata = incoming.ShowMetadata.Head(); if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) { await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Poster); await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.FanArt); } return existing; } private async Task> ScanSeasons( PlexLibrary plexMediaSourceLibrary, PlexShow show, PlexConnection connection, PlexServerAuthToken token) { Either> entries = await _plexServerApiClient.GetShowSeasons( plexMediaSourceLibrary, show, connection, token); return await entries.Match>>( async seasonEntries => { foreach (PlexSeason incoming in seasonEntries) { incoming.ShowId = show.Id; // TODO: figure out how to rebuild playlists Either maybeSeason = await _televisionRepository .GetOrAddPlexSeason(plexMediaSourceLibrary, incoming) .BindT(existing => UpdateArtwork(existing, incoming)); await maybeSeason.Match( async season => await ScanEpisodes(plexMediaSourceLibrary, season, connection, token), error => { _logger.LogWarning( "Error processing plex show at {Key}: {Error}", incoming.Key, error.Value); return Task.CompletedTask; }); } var seasonKeys = seasonEntries.Map(s => s.Key).ToList(); await _televisionRepository.RemoveMissingPlexSeasons(show.Key, seasonKeys); return Unit.Default; }, error => { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", plexMediaSourceLibrary.Name, error.Value); return Left(error).AsTask(); }); } private async Task> UpdateArtwork(PlexSeason existing, PlexSeason incoming) { SeasonMetadata existingMetadata = existing.SeasonMetadata.Head(); SeasonMetadata incomingMetadata = incoming.SeasonMetadata.Head(); if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) { await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Poster); } return existing; } private async Task> ScanEpisodes( PlexLibrary plexMediaSourceLibrary, PlexSeason season, PlexConnection connection, PlexServerAuthToken token) { Either> entries = await _plexServerApiClient.GetSeasonEpisodes( plexMediaSourceLibrary, season, connection, token); return await entries.Match>>( async episodeEntries => { foreach (PlexEpisode incoming in episodeEntries) { incoming.SeasonId = season.Id; // TODO: figure out how to rebuild playlists Either maybeEpisode = await _televisionRepository .GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming) .BindT(existing => UpdateStatistics(existing, incoming, connection, token)) .BindT(existing => UpdateArtwork(existing, incoming)); maybeEpisode.IfLeft( error => _logger.LogWarning( "Error processing plex episode at {Key}: {Error}", incoming.Key, error.Value)); } var episodeKeys = episodeEntries.Map(s => s.Key).ToList(); await _televisionRepository.RemoveMissingPlexEpisodes(season.Key, episodeKeys); return Unit.Default; }, error => { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", plexMediaSourceLibrary.Name, error.Value); return Left(error).AsTask(); }); } private async Task> UpdateStatistics( PlexEpisode existing, PlexEpisode incoming, PlexConnection connection, PlexServerAuthToken token) { MediaVersion existingVersion = existing.MediaVersions.Head(); MediaVersion incomingVersion = incoming.MediaVersions.Head(); if (incomingVersion.DateUpdated > existingVersion.DateUpdated || string.IsNullOrWhiteSpace(existingVersion.SampleAspectRatio)) { Either maybeStatistics = await _plexServerApiClient.GetStatistics(incoming.Key.Split("/").Last(), connection, token); await maybeStatistics.Match( async mediaVersion => { existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio ?? "1:1"; existingVersion.VideoScanKind = mediaVersion.VideoScanKind; existingVersion.DateUpdated = incomingVersion.DateUpdated; await _metadataRepository.UpdateStatistics(existingVersion); }, _ => Task.CompletedTask); } return Right(existing); } private async Task> UpdateArtwork(PlexEpisode existing, PlexEpisode incoming) { EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head(); EpisodeMetadata incomingMetadata = incoming.EpisodeMetadata.Head(); if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) { await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Thumbnail); } return existing; } } }