public async Task <Either <BaseError, Tuple <EpisodeMetadata, MediaVersion> > > GetEpisodeMetadataAndStatistics( PlexLibrary library, string key, PlexConnection connection, PlexServerAuthToken token) { try { IPlexServerApi service = XmlServiceFor(connection.Uri); Option <PlexXmlVideoMetadataResponseContainer> maybeResponse = await service .GetVideoMetadata(key, token.AuthToken) .Map(Optional) .Map(r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)); return(maybeResponse.Match( response => { Option <MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata); return maybeVersion.Match <Either <BaseError, Tuple <EpisodeMetadata, MediaVersion> > >( version => Tuple( ProjectToEpisodeMetadata(response.Metadata, library.MediaSourceId), version), () => BaseError.New("Unable to locate metadata")); }, () => BaseError.New("Unable to locate metadata"))); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
private async Task <Either <BaseError, PlexEpisode> > 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 <BaseError, MediaVersion> 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.UpdatePlexStatistics(existingVersion); }, _ => Task.CompletedTask); } return(Right <BaseError, PlexEpisode>(existing)); }
public async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary library, string ffmpegPath, string ffprobePath, bool deepScan, CancellationToken cancellationToken) { List <PlexPathReplacement> pathReplacements = await _mediaSourceRepository.GetPlexPathReplacements(library.MediaSourceId); string GetLocalPath(PlexMovie movie) { return(_plexPathReplacementService.GetReplacementPlexPath( pathReplacements, movie.GetHeadVersion().MediaFiles.Head().Path, false)); } return(await ScanLibrary( _plexMovieRepository, new PlexConnectionParameters(connection, token), library, GetLocalPath, ffmpegPath, ffprobePath, deepScan, cancellationToken)); }
public async Task <Either <BaseError, List <PlexLibrary> > > GetLibraries( PlexConnection connection, PlexServerAuthToken token) { try { IPlexServerApi service = RestService.For <IPlexServerApi>( new HttpClient { BaseAddress = new Uri(connection.Uri), Timeout = TimeSpan.FromSeconds(10) }); List <PlexLibraryResponse> directory = await service.GetLibraries(token.AuthToken).Map(r => r.MediaContainer.Directory); return(directory // .Filter(l => l.Hidden == 0) .Filter(l => l.Type.ToLowerInvariant() is "movie" or "show") .Map(Project) .Somes() .ToList()); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
private async Task <Either <BaseError, Unit> > ScanSeasons( PlexLibrary plexMediaSourceLibrary, PlexShow show, PlexConnection connection, PlexServerAuthToken token) { Either <BaseError, List <PlexSeason> > entries = await _plexServerApiClient.GetShowSeasons( plexMediaSourceLibrary, show, connection, token); return(await entries.Match <Task <Either <BaseError, Unit> > >( async seasonEntries => { foreach (PlexSeason incoming in seasonEntries) { incoming.ShowId = show.Id; // TODO: figure out how to rebuild playlists Either <BaseError, PlexSeason> 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 <BaseError, Unit>(error).AsTask(); })); }
private async Task <Either <BaseError, Unit> > ScanEpisodes( PlexLibrary plexMediaSourceLibrary, PlexSeason season, PlexConnection connection, PlexServerAuthToken token) { Either <BaseError, List <PlexEpisode> > entries = await _plexServerApiClient.GetSeasonEpisodes( plexMediaSourceLibrary, season, connection, token); return(await entries.Match <Task <Either <BaseError, Unit> > >( async episodeEntries => { foreach (PlexEpisode incoming in episodeEntries) { incoming.SeasonId = season.Id; // TODO: figure out how to rebuild playlists Either <BaseError, PlexEpisode> 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 <BaseError, Unit>(error).AsTask(); })); }
public async Task <Either <BaseError, List <PlexShow> > > GetShowLibraryContents( PlexLibrary library, PlexConnection connection, PlexServerAuthToken token) { try { IPlexServerApi service = RestService.For <IPlexServerApi>(connection.Uri); return(await service.GetLibrarySectionContents(library.Key, token.AuthToken) .Map(r => r.MediaContainer.Metadata) .Map(list => list.Map(metadata => ProjectToShow(metadata, library.MediaSourceId)).ToList())); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
public async Task <Either <BaseError, List <PlexSeason> > > GetShowSeasons( PlexLibrary library, PlexShow show, PlexConnection connection, PlexServerAuthToken token) { try { IPlexServerApi service = XmlServiceFor(connection.Uri); return(await service.GetShowChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken) .Map(r => r.Metadata) .Map(list => list.Map(metadata => ProjectToSeason(metadata, library.MediaSourceId)).ToList())); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
public async Task <Either <BaseError, List <PlexEpisode> > > GetSeasonEpisodes( PlexLibrary library, PlexSeason season, PlexConnection connection, PlexServerAuthToken token) { try { 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))) .Map(ProcessMultiEpisodeFiles)); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
public async Task <Either <BaseError, ShowMetadata> > GetShowMetadata( PlexLibrary library, string key, PlexConnection connection, PlexServerAuthToken token) { try { IPlexServerApi service = XmlServiceFor(connection.Uri); return(await service.GetDirectoryMetadata(key, token.AuthToken) .Map(Optional) .MapT(response => ProjectToShowMetadata(response.Metadata, library.MediaSourceId)) .Map(o => o.ToEither <BaseError>("Unable to locate metadata"))); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
public async Task <Either <BaseError, MovieMetadata> > GetMovieMetadata( PlexLibrary library, string key, PlexConnection connection, PlexServerAuthToken token) { try { IPlexServerApi service = XmlServiceFor(connection.Uri); return(await service.GetVideoMetadata(key, token.AuthToken) .Map(Optional) .Map(r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0)) .MapT(response => ProjectToMovieMetadata(response.Metadata, library.MediaSourceId)) .Map(o => o.ToEither <BaseError>("Unable to locate metadata"))); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
public async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary library, string ffmpegPath, string ffprobePath, bool deepScan, CancellationToken cancellationToken) { try { Either <BaseError, List <PlexShow> > entries = await _plexServerApiClient.GetShowLibraryContents( library, connection, token); foreach (BaseError error in entries.LeftToSeq()) { return(error); } return(await ScanLibrary( connection, token, library, ffmpegPath, ffprobePath, deepScan, entries.RightToSeq().Flatten().ToList(), cancellationToken)); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { // always commit the search index to prevent corruption _searchIndex.Commit(); } }
public async Task <bool> Ping( PlexConnection connection, PlexServerAuthToken token) { try { IPlexServerApi service = RestService.For <IPlexServerApi>( new HttpClient { BaseAddress = new Uri(connection.Uri), Timeout = TimeSpan.FromSeconds(5) }); await service.Ping(token.AuthToken); return(true); } catch (Exception) { return(false); } }
private async Task SynchronizeServer(List <PlexMediaSource> allExisting, PlexMediaSource server) { if (server.Connections.All(c => c.Uri != LocalhostUri)) { var localhost = new PlexConnection { PlexMediaSource = server, PlexMediaSourceId = server.Id, Uri = LocalhostUri }; server.Connections.Add(localhost); } Option <PlexMediaSource> maybeExisting = allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier); foreach (PlexMediaSource existing in maybeExisting) { existing.Platform = server.Platform; existing.PlatformVersion = server.PlatformVersion; existing.ProductVersion = server.ProductVersion; existing.ServerName = server.ServerName; var toAdd = server.Connections .Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList(); var toRemove = existing.Connections .Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList(); await _mediaSourceRepository.Update(existing, toAdd, toRemove); await FindConnectionToActivate(existing); } if (maybeExisting.IsNone) { await _mediaSourceRepository.Add(server); await FindConnectionToActivate(server); } }
public async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary plexMediaSourceLibrary) { Either <BaseError, List <PlexShow> > entries = await _plexServerApiClient.GetShowLibraryContents( plexMediaSourceLibrary, connection, token); return(await entries.Match <Task <Either <BaseError, Unit> > >( async showEntries => { foreach (PlexShow incoming in showEntries) { // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <PlexShow> > maybeShow = await _televisionRepository .GetOrAddPlexShow(plexMediaSourceLibrary, incoming) .BindT(existing => UpdateMetadata(existing, incoming)) .BindT(existing => UpdateArtwork(existing, incoming)); await maybeShow.Match( async result => { if (result.IsAdded) { await _searchIndex.AddItems(new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(new List <MediaItem> { result.Item }); } await ScanSeasons(plexMediaSourceLibrary, result.Item, 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(); List <int> ids = await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys); await _searchIndex.RemoveItems(ids); return Unit.Default; }, error => { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", plexMediaSourceLibrary.Name, error.Value); return Left <BaseError, Unit>(error).AsTask(); })); }
private async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary library, string ffmpegPath, string ffprobePath, bool deepScan, List <PlexShow> showEntries, CancellationToken cancellationToken) { List <PlexItemEtag> existingShows = await _plexTelevisionRepository.GetExistingPlexShows(library); List <PlexPathReplacement> pathReplacements = await _mediaSourceRepository .GetPlexPathReplacements(library.MediaSourceId); foreach (PlexShow incoming in showEntries) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)showEntries.IndexOf(incoming) / showEntries.Count; await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion), cancellationToken); // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <PlexShow> > maybeShow = await _televisionRepository .GetOrAddPlexShow(library, incoming) .BindT(existing => UpdateMetadata(existing, incoming, library, connection, token, deepScan)) .BindT(existing => UpdateArtwork(existing, incoming)); if (maybeShow.IsLeft) { foreach (BaseError error in maybeShow.LeftToSeq()) { _logger.LogWarning( "Error processing plex show at {Key}: {Error}", incoming.Key, error.Value); } continue; } foreach (MediaItemScanResult <PlexShow> result in maybeShow.RightToSeq()) { Either <BaseError, Unit> scanResult = await ScanSeasons( library, pathReplacements, result.Item, connection, token, ffmpegPath, ffprobePath, deepScan, cancellationToken); foreach (ScanCanceled error in scanResult.LeftToSeq().OfType <ScanCanceled>()) { return(error); } await _plexTelevisionRepository.SetPlexEtag(result.Item, incoming.Etag); // TODO: if any seasons are unavailable or not found, flag show as unavailable/not found if (result.IsAdded) { await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems( _searchRepository, new List <MediaItem> { result.Item }); } } } // trash items that are no longer present on the media server var fileNotFoundKeys = existingShows.Map(m => m.Key).Except(showEntries.Map(m => m.Key)).ToList(); List <int> ids = await _plexTelevisionRepository.FlagFileNotFoundShows(library, fileNotFoundKeys); await _searchIndex.RebuildItems(_searchRepository, ids); await _mediator.Publish(new LibraryScanProgress(library.Id, 0), cancellationToken); return(Unit.Default); }
private async Task <Either <BaseError, MediaItemScanResult <PlexShow> > > UpdateMetadata( MediaItemScanResult <PlexShow> result, PlexShow incoming, PlexLibrary library, PlexConnection connection, PlexServerAuthToken token, bool deepScan) { PlexShow existing = result.Item; ShowMetadata existingMetadata = existing.ShowMetadata.Head(); if (result.IsAdded || existing.Etag != incoming.Etag || deepScan) { Either <BaseError, ShowMetadata> maybeMetadata = await _plexServerApiClient.GetShowMetadata( library, incoming.Key.Replace("/children", string.Empty).Split("/").Last(), connection, token); await maybeMetadata.Match( async fullMetadata => { if (existingMetadata.MetadataKind != MetadataKind.External) { existingMetadata.MetadataKind = MetadataKind.External; await _metadataRepository.MarkAsExternal(existingMetadata); } if (existingMetadata.ContentRating != fullMetadata.ContentRating) { existingMetadata.ContentRating = fullMetadata.ContentRating; await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating); result.IsUpdated = true; } foreach (Genre genre in existingMetadata.Genres .Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Genres.Remove(genre); if (await _metadataRepository.RemoveGenre(genre)) { result.IsUpdated = true; } } foreach (Genre genre in fullMetadata.Genres .Filter(g => existingMetadata.Genres.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Genres.Add(genre); if (await _televisionRepository.AddGenre(existingMetadata, genre)) { result.IsUpdated = true; } } foreach (Studio studio in existingMetadata.Studios .Filter(s => fullMetadata.Studios.All(s2 => s2.Name != s.Name)) .ToList()) { existingMetadata.Studios.Remove(studio); if (await _metadataRepository.RemoveStudio(studio)) { result.IsUpdated = true; } } foreach (Studio studio in fullMetadata.Studios .Filter(s => existingMetadata.Studios.All(s2 => s2.Name != s.Name)) .ToList()) { existingMetadata.Studios.Add(studio); if (await _televisionRepository.AddStudio(existingMetadata, studio)) { result.IsUpdated = true; } } foreach (Actor actor in existingMetadata.Actors .Filter( a => fullMetadata.Actors.All( a2 => a2.Name != a.Name || a.Artwork == null && a2.Artwork != null)) .ToList()) { existingMetadata.Actors.Remove(actor); if (await _metadataRepository.RemoveActor(actor)) { result.IsUpdated = true; } } foreach (Actor actor in fullMetadata.Actors .Filter(a => existingMetadata.Actors.All(a2 => a2.Name != a.Name)) .ToList()) { existingMetadata.Actors.Add(actor); if (await _televisionRepository.AddActor(existingMetadata, actor)) { result.IsUpdated = true; } } foreach (MetadataGuid guid in existingMetadata.Guids .Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid)) .ToList()) { existingMetadata.Guids.Remove(guid); if (await _metadataRepository.RemoveGuid(guid)) { result.IsUpdated = true; } } foreach (MetadataGuid guid in fullMetadata.Guids .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) .ToList()) { existingMetadata.Guids.Add(guid); if (await _metadataRepository.AddGuid(existingMetadata, guid)) { result.IsUpdated = true; } } foreach (Tag tag in existingMetadata.Tags .Filter(g => fullMetadata.Tags.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Tags.Remove(tag); if (await _metadataRepository.RemoveTag(tag)) { result.IsUpdated = true; } } foreach (Tag tag in fullMetadata.Tags .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Tags.Add(tag); if (await _televisionRepository.AddTag(existingMetadata, tag)) { result.IsUpdated = true; } } if (result.IsUpdated) { await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated); } }, _ => Task.CompletedTask); } return(result); }
private async Task <Either <BaseError, Unit> > ScanSeasons( PlexLibrary library, List <PlexPathReplacement> pathReplacements, PlexShow show, PlexConnection connection, PlexServerAuthToken token, string ffmpegPath, string ffprobePath, bool deepScan, CancellationToken cancellationToken) { List <PlexItemEtag> existingSeasons = await _plexTelevisionRepository.GetExistingPlexSeasons(library, show); Either <BaseError, List <PlexSeason> > entries = await _plexServerApiClient.GetShowSeasons( library, show, connection, token); foreach (BaseError error in entries.LeftToSeq()) { return(error); } var seasonEntries = entries.RightToSeq().Flatten().ToList(); foreach (PlexSeason incoming in seasonEntries) { incoming.ShowId = show.Id; // TODO: figure out how to rebuild playlists Either <BaseError, PlexSeason> maybeSeason = await _televisionRepository .GetOrAddPlexSeason(library, incoming) .BindT(existing => UpdateMetadataAndArtwork(existing, incoming, deepScan)); foreach (BaseError error in maybeSeason.LeftToSeq()) { _logger.LogWarning( "Error processing plex season at {Key}: {Error}", incoming.Key, error.Value); return(error); } foreach (PlexSeason season in maybeSeason.RightToSeq()) { Either <BaseError, Unit> scanResult = await ScanEpisodes( library, pathReplacements, season, connection, token, ffmpegPath, ffprobePath, deepScan, cancellationToken); foreach (ScanCanceled error in scanResult.LeftToSeq().OfType <ScanCanceled>()) { return(error); } await _plexTelevisionRepository.SetPlexEtag(season, incoming.Etag); season.Show = show; // TODO: if any seasons are unavailable or not found, flag show as unavailable/not found await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { season }); } } var fileNotFoundKeys = existingSeasons.Map(m => m.Key).Except(seasonEntries.Map(m => m.Key)).ToList(); List <int> ids = await _plexTelevisionRepository.FlagFileNotFoundSeasons(library, fileNotFoundKeys); await _searchIndex.RebuildItems(_searchRepository, ids); return(Unit.Default); }
private async Task <Either <BaseError, Unit> > ScanEpisodes( PlexLibrary library, List <PlexPathReplacement> pathReplacements, PlexSeason season, PlexConnection connection, PlexServerAuthToken token, string ffmpegPath, string ffprobePath, bool deepScan, CancellationToken cancellationToken) { List <PlexItemEtag> existingEpisodes = await _plexTelevisionRepository.GetExistingPlexEpisodes(library, season); Either <BaseError, List <PlexEpisode> > entries = await _plexServerApiClient.GetSeasonEpisodes( library, season, connection, token); foreach (BaseError error in entries.LeftToSeq()) { return(error); } var episodeEntries = entries.RightToSeq().Flatten().ToList(); foreach (PlexEpisode incoming in episodeEntries) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } if (await ShouldScanItem(library, pathReplacements, existingEpisodes, incoming, deepScan) == false) { continue; } incoming.SeasonId = season.Id; // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <PlexEpisode> > maybeEpisode = await _televisionRepository .GetOrAddPlexEpisode(library, incoming) .BindT(existing => UpdateMetadata(existing, incoming)) .BindT( existing => UpdateStatistics( pathReplacements, existing, incoming, library, connection, token, ffmpegPath, ffprobePath, deepScan)) .BindT(existing => UpdateSubtitles(pathReplacements, existing, incoming)) .BindT(existing => UpdateArtwork(existing, incoming)); foreach (BaseError error in maybeEpisode.LeftToSeq()) { switch (error) { case ScanCanceled: return(error); default: _logger.LogWarning( "Error processing plex episode at {Key}: {Error}", incoming.Key, error.Value); break; } } foreach (MediaItemScanResult <PlexEpisode> result in maybeEpisode.RightToSeq()) { await _plexTelevisionRepository.SetPlexEtag(result.Item, incoming.Etag); string plexPath = incoming.MediaVersions.Head().MediaFiles.Head().Path; string localPath = _plexPathReplacementService.GetReplacementPlexPath( pathReplacements, plexPath, false); if (_localFileSystem.FileExists(localPath)) { await _plexTelevisionRepository.FlagNormal(library, result.Item); } else { await _plexTelevisionRepository.FlagUnavailable(library, result.Item); } if (result.IsAdded) { await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { result.Item }); } else { await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { result.Item }); } } } var fileNotFoundKeys = existingEpisodes.Map(m => m.Key).Except(episodeEntries.Map(m => m.Key)).ToList(); List <int> ids = await _plexTelevisionRepository.FlagFileNotFoundEpisodes(library, fileNotFoundKeys); await _searchIndex.RebuildItems(_searchRepository, ids); _searchIndex.Commit(); return(Unit.Default); }
private async Task <Either <BaseError, MediaItemScanResult <PlexEpisode> > > UpdateStatistics( List <PlexPathReplacement> pathReplacements, MediaItemScanResult <PlexEpisode> result, PlexEpisode incoming, PlexLibrary library, PlexConnection connection, PlexServerAuthToken token, string ffmpegPath, string ffprobePath, bool deepScan) { PlexEpisode existing = result.Item; MediaVersion existingVersion = existing.MediaVersions.Head(); MediaVersion incomingVersion = incoming.MediaVersions.Head(); if (result.IsAdded || existing.Etag != incoming.Etag || deepScan || existingVersion.Streams.Count == 0) { foreach (MediaFile incomingFile in incomingVersion.MediaFiles.HeadOrNone()) { foreach (MediaFile existingFile in existingVersion.MediaFiles.HeadOrNone()) { if (incomingFile.Path != existingFile.Path) { _logger.LogDebug( "Plex episode has moved from {OldPath} to {NewPath}", existingFile.Path, incomingFile.Path); existingFile.Path = incomingFile.Path; await _televisionRepository.UpdatePath(existingFile.Id, incomingFile.Path); } } } Either <BaseError, bool> refreshResult = true; string localPath = _plexPathReplacementService.GetReplacementPlexPath( pathReplacements, incoming.MediaVersions.Head().MediaFiles.Head().Path, false); if ((existing.Etag != incoming.Etag || existingVersion.Streams.Count == 0) && _localFileSystem.FileExists(localPath)) { _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath); refreshResult = await _localStatisticsProvider.RefreshStatistics( ffmpegPath, ffprobePath, existing, localPath); } foreach (BaseError error in refreshResult.LeftToSeq()) { _logger.LogWarning( "Unable to refresh {Attribute} for media item {Path}. Error: {Error}", "Statistics", localPath, error.Value); } foreach (var _ in refreshResult.RightToSeq()) { foreach (MediaItem updated in await _searchRepository.GetItemToIndex(incoming.Id)) { await _searchIndex.UpdateItems( _searchRepository, new List <MediaItem> { updated }); } Either <BaseError, Tuple <EpisodeMetadata, MediaVersion> > maybeStatistics = await _plexServerApiClient.GetEpisodeMetadataAndStatistics( library, incoming.Key.Split("/").Last(), connection, token); foreach (Tuple <EpisodeMetadata, MediaVersion> tuple in maybeStatistics.RightToSeq()) { (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; Option <EpisodeMetadata> maybeExisting = existing.EpisodeMetadata .Find(em => em.EpisodeNumber == incomingMetadata.EpisodeNumber); foreach (EpisodeMetadata existingMetadata in maybeExisting) { 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 (Tag tag in existingMetadata.Tags .Filter(g => incomingMetadata.Tags.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Tags.Remove(tag); await _metadataRepository.RemoveTag(tag); } foreach (Tag tag in incomingMetadata.Tags .Filter(g => existingMetadata.Tags.All(g2 => g2.Name != g.Name)) .ToList()) { existingMetadata.Tags.Add(tag); await _televisionRepository.AddTag(existingMetadata, tag); } } existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; existingVersion.VideoScanKind = mediaVersion.VideoScanKind; existingVersion.DateUpdated = mediaVersion.DateUpdated; await _metadataRepository.UpdatePlexStatistics(existingVersion.Id, mediaVersion); } } } return(result); }
public async Task <Either <BaseError, Unit> > ScanLibrary( PlexConnection connection, PlexServerAuthToken token, PlexLibrary plexMediaSourceLibrary) { Either <BaseError, List <PlexMovie> > entries = await _plexServerApiClient.GetMovieLibraryContents( plexMediaSourceLibrary, connection, token); await entries.Match( async movieEntries => { foreach (PlexMovie incoming in movieEntries) { // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <PlexMovie> > maybeMovie = await _movieRepository .GetOrAdd(plexMediaSourceLibrary, incoming) .BindT(existing => UpdateStatistics(existing, incoming, connection, token)) .BindT(existing => UpdateMetadata(existing, incoming)) .BindT(existing => UpdateArtwork(existing, incoming)); await maybeMovie.Match( async result => { if (result.IsAdded) { await _searchIndex.AddItems(new List <MediaItem> { result.Item }); } else if (result.IsUpdated) { await _searchIndex.UpdateItems(new List <MediaItem> { result.Item }); } }, error => { _logger.LogWarning( "Error processing plex movie at {Key}: {Error}", incoming.Key, error.Value); return(Task.CompletedTask); }); } var movieKeys = movieEntries.Map(s => s.Key).ToList(); List <int> ids = await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys); await _searchIndex.RemoveItems(ids); }, error => { _logger.LogWarning( "Error synchronizing plex library {Path}: {Error}", plexMediaSourceLibrary.Name, error.Value); return(Task.CompletedTask); }); return(Unit.Default); }