private async Task <Either <BaseError, MediaItemScanResult <PlexMovie> > > AddPlexMovie( TvContext context, PlexLibrary library, PlexMovie item) { try { item.LibraryPathId = library.Paths.Head().Id; await context.PlexMovies.AddAsync(item); await context.SaveChangesAsync(); await context.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); await context.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(); return(new MediaItemScanResult <PlexMovie>(item) { IsAdded = true }); } catch (Exception ex) { return(BaseError.New(ex.ToString())); } }
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())); } }
public async Task <Either <BaseError, MediaItemScanResult <PlexShow> > > GetOrAddPlexShow( PlexLibrary library, PlexShow item) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option <PlexShow> maybeExisting = await dbContext.PlexShows .AsNoTracking() .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Genres) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Tags) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Studios) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Artwork) .Include(i => i.LibraryPath) .ThenInclude(lp => lp.Library) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); return(await maybeExisting.Match( plexShow => Right <BaseError, MediaItemScanResult <PlexShow> >( new MediaItemScanResult <PlexShow>(plexShow) { IsAdded = true }).AsTask(), async() => await AddPlexShow(dbContext, library, item))); }
private async Task <bool> ShouldScanItem( PlexLibrary library, List <PlexPathReplacement> pathReplacements, List <PlexItemEtag> existingEpisodes, PlexEpisode incoming, bool deepScan) { // deep scan will pull every episode individually from the plex api if (!deepScan) { Option <PlexItemEtag> maybeExisting = existingEpisodes.Find(ie => ie.Key == incoming.Key); string existingTag = await maybeExisting .Map(e => e.Etag ?? string.Empty) .IfNoneAsync(string.Empty); MediaItemState existingState = await maybeExisting .Map(e => e.State) .IfNoneAsync(MediaItemState.Normal); string plexPath = incoming.MediaVersions.Head().MediaFiles.Head().Path; string localPath = _plexPathReplacementService.GetReplacementPlexPath( pathReplacements, plexPath, false); // if media is unavailable, only scan if file now exists if (existingState == MediaItemState.Unavailable) { if (!_localFileSystem.FileExists(localPath)) { return(false); } } else if (existingTag == incoming.Etag) { if (!_localFileSystem.FileExists(localPath)) { foreach (int id in await _plexTelevisionRepository.FlagUnavailable(library, incoming)) { await _searchIndex.RebuildItems(_searchRepository, new List <int> { id }); } } // _logger.LogDebug("NOOP: etag has not changed for plex episode with key {Key}", incoming.Key); return(false); } // _logger.LogDebug( // "UPDATE: Etag has changed for episode {Episode}", // $"s{season.SeasonNumber}e{incoming.EpisodeMetadata.Head().EpisodeNumber}"); } return(true); }
public BrowseLibrary(ContextMenu contextMenu, PlexLibrary library) { MediaObjectsData = new ObservableCollection <IPlexMediaObject>(); MediaObjects = CollectionViewSource.GetDefaultView(MediaObjectsData); MediaObjects.Filter = MediaObjects_Filter; Library = library; InitializeComponent(); }
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(); })); }
public async Task <Either <BaseError, PlexSeason> > GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option <PlexSeason> maybeExisting = await dbContext.PlexSeasons .AsNoTracking() .Include(i => i.SeasonMetadata) .ThenInclude(mm => mm.Artwork) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); return(await maybeExisting.Match( plexSeason => Right <BaseError, PlexSeason>(plexSeason).AsTask(), async() => await AddPlexSeason(dbContext, library, item))); }
public async Task <Either <BaseError, PlexEpisode> > GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option <PlexEpisode> maybeExisting = await dbContext.PlexEpisodes .AsNoTracking() .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Artwork) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); return(await maybeExisting.Match( plexEpisode => Right <BaseError, PlexEpisode>(plexEpisode).AsTask(), async() => await AddPlexEpisode(dbContext, library, item))); }
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(); })); }
private void SetupDatabase() { PlexServer plexServer = FakeDbData.GetPlexServer().Generate(); PlexLibrary plexLibrary = FakeDbData.GetPlexLibrary(1, 1, PlexMediaType.TvShow).Generate(); try { _dbContext.PlexServers.Add(plexServer); _dbContext.PlexLibraries.Add(plexLibrary); _dbContext.SaveChanges(); } catch (Exception e) { Log.Fatal(e); throw; } }
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, 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 <List <int> > RemoveMissingPlexShows(PlexLibrary library, List <string> showKeys) { List <int> ids = await _dbConnection.QueryAsync <int>( @"SELECT m.Id FROM MediaItem m INNER JOIN PlexShow ps ON ps.Id = m.Id INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId WHERE lp.LibraryId = @LibraryId AND ps.Key not in @Keys", new { LibraryId = library.Id, Keys = showKeys }).Map(result => result.ToList()); await _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m INNER JOIN PlexShow ps ON ps.Id = m.Id INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId WHERE lp.LibraryId = @LibraryId AND ps.Key not in @Keys)", new { LibraryId = library.Id, Keys = showKeys }); return(ids); }
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, 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(); } }
/// <summary> /// Refresh the <see cref="PlexLibrary"/>, by first deleting all (related) media and the re-adding the media again. /// </summary> /// <param name="plexLibrary">The <see cref="PlexLibrary"/> to refresh.</param> /// <returns></returns> private async Task <Result> RefreshPlexMovieLibrary(PlexLibrary plexLibrary) { if (plexLibrary == null) { return(ResultExtensions.IsNull(nameof(plexLibrary))); } // Update the MetaData of this library var updateMetaDataResult = plexLibrary.UpdateMetaData(); if (updateMetaDataResult.IsFailed) { return(updateMetaDataResult); } var updateResult = await _mediator.Send(new UpdatePlexLibraryByIdCommand(plexLibrary)); if (updateResult.IsFailed) { return(updateResult.ToResult()); } var deleteResult = await _mediator.Send(new DeleteMediaFromPlexLibraryCommand(plexLibrary.Id)); if (deleteResult.IsFailed) { return(deleteResult.ToResult()); } var createResult = await _mediator.Send(new CreateUpdateOrDeletePlexMoviesCommand(plexLibrary)); if (createResult.IsFailed) { return(createResult.ToResult()); } await _signalRService.SendLibraryProgressUpdate(plexLibrary.Id, plexLibrary.MediaCount, plexLibrary.MediaCount); return(Result.Ok()); }
/// <summary> /// Returns the latest version of the <see cref="PlexLibrary"/> with the included media. Id and PlexServerId are copied over from the input parameter. /// </summary> /// <param name="plexLibrary"></param> /// <param name="authToken">The token used to authenticate with the <see cref="PlexServer"/>.</param> /// <param name="plexServerBaseUrl"></param> /// <returns></returns> public async Task <PlexLibrary> GetLibraryMediaAsync(PlexLibrary plexLibrary, string authToken) { // Retrieve updated version of the PlexLibrary var plexLibraries = await GetLibrarySectionsAsync(authToken, plexLibrary.ServerUrl); if (!plexLibraries.Any()) { return(null); } var updatedPlexLibrary = plexLibraries.Find(x => x.Key == plexLibrary.Key); updatedPlexLibrary.Id = plexLibrary.Id; updatedPlexLibrary.PlexServerId = plexLibrary.PlexServerId; updatedPlexLibrary.CheckedAt = DateTime.Now; // Retrieve the media for this library var result = await _plexApi.GetMetadataForLibraryAsync(authToken, plexLibrary.ServerUrl, plexLibrary.Key); if (result == null) { return(null); } // Determine how to map based on the Library type. switch (result.MediaContainer.ViewGroup) { case "movie": updatedPlexLibrary.Movies = _mapper.Map <List <PlexMovie> >(result.MediaContainer.Metadata); break; case "show": updatedPlexLibrary.TvShows = _mapper.Map <List <PlexTvShow> >(result.MediaContainer.Metadata); break; } return(updatedPlexLibrary); }
public async Task Update(PlexLibrary plexMediaSourceLibrary) { await using TvContext context = _dbContextFactory.CreateDbContext(); context.PlexLibraries.Update(plexMediaSourceLibrary); await context.SaveChangesAsync(); }
/// <summary> /// Retrieves all tvshow, season and episode data and stores it in the database. /// </summary> /// <param name="authToken"></param> /// <param name="plexLibrary"></param> /// <returns></returns> private async Task <Result> RefreshPlexTvShowLibrary(string authToken, PlexLibrary plexLibrary) { if (plexLibrary == null) { return(ResultExtensions.IsNull("plexLibrary").LogError()); } if (plexLibrary.Type != PlexMediaType.TvShow) { return(Result.Fail("PlexLibrary is not of type TvShow").LogError()); } if (plexLibrary.TvShows.Count == 0) { return(Result.Fail("PlexLibrary does not contain any TvShows and thus cannot request the corresponding media").LogError()); } var result = await _mediator.Send(new GetPlexLibraryByIdWithServerQuery(plexLibrary.Id)); if (result.IsFailed) { return(result); } // Request seasons and episodes for every tv show var plexLibraryDb = result.Value; var serverUrl = plexLibraryDb.PlexServer.ServerUrl; await _signalRService.SendLibraryProgressUpdate(plexLibrary.Id, 0, 3); var timer = new Stopwatch(); timer.Start(); var rawSeasonDataResult = await _plexServiceApi.GetAllSeasonsAsync(authToken, serverUrl, plexLibrary.Key); if (rawSeasonDataResult.IsFailed) { return(rawSeasonDataResult.ToResult()); } // Phase 1 of 4: Season data was retrieved successfully. await _signalRService.SendLibraryProgressUpdate(plexLibrary.Id, 1, 4); var rawEpisodesDataResult = await _plexServiceApi.GetAllEpisodesAsync(authToken, serverUrl, plexLibrary.Key); if (rawEpisodesDataResult.IsFailed) { return(rawEpisodesDataResult.ToResult()); } // Phase 2 of 4: Episode data was retrieved successfully. await _signalRService.SendLibraryProgressUpdate(plexLibrary.Id, 2, 4); foreach (var plexTvShow in plexLibrary.TvShows) { plexTvShow.Seasons = rawSeasonDataResult.Value.FindAll(x => x.ParentKey == plexTvShow.Key); foreach (var plexTvShowSeason in plexTvShow.Seasons) { plexTvShowSeason.PlexLibraryId = plexLibrary.Id; plexTvShowSeason.PlexLibrary = plexLibrary; plexTvShowSeason.TvShow = plexTvShow; plexTvShowSeason.Episodes = rawEpisodesDataResult.Value.FindAll(x => x.ParentKey == plexTvShowSeason.Key); // Set libraryId in each episode plexTvShowSeason.Episodes.ForEach(x => x.PlexLibraryId = plexLibrary.Id); plexTvShowSeason.MediaSize = plexTvShowSeason.Episodes.Sum(x => x.MediaSize); } plexTvShow.MediaSize = plexTvShow.Seasons.Sum(x => x.MediaSize); } // Phase 3 of 4: PlexLibrary media data was parsed successfully. await _signalRService.SendLibraryProgressUpdate(plexLibrary.Id, 3, 4); Log.Debug($"Finished retrieving all media for library {plexLibraryDb.Title} in {timer.Elapsed.TotalSeconds}"); timer.Restart(); // Update the MetaData of this library var updateMetaDataResult = plexLibrary.UpdateMetaData(); if (updateMetaDataResult.IsFailed) { return(updateMetaDataResult); } var updateResult = await _mediator.Send(new UpdatePlexLibraryByIdCommand(plexLibrary)); if (updateResult.IsFailed) { return(updateResult.ToResult()); } var deleteResult = await _mediator.Send(new DeleteMediaFromPlexLibraryCommand(plexLibrary.Id)); if (deleteResult.IsFailed) { return(deleteResult.ToResult()); } var createResult = await _mediator.Send(new CreateUpdateOrDeletePlexTvShowsCommand(plexLibrary)); if (createResult.IsFailed) { return(createResult.ToResult()); } Log.Debug($"Finished updating all media in the database for library {plexLibraryDb.Title} in {timer.Elapsed.TotalSeconds}"); // Phase 4 of 4: Database has been successfully updated with new library data. await _signalRService.SendLibraryProgressUpdate(plexLibrary.Id, 4, 4); return(Result.Ok()); }
public Task <List <int> > RemoveMissingPlexShows(PlexLibrary library, List <string> showKeys) => throw new NotSupportedException();
public CreateUpdateOrDeletePlexMoviesCommand(PlexLibrary plexLibrary) { PlexLibrary = plexLibrary; }
public Task <Either <BaseError, PlexSeason> > GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) => throw new NotSupportedException();
public Task <Either <BaseError, MediaItemScanResult <PlexEpisode> > > GetOrAddPlexEpisode( PlexLibrary library, PlexEpisode item) => throw new NotSupportedException();
public Task <List <PlexItemEtag> > GetExistingPlexEpisodes(PlexLibrary library, PlexSeason season) => throw new NotSupportedException();
public CreateUpdateOrDeletePlexTvShowsCommand(PlexLibrary plexLibrary) { PlexLibrary = plexLibrary; }
public UpdatePlexLibraryByIdCommand(PlexLibrary plexLibrary) { PlexLibrary = plexLibrary; }
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); }
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); }