Browse Source

proxy jellyfin and emby artwork for xmltv (#210)

* fix xmltv artwork for jf and emby

* proxy jellyfin and emby artwork for xmltv
pull/211/head
Jason Dove 5 years ago committed by GitHub
parent
commit
c9e20e28df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs
  2. 8
      ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParameters.cs
  3. 73
      ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs
  4. 4
      ErsatzTV.Application/Jellyfin/JellyfinConnectionParametersViewModel.cs
  5. 8
      ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParameters.cs
  6. 73
      ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParametersHandler.cs
  7. 6
      ErsatzTV.Application/MediaCards/Mapper.cs
  8. 2
      ErsatzTV.Application/Movies/Mapper.cs
  9. 2
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  10. 2
      ErsatzTV.Application/Television/Mapper.cs
  11. 5
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs
  12. 34
      ErsatzTV.Core/Emby/EmbyUrl.cs
  13. 32
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  14. 3
      ErsatzTV.Core/Jellyfin/JellyfinPathReplacementService.cs
  15. 34
      ErsatzTV.Core/Jellyfin/JellyfinUrl.cs
  16. 77
      ErsatzTV/Controllers/ArtworkController.cs

4
ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Emby
{
public record EmbyConnectionParametersViewModel(string Address);
}

8
ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParameters.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
}

73
ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
namespace ErsatzTV.Application.Emby.Queries
{
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
Either<BaseError, EmbyConnectionParametersViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache;
public GetEmbyConnectionParametersHandler(
IMemoryCache memoryCache,
IMediaSourceRepository mediaSourceRepository)
{
_memoryCache = memoryCache;
_mediaSourceRepository = mediaSourceRepository;
}
public async Task<Either<BaseError, EmbyConnectionParametersViewModel>> Handle(
GetEmbyConnectionParameters request,
CancellationToken cancellationToken)
{
if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters))
{
return parameters;
}
Either<BaseError, EmbyConnectionParametersViewModel> maybeParameters =
await Validate()
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address))
.Map(v => v.ToEither<EmbyConnectionParametersViewModel>());
return maybeParameters.Match(
p =>
{
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
return maybeParameters;
},
error => error);
}
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
EmbyMediaSourceMustExist()
.BindT(MediaSourceMustHaveActiveConnection);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
.Map(
v => v.ToValidation<BaseError>(
"Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.FirstOrDefault();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection);
}
}

4
ErsatzTV.Application/Jellyfin/JellyfinConnectionParametersViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Jellyfin
{
public record JellyfinConnectionParametersViewModel(string Address);
}

8
ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParameters.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Jellyfin.Queries
{
public record GetJellyfinConnectionParameters : IRequest<Either<BaseError, JellyfinConnectionParametersViewModel>>;
}

73
ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParametersHandler.cs

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
namespace ErsatzTV.Application.Jellyfin.Queries
{
public class GetJellyfinConnectionParametersHandler : IRequestHandler<GetJellyfinConnectionParameters,
Either<BaseError, JellyfinConnectionParametersViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache;
public GetJellyfinConnectionParametersHandler(
IMemoryCache memoryCache,
IMediaSourceRepository mediaSourceRepository)
{
_memoryCache = memoryCache;
_mediaSourceRepository = mediaSourceRepository;
}
public async Task<Either<BaseError, JellyfinConnectionParametersViewModel>> Handle(
GetJellyfinConnectionParameters request,
CancellationToken cancellationToken)
{
if (_memoryCache.TryGetValue(request, out JellyfinConnectionParametersViewModel parameters))
{
return parameters;
}
Either<BaseError, JellyfinConnectionParametersViewModel> maybeParameters =
await Validate()
.MapT(cp => new JellyfinConnectionParametersViewModel(cp.ActiveConnection.Address))
.Map(v => v.ToEither<JellyfinConnectionParametersViewModel>());
return maybeParameters.Match(
p =>
{
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
return maybeParameters;
},
error => error);
}
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
JellyfinMediaSourceMustExist()
.BindT(MediaSourceMustHaveActiveConnection);
private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist() =>
_mediaSourceRepository.GetAllJellyfin().Map(list => list.HeadOrNone())
.Map(
v => v.ToValidation<BaseError>(
"Jellyfin media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
JellyfinMediaSource jellyfinMediaSource)
{
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.FirstOrDefault();
return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection))
.ToValidation<BaseError>("Jellyfin media source requires an active connection");
}
private record ConnectionParameters(
JellyfinMediaSource JellyfinMediaSource,
JellyfinConnection ActiveConnection);
}
}

