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 LanguageExt; using MediatR; using Microsoft.Extensions.Logging; using Unit = LanguageExt.Unit; namespace ErsatzTV.Application.MediaSources.Commands { public class ScanLocalLibraryHandler : IRequestHandler>, IRequestHandler>, IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly IEntityLocker _entityLocker; private readonly ILibraryRepository _libraryRepository; private readonly ILogger _logger; private readonly IMovieFolderScanner _movieFolderScanner; private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; private readonly ITelevisionFolderScanner _televisionFolderScanner; public ScanLocalLibraryHandler( ILibraryRepository libraryRepository, IConfigElementRepository configElementRepository, IMovieFolderScanner movieFolderScanner, ITelevisionFolderScanner televisionFolderScanner, IMusicVideoFolderScanner musicVideoFolderScanner, IEntityLocker entityLocker, ILogger logger) { _libraryRepository = libraryRepository; _configElementRepository = configElementRepository; _movieFolderScanner = movieFolderScanner; _televisionFolderScanner = televisionFolderScanner; _musicVideoFolderScanner = musicVideoFolderScanner; _entityLocker = entityLocker; _logger = logger; } public Task> Handle( ForceRescanLocalLibrary request, CancellationToken cancellationToken) => Handle(request); 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, bool rescan) = parameters; var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero); if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) { var sw = new Stopwatch(); sw.Start(); DateTimeOffset effectiveLastScan = rescan ? DateTimeOffset.MinValue : lastScan; foreach (LibraryPath libraryPath in localLibrary.Paths) { switch (localLibrary.MediaKind) { case LibraryMediaKind.Movies: await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); break; case LibraryMediaKind.Shows: await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); break; case LibraryMediaKind.MusicVideos: await _musicVideoFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); break; } } localLibrary.LastScan = DateTime.UtcNow; await _libraryRepository.UpdateLastScan(localLibrary); sw.Stop(); _logger.LogDebug( "Scan of library {Name} completed in {Duration}", localLibrary.Name, TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds)); } else { _logger.LogDebug( "Skipping unforced scan of library {Name}", localLibrary.Name); } _entityLocker.UnlockLibrary(localLibrary.Id); return Unit.Default; } private async Task> Validate(IScanLocalLibrary request) => (await LocalLibraryMustExist(request), await ValidateFFprobePath()) .Apply( (library, ffprobePath) => new RequestParameters( library, ffprobePath, request.ForceScan, request.Rescan)); 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 record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan, bool Rescan); } }