Stream custom live channels using your own media
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

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);
}
}
}