Browse Source

use blurhash for song backgrounds (#526)

* generate blurhash for all local artwork

* use blurhash song background if available

* only write blur hash to disk once

* use multiple blur hashes

* update changelog

* fix song detail outline

* reset song metadata (artwork)
pull/527/head
Jason Dove 5 years ago committed by GitHub
parent
commit
3773bbec19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 3
      ErsatzTV.Core/Domain/Metadata/Artwork.cs
  3. 16
      ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs
  4. 6
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  5. 13
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  6. 63
      ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs
  7. 4
      ErsatzTV.Core/FFmpeg/SubtitleBuilder.cs
  8. 2
      ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs
  9. 3
      ErsatzTV.Core/Interfaces/Images/IImageCache.cs
  10. 8
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  11. 34
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  12. 1
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  13. 28
      ErsatzTV.Infrastructure/Images/ImageCache.cs
  14. 3846
      ErsatzTV.Infrastructure/Migrations/20211203040447_Add_ArtworkBlurHash.Designer.cs
  15. 25
      ErsatzTV.Infrastructure/Migrations/20211203040447_Add_ArtworkBlurHash.cs
  16. 3852
      ErsatzTV.Infrastructure/Migrations/20211203174753_Add_MoreArtworkBlurHashes.Designer.cs
  17. 45
      ErsatzTV.Infrastructure/Migrations/20211203174753_Add_MoreArtworkBlurHashes.cs
  18. 3852
      ErsatzTV.Infrastructure/Migrations/20211203182334_Reset_SongMetadataBlurHash.Designer.cs
  19. 31
      ErsatzTV.Infrastructure/Migrations/20211203182334_Reset_SongMetadataBlurHash.cs
  20. 9
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

2
CHANGELOG.md

@ -9,9 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix unicode song metadata on Windows - Fix unicode song metadata on Windows
- Fix unicode console output on Windows - Fix unicode console output on Windows
- Fix TV Show NFO metadata processing when `year` is missing - Fix TV Show NFO metadata processing when `year` is missing
- Fix song detail outline to help legibility on white backgrounds
### Changed ### Changed
- Use custom log database backend which should be more portable (i.e. work in osx-arm64) - Use custom log database backend which should be more portable (i.e. work in osx-arm64)
- Use cover art blurhashes for song backgrounds instead of solid colors or box blur
## [0.3.1-alpha] - 2021-11-30 ## [0.3.1-alpha] - 2021-11-30
### Fixed ### Fixed

3
ErsatzTV.Core/Domain/Metadata/Artwork.cs

@ -6,6 +6,9 @@ namespace ErsatzTV.Core.Domain
{ {
public int Id { get; set; } public int Id { get; set; }
public string Path { get; set; } public string Path { get; set; }
public string BlurHash43 { get; set; }
public string BlurHash54 { get; set; }
public string BlurHash64 { get; set; }
public ArtworkKind ArtworkKind { get; set; } public ArtworkKind ArtworkKind { get; set; }
public DateTime DateAdded { get; set; } public DateTime DateAdded { get; set; }
public DateTime DateUpdated { get; set; } public DateTime DateUpdated { get; set; }

16
ErsatzTV.Core/FFmpeg/FFmpegComplexFilterBuilder.cs

@ -27,7 +27,6 @@ namespace ErsatzTV.Core.FFmpeg
private string _videoEncoder; private string _videoEncoder;
private Option<string> _subtitle; private Option<string> _subtitle;
private bool _boxBlur; private bool _boxBlur;
private Option<int> _randomColor;
public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind) public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind)
{ {
@ -102,12 +101,6 @@ namespace ErsatzTV.Core.FFmpeg
return this; return this;
} }
public FFmpegComplexFilterBuilder WithRandomColor(Option<int> randomColor)
{
_randomColor = randomColor;
return this;
}
public FFmpegComplexFilterBuilder WithSubtitleFile(Option<string> subtitleFile) public FFmpegComplexFilterBuilder WithSubtitleFile(Option<string> subtitleFile)
{ {
foreach (string file in subtitleFile) foreach (string file in subtitleFile)
@ -250,7 +243,7 @@ namespace ErsatzTV.Core.FFmpeg
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear" _ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
}; };
if (_randomColor.IsNone && !string.IsNullOrWhiteSpace(filter)) if (!string.IsNullOrWhiteSpace(filter))
{ {
videoFilterQueue.Add(filter); videoFilterQueue.Add(filter);
} }
@ -276,7 +269,7 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add(format); videoFilterQueue.Add(format);
} }
if (scaleOrPad && _boxBlur == false && _randomColor.IsNone) if (scaleOrPad && _boxBlur == false)
{ {
videoFilterQueue.Add("setsar=1"); videoFilterQueue.Add("setsar=1");
} }
@ -286,10 +279,9 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add("boxblur=40"); videoFilterQueue.Add("boxblur=40");
} }
foreach (int color in _randomColor) if (videoOnly)
{ {
videoFilterQueue.Add( videoFilterQueue.Add("deband");
$"palettegen=max_colors=8,crop=1:1:{color}:0,scale={_resolution.Width}:{_resolution.Height},setsar=1");
} }
foreach (ChannelWatermark watermark in _watermark) foreach (ChannelWatermark watermark in _watermark)

