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 @@
namespace ErsatzTV.Application.Emby
{
public record EmbyConnectionParametersViewModel(string Address);
}

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

@ -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 @@
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 @@
namespace ErsatzTV.Application.Jellyfin
{
public record JellyfinConnectionParametersViewModel(string Address);
}

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

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

2
ErsatzTV.Application/Movies/Mapper.cs

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

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

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

2
ErsatzTV.Application/Television/Mapper.cs

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

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

@ -466,9 +466,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("copy"); actual.VideoCodec.Should().Be("copy");
} }
[Test] [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 var ffmpegProfile = new FFmpegProfile
{ {

34
ErsatzTV.Core/Emby/EmbyUrl.cs

@ -21,11 +21,41 @@ namespace ErsatzTV.Core.Emby
string pathSegment = split[0]; string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]); QueryParamCollection query = Url.ParseQueryParams(split[1]);
Url x = Url.Parse(address) return Url.Parse(address)
.AppendPathSegment(pathSegment) .AppendPathSegment(pathSegment)
.SetQueryParams(query); .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;
using System.Text; using System.Text;
using System.Xml; using System.Xml;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin;
using LanguageExt; using LanguageExt;
using LanguageExt.UnsafeValueAccess; using LanguageExt.UnsafeValueAccess;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
@ -108,9 +110,7 @@ namespace ErsatzTV.Core.Iptv
string poster = Optional(metadata.Artwork).Flatten() string poster = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Poster) .Filter(a => a.ArtworkKind == ArtworkKind.Poster)
.HeadOrNone() .HeadOrNone()
.Match( .Match(GetPoster, () => string.Empty);
artwork => $"{_scheme}://{_host}/iptv/artwork/posters/{artwork.Path}",
() => string.Empty);
if (!string.IsNullOrWhiteSpace(poster)) if (!string.IsNullOrWhiteSpace(poster))
{ {
@ -147,9 +147,7 @@ namespace ErsatzTV.Core.Iptv
string poster = Optional(metadata.Artwork).Flatten() string poster = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Poster) .Filter(a => a.ArtworkKind == ArtworkKind.Poster)
.HeadOrNone() .HeadOrNone()
.Match( .Match(GetPoster, () => string.Empty);
artwork => $"{_scheme}://{_host}/iptv/artwork/posters/{artwork.Path}",
() => string.Empty);
if (!string.IsNullOrWhiteSpace(poster)) if (!string.IsNullOrWhiteSpace(poster))
{ {
@ -208,6 +206,28 @@ namespace ErsatzTV.Core.Iptv
return Encoding.UTF8.GetString(ms.ToArray()); 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) private static string GetTitle(PlayoutItem playoutItem)
{ {
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle)) if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))

3
ErsatzTV.Core/Jellyfin/JellyfinPathReplacementService.cs

@ -56,7 +56,8 @@ namespace ErsatzTV.Core.Jellyfin
replacement => replacement =>
{ {
string finalPath = path.Replace(replacement.JellyfinPath, replacement.LocalPath); 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(@"\", @"/"); finalPath = finalPath.Replace(@"\", @"/");
} }

34
ErsatzTV.Core/Jellyfin/JellyfinUrl.cs

@ -21,11 +21,41 @@ namespace ErsatzTV.Core.Jellyfin
string pathSegment = split[0]; string pathSegment = split[0];
QueryParamCollection query = Url.ParseQueryParams(split[1]); QueryParamCollection query = Url.ParseQueryParams(split[1]);
Url x = Url.Parse(address) return Url.Parse(address)
.AppendPathSegment(pathSegment) .AppendPathSegment(pathSegment)
.SetQueryParams(query); .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 @@
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Emby;
using ErsatzTV.Application.Emby.Queries;
using ErsatzTV.Application.Images; using ErsatzTV.Application.Images;
using ErsatzTV.Application.Images.Queries; using ErsatzTV.Application.Images.Queries;
using ErsatzTV.Application.Jellyfin;
using ErsatzTV.Application.Jellyfin.Queries;
using ErsatzTV.Application.Plex; using ErsatzTV.Application.Plex;
using ErsatzTV.Application.Plex.Queries; using ErsatzTV.Application.Plex.Queries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin;
using Flurl;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -49,6 +56,30 @@ namespace ErsatzTV.Controllers
Right: r => new FileContentResult(r.Contents, r.MimeType)); 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("/iptv/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
[HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")] [HttpGet("/artwork/posters/plex/{plexMediaSourceId}/{*path}")]
public Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path) => public Task<IActionResult> GetPlexPoster(int plexMediaSourceId, string path) =>
@ -101,5 +132,51 @@ namespace ErsatzTV.Controllers
response.Content.Headers.ContentType?.MediaType ?? "image/jpeg"); 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