private async Task SaveTroubleshootingData(string channelNumber, string output) { try { var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber)); FileInfo[] allFiles = directory.GetFiles(); string playlistFileName = Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8"); string playlistContents = string.Empty; if (_localFileSystem.FileExists(playlistFileName)) { playlistContents = await File.ReadAllTextAsync(playlistFileName); } var data = new TroubleshootingData(allFiles, playlistContents, output); string serialized = data.Serialize(); string file = _tempFilePool.GetNextTempFile(TempFileCategory.BadTranscodeFolder); await File.WriteAllTextAsync(file, serialized); _logger.LogWarning("Transcode folder is in bad state; troubleshooting info saved to {File}", file); } catch (Exception ex) { _client.Notify(ex); } }
public async Task <Either <BaseError, Unit> > ScanFolder(LibraryPath libraryPath, string ffprobePath) { if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) { return(new MediaSourceInaccessible()); } var allShowFolders = _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) .OrderBy(identity) .ToList(); foreach (string showFolder in allShowFolders) { Either <BaseError, MediaItemScanResult <Show> > maybeShow = await FindOrCreateShow(libraryPath.Id, showFolder) .BindT(show => UpdateMetadataForShow(show, showFolder)) .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Poster)) .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.FanArt)); 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(libraryPath, ffprobePath, result.Item, showFolder); }, _ => Task.FromResult(Unit.Default)); } foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Removing missing episode at {Path}", path); await _televisionRepository.DeleteByPath(libraryPath, path); } } await _televisionRepository.DeleteEmptySeasons(libraryPath); List <int> ids = await _televisionRepository.DeleteEmptyShows(libraryPath); await _searchIndex.RemoveItems(ids); return(Unit.Default); }
private async Task <Either <BaseError, PlayoutItemWithPath> > ValidatePlayoutItemPath(PlayoutItem playoutItem) { string path = await GetPlayoutItemPath(playoutItem); if (_localFileSystem.FileExists(path)) { return(new PlayoutItemWithPath(playoutItem, path)); } return(new PlayoutItemDoesNotExistOnDisk(path)); }
private async Task <Either <BaseError, PlayoutItemWithPath> > ValidatePlayoutItemPath(PlayoutItem playoutItem) { string path = await GetPlayoutItemPath(playoutItem); // TODO: this won't work with url streaming from plex if (_localFileSystem.FileExists(path)) { return(new PlayoutItemWithPath(playoutItem, path)); } return(new PlayoutItemDoesNotExistOnDisk(path)); }
private async Task <Unit> ExtractSubtitles( TvContext dbContext, int mediaItemId, string ffmpegPath, CancellationToken cancellationToken) { Option <MediaItem> maybeMediaItem = await dbContext.MediaItems .Include(mi => (mi as Episode).MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(mi => (mi as Episode).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(mi => (mi as Episode).EpisodeMetadata) .ThenInclude(em => em.Subtitles) .Include(mi => (mi as Movie).MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(mi => (mi as Movie).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(mi => (mi as Movie).MovieMetadata) .ThenInclude(em => em.Subtitles) .Include(mi => (mi as MusicVideo).MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(mi => (mi as MusicVideo).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(mi => (mi as MusicVideo).MusicVideoMetadata) .ThenInclude(em => em.Subtitles) .Include(mi => (mi as OtherVideo).MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(mi => (mi as OtherVideo).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(mi => (mi as OtherVideo).OtherVideoMetadata) .ThenInclude(em => em.Subtitles) .SelectOneAsync(e => e.Id, e => e.Id == mediaItemId); foreach (MediaItem mediaItem in maybeMediaItem) { foreach (List <Subtitle> allSubtitles in GetSubtitles(mediaItem)) { var subtitlesToExtract = new List <SubtitleToExtract>(); // find each subtitle that needs extraction IEnumerable <Subtitle> subtitles = allSubtitles .Filter( s => s.SubtitleKind == SubtitleKind.Embedded && s.IsExtracted == false && s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"); // find cache paths for each subtitle foreach (Subtitle subtitle in subtitles) { Option <string> maybePath = GetRelativeOutputPath(mediaItem.Id, subtitle); foreach (string path in maybePath) { subtitlesToExtract.Add(new SubtitleToExtract(subtitle, path)); } } string mediaItemPath = await GetMediaItemPath(mediaItem); ArgumentsBuilder args = new ArgumentsBuilder() .Add("-nostdin") .Add("-hide_banner") .Add("-i").Add(mediaItemPath); foreach (SubtitleToExtract subtitle in subtitlesToExtract) { string fullOutputPath = Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.OutputPath); Directory.CreateDirectory(Path.GetDirectoryName(fullOutputPath)); if (_localFileSystem.FileExists(fullOutputPath)) { File.Delete(fullOutputPath); } args.Add("-map").Add($"0:{subtitle.Subtitle.StreamIndex}").Add("-c").Add("copy") .Add(fullOutputPath); } BufferedCommandResult result = await Cli.Wrap(ffmpegPath) .WithArguments(args.Build()) .WithValidation(CommandResultValidation.None) .ExecuteBufferedAsync(cancellationToken); if (result.ExitCode == 0) { foreach (SubtitleToExtract subtitle in subtitlesToExtract) { subtitle.Subtitle.IsExtracted = true; subtitle.Subtitle.Path = subtitle.OutputPath; } int count = await dbContext.SaveChangesAsync(cancellationToken); _logger.LogDebug("Successfully extracted {Count} subtitles", count); } else { _logger.LogError("Failed to extract subtitles. {Error}", result.StandardError); } } } return(Unit.Default); }
private async Task <Either <BaseError, Unit> > ProcessSeasons( string address, string apiKey, EmbyLibrary library, string ffmpegPath, string ffprobePath, List <EmbyPathReplacement> pathReplacements, EmbyShow show, List <EmbyItemEtag> existingSeasons, List <EmbySeason> seasons, CancellationToken cancellationToken) { foreach (EmbySeason incoming in seasons) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } Option <EmbyItemEtag> maybeExisting = existingSeasons.Find(ie => ie.ItemId == incoming.ItemId); if (maybeExisting.IsNone) { incoming.LibraryPathId = library.Paths.Head().Id; _logger.LogDebug( "INSERT: Item id is new for show {Show} season {Season}", show.ShowMetadata.Head().Title, incoming.SeasonMetadata.Head().Title); if (await _televisionRepository.AddSeason(show, incoming)) { incoming.Show = show; await _searchIndex.AddItems(_searchRepository, new List <MediaItem> { incoming }); } } foreach (EmbyItemEtag existing in maybeExisting) { if (existing.Etag != incoming.Etag) { _logger.LogDebug( "UPDATE: Etag has changed for show {Show} season {Season}", show.ShowMetadata.Head().Title, incoming.SeasonMetadata.Head().Title); incoming.ShowId = show.Id; incoming.LibraryPathId = library.Paths.Head().Id; foreach (EmbySeason updated in await _televisionRepository.Update(incoming)) { incoming.Show = show; foreach (MediaItem toIndex in await _searchRepository.GetItemToIndex(updated.Id)) { await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { toIndex }); } } } } List <EmbyItemEtag> existingEpisodes = await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId); Either <BaseError, List <EmbyEpisode> > maybeEpisodes = await _embyApiClient.GetEpisodeLibraryItems(address, apiKey, incoming.ItemId); foreach (BaseError error in maybeEpisodes.LeftToSeq()) { _logger.LogWarning( "Error synchronizing emby library {Path}: {Error}", library.Name, error.Value); } foreach (List <EmbyEpisode> episodes in maybeEpisodes.RightToSeq()) { var validEpisodes = new List <EmbyEpisode>(); foreach (EmbyEpisode episode in episodes) { string localPath = _pathReplacementService.GetReplacementEmbyPath( pathReplacements, episode.MediaVersions.Head().MediaFiles.Head().Path, false); if (!_localFileSystem.FileExists(localPath)) { _logger.LogWarning( "Skipping emby episode that does not exist at {Path}", localPath); } else { validEpisodes.Add(episode); } } Either <BaseError, Unit> scanResult = await ProcessEpisodes( show.ShowMetadata.Head().Title, incoming.SeasonMetadata.Head().Title, library, ffmpegPath, ffprobePath, pathReplacements, incoming, existingEpisodes, validEpisodes, cancellationToken); foreach (ScanCanceled error in scanResult.LeftToSeq().OfType <ScanCanceled>()) { return(error); } foreach (Unit _ in scanResult.RightToSeq()) { var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList(); var episodeIds = existingEpisodes .Filter(i => !incomingEpisodeIds.Contains(i.ItemId)) .Map(m => m.ItemId) .ToList(); List <int> missingEpisodeIds = await _televisionRepository.RemoveMissingEpisodes(library, episodeIds); await _searchIndex.RemoveItems(missingEpisodeIds); _searchIndex.Commit(); } } } return(Unit.Default); }
public async Task <Either <BaseError, Unit> > ScanFolder( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, decimal progressMin, decimal progressMax, CancellationToken cancellationToken) { try { decimal progressSpread = progressMax - progressMin; var allShowFolders = _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) .OrderBy(identity) .ToList(); foreach (string showFolder in allShowFolders) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)allShowFolders.IndexOf(showFolder) / allShowFolders.Count; await _mediator.Publish( new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion *progressSpread), cancellationToken); Either <BaseError, MediaItemScanResult <Show> > maybeShow = await FindOrCreateShow(libraryPath.Id, showFolder) .BindT(show => UpdateMetadataForShow(show, showFolder)) .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Poster, cancellationToken)) .BindT(show => UpdateArtworkForShow(show, showFolder, ArtworkKind.FanArt, cancellationToken)) .BindT( show => UpdateArtworkForShow(show, showFolder, ArtworkKind.Thumbnail, cancellationToken)); foreach (BaseError error in maybeShow.LeftToSeq()) { _logger.LogWarning( "Error processing show in folder {Folder}: {Error}", showFolder, error.Value); } foreach (MediaItemScanResult <Show> result in maybeShow.RightToSeq()) { Either <BaseError, Unit> scanResult = await ScanSeasons( libraryPath, ffmpegPath, ffprobePath, result.Item, showFolder, cancellationToken); foreach (ScanCanceled error in scanResult.LeftToSeq().OfType <ScanCanceled>()) { return(error); } 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 }); } } } foreach (string path in await _televisionRepository.FindEpisodePaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Flagging missing episode at {Path}", path); List <int> episodeIds = await FlagFileNotFound(libraryPath, path); await _searchIndex.RebuildItems(_searchRepository, episodeIds); } else if (Path.GetFileName(path).StartsWith("._")) { _logger.LogInformation("Removing dot underscore file at {Path}", path); await _televisionRepository.DeleteByPath(libraryPath, path); } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); await _televisionRepository.DeleteEmptySeasons(libraryPath); List <int> ids = await _televisionRepository.DeleteEmptyShows(libraryPath); await _searchIndex.RemoveItems(ids); return(Unit.Default); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }
private async Task <Either <BaseError, Unit> > ScanLibrary( IMediaServerMovieRepository <TLibrary, TMovie, TEtag> movieRepository, TConnectionParameters connectionParameters, TLibrary library, Func <TMovie, string> getLocalPath, string ffmpegPath, string ffprobePath, List <TMovie> movieEntries, bool deepScan, CancellationToken cancellationToken) { List <TEtag> existingMovies = await movieRepository.GetExistingMovies(library); foreach (TMovie incoming in movieEntries) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)movieEntries.IndexOf(incoming) / movieEntries.Count; await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion), cancellationToken); string localPath = getLocalPath(incoming); if (await ShouldScanItem(movieRepository, library, existingMovies, incoming, localPath, deepScan) == false) { continue; } Either <BaseError, MediaItemScanResult <TMovie> > maybeMovie = await movieRepository .GetOrAdd(library, incoming) .MapT( result => { result.LocalPath = localPath; return(result); }) .BindT(existing => UpdateMetadata(connectionParameters, library, existing, incoming, deepScan)) .BindT(existing => UpdateStatistics(existing, incoming, ffmpegPath, ffprobePath)) .BindT(UpdateSubtitles); if (maybeMovie.IsLeft) { foreach (BaseError error in maybeMovie.LeftToSeq()) { _logger.LogWarning( "Error processing movie {Title}: {Error}", incoming.MovieMetadata.Head().Title, error.Value); } continue; } foreach (MediaItemScanResult <TMovie> result in maybeMovie.RightToSeq()) { await movieRepository.SetEtag(result.Item, MediaServerEtag(incoming)); if (_localFileSystem.FileExists(result.LocalPath)) { if (await movieRepository.FlagNormal(library, result.Item)) { result.IsUpdated = true; } } else { Option <int> flagResult = await movieRepository.FlagUnavailable(library, result.Item); if (flagResult.IsSome) { result.IsUpdated = true; } } 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 fileNotFoundItemIds = existingMovies.Map(m => m.MediaServerItemId) .Except(movieEntries.Map(MediaServerItemId)).ToList(); List <int> ids = await movieRepository.FlagFileNotFound(library, fileNotFoundItemIds); await _searchIndex.RebuildItems(_searchRepository, ids); await _mediator.Publish(new LibraryScanProgress(library.Id, 0), cancellationToken); return(Unit.Default); }
public async Task <Either <BaseError, Unit> > ScanFolder( LibraryPath libraryPath, string ffprobePath, string ffmpegPath, decimal progressMin, decimal progressMax, CancellationToken cancellationToken) { try { decimal progressSpread = progressMax - progressMin; var foldersCompleted = 0; var folderQueue = new Queue <string>(); if (ShouldIncludeFolder(libraryPath.Path)) { folderQueue.Enqueue(libraryPath.Path); } foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) .OrderBy(identity)) { folderQueue.Enqueue(folder); } while (folderQueue.Count > 0) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); await _mediator.Publish( new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion *progressSpread), cancellationToken); string songFolder = folderQueue.Dequeue(); foldersCompleted++; var filesForEtag = _localFileSystem.ListFiles(songFolder).ToList(); var allFiles = filesForEtag .Filter(f => AudioFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => !Path.GetFileName(f).StartsWith("._")) .ToList(); foreach (string subdirectory in _localFileSystem.ListSubdirectories(songFolder) .Filter(ShouldIncludeFolder) .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } string etag = FolderEtag.Calculate(songFolder, _localFileSystem); Option <LibraryFolder> knownFolder = libraryPath.LibraryFolders .Filter(f => f.Path == songFolder) .HeadOrNone(); // skip folder if etag matches if (!allFiles.Any() || await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } _logger.LogDebug( "UPDATE: Etag has changed for folder {Folder}", songFolder); foreach (string file in allFiles.OrderBy(identity)) { Either <BaseError, MediaItemScanResult <Song> > maybeSong = await _songRepository .GetOrAdd(libraryPath, file) .BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath)) .BindT(video => UpdateMetadata(video, ffprobePath)) .BindT(video => UpdateThumbnail(video, ffmpegPath, cancellationToken)) .BindT(FlagNormal); foreach (BaseError error in maybeSong.LeftToSeq()) { _logger.LogWarning("Error processing song at {Path}: {Error}", file, error.Value); } foreach (MediaItemScanResult <Song> result in maybeSong.RightToSeq()) { 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 }); } await _libraryRepository.SetEtag(libraryPath, knownFolder, songFolder, etag); } } } foreach (string path in await _songRepository.FindSongPaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Flagging missing song at {Path}", path); List <int> songIds = await FlagFileNotFound(libraryPath, path); await _searchIndex.RebuildItems(_searchRepository, songIds); } else if (Path.GetFileName(path).StartsWith("._")) { _logger.LogInformation("Removing dot underscore file at {Path}", path); List <int> songIds = await _songRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(songIds); } } await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); return(Unit.Default); } catch (Exception ex) when(ex is TaskCanceledException or OperationCanceledException) { return(new ScanCanceled()); } finally { _searchIndex.Commit(); } }
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); }
public async Task <Either <BaseError, Unit> > ScanFolder(LibraryPath libraryPath, string ffprobePath) { if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) { return(new MediaSourceInaccessible()); } var folderQueue = new Queue <string>(); foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) { folderQueue.Enqueue(folder); } while (folderQueue.Count > 0) { string movieFolder = folderQueue.Dequeue(); var allFiles = _localFileSystem.ListFiles(movieFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter( f => !ExtraFiles.Any( e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) .ToList(); if (allFiles.Count == 0) { foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } continue; } foreach (string file in allFiles.OrderBy(identity)) { // TODO: figure out how to rebuild playlists Either <BaseError, MediaItemScanResult <Movie> > maybeMovie = await _movieRepository .GetOrAdd(libraryPath, file) .BindT(movie => UpdateStatistics(movie, ffprobePath)) .BindT(UpdateMetadata) .BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster)) .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt)); 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 movie at {Path}: {Error}", file, error.Value); return(Task.CompletedTask); }); } } foreach (string path in await _movieRepository.FindMoviePaths(libraryPath)) { if (!_localFileSystem.FileExists(path)) { _logger.LogInformation("Removing missing movie at {Path}", path); List <int> ids = await _movieRepository.DeleteByPath(libraryPath, path); await _searchIndex.RemoveItems(ids); } } return(Unit.Default); }