6
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -285,8 +285,7 @@ namespace ErsatzTV.Core.FFmpeg
string videoPath, string videoPath,
Option<string> codec, Option<string> codec,
Option<string> pixelFormat, Option<string> pixelFormat,
bool boxBlur, bool boxBlur)
Option<int> randomColor)
{ {
_noAutoScale = true; _noAutoScale = true;
_outputFramerate = 30; _outputFramerate = 30;
@ -294,8 +293,7 @@ namespace ErsatzTV.Core.FFmpeg
_complexFilterBuilder = _complexFilterBuilder _complexFilterBuilder = _complexFilterBuilder
.WithInputCodec(codec) .WithInputCodec(codec)
.WithInputPixelFormat(pixelFormat) .WithInputPixelFormat(pixelFormat)
.WithBoxBlur(boxBlur) .WithBoxBlur(boxBlur);
.WithRandomColor(randomColor);
_arguments.Add("-i"); _arguments.Add("-i");
_arguments.Add(videoPath); _arguments.Add(videoPath);

13
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -68,7 +68,7 @@ namespace ErsatzTV.Core.FFmpeg
outPoint); outPoint);
Option<WatermarkOptions> watermarkOptions = Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion, None); await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
.WithThreads(playbackSettings.ThreadCount) .WithThreads(playbackSettings.ThreadCount)
@ -276,7 +276,7 @@ namespace ErsatzTV.Core.FFmpeg
MediaVersion videoVersion, MediaVersion videoVersion,
string videoPath, string videoPath,
bool boxBlur, bool boxBlur,
Option<int> randomColor, Option<string> watermarkPath,
ChannelWatermarkLocation watermarkLocation, ChannelWatermarkLocation watermarkLocation,
int horizontalMarginPercent, int horizontalMarginPercent,
int verticalMarginPercent, int verticalMarginPercent,
@ -303,7 +303,7 @@ namespace ErsatzTV.Core.FFmpeg
: None; : None;
Option<WatermarkOptions> watermarkOptions = Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion, watermarkOverride); await GetWatermarkOptions(channel, globalWatermark, videoVersion, watermarkOverride, watermarkPath);
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettings playbackSettings =
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); _playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
@ -323,7 +323,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
.WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat, boxBlur, randomColor) .WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat, boxBlur)
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution) .WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution)
.WithSubtitleFile(subtitleFile); .WithSubtitleFile(subtitleFile);
@ -370,7 +370,8 @@ namespace ErsatzTV.Core.FFmpeg
Channel channel, Channel channel,
Option<ChannelWatermark> globalWatermark, Option<ChannelWatermark> globalWatermark,
MediaVersion videoVersion, MediaVersion videoVersion,
Option<ChannelWatermark> watermarkOverride) Option<ChannelWatermark> watermarkOverride,
Option<string> watermarkPath)
{ {
if (videoVersion is BackgroundImageMediaVersion) if (videoVersion is BackgroundImageMediaVersion)
{ {
@ -384,7 +385,7 @@ namespace ErsatzTV.Core.FFmpeg
{ {
return new WatermarkOptions( return new WatermarkOptions(
watermarkOverride, watermarkOverride,
videoVersion.MediaFiles.Head().Path, await watermarkPath.IfNoneAsync(videoVersion.MediaFiles.Head().Path),
0, 0,
false); false);
} }

63
ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
@ -50,7 +51,7 @@ namespace ErsatzTV.Core.FFmpeg
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 } new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
} }
}; };
string[] backgrounds = string[] backgrounds =
{ {
"background_blank.png", "background_blank.png",
@ -60,12 +61,13 @@ namespace ErsatzTV.Core.FFmpeg
}; };
// use random ETV color by default // use random ETV color by default
string artworkPath = Path.Combine( string backgroundPath = Path.Combine(
FileSystemLayout.ResourcesCacheFolder, FileSystemLayout.ResourcesCacheFolder,
backgrounds[NextRandom(backgrounds.Length)]); backgrounds[NextRandom(backgrounds.Length)]);
Option<string> watermarkPath = None;
var boxBlur = false; var boxBlur = false;
Option<int> randomColor = None;
const int HORIZONTAL_MARGIN_PERCENT = 3; const int HORIZONTAL_MARGIN_PERCENT = 3;
const int VERTICAL_MARGIN_PERCENT = 5; const int VERTICAL_MARGIN_PERCENT = 5;
@ -135,7 +137,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithFontName("OPTIKabel-Heavy") .WithFontName("OPTIKabel-Heavy")
.WithFontSize(fontSize) .WithFontSize(fontSize)
.WithPrimaryColor("&HFFFFFF") .WithPrimaryColor("&HFFFFFF")
.WithOutlineColor("&H555555") .WithOutlineColor("&H444444")
.WithAlignment(0) .WithAlignment(0)
.WithMarginRight(rightMargin) .WithMarginRight(rightMargin)
.WithMarginLeft(leftMargin) .WithMarginLeft(leftMargin)
@ -149,23 +151,6 @@ namespace ErsatzTV.Core.FFmpeg
foreach (Artwork artwork in Optional( foreach (Artwork artwork in Optional(
metadata.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Thumbnail))) metadata.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Thumbnail)))
{ {
int backgroundRoll = NextRandom(16);
if (backgroundRoll < 8)
{
randomColor = backgroundRoll;
}
else
{
boxBlur = true;
}
string customPath = _imageCache.GetPathForImage(
artwork.Path,
ArtworkKind.Thumbnail,
Option<int>.None);
artworkPath = customPath;
// signal that we want to use cover art as watermark // signal that we want to use cover art as watermark
videoVersion = new CoverArtMediaVersion videoVersion = new CoverArtMediaVersion
{ {
@ -179,10 +164,42 @@ namespace ErsatzTV.Core.FFmpeg
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 } new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
} }
}; };
string customPath = _imageCache.GetPathForImage(
artwork.Path,
ArtworkKind.Thumbnail,
Option<int>.None);
watermarkPath = customPath;
// randomize selected blur hash
var hashes = new List<string>
{
artwork.BlurHash43,
artwork.BlurHash54,
artwork.BlurHash64
}.Filter(s => !string.IsNullOrWhiteSpace(s)).ToList();
if (hashes.Any())
{
string hash = hashes[NextRandom(hashes.Count)];
backgroundPath = await _imageCache.WriteBlurHash(
hash,
channel.FFmpegProfile.Resolution);
videoVersion.Height = channel.FFmpegProfile.Resolution.Height;
videoVersion.Width = channel.FFmpegProfile.Resolution.Width;
}
else
{
backgroundPath = customPath;
boxBlur = true;
}
} }
} }
string videoPath = artworkPath; string videoPath = backgroundPath;
videoVersion.MediaFiles = new List<MediaFile> videoVersion.MediaFiles = new List<MediaFile>
{ {
@ -197,7 +214,7 @@ namespace ErsatzTV.Core.FFmpeg
videoVersion, videoVersion,
videoPath, videoPath,
boxBlur, boxBlur,
randomColor, watermarkPath,
watermarkLocation, watermarkLocation,
HORIZONTAL_MARGIN_PERCENT, HORIZONTAL_MARGIN_PERCENT,
VERTICAL_MARGIN_PERCENT, VERTICAL_MARGIN_PERCENT,

4
ErsatzTV.Core/FFmpeg/SubtitleBuilder.cs

@ -118,8 +118,8 @@ namespace ErsatzTV.Core.FFmpeg
} }
sb.AppendLine("[V4+ Styles]"); sb.AppendLine("[V4+ Styles]");
sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BorderStyle, Shadow, Alignment, Encoding"); sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BorderStyle, Outline, Shadow, Alignment, Encoding");
sb.AppendLine($"Style: Default,{await _fontName.IfNoneAsync("")},{await _fontSize.IfNoneAsync(32)},{await _primaryColor.IfNoneAsync("")},{await _outlineColor.IfNoneAsync("")},{await _borderStyle.IfNoneAsync(0)},{await _shadow.IfNoneAsync(0)}, {await _alignment.IfNoneAsync(0)},1"); sb.AppendLine($"Style: Default,{await _fontName.IfNoneAsync("")},{await _fontSize.IfNoneAsync(32)},{await _primaryColor.IfNoneAsync("")},{await _outlineColor.IfNoneAsync("")},{await _borderStyle.IfNoneAsync(0)},1,{await _shadow.IfNoneAsync(0)},{await _alignment.IfNoneAsync(0)},1");
sb.AppendLine("[Events]"); sb.AppendLine("[Events]");
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");

