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> > ScanSeasons( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, Show show, string showFolder, CancellationToken cancellationToken) { foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showFolder).Filter(ShouldIncludeFolder) .OrderBy(identity)) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } string etag = FolderEtag.CalculateWithSubfolders(seasonFolder, _localFileSystem); Option <LibraryFolder> knownFolder = libraryPath.LibraryFolders .Filter(f => f.Path == seasonFolder) .HeadOrNone(); // skip folder if etag matches if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } Option <int> maybeSeasonNumber = SeasonNumberForFolder(seasonFolder); foreach (int seasonNumber in maybeSeasonNumber) { Either <BaseError, Season> maybeSeason = await _televisionRepository .GetOrAddSeason(show, libraryPath.Id, seasonNumber) .BindT(EnsureMetadataExists) .BindT(season => UpdatePoster(season, seasonFolder, cancellationToken)); foreach (BaseError error in maybeSeason.LeftToSeq()) { _logger.LogWarning( "Error processing season in folder {Folder}: {Error}", seasonFolder, error.Value); } foreach (Season season in maybeSeason.RightToSeq()) { Either <BaseError, Unit> scanResult = await ScanEpisodes( libraryPath, ffmpegPath, ffprobePath, season, seasonFolder, cancellationToken); foreach (ScanCanceled error in scanResult.LeftToSeq().OfType <ScanCanceled>()) { return(error); } await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag); season.Show = show; await _searchIndex.UpdateItems(_searchRepository, new List <MediaItem> { season }); } } } return(Unit.Default); }
private async Task <Either <BaseError, Unit> > ScanMusicVideos( LibraryPath libraryPath, string ffmpegPath, string ffprobePath, Artist artist, string artistFolder, CancellationToken cancellationToken) { var folderQueue = new Queue <string>(); folderQueue.Enqueue(artistFolder); while (folderQueue.Count > 0) { if (cancellationToken.IsCancellationRequested) { return(new ScanCanceled()); } string musicVideoFolder = folderQueue.Dequeue(); // _logger.LogDebug("Scanning music video folder {Folder}", musicVideoFolder); var allFiles = _localFileSystem.ListFiles(musicVideoFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter(f => !Path.GetFileName(f).StartsWith("._")) .ToList(); foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder) .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } string etag = FolderEtag.Calculate(musicVideoFolder, _localFileSystem); Option <LibraryFolder> knownFolder = libraryPath.LibraryFolders .Filter(f => f.Path == musicVideoFolder) .HeadOrNone(); // skip folder if etag matches if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } foreach (string file in allFiles.OrderBy(identity)) { // TODO: figure out how to rebuild playouts Either <BaseError, MediaItemScanResult <MusicVideo> > maybeMusicVideo = await _musicVideoRepository .GetOrAdd(artist, libraryPath, file) .BindT(musicVideo => UpdateStatistics(musicVideo, ffmpegPath, ffprobePath)) .BindT(UpdateMetadata) .BindT(result => UpdateThumbnail(result, cancellationToken)) .BindT(UpdateSubtitles) .BindT(FlagNormal); foreach (BaseError error in maybeMusicVideo.LeftToSeq()) { _logger.LogWarning("Error processing music video at {Path}: {Error}", file, error.Value); } foreach (MediaItemScanResult <MusicVideo> result in maybeMusicVideo.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, musicVideoFolder, etag); } } } return(Unit.Default); }