Merge pull request #17492 from GOvEy1nw/fix/image-cache-overlay-key

fix(images): disambiguate progress overlay cache keys
This commit is contained in:
Cody Robibero
2026-08-07 21:42:16 -04:00
committed by GitHub
3 changed files with 164 additions and 4 deletions
+31 -4
View File
@@ -31,7 +31,7 @@ namespace Jellyfin.Drawing;
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
private const char Version = '3';
private const char Version = '4';
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif", ".svg" };
@@ -251,6 +251,33 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <summary>
/// Gets the cache file path based on a set of parameters.
/// </summary>
/// <param name="originalPath">The original image path.</param>
/// <param name="dateModified">The source image modification date.</param>
/// <param name="format">The output format.</param>
/// <param name="options">The image processing options.</param>
/// <returns>The transformed image cache path.</returns>
internal string GetCacheFilePath(
string originalPath,
DateTime dateModified,
ImageFormat format,
ImageProcessingOptions options)
=> GetCacheFilePath(
originalPath,
options.Width,
options.Height,
options.MaxWidth,
options.MaxHeight,
options.FillWidth,
options.FillHeight,
options.Quality,
dateModified,
format,
options.PercentPlayed,
options.UnplayedCount,
options.Blur,
options.BackgroundColor,
options.ForegroundLayer);
private string GetCacheFilePath(
string originalPath,
int? width,
@@ -318,13 +345,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
if (percentPlayed > 0)
{
filename.Append(",p=");
filename.Append(percentPlayed);
filename.Append(",pp=");
filename.Append(percentPlayed.ToString(CultureInfo.InvariantCulture));
}
if (unwatchedCount.HasValue)
{
filename.Append(",p=");
filename.Append(",uc=");
filename.Append(unwatchedCount.Value);
}
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -12,6 +13,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: InternalsVisibleTo("Jellyfin.Server.Integration.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
@@ -0,0 +1,131 @@
using System;
using System.Globalization;
using System.IO;
using Jellyfin.Drawing;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Server.Integration.Tests;
public sealed class ImageProcessorTests : IDisposable
{
private const string CacheRoot = "image-cache";
private const string OriginalPath = "/media/poster.jpg";
private const string NoOverlayCacheKey = "/media/poster.jpg,quality=90,datemodified=638800000000000000,f=Jpg,width=200,height=300,maxwidth=400,maxheight=500,fillwidth=600,fillheight=700,blur=2,b=000000,fl=layer,v=4";
private static readonly DateTime _dateModified = new(638800000000000000, DateTimeKind.Utc);
private readonly ImageProcessor _imageProcessor;
public ImageProcessorTests()
{
var applicationPaths = new Mock<IServerApplicationPaths>();
applicationPaths.SetupGet(paths => paths.ImageCachePath).Returns(CacheRoot);
var configurationManager = new Mock<IServerConfigurationManager>();
configurationManager
.SetupGet(manager => manager.Configuration)
.Returns(new ServerConfiguration { ParallelImageEncodingLimit = 1 });
_imageProcessor = new ImageProcessor(
NullLogger<ImageProcessor>.Instance,
applicationPaths.Object,
Mock.Of<IFileSystem>(),
Mock.Of<IImageEncoder>(),
configurationManager.Object);
}
[Fact]
public void GetCacheFilePath_DifferentOverlayTypes_ReturnDifferentPaths()
{
var percentPlayedPath = GetCacheFilePath(percentPlayed: 1);
var unwatchedCountPath = GetCacheFilePath(unwatchedCount: 1);
Assert.NotEqual(percentPlayedPath, unwatchedCountPath);
}
[Fact]
public void GetCacheFilePath_DifferentPercentPlayedValues_ReturnDifferentPaths()
{
var firstPath = GetCacheFilePath(percentPlayed: 12.5);
var secondPath = GetCacheFilePath(percentPlayed: 75.5);
Assert.NotEqual(firstPath, secondPath);
}
[Fact]
public void GetCacheFilePath_DifferentUnwatchedCountValues_ReturnDifferentPaths()
{
var firstPath = GetCacheFilePath(unwatchedCount: 1);
var secondPath = GetCacheFilePath(unwatchedCount: 2);
Assert.NotEqual(firstPath, secondPath);
}
[Fact]
public void GetCacheFilePath_DifferentCultures_ReturnSamePath()
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
var expectedPath = GetCacheFilePath(percentPlayed: 12.5);
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
var actualPath = GetCacheFilePath(percentPlayed: 12.5);
Assert.Equal(expectedPath, actualPath);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Fact]
public void GetCacheFilePath_NoOverlay_UsesVersionFourWithExistingSerialization()
{
var expectedPath = _imageProcessor.GetCachePath(
Path.Combine(CacheRoot, "resized-images"),
NoOverlayCacheKey,
".jpg");
Assert.Equal(expectedPath, GetCacheFilePath());
}
public void Dispose()
{
_imageProcessor.Dispose();
}
private string GetCacheFilePath(double percentPlayed = 0, int? unwatchedCount = null)
{
var options = new ImageProcessingOptions
{
Width = 200,
Height = 300,
MaxWidth = 400,
MaxHeight = 500,
FillWidth = 600,
FillHeight = 700,
Quality = 90,
PercentPlayed = percentPlayed,
UnplayedCount = unwatchedCount,
Blur = 2,
BackgroundColor = "000000",
ForegroundLayer = "layer"
};
return _imageProcessor.GetCacheFilePath(
OriginalPath,
_dateModified,
ImageFormat.Jpg,
options);
}
}