using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace ErsatzTV.Application.Jellyfin; public class SynchronizeJellyfinAdminUserIdHandler : IRequestHandler> { private readonly IJellyfinApiClient _jellyfinApiClient; private readonly IJellyfinSecretStore _jellyfinSecretStore; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMemoryCache _memoryCache; public SynchronizeJellyfinAdminUserIdHandler( IMemoryCache memoryCache, IMediaSourceRepository mediaSourceRepository, IJellyfinSecretStore jellyfinSecretStore, IJellyfinApiClient jellyfinApiClient, ILogger logger) { _memoryCache = memoryCache; _mediaSourceRepository = mediaSourceRepository; _jellyfinSecretStore = jellyfinSecretStore; _jellyfinApiClient = jellyfinApiClient; _logger = logger; } public Task> Handle( SynchronizeJellyfinAdminUserId request, CancellationToken cancellationToken) => Validate(request) .Map(v => v.ToEither()) .BindT(PerformSync); private async Task> PerformSync(ConnectionParameters parameters) { if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", out string _)) { return Unit.Default; } Either maybeUserId = await _jellyfinApiClient.GetAdminUserId( parameters.ActiveConnection.Address, parameters.ApiKey); return await maybeUserId.Match( userId => { // _logger.LogDebug("Jellyfin admin user id is {UserId}", userId); _memoryCache.Set($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", userId); return Task.FromResult>(Unit.Default); }, async error => { // clear api key if unable to sync with jellyfin if (error.Value.Contains("Unauthorized")) { await _jellyfinSecretStore.SaveSecrets( new JellyfinSecrets { Address = parameters.ActiveConnection.Address, ApiKey = null }); } return Left(error); }); } private Task> Validate(SynchronizeJellyfinAdminUserId request) => MediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveApiKey); private Task> MediaSourceMustExist( SynchronizeJellyfinAdminUserId request) => _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) .Map(o => o.ToValidation("Jellyfin media source does not exist.")); private Validation MediaSourceMustHaveActiveConnection( JellyfinMediaSource jellyfinMediaSource) { Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) .ToValidation("Jellyfin media source requires an active connection"); } private async Task> MediaSourceMustHaveApiKey( ConnectionParameters connectionParameters) { JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) .Where(match => match) .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) .ToValidation("Jellyfin media source requires an api key"); } private sealed record ConnectionParameters( JellyfinMediaSource JellyfinMediaSource, JellyfinConnection ActiveConnection) { public string ApiKey { get; set; } } }