using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Plex; using Microsoft.Extensions.Logging; namespace ErsatzTV.Application.Plex; public class SynchronizePlexLibrariesHandler : IRequestHandler> { private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexServerApiClient _plexServerApiClient; private readonly ISearchIndex _searchIndex; public SynchronizePlexLibrariesHandler( IMediaSourceRepository mediaSourceRepository, IPlexSecretStore plexSecretStore, IPlexServerApiClient plexServerApiClient, ILogger logger, ISearchIndex searchIndex) { _mediaSourceRepository = mediaSourceRepository; _plexSecretStore = plexSecretStore; _plexServerApiClient = plexServerApiClient; _logger = logger; _searchIndex = searchIndex; } public Task> Handle( SynchronizePlexLibraries request, CancellationToken cancellationToken) => Validate(request) .MapT(SynchronizeLibraries) .Bind(v => v.ToEitherAsync()); private Task> Validate(SynchronizePlexLibraries request) => MediaSourceMustExist(request) .BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveToken); private Task> MediaSourceMustExist(SynchronizePlexLibraries request) => _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) .Map(o => o.ToValidation("Plex media source 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 async Task SynchronizeLibraries(ConnectionParameters connectionParameters) { Either> maybeLibraries = await _plexServerApiClient.GetLibraries( connectionParameters.ActiveConnection, connectionParameters.PlexServerAuthToken); foreach (BaseError error in maybeLibraries.LeftToSeq()) { _logger.LogWarning( "Unable to synchronize libraries from plex server {PlexServer}: {Error}", connectionParameters.PlexMediaSource.ServerName, error.Value); } foreach (List libraries in maybeLibraries.RightToSeq()) { var existing = connectionParameters.PlexMediaSource.Libraries.OfType().ToList(); var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList(); var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList(); var toUpdate = libraries .Filter(l => toAdd.All(a => a.Key != l.Key) && toRemove.All(r => r.Key != l.Key)).ToList(); List ids = await _mediaSourceRepository.UpdateLibraries( connectionParameters.PlexMediaSource.Id, toAdd, toRemove, toUpdate); if (ids.Count != 0) { await _searchIndex.RemoveItems(ids); _searchIndex.Commit(); } } return Unit.Default; } private sealed record ConnectionParameters( PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) { public PlexServerAuthToken PlexServerAuthToken { get; set; } } }