2
ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs

@ -50,7 +50,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
MediaVersion videoVersion, MediaVersion videoVersion,
string videoPath, string videoPath,
bool boxBlur, bool boxBlur,
Option<int> randomColor, Option<string> watermarkPath,
ChannelWatermarkLocation watermarkLocation, ChannelWatermarkLocation watermarkLocation,
int horizontalMarginPercent, int horizontalMarginPercent,
int verticalMarginPercent, int verticalMarginPercent,

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

@ -1,6 +1,7 @@
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using LanguageExt; using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Images namespace ErsatzTV.Core.Interfaces.Images
@ -12,5 +13,7 @@ namespace ErsatzTV.Core.Interfaces.Images
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind); Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight); string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight);
Task<bool> IsAnimated(string fileName); Task<bool> IsAnimated(string fileName);
Task<string> CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y);
Task<string> WriteBlurHash(string blurHash, IDisplaySize targetSize);
} }
} }

8
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -180,6 +180,9 @@ namespace ErsatzTV.Core.Metadata
{ {
artwork.Path = cacheName; artwork.Path = cacheName;
artwork.DateUpdated = lastWriteTime; artwork.DateUpdated = lastWriteTime;
artwork.BlurHash43 = await _imageCache.CalculateBlurHash(cacheName, artworkKind, 4, 3);
artwork.BlurHash54 = await _imageCache.CalculateBlurHash(cacheName, artworkKind, 5, 4);
artwork.BlurHash64 = await _imageCache.CalculateBlurHash(cacheName, artworkKind, 6, 4);
await _metadataRepository.UpdateArtworkPath(artwork); await _metadataRepository.UpdateArtworkPath(artwork);
}, },
async () => async () =>
@ -189,7 +192,10 @@ namespace ErsatzTV.Core.Metadata
Path = cacheName, Path = cacheName,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,
DateUpdated = lastWriteTime, DateUpdated = lastWriteTime,
ArtworkKind = artworkKind ArtworkKind = artworkKind,
BlurHash43 = await _imageCache.CalculateBlurHash(cacheName, artworkKind, 4, 3),
BlurHash54 = await _imageCache.CalculateBlurHash(cacheName, artworkKind, 5, 4),
BlurHash64 = await _imageCache.CalculateBlurHash(cacheName, artworkKind, 6, 4)
}; };
metadata.Artwork.Add(artwork); metadata.Artwork.Add(artwork);
await _metadataRepository.AddArtwork(metadata, artwork); await _metadataRepository.AddArtwork(metadata, artwork);