6
ErsatzTV.Application/MediaCards/Mapper.cs

@ -122,7 +122,7 @@ namespace ErsatzTV.Application.MediaCards @@ -122,7 +122,7 @@ namespace ErsatzTV.Application.MediaCards
else if (maybeEmby.IsSome && artwork.StartsWith("emby://"))
{
artwork = EmbyUrl.ForArtwork(maybeEmby, artwork)
.SetQueryParam("fillHeight", 440);
.SetQueryParam("maxHeight", 440);
}
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork);
@ -152,7 +152,7 @@ namespace ErsatzTV.Application.MediaCards @@ -152,7 +152,7 @@ namespace ErsatzTV.Application.MediaCards
else if (maybeEmby.IsSome && poster.StartsWith("emby://"))
{
poster = EmbyUrl.ForArtwork(maybeEmby, poster)
.SetQueryParam("fillHeight", 440);
.SetQueryParam("maxHeight", 440);
}
return poster;
@ -174,7 +174,7 @@ namespace ErsatzTV.Application.MediaCards @@ -174,7 +174,7 @@ namespace ErsatzTV.Application.MediaCards
else if (maybeEmby.IsSome && thumb.StartsWith("emby://"))
{
thumb = EmbyUrl.ForArtwork(maybeEmby, thumb)
.SetQueryParam("fillHeight", 220);
.SetQueryParam("maxHeight", 220);
}
return thumb;

2
ErsatzTV.Application/Movies/Mapper.cs

@ -76,7 +76,7 @@ namespace ErsatzTV.Application.Movies @@ -76,7 +76,7 @@ namespace ErsatzTV.Application.Movies
Url url = EmbyUrl.ForArtwork(maybeEmby, artwork);
if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail)
{
url.SetQueryParam("fillHeight", 440);
url.SetQueryParam("maxHeight", 440);
}
artwork = url;

2
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -19,9 +19,9 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -19,9 +19,9 @@ namespace ErsatzTV.Application.Streaming.Queries
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly ILocalFileSystem _localFileSystem;
private readonly IPlayoutRepository _playoutRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService;

2
ErsatzTV.Application/Television/Mapper.cs

@ -104,7 +104,7 @@ namespace ErsatzTV.Application.Television @@ -104,7 +104,7 @@ namespace ErsatzTV.Application.Television
Url url = EmbyUrl.ForArtwork(maybeEmby, artwork);
if (artworkKind == ArtworkKind.Poster)
{
url.SetQueryParam("fillHeight", 440);
url.SetQueryParam("maxHeight", 440);
}
artwork = url;

5
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs

@ -466,9 +466,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -466,9 +466,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy");
}
[Test]
public void Should_SetCorrectVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_And_Framerate_ForTransportStream()
public void
Should_SetCorrectVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_And_Framerate_ForTransportStream()
{
var ffmpegProfile = new FFmpegProfile
{

34
ErsatzTV.Core/Emby/EmbyUrl.cs

@ -21,11 +21,41 @@ namespace ErsatzTV.Core.Emby @@ -21,11 +21,41 @@ namespace ErsatzTV.Core.Emby
string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]);
Url x = Url.Parse(address)
return Url.Parse(address)
.AppendPathSegment(pathSegment)
.SetQueryParams(query);
}
public static Url ForArtwork(string address, string artwork)
{
string[] split = artwork.Replace("emby://", string.Empty).Split('?');
if (split.Length != 2)
{
return artwork;
}
string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]);
return x;
return Url.Parse(address)
.AppendPathSegment(pathSegment)
.SetQueryParams(query);
}
public static Url ProxyForArtwork(string scheme, string host, string artwork)
{
string[] split = artwork.Replace("emby://", string.Empty).Split('?');
if (split.Length != 2)
{
return artwork;
}
string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]);
return Url.Parse($"{scheme}://{host}/iptv/artwork/posters/emby")
.AppendPathSegment(pathSegment)
.SetQueryParams(query);
}
}
}

