From 8a3e13f4cef1281a15e43a6b661e4c12d8f15f30 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 28 Feb 2021 16:38:34 -0600 Subject: [PATCH] proxy plex movie thumbnails (posters) --- .../Queries/GetMovieCardsHandler.cs | 4 +- .../Plex/Queries/GetPlexImage.cs | 9 ++ .../Plex/Queries/GetPlexImageHandler.cs | 107 ++++++++++++++++++ .../Plex/PlexServerApiClient.cs | 22 +++- ErsatzTV/Controllers/ArtworkController.cs | 11 ++ 5 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 ErsatzTV.Application/Plex/Queries/GetPlexImage.cs create mode 100644 ErsatzTV.Application/Plex/Queries/GetPlexImageHandler.cs diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs index 1a55618ce..eed22c7cf 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs @@ -16,9 +16,7 @@ namespace ErsatzTV.Application.MediaCards.Queries public GetMovieCardsHandler(IMovieRepository movieRepository) => _movieRepository = movieRepository; - public async Task Handle( - GetMovieCards request, - CancellationToken cancellationToken) + public async Task Handle(GetMovieCards request, CancellationToken cancellationToken) { int count = await _movieRepository.GetMovieCount(); diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexImage.cs b/ErsatzTV.Application/Plex/Queries/GetPlexImage.cs new file mode 100644 index 000000000..26ad8f14a --- /dev/null +++ b/ErsatzTV.Application/Plex/Queries/GetPlexImage.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Application.Images; +using ErsatzTV.Core; +using LanguageExt; +using MediatR; + +namespace ErsatzTV.Application.Plex.Queries +{ + public record GetPlexImage(int PlexMediaSourceId, string Path) : IRequest>; +} diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexImageHandler.cs b/ErsatzTV.Application/Plex/Queries/GetPlexImageHandler.cs new file mode 100644 index 000000000..d162ef1e2 --- /dev/null +++ b/ErsatzTV.Application/Plex/Queries/GetPlexImageHandler.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Application.Images; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Plex; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Plex; +using LanguageExt; +using MediatR; +using Microsoft.Extensions.Caching.Memory; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Application.Plex.Queries +{ + public class GetPlexImageHandler : IRequestHandler> + { + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMemoryCache _memoryCache; + private readonly IPlexSecretStore _plexSecretStore; + + public GetPlexImageHandler( + IMemoryCache memoryCache, + IMediaSourceRepository mediaSourceRepository, + IPlexSecretStore plexSecretStore) + { + _memoryCache = memoryCache; + _mediaSourceRepository = mediaSourceRepository; + _plexSecretStore = plexSecretStore; + } + + public async Task> + Handle(GetPlexImage request, CancellationToken cancellationToken) + { + if (!_memoryCache.TryGetValue(request, out ConnectionParameters parameters)) + { + Either maybeParameters = + await Validate(request).Map(v => v.ToEither()); + return await maybeParameters.Match( + p => + { + _memoryCache.Set(request, p, TimeSpan.FromHours(1)); + return RequestImageFromPlex(request, p); + }, + error => Left(error).AsTask()); + } + + return await RequestImageFromPlex(request, parameters); + } + + private async Task> RequestImageFromPlex( + GetPlexImage request, + ConnectionParameters connectionParameters) + { + var url = $"{connectionParameters.ActiveConnection.Uri}/{request.Path}"; + + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Plex-Token", connectionParameters.PlexServerAuthToken.AuthToken); + HttpResponseMessage response = await client.GetAsync(url); + if (response.IsSuccessStatusCode) + { + return new ImageViewModel( + await response.Content.ReadAsByteArrayAsync(), + response.Content.Headers.ContentType?.MediaType ?? "image/jpeg"); + } + + return BaseError.New($"Unsuccessful response from plex image request: {response.StatusCode}"); + } + + private Task> Validate(GetPlexImage request) => + PlexMediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveToken); + + private Task> PlexMediaSourceMustExist(GetPlexImage request) => + _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) + .Map( + v => v.ToValidation( + $"Plex media source {request.PlexMediaSourceId} 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 record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) + { + public PlexServerAuthToken PlexServerAuthToken { get; set; } + } + } +} diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index 058063743..23e6be302 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -46,7 +46,7 @@ namespace ErsatzTV.Infrastructure.Plex IPlexServerApi service = RestService.For(connection.Uri); return await service.GetLibrarySectionContents(library.Key, token.AuthToken) .Map(r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)) - .Map(list => list.Map(ProjectToMovie).ToList()); + .Map(list => list.Map(metadata => ProjectToMovie(metadata, library.MediaSourceId)).ToList()); } catch (Exception ex) { @@ -119,10 +119,11 @@ namespace ErsatzTV.Infrastructure.Plex Media = response.Media.Map(Project).ToList() }; - private static PlexMovie ProjectToMovie(PlexMetadataResponse response) + private static PlexMovie ProjectToMovie(PlexMetadataResponse response, int mediaSourceId) { PlexMediaResponse media = response.Media.Head(); PlexPartResponse part = media.Part.Head(); + DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; var metadata = new MovieMetadata @@ -132,11 +133,24 @@ namespace ErsatzTV.Infrastructure.Plex ReleaseDate = DateTime.Parse(response.OriginallyAvailableAt), Year = response.Year, Tagline = response.Tagline, - DateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime, + DateAdded = dateAdded, DateUpdated = lastWriteTime }; - // TODO: artwork + if (!string.IsNullOrWhiteSpace(response.Thumb)) + { + var path = $"plex/{mediaSourceId}{response.Thumb}"; + var artwork = new Artwork + { + ArtworkKind = ArtworkKind.Poster, + Path = path, + DateAdded = dateAdded, + DateUpdated = lastWriteTime + }; + + metadata.Artwork ??= new List(); + metadata.Artwork.Add(artwork); + } var version = new MediaVersion { diff --git a/ErsatzTV/Controllers/ArtworkController.cs b/ErsatzTV/Controllers/ArtworkController.cs index 66014ecd4..5b482812f 100644 --- a/ErsatzTV/Controllers/ArtworkController.cs +++ b/ErsatzTV/Controllers/ArtworkController.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using ErsatzTV.Application.Images; using ErsatzTV.Application.Images.Queries; +using ErsatzTV.Application.Plex.Queries; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using LanguageExt; @@ -27,6 +28,16 @@ namespace ErsatzTV.Controllers Right: r => new FileContentResult(r.Contents, r.MimeType)); } + [HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")] + public async Task GetPlexPoster(int plexMediaSourceId, string path) + { + Either imageContents = + await _mediator.Send(new GetPlexImage(plexMediaSourceId, path)); + return imageContents.Match( + Left: _ => new NotFoundResult(), + Right: r => new FileContentResult(r.Contents, r.MimeType)); + } + [HttpGet("/artwork/thumbnails/{fileName}")] public async Task GetThumbnail(string fileName) {