using System.Threading.Channels; using ErsatzTV.Application; using ErsatzTV.Application.Plex; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using MediatR; namespace ErsatzTV.Services; public class PlexService : BackgroundService { private readonly ChannelReader _channel; private readonly ILogger _logger; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly SystemStartup _systemStartup; public PlexService( ChannelReader channel, IServiceScopeFactory serviceScopeFactory, SystemStartup systemStartup, ILogger logger) { _channel = channel; _serviceScopeFactory = serviceScopeFactory; _systemStartup = systemStartup; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.Yield(); await _systemStartup.WaitForDatabase(stoppingToken); await _systemStartup.WaitForSearchIndex(stoppingToken); if (stoppingToken.IsCancellationRequested) { return; } try { if (!File.Exists(FileSystemLayout.PlexSecretsPath)) { await File.WriteAllTextAsync(FileSystemLayout.PlexSecretsPath, "{}", stoppingToken); } _logger.LogInformation( "Plex service started; secrets are at {PlexSecretsPath}", FileSystemLayout.PlexSecretsPath); // synchronize sources on startup await SynchronizeSources(new SynchronizePlexMediaSources(), stoppingToken); await foreach (IPlexBackgroundServiceRequest request in _channel.ReadAllAsync(stoppingToken)) { try { Task requestTask; switch (request) { case TryCompletePlexPinFlow pinRequest: requestTask = CompletePinFlow(pinRequest, stoppingToken); break; case SynchronizePlexMediaSources sourcesRequest: requestTask = SynchronizeSources(sourcesRequest, stoppingToken); break; default: throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}"); } await requestTask; } catch (Exception ex) { _logger.LogWarning(ex, "Failed to process plex background service request"); } } } catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) { _logger.LogInformation("Plex service shutting down"); } } private async Task> SynchronizeSources( SynchronizePlexMediaSources request, CancellationToken cancellationToken) { #if DEBUG_NO_SYNC _logger.LogDebug("Skipping plex media source sync"); await Task.Delay(10, cancellationToken); _ = request; return []; #else using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); Either> result = await mediator.Send(request, cancellationToken); return result.Match( sources => { if (sources.Count != 0) { _logger.LogInformation("Successfully synchronized plex media sources"); } return sources; }, error => { _logger.LogWarning( "Unable to synchronize plex media sources: {Error}", error.Value); return new List(); }); #endif } private async Task CompletePinFlow( TryCompletePlexPinFlow request, CancellationToken cancellationToken) { using IServiceScope scope = _serviceScopeFactory.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); Either result = await mediator.Send(request, cancellationToken); foreach (bool success in result.RightToSeq()) { if (success) { _logger.LogInformation("Successfully authenticated with plex"); } else { _logger.LogInformation("Plex authentication timeout"); } } foreach (BaseError error in result.LeftToSeq()) { _logger.LogWarning("Unable to poll plex token: {Error}", error.Value); } } }