32
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -4,6 +4,8 @@ using System.Linq; @@ -4,6 +4,8 @@ using System.Linq;
using System.Text;
using System.Xml;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin;
using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using static LanguageExt.Prelude;
@ -108,9 +110,7 @@ namespace ErsatzTV.Core.Iptv @@ -108,9 +110,7 @@ namespace ErsatzTV.Core.Iptv
string poster = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Poster)
.HeadOrNone()
.Match(
artwork => $"{_scheme}://{_host}/iptv/artwork/posters/{artwork.Path}",
() => string.Empty);
.Match(GetPoster, () => string.Empty);
if (!string.IsNullOrWhiteSpace(poster))
{
@ -147,9 +147,7 @@ namespace ErsatzTV.Core.Iptv @@ -147,9 +147,7 @@ namespace ErsatzTV.Core.Iptv
string poster = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Poster)
.HeadOrNone()
.Match(
artwork => $"{_scheme}://{_host}/iptv/artwork/posters/{artwork.Path}",
() => string.Empty);
.Match(GetPoster, () => string.Empty);
if (!string.IsNullOrWhiteSpace(poster))
{
@ -208,6 +206,28 @@ namespace ErsatzTV.Core.Iptv @@ -208,6 +206,28 @@ namespace ErsatzTV.Core.Iptv
return Encoding.UTF8.GetString(ms.ToArray());
}
private string GetPoster(Artwork artwork)
{
string poster = artwork.Path;
if (poster.StartsWith("jellyfin://"))
{
poster = JellyfinUrl.ProxyForArtwork(_scheme, _host, poster)
.SetQueryParam("fillHeight", 440);
}
else if (poster.StartsWith("emby://"))
{
poster = EmbyUrl.ProxyForArtwork(_scheme, _host, poster)
.SetQueryParam("maxHeight", 440);
}
else
{
poster = $"{_scheme}://{_host}/iptv/artwork/posters/{artwork.Path}";
}
return poster;
}
private static string GetTitle(PlayoutItem playoutItem)
{
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))

3
ErsatzTV.Core/Jellyfin/JellyfinPathReplacementService.cs

@ -56,7 +56,8 @@ namespace ErsatzTV.Core.Jellyfin @@ -56,7 +56,8 @@ namespace ErsatzTV.Core.Jellyfin
replacement =>
{
string finalPath = path.Replace(replacement.JellyfinPath, replacement.LocalPath);
if (IsWindows(replacement.JellyfinMediaSource, path) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
if (IsWindows(replacement.JellyfinMediaSource, path) &&
!_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
{
finalPath = finalPath.Replace(@"\", @"/");
}

34
ErsatzTV.Core/Jellyfin/JellyfinUrl.cs

@ -21,11 +21,41 @@ namespace ErsatzTV.Core.Jellyfin @@ -21,11 +21,41 @@ namespace ErsatzTV.Core.Jellyfin
string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]);
Url x = Url.Parse(address)
return Url.Parse(address)
.AppendPathSegment(pathSegment)
.SetQueryParams(query);
}
public static Url ForArtwork(string address, string artwork)
{
string[] split = artwork.Replace("jellyfin://", string.Empty).Split('?');
if (split.Length != 2)
{
return artwork;
}
string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]);
return x;
return Url.Parse(address)
.AppendPathSegment(pathSegment)
.SetQueryParams(query);
}
public static Url ProxyForArtwork(string scheme, string host, string artwork)
{
string[] split = artwork.Replace("jellyfin://", string.Empty).Split('?');
if (split.Length != 2)
{
return artwork;
}
string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]);
return Url.Parse($"{scheme}://{host}/iptv/artwork/posters/jellyfin")
.AppendPathSegment(pathSegment)
.SetQueryParams(query);
}
}
}

