Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9905d0542 | ||
|
|
c9e20e28df | ||
|
|
f9427cac99 | ||
|
|
141a34933d | ||
|
|
0962a1429a | ||
|
|
f8b45ed9db |
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+11
-1
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
services:
|
||||
ersatztv:
|
||||
privileged: true
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/nvidia/Dockerfile
|
||||
|
||||
Reference in New Issue
Block a user