34
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -209,51 +209,51 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Unit> UpdateArtworkPath(Artwork artwork) => public Task<Unit> UpdateArtworkPath(Artwork artwork) =>
_dbConnection.ExecuteAsync( _dbConnection.ExecuteAsync(
"UPDATE Artwork SET Path = @Path, DateUpdated = @DateUpdated WHERE Id = @Id", "UPDATE Artwork SET Path = @Path, DateUpdated = @DateUpdated, BlurHash43 = @BlurHash43, BlurHash43 = @BlurHash54, BlurHash43 = @BlurHash64 WHERE Id = @Id",
new { artwork.Path, artwork.DateUpdated, artwork.Id }).ToUnit(); new { artwork.Path, artwork.DateUpdated, artwork.BlurHash43, artwork.BlurHash54, artwork.BlurHash64, artwork.Id }).ToUnit();
public Task<Unit> AddArtwork(Metadata metadata, Artwork artwork) public Task<Unit> AddArtwork(Metadata metadata, Artwork artwork)
{ {
var parameters = new var parameters = new
{ {
artwork.ArtworkKind, metadata.Id, artwork.DateAdded, artwork.DateUpdated, artwork.Path artwork.ArtworkKind, metadata.Id, artwork.DateAdded, artwork.DateUpdated, artwork.Path, artwork.BlurHash43, artwork.BlurHash54, artwork.BlurHash64
}; };
return metadata switch return metadata switch
{ {
MovieMetadata => _dbConnection.ExecuteAsync( MovieMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, MovieMetadataId, DateAdded, DateUpdated, Path) @"INSERT INTO Artwork (ArtworkKind, MovieMetadataId, DateAdded, DateUpdated, Path, BlurHash43, BlurHash54, BlurHash64)
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @BlurHash43, @BlurHash54, @BlurHash64)",
parameters) parameters)
.ToUnit(), .ToUnit(),
ShowMetadata => _dbConnection.ExecuteAsync( ShowMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, ShowMetadataId, DateAdded, DateUpdated, Path) @"INSERT INTO Artwork (ArtworkKind, ShowMetadataId, DateAdded, DateUpdated, Path, BlurHash43, BlurHash54, BlurHash64)
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @BlurHash43, @BlurHash54, @BlurHash64)",
parameters) parameters)
.ToUnit(), .ToUnit(),
SeasonMetadata => _dbConnection.ExecuteAsync( SeasonMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, SeasonMetadataId, DateAdded, DateUpdated, Path) @"INSERT INTO Artwork (ArtworkKind, SeasonMetadataId, DateAdded, DateUpdated, Path, BlurHash43, BlurHash54, BlurHash64)
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @BlurHash43, @BlurHash54, @BlurHash64)",
parameters) parameters)
.ToUnit(), .ToUnit(),
EpisodeMetadata => _dbConnection.ExecuteAsync( EpisodeMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, EpisodeMetadataId, DateAdded, DateUpdated, Path) @"INSERT INTO Artwork (ArtworkKind, EpisodeMetadataId, DateAdded, DateUpdated, Path, BlurHash43, BlurHash54, BlurHash64)
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @BlurHash43, @BlurHash54, @BlurHash64)",
parameters) parameters)
.ToUnit(), .ToUnit(),
ArtistMetadata => _dbConnection.ExecuteAsync( ArtistMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, ArtistMetadataId, DateAdded, DateUpdated, Path) @"INSERT INTO Artwork (ArtworkKind, ArtistMetadataId, DateAdded, DateUpdated, Path, BlurHash43, BlurHash54, BlurHash64)
Values (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", Values (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @BlurHash43, @BlurHash54, @BlurHash64)",
parameters) parameters)
.ToUnit(), .ToUnit(),
MusicVideoMetadata => _dbConnection.ExecuteAsync( MusicVideoMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, MusicVideoMetadataId, DateAdded, DateUpdated, Path) @"INSERT INTO Artwork (ArtworkKind, MusicVideoMetadataId, DateAdded, DateUpdated, Path, BlurHash43, BlurHash54, BlurHash64)
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @BlurHash43, @BlurHash54, @BlurHash64)",
parameters) parameters)
.ToUnit(), .ToUnit(),
SongMetadata => _dbConnection.ExecuteAsync( SongMetadata => _dbConnection.ExecuteAsync(
@"INSERT INTO Artwork (ArtworkKind, SongMetadataId, DateAdded, DateUpdated, Path) @"INSERT INTO Artwork (ArtworkKind, SongMetadataId, DateAdded, DateUpdated, Path, BlurHash43, BlurHash54, BlurHash64)
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @BlurHash43, @BlurHash54, @BlurHash64)",
parameters) parameters)
.ToUnit(), .ToUnit(),
_ => Task.FromResult(Unit.Default) _ => Task.FromResult(Unit.Default)