77
ErsatzTV/Controllers/ArtworkController.cs

@ -2,12 +2,19 @@ @@ -2,12 +2,19 @@
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Emby.Queries;
using ErsatzTV.Application.Images;
using ErsatzTV.Application.Images.Queries;
using ErsatzTV.Application.Jellyfin;
using ErsatzTV.Application.Jellyfin.Queries;
using ErsatzTV.Application.Plex;
using ErsatzTV.Application.Plex.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin;
using Flurl;
using LanguageExt;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@ -49,6 +56,30 @@ namespace ErsatzTV.Controllers @@ -49,6 +56,30 @@ namespace ErsatzTV.Controllers
Right: r => new FileContentResult(r.Contents, r.MimeType));
}
[HttpGet("/iptv/artwork/posters/jellyfin/{*path}")]
[HttpGet("/artwork/posters/jellyfin/{*path}")]
public Task<IActionResult> GetJellyfinPoster(string path)
{
if (Request.QueryString.HasValue)
{
path += Request.QueryString.Value;
}
return GetJellyfinArtwork(path);
}
[HttpGet("/iptv/artwork/posters/emby/{*path}")]
[HttpGet("/artwork/posters/emby/{*path}")]
public Task<IActionResult> GetEmbyPoster(string path)
{
if (Request.QueryString.HasValue)
{
path += Request.QueryString.Value;
}
return GetEmbyArtwork(path);
}
[HttpGet("/iptv/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
[HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
public Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path) =>
@ -101,5 +132,51 @@ namespace ErsatzTV.Controllers @@ -101,5 +132,51 @@ namespace ErsatzTV.Controllers
response.Content.Headers.ContentType?.MediaType ?? "image/jpeg");
});
}
private async Task<IActionResult> GetJellyfinArtwork(string path)
{
Either<BaseError, JellyfinConnectionParametersViewModel> connectionParameters =
await _mediator.Send(new GetJellyfinConnectionParameters());
return await connectionParameters.Match<Task<IActionResult>>(
Left: _ => new NotFoundResult().AsTask<IActionResult>(),
Right: async vm =>
{
HttpClient client = _httpClientFactory.CreateClient();
Url fullPath = JellyfinUrl.ForArtwork(vm.Address, path);
HttpResponseMessage response = await client.GetAsync(
fullPath,
HttpCompletionOption.ResponseHeadersRead);
Stream stream = await response.Content.ReadAsStreamAsync();
return new FileStreamResult(
stream,
response.Content.Headers.ContentType?.MediaType ?? "image/jpeg");
});
}
private async Task<IActionResult> GetEmbyArtwork(string path)
{
Either<BaseError, EmbyConnectionParametersViewModel> connectionParameters =
await _mediator.Send(new GetEmbyConnectionParameters());
return await connectionParameters.Match<Task<IActionResult>>(
Left: _ => new NotFoundResult().AsTask<IActionResult>(),
Right: async vm =>
{
HttpClient client = _httpClientFactory.CreateClient();
Url fullPath = EmbyUrl.ForArtwork(vm.Address, path);
HttpResponseMessage response = await client.GetAsync(
fullPath,
HttpCompletionOption.ResponseHeadersRead);
Stream stream = await response.Content.ReadAsStreamAsync();
return new FileStreamResult(
stream,
response.Content.Headers.ContentType?.MediaType ?? "image/jpeg");
});
}
}
}

Loading…
Cancel
Save