using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.MediaSources; using ErsatzTV.Core.Plex; using Microsoft.Extensions.Logging; namespace ErsatzTV.Scanner.Application.Plex; public class SynchronizePlexLibraryByIdHandler : IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly ILibraryRepository _libraryRepository; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediator _mediator; private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; private readonly IPlexOtherVideoLibraryScanner _plexOtherVideoLibraryScanner; private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner; public SynchronizePlexLibraryByIdHandler( IMediator mediator, IMediaSourceRepository mediaSourceRepository, IConfigElementRepository configElementRepository, IPlexSecretStore plexSecretStore, IPlexMovieLibraryScanner plexMovieLibraryScanner, IPlexOtherVideoLibraryScanner plexOtherVideoLibraryScanner, IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner, ILibraryRepository libraryRepository, ILogger logger) { _mediator = mediator; _mediaSourceRepository = mediaSourceRepository; _configElementRepository = configElementRepository; _plexSecretStore = plexSecretStore; _plexMovieLibraryScanner = plexMovieLibraryScanner; _plexOtherVideoLibraryScanner = plexOtherVideoLibraryScanner; _plexTelevisionLibraryScanner = plexTelevisionLibraryScanner; _libraryRepository = libraryRepository; _logger = logger; } public async Task> Handle( SynchronizePlexLibraryById request, CancellationToken cancellationToken) { Validation validation = await Validate(request); return await validation.Match( parameters => Synchronize(parameters, cancellationToken), error => Task.FromResult>(error.Join())); } private async Task> Synchronize( RequestParameters parameters, CancellationToken cancellationToken) { var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); if (parameters.ForceScan || parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now) { Either result = parameters.Library.MediaKind switch { LibraryMediaKind.Movies => await _plexMovieLibraryScanner.ScanLibrary( parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.PlexServerAuthToken, parameters.Library, parameters.DeepScan, cancellationToken), LibraryMediaKind.OtherVideos => await _plexOtherVideoLibraryScanner.ScanLibrary( parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.PlexServerAuthToken, parameters.Library, parameters.DeepScan, cancellationToken), LibraryMediaKind.Shows => await _plexTelevisionLibraryScanner.ScanLibrary( parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.PlexServerAuthToken, parameters.Library, parameters.DeepScan, cancellationToken), _ => Unit.Default }; if (result.IsRight) { parameters.Library.LastScan = DateTime.UtcNow; await _libraryRepository.UpdateLastScan(parameters.Library); } foreach (BaseError error in result.LeftToSeq()) { _logger.LogError("Error synchronizing plex library: {Error}", error); } return result.Map(_ => parameters.Library.Name); } _logger.LogDebug( "Skipping unforced scan of plex media library {Name}", parameters.Library.Name); // send an empty progress update for the library name await _mediator.Publish( new ScannerProgressUpdate( parameters.Library.Id, parameters.Library.Name, 0, Array.Empty(), Array.Empty()), cancellationToken); return parameters.Library.Name; } private async Task> Validate(SynchronizePlexLibraryById request) => (await ValidateConnection(request), await PlexLibraryMustExist(request), await ValidateLibraryRefreshInterval()) .Apply( (connectionParameters, plexLibrary, libraryRefreshInterval) => new RequestParameters( connectionParameters, plexLibrary, request.ForceScan, libraryRefreshInterval, request.DeepScan )); private Task> ValidateConnection( SynchronizePlexLibraryById request) => PlexMediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveToken); private Task> PlexMediaSourceMustExist( SynchronizePlexLibraryById request) => _mediaSourceRepository.GetPlexByLibraryId(request.PlexLibraryId) .Map( v => v.ToValidation( $"Plex media source for library {request.PlexLibraryId} does not exist.")); private Validation MediaSourceMustHaveActiveConnection( PlexMediaSource plexMediaSource) { Option maybeConnection = plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) .ToValidation("Plex media source requires an active connection"); } private async Task> MediaSourceMustHaveToken( ConnectionParameters connectionParameters) { Option maybeToken = await _plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) .ToValidation("Plex media source requires a token"); } private Task> PlexLibraryMustExist( SynchronizePlexLibraryById request) => _mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId) .Map(v => v.ToValidation($"Plex library {request.PlexLibraryId} does not exist.")); private Task> ValidateLibraryRefreshInterval() => _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) .FilterT(lri => lri is >= 0 and < 1_000_000) .Map(lri => lri.ToValidation("Library refresh interval is invalid")); private record RequestParameters( ConnectionParameters ConnectionParameters, PlexLibrary Library, bool ForceScan, int LibraryRefreshInterval, bool DeepScan); private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) { public PlexServerAuthToken? PlexServerAuthToken { get; set; } } }