Browse Source

cache local artwork on disk (#217)

pull/219/head
Jason Dove 5 years ago committed by GitHub
parent
commit
6504ca10a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      ErsatzTV.Application/Images/CachedImagePathViewModel.cs
  2. 5
      ErsatzTV.Application/Images/ImageViewModel.cs
  3. 5
      ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs
  4. 72
      ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs
  5. 69
      ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs
  6. 1
      ErsatzTV.Core/Interfaces/Images/IImageCache.cs
  7. 22
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  8. 24
      ErsatzTV/Controllers/ArtworkController.cs
  9. 8
      ErsatzTV/Controllers/IptvController.cs

4
ErsatzTV.Application/Images/CachedImagePathViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Images
{
public record CachedImagePathViewModel(string FileName, string MimeType);
}

5
ErsatzTV.Application/Images/ImageViewModel.cs

@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
namespace ErsatzTV.Application.Images
{
// ReSharper disable once SuggestBaseTypeForParameter
public record ImageViewModel(byte[] Contents, string MimeType);
}

5
ErsatzTV.Application/Images/Queries/GetImageContents.cs → ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs

@ -5,6 +5,7 @@ using MediatR; @@ -5,6 +5,7 @@ using MediatR;
namespace ErsatzTV.Application.Images.Queries
{
public record GetImageContents
(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<Either<BaseError, ImageViewModel>>;
public record GetCachedImagePath
(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<
Either<BaseError, CachedImagePathViewModel>>;
}

72
ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
using MediatR;
using Winista.Mime;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Images.Queries
{
public class
GetCachedImagePathHandler : IRequestHandler<GetCachedImagePath, Either<BaseError, CachedImagePathViewModel>>
{
private static readonly MimeTypes MimeTypes = new();
private readonly IImageCache _imageCache;
public GetCachedImagePathHandler(IImageCache imageCache) => _imageCache = imageCache;
public async Task<Either<BaseError, CachedImagePathViewModel>> Handle(
GetCachedImagePath request,
CancellationToken cancellationToken)
{
try
{
MimeType mimeType;
string cachePath = _imageCache.GetPathForImage(
request.FileName,
request.ArtworkKind,
Optional(request.MaxHeight));
if (!File.Exists(cachePath))
{
if (request.MaxHeight.HasValue)
{
string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None);
byte[] contents = await File.ReadAllBytesAsync(originalPath, cancellationToken);
Either<BaseError, byte[]> resizeResult =
await _imageCache.ResizeImage(contents, request.MaxHeight.Value);
resizeResult.IfRight(result => contents = result);
string baseFolder = Path.GetDirectoryName(cachePath);
if (baseFolder != null && !Directory.Exists(baseFolder))
{
Directory.CreateDirectory(baseFolder);
}
await File.WriteAllBytesAsync(cachePath, contents, cancellationToken);
mimeType = new MimeType("image/jpeg");
}
else
{
return BaseError.New($"Artwork does not exist on disk at {cachePath}");
}
}
else
{
mimeType = MimeTypes.GetMimeTypeFromFile(cachePath);
}
return new CachedImagePathViewModel(cachePath, mimeType.Name);
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}

69
ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs

@ -1,69 +0,0 @@ @@ -1,69 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
using Winista.Mime;
namespace ErsatzTV.Application.Images.Queries
{
public class GetImageContentsHandler : IRequestHandler<GetImageContents, Either<BaseError, ImageViewModel>>
{
private static readonly MimeTypes MimeTypes = new();
private readonly IImageCache _imageCache;
private readonly IMemoryCache _memoryCache;
public GetImageContentsHandler(IImageCache imageCache, IMemoryCache memoryCache)
{
_imageCache = imageCache;
_memoryCache = memoryCache;
}
public async Task<Either<BaseError, ImageViewModel>> Handle(
GetImageContents request,
CancellationToken cancellationToken)
{
try
{
return await _memoryCache.GetOrCreateAsync(
request.FileName,
async entry =>
{
entry.SlidingExpiration = TimeSpan.FromHours(1);
string subfolder = request.FileName.Substring(0, 2);
string baseFolder = request.ArtworkKind switch
{
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder
};
string fileName = Path.Combine(baseFolder, request.FileName);
byte[] contents = await File.ReadAllBytesAsync(fileName, cancellationToken);
if (request.MaxHeight.HasValue)
{
Either<BaseError, byte[]> resizeResult = await _imageCache
.ResizeImage(contents, request.MaxHeight.Value);
resizeResult.IfRight(result => contents = result);
}
MimeType mimeType = MimeTypes.GetMimeType(contents);
return new ImageViewModel(contents, mimeType.Name);
});
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}

1
ErsatzTV.Core/Interfaces/Images/IImageCache.cs

@ -9,5 +9,6 @@ namespace ErsatzTV.Core.Interfaces.Images @@ -9,5 +9,6 @@ namespace ErsatzTV.Core.Interfaces.Images
Task<Either<BaseError, byte[]>> ResizeImage(byte[] imageBuffer, int height);
Task<Either<BaseError, string>> SaveArtworkToCache(byte[] imageBuffer, ArtworkKind artworkKind);
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight);
}
}

22
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -50,7 +50,7 @@ namespace ErsatzTV.Infrastructure.Images @@ -50,7 +50,7 @@ namespace ErsatzTV.Infrastructure.Images
{
byte[] hash = Crypto.ComputeHash(imageBuffer);
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
string subfolder = hex.Substring(0, 2);
string subfolder = hex[..2];
string baseFolder = artworkKind switch
{
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
@ -82,7 +82,7 @@ namespace ErsatzTV.Infrastructure.Images @@ -82,7 +82,7 @@ namespace ErsatzTV.Infrastructure.Images
var filenameKey = $"{path}:{_localFileSystem.GetLastWriteTime(path).ToFileTimeUtc()}";
byte[] hash = Crypto.ComputeHash(Encoding.UTF8.GetBytes(filenameKey));
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
string subfolder = hex.Substring(0, 2);
string subfolder = hex[..2];
string baseFolder = artworkKind switch
{
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
@ -102,5 +102,23 @@ namespace ErsatzTV.Infrastructure.Images @@ -102,5 +102,23 @@ namespace ErsatzTV.Infrastructure.Images
return BaseError.New(ex.ToString());
}
}
public string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight)
{
string subfolder = maybeMaxHeight.Match(
maxHeight => Path.Combine(maxHeight.ToString(), fileName[..2]),
() => fileName[..2]);
string baseFolder = artworkKind switch
{
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
ArtworkKind.FanArt => Path.Combine(FileSystemLayout.FanArtCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder
};
return Path.Combine(baseFolder, fileName);
}
}
}

24
ErsatzTV/Controllers/ArtworkController.cs

@ -39,21 +39,21 @@ namespace ErsatzTV.Controllers @@ -39,21 +39,21 @@ namespace ErsatzTV.Controllers
[HttpGet("/artwork/posters/{fileName}")]
public async Task<IActionResult> GetPoster(string fileName)
{
Either<BaseError, ImageViewModel> imageContents =
await _mediator.Send(new GetImageContents(fileName, ArtworkKind.Poster, 440));
return imageContents.Match<IActionResult>(
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Poster, 440));
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new FileContentResult(r.Contents, r.MimeType));
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
[HttpGet("/artwork/fanart/{fileName}")]
public async Task<IActionResult> GetFanArt(string fileName)
{
Either<BaseError, ImageViewModel> imageContents =
await _mediator.Send(new GetImageContents(fileName, ArtworkKind.FanArt));
return imageContents.Match<IActionResult>(
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.FanArt));
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new FileContentResult(r.Contents, r.MimeType));
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
@ -108,11 +108,11 @@ namespace ErsatzTV.Controllers @@ -108,11 +108,11 @@ namespace ErsatzTV.Controllers
[HttpGet("/artwork/thumbnails/{fileName}")]
public async Task<IActionResult> GetThumbnail(string fileName)
{
Either<BaseError, ImageViewModel> imageContents =
await _mediator.Send(new GetImageContents(fileName, ArtworkKind.Thumbnail, 220));
return imageContents.Match<IActionResult>(
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Thumbnail, 220));
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new FileContentResult(r.Contents, r.MimeType));
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
private async Task<IActionResult> GetPlexArtwork(int plexMediaSourceId, string transcodePath)

8
ErsatzTV/Controllers/IptvController.cs

@ -67,11 +67,11 @@ namespace ErsatzTV.Controllers @@ -67,11 +67,11 @@ namespace ErsatzTV.Controllers
[HttpGet("iptv/logos/{fileName}")]
public async Task<IActionResult> GetImage(string fileName)
{
Either<BaseError, ImageViewModel> imageContents =
await _mediator.Send(new GetImageContents(fileName, ArtworkKind.Logo));
return imageContents.Match<IActionResult>(
Either<BaseError, CachedImagePathViewModel> cachedImagePath =
await _mediator.Send(new GetCachedImagePath(fileName, ArtworkKind.Logo));
return cachedImagePath.Match<IActionResult>(
Left: _ => new NotFoundResult(),
Right: r => new FileContentResult(r.Contents, r.MimeType));
Right: r => new PhysicalFileResult(r.FileName, r.MimeType));
}
}
}

Loading…
Cancel
Save