using System; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using LanguageExt; using MediatR; using Microsoft.Extensions.Logging; using Unit = LanguageExt.Unit; namespace ErsatzTV.Application.MediaSources.Commands { public class ScanLocalLibraryHandler : IRequestHandler>, IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly IEntityLocker _entityLocker; private readonly ILibraryRepository _libraryRepository; private readonly ILogger _logger; private readonly IMediator _mediator; private readonly IMovieFolderScanner _movieFolderScanner; private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; private readonly IOtherVideoFolderScanner _otherVideoFolderScanner; private readonly ISongFolderScanner _songFolderScanner; private readonly ITelevisionFolderScanner _televisionFolderScanner; public ScanLocalLibraryHandler( ILibraryRepository libraryRepository, IConfigElementRepository configElementRepository, IMovieFolderScanner movieFolderScanner, ITelevisionFolderScanner televisionFolderScanner, IMusicVideoFolderScanner musicVideoFolderScanner, IOtherVideoFolderScanner otherVideoFolderScanner, ISongFolderScanner songFolderScanner, IEntityLocker entityLocker, IMediator mediator, ILogger logger) { _libraryRepository = libraryRepository; _configElementRepository = configElementRepository; _movieFolderScanner = movieFolderScanner; _televisionFolderScanner = televisionFolderScanner; _musicVideoFolderScanner = musicVideoFolderScanner; _otherVideoFolderScanner = otherVideoFolderScanner; _songFolderScanner = songFolderScanner; _entityLocker = entityLocker; _mediator = mediator; _logger = logger; } public Task> Handle( ForceScanLocalLibrary request, CancellationToken cancellationToken) => Handle(request); public Task> Handle( ScanLocalLibraryIfNeeded request, CancellationToken cancellationToken) => Handle(request); private Task> Handle(IScanLocalLibrary request) => Validate(request) .MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalLibrary.Name)) .Bind(v => v.ToEitherAsync()); private async Task PerformScan(RequestParameters parameters) { (LocalLibrary localLibrary, string ffprobePath, bool forceScan, int libraryRefreshInterval) = parameters; var sw = new Stopwatch(); sw.Start(); var scanned = false; for (var i = 0; i < localLibrary.Paths.Count; i++) { LibraryPath libraryPath = localLibrary.Paths[i]; decimal progressMin = (decimal) i / localLibrary.Paths.Count; decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count; var lastScan = new DateTimeOffset(libraryPath.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); if (forceScan || nextScan < DateTimeOffset.Now) { scanned = true; switch (localLibrary.MediaKind) { case LibraryMediaKind.Movies: await _movieFolderScanner.ScanFolder( libraryPath, ffprobePath, progressMin, progressMax); break; case LibraryMediaKind.Shows: await _televisionFolderScanner.ScanFolder( libraryPath, ffprobePath, progressMin, progressMax); break; case LibraryMediaKind.MusicVideos: await _musicVideoFolderScanner.ScanFolder( libraryPath, ffprobePath, progressMin, progressMax); break; case LibraryMediaKind.OtherVideos: await _otherVideoFolderScanner.ScanFolder( libraryPath, ffprobePath, progressMin, progressMax); break; case LibraryMediaKind.Songs: await _songFolderScanner.ScanFolder( libraryPath, ffprobePath, progressMin, progressMax); break; } libraryPath.LastScan = DateTime.UtcNow; await _libraryRepository.UpdateLastScan(libraryPath); } await _mediator.Publish(new LibraryScanProgress(libraryPath.LibraryId, progressMax)); } sw.Stop(); if (scanned) { _logger.LogDebug( "Scan of library {Name} completed in {Duration}", localLibrary.Name, TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds)); } else { _logger.LogDebug( "Skipping unforced scan of local media library {Name}", localLibrary.Name); } await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0)); _entityLocker.UnlockLibrary(localLibrary.Id); return Unit.Default; } private async Task> Validate(IScanLocalLibrary request) => (await LocalLibraryMustExist(request), await ValidateFFprobePath(), await ValidateLibraryRefreshInterval()) .Apply( (library, ffprobePath, libraryRefreshInterval) => new RequestParameters( library, ffprobePath, request.ForceScan, libraryRefreshInterval)); private Task> LocalLibraryMustExist( IScanLocalLibrary request) => _libraryRepository.Get(request.LibraryId) .Map(maybeLibrary => maybeLibrary.Map(ms => ms as LocalLibrary)) .Map(v => v.ToValidation($"Local library {request.LibraryId} does not exist.")); private Task> ValidateFFprobePath() => _configElementRepository.GetValue(ConfigElementKey.FFprobePath) .FilterT(File.Exists) .Map( ffprobePath => ffprobePath.ToValidation("FFprobe path does not exist on the file system")); private Task> ValidateLibraryRefreshInterval() => _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) .FilterT(lri => lri > 0) .Map(lri => lri.ToValidation("Library refresh interval is invalid")); private record RequestParameters( LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan, int LibraryRefreshInterval); } }