1
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blurhash.ImageSharp" Version="1.1.1" />
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00015" /> <PackageReference Include="Lucene.Net" Version="4.8.0-beta00015" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00015" /> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00015" />

28
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -14,6 +14,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
namespace ErsatzTV.Infrastructure.Images namespace ErsatzTV.Infrastructure.Images
@ -181,5 +182,32 @@ namespace ErsatzTV.Infrastructure.Images
return false; return false;
} }
} }
public async Task<string> CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y)
{
var encoder = new Blurhash.ImageSharp.Encoder();
string targetFile = GetPathForImage(fileName, artworkKind, Option<int>.None);
await using var fs = new FileStream(targetFile, FileMode.Open, FileAccess.Read);
using var image = await Image.LoadAsync<Rgb24>(fs);
return encoder.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);
var decoder = new Blurhash.ImageSharp.Decoder();
using Image<Rgb24> image = decoder.Decode(blurHash, targetSize.Width, targetSize.Height);
await image.SaveAsPngAsync(targetFile);
}
return targetFile;
}
} }
} }

3846
ErsatzTV.Infrastructure/Migrations/20211203040447_Add_ArtworkBlurHash.Designer.cs generated

File diff suppressed because it is too large Load Diff

25
ErsatzTV.Infrastructure/Migrations/20211203040447_Add_ArtworkBlurHash.cs

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_ArtworkBlurHash : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BlurHash",
table: "Artwork",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BlurHash",
table: "Artwork");
}
}
}

