Browse Source

multiple bug fixes (#2320)

* fix incorrect media counts in local libraries

* completely replace imagesharp with skiasharp

* fix song troubleshooting playback

* fix usings
pull/2321/head
Jason Dove 5 months ago committed by GitHub
parent
commit
d5608ac75f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 36
      ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs
  3. 66
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  4. 7
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  5. 55
      ErsatzTV.Infrastructure/Images/ImageCache.cs

2
CHANGELOG.md

@ -65,6 +65,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -65,6 +65,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix playback of anamorphic content from Jellyfin
- This fix requires a manual deep scan of any affected Jellyfin library
- Fix bug where multiple Plex servers would mix their episodes
- Fix incorrect media item counts after removing paths from local libraries
- Fix song playback in playback troubleshooting
### Changed
- Allow multiple watermarks in playback troubleshooting

36
ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs

@ -53,45 +53,51 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase, @@ -53,45 +53,51 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
.Filter(ep => incoming.Paths.All(p => NormalizePath(p.Path) != NormalizePath(ep.Path)))
.ToList();
var toRemoveIds = toRemove.Map(lp => lp.Id).ToList();
var toRemoveIds = toRemove.Map(lp => lp.Id).ToHashSet();
await dbContext.Connection.ExecuteAsync(
int changeCount = 0;
// save item ids first; will need to remove from search index
List<int> itemsToRemove = await dbContext.MediaItems
.AsNoTracking()
.Filter(mi => toRemoveIds.Contains(mi.LibraryPathId))
.Map(mi => mi.Id)
.ToListAsync();
changeCount += await dbContext.Connection.ExecuteAsync(
"DELETE FROM MediaItem WHERE LibraryPathId IN @Ids",
new { Ids = toRemoveIds });
// delete all library folders (children first)
IOrderedQueryable<LibraryFolder> orderedFolders = dbContext.LibraryFolders
.AsNoTracking()
.Filter(lf => toRemoveIds.Contains(lf.LibraryPathId))
.OrderByDescending(lp => lp.Path.Length);
foreach (LibraryFolder folder in orderedFolders)
{
await dbContext.Connection.ExecuteAsync(
changeCount += await dbContext.Connection.ExecuteAsync(
"DELETE FROM LibraryFolder WHERE Id = @LibraryFolderId",
new { LibraryFolderId = folder.Id });
}
await dbContext.LibraryPaths
changeCount += await dbContext.LibraryPaths
.Filter(lp => toRemoveIds.Contains(lp.Id))
.ExecuteDeleteAsync();
existing.Paths.AddRange(toAdd);
if (await dbContext.SaveChangesAsync() > 0)
{
List<int> itemsToRemove = await dbContext.MediaItems
.AsNoTracking()
.Filter(mi => toRemoveIds.Contains(mi.LibraryPathId))
.Map(mi => mi.Id)
.ToListAsync();
changeCount += await dbContext.SaveChangesAsync();
if (changeCount > 0)
{
await _searchIndex.RemoveItems(itemsToRemove);
_searchIndex.Commit();
}
if ((toAdd.Count > 0 || toRemove.Count > 0) && _entityLocker.LockLibrary(existing.Id))
{
await _scannerWorkerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id));
if (_entityLocker.LockLibrary(existing.Id))
{
await _scannerWorkerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id));
}
}
return ProjectToViewModel(existing);

66
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs

@ -13,6 +13,7 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -13,6 +13,7 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Notifications;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.State;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -27,6 +28,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -27,6 +28,7 @@ public class PrepareTroubleshootingPlaybackHandler(
IEmbyPathReplacementService embyPathReplacementService,
IFFmpegProcessService ffmpegProcessService,
ILocalFileSystem localFileSystem,
ISongVideoGenerator songVideoGenerator,
IEntityLocker entityLocker,
IMediator mediator,
ILogger<PrepareTroubleshootingPlaybackHandler> logger)
@ -80,6 +82,17 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -80,6 +82,17 @@ public class PrepareTroubleshootingPlaybackHandler(
return BaseError.New("Media item does not exist on disk");
}
var channel = new Channel(Guid.Empty)
{
Artwork = [],
Name = "ETV",
Number = ".troubleshooting",
FFmpegProfile = ffmpegProfile,
StreamingMode = StreamingMode.HttpLiveStreamingSegmenter,
StreamSelectorMode = ChannelStreamSelectorMode.Troubleshooting,
SubtitleMode = SUBTITLE_MODE
};
List<ChannelWatermark> watermarks = [];
if (request.WatermarkIds.Count > 0)
{
@ -90,6 +103,44 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -90,6 +103,44 @@ public class PrepareTroubleshootingPlaybackHandler(
watermarks.AddRange(channelWatermarks);
}
string videoPath = mediaPath;
MediaVersion videoVersion = version;
if (mediaItem is Song song)
{
(videoPath, videoVersion) = await songVideoGenerator.GenerateSongVideo(
song,
channel,
Option<ChannelWatermark>.None,
Option<ChannelWatermark>.None,
ffmpegPath,
ffprobePath,
CancellationToken.None);
// override watermark as song_progress_overlay.png
if (videoVersion is BackgroundImageMediaVersion { IsSongWithProgress: true })
{
double ratio = channel.FFmpegProfile.Resolution.Width /
(double)channel.FFmpegProfile.Resolution.Height;
bool is43 = Math.Abs(ratio - 4.0 / 3.0) < 0.01;
string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png";
watermarks.Clear();
watermarks.Add(new ChannelWatermark
{
Mode = ChannelWatermarkMode.Permanent,
Size = WatermarkSize.Scaled,
WidthPercent = 100,
HorizontalMarginPercent = 0,
VerticalMarginPercent = 0,
Opacity = 100,
Location = WatermarkLocation.TopLeft,
ImageSource = ChannelWatermarkImageSource.Resource,
Image = image
});
}
}
DateTimeOffset now = DateTimeOffset.Now;
var duration = TimeSpan.FromSeconds(Math.Min(version.Duration.TotalSeconds, 30));
@ -130,19 +181,10 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -130,19 +181,10 @@ public class PrepareTroubleshootingPlaybackHandler(
ffmpegPath,
ffprobePath,
true,
new Channel(Guid.Empty)
{
Artwork = [],
Name = "ETV",
Number = ".troubleshooting",
FFmpegProfile = ffmpegProfile,
StreamingMode = StreamingMode.HttpLiveStreamingSegmenter,
StreamSelectorMode = ChannelStreamSelectorMode.Troubleshooting,
SubtitleMode = SUBTITLE_MODE
},
version,
channel,
videoVersion,
new MediaItemAudioVersion(mediaItem, version),
mediaPath,
videoPath,
mediaPath,
_ => GetSelectedSubtitle(mediaItem, request),
string.Empty,

7
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blurhash.ImageSharp" Version="4.0.0" />
<PackageReference Include="Blurhash.SkiaSharp" Version="2.0.0" />
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
@ -35,13 +35,8 @@ @@ -35,13 +35,8 @@
<PackageReference Include="Refit.Xml" Version="8.0.0" />
<PackageReference Include="RichTextKit.Stbear" Version="0.4.167.3" />
<PackageReference Include="Scriban.Signed" Version="6.2.1" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SkiaSharp" Version="3.119.0" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<!-- transitive; upgrading for vuln -->
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="TimeZoneConverter" Version="7.0.0" />
</ItemGroup>

55
ErsatzTV.Infrastructure/Images/ImageCache.cs

@ -1,18 +1,15 @@ @@ -1,18 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Blurhash.ImageSharp;
using Blurhash.SkiaSharp;
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;
using SkiaSharp;
namespace ErsatzTV.Infrastructure.Images;
@ -123,28 +120,42 @@ public class ImageCache : IImageCache @@ -123,28 +120,42 @@ public class ImageCache : IImageCache
return Path.Combine(baseFolder, fileName);
}
public async Task<string> CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y)
public 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))
using (var image = SKBitmap.Decode(targetFile))
{
// resize before calculating blur hash; it doesn't need giant images
if (image.Height > 200)
if (image.Height > 200 || image.Width > 200)
{
image.Mutate(i => i.Resize(0, 200));
}
else if (image.Width > 200)
{
image.Mutate(i => i.Resize(200, 0));
int width, height;
if (image.Width > image.Height)
{
width = 200;
height = (int)Math.Round(image.Height * (200.0 / image.Width));
}
else
{
height = 200;
width = (int)Math.Round(image.Width * (200.0 / image.Height));
}
var info = new SKImageInfo(width, height);
// ReSharper disable once ConvertToUsingDeclaration
using (SKBitmap resized = image.Resize(info, SKSamplingOptions.Default))
{
return Task.FromResult(Blurhasher.Encode(resized, x, y));
}
}
return Blurhasher.Encode(image, x, y);
return Task.FromResult(Blurhasher.Encode(image, x, y));
}
}
public async Task<string> WriteBlurHash(string blurHash, IDisplaySize targetSize)
public Task<string> WriteBlurHash(string blurHash, IDisplaySize targetSize)
{
byte[] bytes = Encoding.UTF8.GetBytes(blurHash);
string base64 = Convert.ToBase64String(bytes).Replace("+", "_").Replace("/", "-").Replace("=", "");
@ -155,17 +166,19 @@ public class ImageCache : IImageCache @@ -155,17 +166,19 @@ public class ImageCache : IImageCache
_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))
using (SKBitmap image = Blurhasher.Decode(blurHash, targetSize.Width, targetSize.Height))
{
await image.SaveAsPngAsync(fs);
using (SKData data = image.Encode(SKEncodedImageFormat.Png, 100))
{
data.SaveTo(fs);
}
}
}
}
return targetFile;
return Task.FromResult(targetFile);
}
[SuppressMessage("Security", "CA5351:Do Not Use Broken Cryptographic Algorithms")]
@ -180,4 +193,4 @@ public class ImageCache : IImageCache @@ -180,4 +193,4 @@ public class ImageCache : IImageCache
return await md5.ComputeHashAsync(fs);
}
}
}
}
Loading…
Cancel
Save