mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
7.2 KiB
183 lines
7.2 KiB
using System.Diagnostics.CodeAnalysis; |
|
using System.Globalization; |
|
using System.Security.Cryptography; |
|
using System.Text; |
|
using Blurhash.ImageSharp; |
|
using ErsatzTV.Core; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.FFmpeg; |
|
using ErsatzTV.Core.Interfaces.FFmpeg; |
|
using ErsatzTV.Core.Interfaces.Images; |
|
using ErsatzTV.Core.Interfaces.Metadata; |
|
using SixLabors.ImageSharp; |
|
using SixLabors.ImageSharp.PixelFormats; |
|
using SixLabors.ImageSharp.Processing; |
|
using Image = SixLabors.ImageSharp.Image; |
|
|
|
namespace ErsatzTV.Infrastructure.Images; |
|
|
|
[SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms")] |
|
public class ImageCache : IImageCache |
|
{ |
|
private static readonly SHA1 Crypto; |
|
private readonly ILocalFileSystem _localFileSystem; |
|
private readonly ITempFilePool _tempFilePool; |
|
|
|
static ImageCache() => Crypto = SHA1.Create(); |
|
|
|
public ImageCache( |
|
ILocalFileSystem localFileSystem, |
|
ITempFilePool tempFilePool) |
|
{ |
|
_localFileSystem = localFileSystem; |
|
_tempFilePool = tempFilePool; |
|
} |
|
|
|
public async Task<Either<BaseError, string>> SaveArtworkToCache(Stream stream, ArtworkKind artworkKind) |
|
{ |
|
try |
|
{ |
|
string tempFileName = _tempFilePool.GetNextTempFile(TempFileCategory.CachedArtwork); |
|
// ReSharper disable once UseAwaitUsing |
|
using (var fs = new FileStream(tempFileName, FileMode.OpenOrCreate, FileAccess.Write)) |
|
{ |
|
// overwrite anything that's already there |
|
fs.SetLength(0); |
|
|
|
await stream.CopyToAsync(fs); |
|
} |
|
|
|
byte[] hash = await ComputeFileHash(tempFileName); |
|
string hex = BitConverter.ToString(hash).Replace("-", string.Empty); |
|
string subfolder = hex[..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), |
|
ArtworkKind.Watermark => Path.Combine(FileSystemLayout.WatermarkCacheFolder, subfolder), |
|
_ => FileSystemLayout.LegacyImageCacheFolder |
|
}; |
|
string target = Path.Combine(baseFolder, hex); |
|
|
|
if (!Directory.Exists(baseFolder)) |
|
{ |
|
Directory.CreateDirectory(baseFolder); |
|
} |
|
|
|
await _localFileSystem.CopyFile(tempFileName, target); |
|
|
|
return hex; |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.Message); |
|
} |
|
} |
|
|
|
public async Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind) |
|
{ |
|
try |
|
{ |
|
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[..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 |
|
}; |
|
string target = Path.Combine(baseFolder, hex); |
|
Either<BaseError, Unit> maybeResult = await _localFileSystem.CopyFile(path, target); |
|
return maybeResult.Match<Either<BaseError, string>>( |
|
_ => hex, |
|
error => error); |
|
} |
|
catch (Exception ex) |
|
{ |
|
return BaseError.New(ex.ToString()); |
|
} |
|
} |
|
|
|
public virtual string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight) |
|
{ |
|
string subfolder = maybeMaxHeight.Match( |
|
maxHeight => Path.Combine(maxHeight.ToString(CultureInfo.InvariantCulture), 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), |
|
ArtworkKind.Watermark => Path.Combine(FileSystemLayout.WatermarkCacheFolder, subfolder), |
|
_ => FileSystemLayout.LegacyImageCacheFolder |
|
}; |
|
|
|
return Path.Combine(baseFolder, fileName); |
|
} |
|
|
|
public async Task<string> CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y) |
|
{ |
|
string targetFile = GetPathForImage(fileName, artworkKind, Option<int>.None); |
|
|
|
// ReSharper disable once ConvertToUsingDeclaration |
|
using (var image = await Image.LoadAsync<Rgba32>(targetFile)) |
|
{ |
|
// resize before calculating blur hash; it doesn't need giant images |
|
if (image.Height > 200) |
|
{ |
|
image.Mutate(i => i.Resize(0, 200)); |
|
} |
|
else if (image.Width > 200) |
|
{ |
|
image.Mutate(i => i.Resize(200, 0)); |
|
} |
|
|
|
return Blurhasher.Encode(image, x, y); |
|
} |
|
} |
|
|
|
public async Task<string> WriteBlurHash(string blurHash, IDisplaySize targetSize) |
|
{ |
|
byte[] bytes = Encoding.UTF8.GetBytes(blurHash); |
|
string base64 = Convert.ToBase64String(bytes).Replace("+", "_").Replace("/", "-").Replace("=", ""); |
|
string targetFile = GetPathForImage(base64, ArtworkKind.Poster, targetSize.Height); |
|
if (!_localFileSystem.FileExists(targetFile)) |
|
{ |
|
string folder = Path.GetDirectoryName(targetFile); |
|
_localFileSystem.EnsureFolderExists(folder); |
|
|
|
// ReSharper disable once ConvertToUsingDeclaration |
|
// ReSharper disable once UseAwaitUsing |
|
using (FileStream fs = File.OpenWrite(targetFile)) |
|
{ |
|
using (Image<Rgb24> image = Blurhasher.Decode(blurHash, targetSize.Width, targetSize.Height)) |
|
{ |
|
await image.SaveAsPngAsync(fs); |
|
} |
|
} |
|
} |
|
|
|
return targetFile; |
|
} |
|
|
|
[SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")] |
|
private static async Task<byte[]> ComputeFileHash(string fileName) |
|
{ |
|
using var md5 = MD5.Create(); |
|
// ReSharper disable once UseAwaitUsing |
|
// ReSharper disable once ConvertToUsingDeclaration |
|
using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) |
|
{ |
|
fs.Position = 0; |
|
return await md5.ComputeHashAsync(fs); |
|
} |
|
} |
|
}
|
|
|