3852
ErsatzTV.Infrastructure/Migrations/20211203174753_Add_MoreArtworkBlurHashes.Designer.cs generated

File diff suppressed because it is too large Load Diff

45
ErsatzTV.Infrastructure/Migrations/20211203174753_Add_MoreArtworkBlurHashes.cs

@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MoreArtworkBlurHashes : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "BlurHash",
table: "Artwork",
newName: "BlurHash64");
migrationBuilder.AddColumn<string>(
name: "BlurHash43",
table: "Artwork",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "BlurHash54",
table: "Artwork",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BlurHash43",
table: "Artwork");
migrationBuilder.DropColumn(
name: "BlurHash54",
table: "Artwork");
migrationBuilder.RenameColumn(
name: "BlurHash64",
table: "Artwork",
newName: "BlurHash");
}
}
}

3852
ErsatzTV.Infrastructure/Migrations/20211203182334_Reset_SongMetadataBlurHash.Designer.cs generated

File diff suppressed because it is too large Load Diff

31
ErsatzTV.Infrastructure/Migrations/20211203182334_Reset_SongMetadataBlurHash.cs

@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Reset_SongMetadataBlurHash : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00' WHERE Id IN
(SELECT LP.Id FROM LibraryPath LP INNER JOIN Library L on L.Id = LP.LibraryId WHERE MediaKind = 5)");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE MediaKind = 5");
migrationBuilder.Sql(
@"UPDATE Artwork SET DateUpdated = '0001-01-01 00:00:00' WHERE SongMetadataId IS NOT NULL");
migrationBuilder.Sql(
@"UPDATE LibraryFolder SET Etag = NULL WHERE Id IN
(SELECT LF.Id FROM LibraryFolder LF INNER JOIN LibraryPath LP on LF.LibraryPathId = LP.Id INNER JOIN Library L on LP.LibraryId = L.Id WHERE MediaKind = 5)");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

9
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -144,6 +144,15 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("ArtworkKind") b.Property<int>("ArtworkKind")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("BlurHash43")
.HasColumnType("TEXT");
b.Property<string>("BlurHash54")
.HasColumnType("TEXT");
b.Property<string>("BlurHash64")
.HasColumnType("TEXT");
b.Property<int?>("ChannelId") b.Property<int?>("ChannelId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

Loading…
Cancel
Save