From 1a68dd040a6f6a489dc310fb2d6e59cda863af76 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sat, 16 Oct 2021 11:55:54 -0500 Subject: [PATCH] find working plex connection on startup (#438) --- CHANGELOG.md | 3 + .../SynchronizePlexMediaSourcesHandler.cs | 95 ++++++++++++++----- .../Interfaces/Plex/IPlexServerApiClient.cs | 4 + .../Repositories/IMediaSourceRepository.cs | 1 - .../Repositories/MediaSourceRepository.cs | 61 ++++-------- .../Plex/IPlexServerApi.cs | 6 ++ .../Plex/PlexServerApiClient.cs | 22 +++++ 7 files changed, 126 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97183d8f7..e862d0706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix double scheduling; this could happen if the app was shutdown during a playout build - Fix updating Jellyfin and Emby TV seasons +### Changed +- Automatically find working Plex address on startup + ## [0.1.4-alpha] - 2021-10-14 ### Fixed - Fix error message/offline stream continuity with channels that use HLS Segmenter diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs index fa6269dfd..b524991a1 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Plex; using LanguageExt; using MediatR; using Microsoft.Extensions.Logging; @@ -23,16 +24,22 @@ namespace ErsatzTV.Application.Plex.Commands private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IPlexTvApiClient _plexTvApiClient; + private readonly IPlexServerApiClient _plexServerApiClient; + private readonly IPlexSecretStore _plexSecretStore; public SynchronizePlexMediaSourcesHandler( IMediaSourceRepository mediaSourceRepository, IPlexTvApiClient plexTvApiClient, + IPlexServerApiClient plexServerApiClient, + IPlexSecretStore plexSecretStore, ChannelWriter channel, IEntityLocker entityLocker, ILogger logger) { _mediaSourceRepository = mediaSourceRepository; _plexTvApiClient = plexTvApiClient; + _plexServerApiClient = plexServerApiClient; + _plexSecretStore = plexSecretStore; _channel = channel; _entityLocker = entityLocker; _logger = logger; @@ -69,32 +76,76 @@ namespace ErsatzTV.Application.Plex.Commands return allExisting; } - private Task SynchronizeServer(List allExisting, PlexMediaSource server) + private async Task SynchronizeServer(List allExisting, PlexMediaSource server) { Option maybeExisting = allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier); - return maybeExisting.Match( - existing => - { - existing.Platform = server.Platform; - existing.PlatformVersion = server.PlatformVersion; - existing.ProductVersion = server.ProductVersion; - existing.ServerName = server.ServerName; - var toAdd = server.Connections - .Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList(); - var toRemove = existing.Connections - .Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList(); - return _mediaSourceRepository.Update(existing, server.Connections, toAdd, toRemove); - }, - async () => - { - if (server.Connections.Any()) - { - server.Connections.Head().IsActive = true; - } - await _mediaSourceRepository.Add(server); - }); + foreach (PlexMediaSource existing in maybeExisting) + { + existing.Platform = server.Platform; + existing.PlatformVersion = server.PlatformVersion; + existing.ProductVersion = server.ProductVersion; + existing.ServerName = server.ServerName; + var toAdd = server.Connections + .Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList(); + var toRemove = existing.Connections + .Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList(); + await _mediaSourceRepository.Update(existing, toAdd, toRemove); + await FindConnectionToActivate(existing); + } + + if (maybeExisting.IsNone) + { + await _mediaSourceRepository.Add(server); + await FindConnectionToActivate(server); + } + } + + private async Task FindConnectionToActivate(PlexMediaSource server) + { + var prioritized = server.Connections.OrderBy(pc => pc.IsActive ? 0 : 1).ToList(); + foreach (PlexConnection connection in server.Connections) + { + connection.IsActive = false; + } + + Option maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier); + foreach (PlexServerAuthToken token in maybeToken) + { + foreach (PlexConnection connection in prioritized) + { + try + { + _logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri); + if (await _plexServerApiClient.Ping(connection, token)) + { + _logger.LogInformation("Located Plex at {Uri}", connection.Uri); + connection.IsActive = true; + break; + } + } + catch + { + // do nothing + } + } + } + + if (maybeToken.IsNone) + { + _logger.LogError( + "Unable to activate Plex connection for server {Server} without auth token", + server.ServerName); + } + + if (server.Connections.All(c => !c.IsActive)) + { + _logger.LogError("Unable to locate Plex"); + server.Connections.Head().IsActive = true; + } + + await _mediaSourceRepository.Update(server, new List(), new List()); } } } diff --git a/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs b/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs index 4a6763eb2..aa13e19d2 100644 --- a/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs +++ b/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs @@ -9,6 +9,10 @@ namespace ErsatzTV.Core.Interfaces.Plex { public interface IPlexServerApiClient { + Task Ping( + PlexConnection connection, + PlexServerAuthToken token); + Task>> GetLibraries( PlexConnection connection, PlexServerAuthToken token); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs index 27a57f3b4..7a5bb4f95 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaSourceRepository.cs @@ -18,7 +18,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task Update( PlexMediaSource plexMediaSource, - List prioritizedConnections, List toAdd, List toDelete); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs index 82e980158..3d3226557 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs @@ -113,55 +113,30 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public async Task Update( PlexMediaSource plexMediaSource, - List sortedConnections, List toAdd, List toDelete) { - await _dbConnection.ExecuteAsync( - @"UPDATE PlexMediaSource SET - ProductVersion = @ProductVersion, - Platform = @Platform, - PlatformVersion = @PlatformVersion, - ServerName = @ServerName - WHERE Id = @Id", - new + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + dbContext.Entry(plexMediaSource).State = EntityState.Modified; + + if (toAdd.Any() || toDelete.Any()) + { + plexMediaSource.Connections.Clear(); + await dbContext.Entry(plexMediaSource).Collection(pms => pms.Connections).LoadAsync(); + + plexMediaSource.Connections.AddRange(toAdd); + plexMediaSource.Connections.RemoveAll(toDelete.Contains); + } + else + { + foreach (PlexConnection connection in plexMediaSource.Connections) { - plexMediaSource.ProductVersion, - plexMediaSource.Platform, - plexMediaSource.PlatformVersion, - plexMediaSource.ServerName, - plexMediaSource.Id - }); - - foreach (PlexConnection add in toAdd) - { - await _dbConnection.ExecuteAsync( - @"INSERT INTO PlexConnection (IsActive, Uri, PlexMediaSourceId) - VALUES (0, @Uri, @PlexMediaSourceId)", - new { add.Uri, PlexMediaSourceId = plexMediaSource.Id }); + dbContext.Entry(connection).State = EntityState.Modified; + } } - foreach (PlexConnection delete in toDelete) - { - await _dbConnection.ExecuteAsync( - @"DELETE FROM PlexConnection WHERE Id = @Id", - new { delete.Id }); - } - - int activeCount = await _dbConnection.QuerySingleAsync( - @"SELECT COUNT(*) FROM PlexConnection WHERE IsActive = 1 AND PlexMediaSourceId = @PlexMediaSourceId", - new { PlexMediaSourceId = plexMediaSource.Id }); - if (activeCount == 0) - { - Option toActivate = - sortedConnections.FirstOrDefault(c => toDelete.All(d => d.Id != c.Id)); - - // update on uri because connections from Plex API don't have our local ids - await toActivate.IfSomeAsync( - async c => await _dbConnection.ExecuteAsync( - @"UPDATE PlexConnection SET IsActive = 1 WHERE Uri = @Uri", - new { c.Uri })); - } + await dbContext.SaveChangesAsync(); } public async Task> UpdateLibraries( diff --git a/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs b/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs index d46e66e58..f79869a16 100644 --- a/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs +++ b/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs @@ -6,6 +6,12 @@ namespace ErsatzTV.Infrastructure.Plex { public interface IPlexServerApi { + [Get("/")] + [Headers("Accept: application/json")] + public Task Ping( + [Query] [AliasAs("X-Plex-Token")] + string token); + [Get("/library/sections")] [Headers("Accept: application/json")] public Task>> GetLibraries( diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index 515b8f7f9..8eb9ebae9 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -30,6 +30,28 @@ namespace ErsatzTV.Infrastructure.Plex _logger = logger; } + public async Task Ping( + PlexConnection connection, + PlexServerAuthToken token) + { + try + { + IPlexServerApi service = RestService.For( + new HttpClient + { + BaseAddress = new Uri(connection.Uri), + Timeout = TimeSpan.FromSeconds(5) + }); + + await service.Ping(token.AuthToken); + return true; + } + catch (Exception) + { + return false; + } + } + public async Task>> GetLibraries( PlexConnection connection, PlexServerAuthToken token)