Compare commits

...
Author SHA1 Message Date
Jason DoveandGitHub c9905d0542 fix resources (offline background and font) (#211) 2021-05-25 15:42:03 -05:00
Jason DoveandGitHub c9e20e28df proxy jellyfin and emby artwork for xmltv (#210)
* fix xmltv artwork for jf and emby

* proxy jellyfin and emby artwork for xmltv
2021-05-25 15:15:15 -05:00
Jason DoveandGitHub f9427cac99 use multiple docker tags again (#209)
* Revert "disable framerate normalization (#208)"

This reverts commit 141a34933d.

* Revert "use linuxserver base docker image (#207)"

This reverts commit 0962a1429a.

* fix playback that only uses fps filter

* nvidia needs privileged
2021-05-25 05:13:51 -05:00
Jason DoveandGitHub 141a34933d disable framerate normalization (#208)
* disable framerate normalization

* fix test
2021-05-24 21:47:26 -05:00
Jason DoveandGitHub 0962a1429a use linuxserver base docker image (#207)
* use one base docker image

* remove nvidia and vaapi tags

* fix playback that only uses fps filter
2021-05-24 21:12:55 -05:00
Jason DoveandGitHub f8b45ed9db fix unc path replacements from jellyfin and emby (#205)
* fix UNC path replacements from non-windows JF and Emby servers

* use emby path replacements for playback
2021-05-24 09:03:39 -05:00
25 changed files with 458 additions and 38 deletions
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Emby
{
public record EmbyConnectionParametersViewModel(string Address);
}
@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
}
@@ -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);
}
}
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Jellyfin
{
public record JellyfinConnectionParametersViewModel(string Address);
}
@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Jellyfin.Queries
{
public record GetJellyfinConnectionParameters : IRequest<Either<BaseError, JellyfinConnectionParametersViewModel>>;
}
@@ -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);
}
}
+3 -3
View File
@@ -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
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
else if (maybeEmby.IsSome && thumb.StartsWith("emby://"))
{
thumb = EmbyUrl.ForArtwork(maybeEmby, thumb)
.SetQueryParam("fillHeight", 220);
.SetQueryParam("maxHeight", 220);
}
return thumb;
+1 -1
View File
@@ -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;
@@ -5,6 +5,7 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
@@ -18,6 +19,7 @@ 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 ILocalFileSystem _localFileSystem;
@@ -31,7 +33,8 @@ namespace ErsatzTV.Application.Streaming.Queries
FFmpegProcessService ffmpegProcessService,
ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService)
IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService)
: base(channelRepository, configElementRepository)
{
_configElementRepository = configElementRepository;
@@ -40,6 +43,7 @@ namespace ErsatzTV.Application.Streaming.Queries
_localFileSystem = localFileSystem;
_plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService;
_embyPathReplacementService = embyPathReplacementService;
}
protected override async Task<Either<BaseError, Process>> GetProcess(
@@ -178,6 +182,12 @@ namespace ErsatzTV.Application.Streaming.Queries
JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath(
jellyfinEpisode.LibraryPathId,
path),
EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath(
embyMovie.LibraryPathId,
path),
EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath(
embyEpisode.LibraryPathId,
path),
_ => path
};
}
+1 -1
View File
@@ -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;
@@ -467,6 +467,36 @@ namespace ErsatzTV.Core.Tests.FFmpeg
actual.VideoCodec.Should().Be("copy");
}
[Test]
public void
Should_SetCorrectVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_And_Framerate_ForTransportStream()
{
var ffmpegProfile = new FFmpegProfile
{
NormalizeVideo = true,
Resolution = new Resolution { Width = 1920, Height = 1080 },
VideoCodec = "libx264",
FrameRate = "24"
};
// not anamorphic
var version = new MediaVersion
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
ffmpegProfile,
version,
new MediaStream { Codec = "libx264" },
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now);
actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse();
actual.VideoCodec.Should().Be("libx264");
}
[Test]
public void
Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream()
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@@ -44,7 +45,7 @@ namespace ErsatzTV.Core.Emby
.SingleOrDefault(
r =>
{
string separatorChar = IsWindows(r.EmbyMediaSource) ? @"\" : @"/";
string separatorChar = IsWindows(r.EmbyMediaSource, path) ? @"\" : @"/";
string prefix = r.EmbyPath.EndsWith(separatorChar)
? r.EmbyPath
: r.EmbyPath + separatorChar;
@@ -55,11 +56,11 @@ namespace ErsatzTV.Core.Emby
replacement =>
{
string finalPath = path.Replace(replacement.EmbyPath, replacement.LocalPath);
if (IsWindows(replacement.EmbyMediaSource) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
if (IsWindows(replacement.EmbyMediaSource, path) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
{
finalPath = finalPath.Replace(@"\", @"/");
}
else if (!IsWindows(replacement.EmbyMediaSource) &&
else if (!IsWindows(replacement.EmbyMediaSource, path) &&
_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
{
finalPath = finalPath.Replace(@"/", @"\");
@@ -79,7 +80,10 @@ namespace ErsatzTV.Core.Emby
() => path);
}
private static bool IsWindows(EmbyMediaSource embyMediaSource) =>
embyMediaSource.OperatingSystem.ToLowerInvariant().StartsWith("windows");
private static bool IsWindows(EmbyMediaSource embyMediaSource, string path)
{
bool isUnc = Uri.TryCreate(path, UriKind.Absolute, out Uri uri) && uri.IsUnc;
return isUnc || embyMediaSource.OperatingSystem.ToLowerInvariant().StartsWith("windows");
}
}
}
+32 -2
View File
@@ -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);
}
return x;
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 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);
}
}
}
@@ -99,7 +99,7 @@ namespace ErsatzTV.Core.FFmpeg
}
if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
NeedToNormalizeVideoCodec(ffmpegProfile, videoStream))
NeedToNormalizeVideoCodec(ffmpegProfile, videoStream) || result.FrameRate.IsSome)
{
result.VideoCodec = ffmpegProfile.VideoCodec;
result.VideoBitrate = ffmpegProfile.VideoBitrate;
+3 -2
View File
@@ -225,7 +225,8 @@ namespace ErsatzTV.Core.FFmpeg
public FFmpegProcessBuilder WithErrorText(IDisplaySize desiredResolution, string text)
{
const string FONT_FILE = "fontfile=Resources/Roboto-Regular.ttf";
string fontPath = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "Roboto-Regular.ttf");
var fontFile = $"fontfile={fontPath}";
const string FONT_COLOR = "fontcolor=white";
const string X = "x=(w-text_w)/2";
const string Y = "y=(h-text_h)/3*2";
@@ -233,7 +234,7 @@ namespace ErsatzTV.Core.FFmpeg
string fontSize = text.Length > 80 ? "fontsize=30" : text.Length > 60 ? "fontsize=40" : "fontsize=60";
return WithFilterComplex(
$"[0:0]scale={desiredResolution.Width}:{desiredResolution.Height},drawtext={FONT_FILE}:{fontSize}:{FONT_COLOR}:{X}:{Y}:text='{text}'[v]",
$"[0:0]scale={desiredResolution.Width}:{desiredResolution.Height},drawtext={fontFile}:{fontSize}:{FONT_COLOR}:{X}:{Y}:text='{text}'[v]",
"[v]",
"1:a");
}
+2 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
@@ -110,7 +111,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
.WithLoopedImage("Resources/background.png")
.WithLoopedImage(Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png"))
.WithLibavfilter()
.WithInput("anullsrc")
.WithErrorText(desiredResolution, errorMessage)
+1
View File
@@ -16,6 +16,7 @@ namespace ErsatzTV.Core
public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3");
public static readonly string LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
public static readonly string ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources");
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
public static readonly string JellyfinSecretsPath = Path.Combine(AppDataFolder, "jellyfin-secrets.json");
+26 -6
View File
@@ -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
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
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
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))
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@@ -44,7 +45,7 @@ namespace ErsatzTV.Core.Jellyfin
.SingleOrDefault(
r =>
{
string separatorChar = IsWindows(r.JellyfinMediaSource) ? @"\" : @"/";
string separatorChar = IsWindows(r.JellyfinMediaSource, path) ? @"\" : @"/";
string prefix = r.JellyfinPath.EndsWith(separatorChar)
? r.JellyfinPath
: r.JellyfinPath + separatorChar;
@@ -55,11 +56,12 @@ namespace ErsatzTV.Core.Jellyfin
replacement =>
{
string finalPath = path.Replace(replacement.JellyfinPath, replacement.LocalPath);
if (IsWindows(replacement.JellyfinMediaSource) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
if (IsWindows(replacement.JellyfinMediaSource, path) &&
!_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
{
finalPath = finalPath.Replace(@"\", @"/");
}
else if (!IsWindows(replacement.JellyfinMediaSource) &&
else if (!IsWindows(replacement.JellyfinMediaSource, path) &&
_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
{
finalPath = finalPath.Replace(@"/", @"\");
@@ -79,7 +81,10 @@ namespace ErsatzTV.Core.Jellyfin
() => path);
}
private static bool IsWindows(JellyfinMediaSource jellyfinMediaSource) =>
jellyfinMediaSource.OperatingSystem.ToLowerInvariant().StartsWith("windows");
private static bool IsWindows(JellyfinMediaSource jellyfinMediaSource, string path)
{
bool isUnc = Uri.TryCreate(path, UriKind.Absolute, out Uri uri) && uri.IsUnc;
return isUnc || jellyfinMediaSource.OperatingSystem.ToLowerInvariant().StartsWith("windows");
}
}
}
+32 -2
View File
@@ -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);
}
return x;
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 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
View File
@@ -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
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
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");
});
}
}
}
+3 -6
View File
@@ -51,12 +51,9 @@
</ItemGroup>
<ItemGroup>
<None Update="Resources\background.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Resources\Roboto-Regular.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<EmbeddedResource Include="Resources\background.png" />
<EmbeddedResource Include="Resources\ErsatzTV.png" />
<EmbeddedResource Include="Resources\Roboto-Regular.ttf" />
</ItemGroup>
</Project>
@@ -0,0 +1,42 @@
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using Microsoft.Extensions.Hosting;
namespace ErsatzTV.Services.RunOnce
{
public class ResourceExtractorService : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!Directory.Exists(FileSystemLayout.ResourcesCacheFolder))
{
Directory.CreateDirectory(FileSystemLayout.ResourcesCacheFolder);
}
Assembly assembly = typeof(ResourceExtractorService).GetTypeInfo().Assembly;
await ExtractResource(assembly, "background.png", cancellationToken);
await ExtractResource(assembly, "ErsatzTV.png", cancellationToken);
await ExtractResource(assembly, "Roboto-Regular.ttf", cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private async Task ExtractResource(Assembly assembly, string name, CancellationToken cancellationToken)
{
await using Stream resource = assembly.GetManifestResourceStream($"ErsatzTV.Resources.{name}");
if (resource != null)
{
await using FileStream fs = File.Create(
Path.Combine(FileSystemLayout.ResourcesCacheFolder, name));
await resource.CopyToAsync(fs, cancellationToken);
}
}
}
}
+1
View File
@@ -255,6 +255,7 @@ namespace ErsatzTV
services.AddHostedService<EndpointValidatorService>();
services.AddHostedService<DatabaseMigratorService>();
services.AddHostedService<CacheCleanerService>();
services.AddHostedService<ResourceExtractorService>();
services.AddHostedService<EmbyService>();
services.AddHostedService<JellyfinService>();
services.AddHostedService<PlexService>();
+1
View File
@@ -2,6 +2,7 @@
services:
ersatztv:
privileged: true
build:
context: ..
dockerfile: docker/nvidia/Dockerfile