using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Repositories; namespace ErsatzTV.Scanner.Application.Emby; public class SynchronizeEmbyCollectionsHandler : IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly IEmbySecretStore _embySecretStore; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IEmbyCollectionScanner _scanner; public SynchronizeEmbyCollectionsHandler( IMediaSourceRepository mediaSourceRepository, IEmbySecretStore embySecretStore, IEmbyCollectionScanner scanner, IConfigElementRepository configElementRepository) { _mediaSourceRepository = mediaSourceRepository; _embySecretStore = embySecretStore; _scanner = scanner; _configElementRepository = configElementRepository; } public async Task> Handle( SynchronizeEmbyCollections request, CancellationToken cancellationToken) { Validation validation = await Validate(request); return await validation.Match( SynchronizeCollections, error => Task.FromResult>(error.Join())); } private async Task> Validate(SynchronizeEmbyCollections request) { Task> mediaSource = MediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveApiKey); return (await mediaSource, await ValidateLibraryRefreshInterval()) .Apply( (connectionParameters, libraryRefreshInterval) => new RequestParameters( connectionParameters, connectionParameters.MediaSource, 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( SynchronizeEmbyCollections request) => _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) .Map(o => o.ToValidation("Emby media source does not exist.")); private static Validation MediaSourceMustHaveActiveConnection( EmbyMediaSource embyMediaSource) { Option maybeConnection = embyMediaSource.Connections.HeadOrNone(); return maybeConnection.Map(connection => new ConnectionParameters(connection, embyMediaSource)) .ToValidation("Emby media source requires an active connection"); } private async Task> MediaSourceMustHaveApiKey( ConnectionParameters connectionParameters) { EmbySecrets secrets = await _embySecretStore.ReadSecrets(); return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) .Where(match => match) .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .ToValidation("Emby media source requires an api key"); } private async Task> SynchronizeCollections(RequestParameters parameters) { 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.Address, parameters.ConnectionParameters.ApiKey); if (result.IsRight) { parameters.MediaSource.LastCollectionsScan = DateTime.UtcNow; await _mediaSourceRepository.UpdateLastCollectionScan(parameters.MediaSource); } return result; } return Unit.Default; } private record RequestParameters( ConnectionParameters ConnectionParameters, EmbyMediaSource MediaSource, bool ForceScan, int LibraryRefreshInterval); private record ConnectionParameters(EmbyConnection ActiveConnection, EmbyMediaSource MediaSource) { public string? ApiKey { get; init; } } }