using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using Microsoft.Extensions.Logging; namespace ErsatzTV.Application.Emby; public class SynchronizeEmbyLibrariesHandler : IRequestHandler> { private readonly IEmbyApiClient _embyApiClient; private readonly IEmbySecretStore _embySecretStore; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly ISearchIndex _searchIndex; public SynchronizeEmbyLibrariesHandler( IMediaSourceRepository mediaSourceRepository, IEmbySecretStore embySecretStore, IEmbyApiClient embyApiClient, ILogger logger, ISearchIndex searchIndex) { _mediaSourceRepository = mediaSourceRepository; _embySecretStore = embySecretStore; _embyApiClient = embyApiClient; _logger = logger; _searchIndex = searchIndex; } public Task> Handle( SynchronizeEmbyLibraries request, CancellationToken cancellationToken) => Validate(request) .MapT(SynchronizeLibraries) .Bind(v => v.ToEitherAsync()); private Task> Validate(SynchronizeEmbyLibraries request) => MediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveApiKey); private Task> MediaSourceMustExist( SynchronizeEmbyLibraries request) => _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) .Map(o => o.ToValidation("Emby media source does not exist.")); private Validation MediaSourceMustHaveActiveConnection( EmbyMediaSource embyMediaSource) { Option maybeConnection = embyMediaSource.Connections.HeadOrNone(); return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) .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 SynchronizeLibraries(ConnectionParameters connectionParameters) { Either> maybeLibraries = await _embyApiClient.GetLibraries( connectionParameters.ActiveConnection.Address, connectionParameters.ApiKey); foreach (BaseError error in maybeLibraries.LeftToSeq()) { _logger.LogWarning( "Unable to synchronize libraries from emby server {EmbyServer}: {Error}", connectionParameters.EmbyMediaSource.ServerName, error.Value); } foreach (List libraries in maybeLibraries.RightToSeq()) { var existing = connectionParameters.EmbyMediaSource.Libraries.OfType() .ToList(); var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList(); var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList(); var toUpdate = libraries .Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList(); List ids = await _mediaSourceRepository.UpdateLibraries( connectionParameters.EmbyMediaSource.Id, toAdd, toRemove, toUpdate); if (ids.Count != 0) { await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); } } return Unit.Default; } private sealed record ConnectionParameters(EmbyMediaSource EmbyMediaSource, EmbyConnection ActiveConnection) { public string ApiKey { get; set; } } }