using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Plex; namespace ErsatzTV.Scanner.Application.Plex; public class SynchronizePlexCollectionsHandler : IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexCollectionScanner _scanner; public SynchronizePlexCollectionsHandler( IMediaSourceRepository mediaSourceRepository, IPlexSecretStore plexSecretStore, IPlexCollectionScanner scanner, IConfigElementRepository configElementRepository) { _mediaSourceRepository = mediaSourceRepository; _plexSecretStore = plexSecretStore; _scanner = scanner; _configElementRepository = configElementRepository; } public async Task> Handle( SynchronizePlexCollections request, CancellationToken cancellationToken) { Validation validation = await Validate(request); return await validation.Match( p => SynchronizeCollections(p, cancellationToken), error => Task.FromResult>(error.Join())); } private async Task> Validate(SynchronizePlexCollections request) { Task> mediaSource = MediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveToken); return (await mediaSource, await ValidateLibraryRefreshInterval()) .Apply( (connectionParameters, libraryRefreshInterval) => new RequestParameters( connectionParameters, connectionParameters.PlexMediaSource, request.ForceScan, libraryRefreshInterval)); } 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 Task> MediaSourceMustExist( SynchronizePlexCollections request) => _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) .Map(o => o.ToValidation("Plex media source does not exist.")); private static 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 async Task> SynchronizeCollections( RequestParameters parameters, CancellationToken cancellationToken) { var lastScan = new DateTimeOffset( parameters.MediaSource.LastCollectionsScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); if (parameters.ForceScan || parameters.LibraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now) { Either result = await _scanner.ScanCollections( parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.PlexServerAuthToken, cancellationToken); if (result.IsRight) { parameters.MediaSource.LastCollectionsScan = DateTime.UtcNow; await _mediaSourceRepository.UpdateLastCollectionScan(parameters.MediaSource); } return result; } return Unit.Default; } private record RequestParameters( ConnectionParameters ConnectionParameters, PlexMediaSource MediaSource, bool ForceScan, int LibraryRefreshInterval); private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) { public PlexServerAuthToken? PlexServerAuthToken { get; set; } } }