aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBeatriz Teixeira <beatriz.teixeira@tecnico.ulisboa.pt>2026-03-26 13:02:54 +0000
committerBeatriz Teixeira <beatriz.teixeira@tecnico.ulisboa.pt>2026-03-29 15:27:32 +0100
commit8ceb8c23cead6afbeb79605c4ef53b7d4372cbd8 (patch)
treef80d7cb79cc3d2d1b3cbc79f2bba012df5fe6504
parentb83378d656adef9c2e0e7df101f7da84ef762fa5 (diff)
fix(dto): prefer PlaylistsFolder primary image for playlists tiles
This patch fixes issue #16032 where the Playlists media folder ignored a user-uploaded Primary image and kept showing the generated collage. The root cause was DTO image precedence on UserView items for CollectionType.playlists. We now prefer the display parent (PlaylistsFolder) Primary image when available by clearing the UserView Primary tag and setting ParentPrimaryImageItemId/ParentPrimaryImageTag. Added tests cover both paths: parent custom image preferred, and fallback to existing UserView Primary when parent has none.
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs15
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceImageInheritanceTests.cs137
2 files changed, 152 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index b392340f71..ac21c1c6d3 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1363,6 +1363,21 @@ namespace Emby.Server.Implementations.Dto
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
{
+ if (item is UserView { ViewType: CollectionType.playlists } playlistsView
+ && options.GetImageLimit(ImageType.Primary) > 0
+ && !playlistsView.DisplayParentId.IsEmpty())
+ {
+ var displayParent = _libraryManager.GetItemById(playlistsView.DisplayParentId);
+ var displayParentPrimaryImage = displayParent?.GetImageInfo(ImageType.Primary, 0);
+
+ if (displayParentPrimaryImage is not null)
+ {
+ dto.ImageTags?.Remove(ImageType.Primary);
+ dto.ParentPrimaryImageItemId = displayParent!.Id;
+ dto.ParentPrimaryImageTag = GetTagAndFillBlurhash(dto, displayParent, displayParentPrimaryImage);
+ }
+ }
+
if (!item.SupportsInheritedParentImages)
{
return;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceImageInheritanceTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceImageInheritanceTests.cs
new file mode 100644
index 0000000000..96625ae670
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Dto/DtoServiceImageInheritanceTests.cs
@@ -0,0 +1,137 @@
+using System;
+using Emby.Server.Implementations.Dto;
+using Emby.Server.Implementations.Playlists;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
+using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Entities;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Dto;
+
+public class DtoServiceImageInheritanceTests
+{
+ [Fact]
+ public void GetBaseItemDto_PlaylistsUserViewWithDisplayParentPrimary_UsesDisplayParentPrimaryImage()
+ {
+ var displayParent = new PlaylistsFolder
+ {
+ Id = Guid.NewGuid(),
+ ImageInfos =
+ [
+ new ItemImageInfo
+ {
+ Type = ImageType.Primary,
+ Path = "/images/playlists-custom.jpg",
+ DateModified = new DateTime(2026, 1, 15, 12, 0, 0, DateTimeKind.Utc)
+ }
+ ]
+ };
+
+ var userView = new UserView
+ {
+ Id = Guid.NewGuid(),
+ ViewType = CollectionType.playlists,
+ DisplayParentId = displayParent.Id,
+ ImageInfos =
+ [
+ new ItemImageInfo
+ {
+ Type = ImageType.Primary,
+ Path = "/images/generated.png",
+ DateModified = new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc)
+ }
+ ]
+ };
+
+ var dtoService = BuildDtoService(displayParent);
+
+ var dto = dtoService.GetBaseItemDto(userView, new DtoOptions(false));
+
+ Assert.NotNull(dto.ParentPrimaryImageItemId);
+ Assert.Equal(displayParent.Id, dto.ParentPrimaryImageItemId);
+ Assert.Equal("/images/playlists-custom.jpg", dto.ParentPrimaryImageTag);
+ Assert.False(dto.ImageTags?.ContainsKey(ImageType.Primary));
+ }
+
+ [Fact]
+ public void GetBaseItemDto_PlaylistsUserViewWithoutDisplayParentPrimary_KeepsOwnPrimaryImage()
+ {
+ var displayParent = new PlaylistsFolder
+ {
+ Id = Guid.NewGuid(),
+ ImageInfos = []
+ };
+
+ var userView = new UserView
+ {
+ Id = Guid.NewGuid(),
+ ViewType = CollectionType.playlists,
+ DisplayParentId = displayParent.Id,
+ ImageInfos =
+ [
+ new ItemImageInfo
+ {
+ Type = ImageType.Primary,
+ Path = "/images/generated.png",
+ DateModified = new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc)
+ }
+ ]
+ };
+
+ var dtoService = BuildDtoService(displayParent);
+
+ var dto = dtoService.GetBaseItemDto(userView, new DtoOptions(false));
+
+ Assert.Null(dto.ParentPrimaryImageItemId);
+ Assert.Null(dto.ParentPrimaryImageTag);
+ Assert.NotNull(dto.ImageTags);
+ Assert.True(dto.ImageTags.ContainsKey(ImageType.Primary));
+ Assert.Equal("/images/generated.png", dto.ImageTags[ImageType.Primary]);
+ }
+
+ private static DtoService BuildDtoService(BaseItem displayParent)
+ {
+ var libraryManager = new Mock<ILibraryManager>();
+ var userDataManager = new Mock<IUserDataManager>();
+ var imageProcessor = new Mock<IImageProcessor>();
+ var providerManager = new Mock<IProviderManager>();
+ var recordingsManager = new Mock<IRecordingsManager>();
+ var appHost = new Mock<IApplicationHost>();
+ var mediaSourceManager = new Mock<IMediaSourceManager>();
+ var liveTvManager = new Mock<ILiveTvManager>();
+ var trickplayManager = new Mock<ITrickplayManager>();
+ var chapterManager = new Mock<IChapterManager>();
+ var logger = new Mock<Microsoft.Extensions.Logging.ILogger<DtoService>>();
+
+ libraryManager
+ .Setup(x => x.GetItemById(displayParent.Id))
+ .Returns(displayParent);
+
+ imageProcessor
+ .Setup(x => x.GetImageCacheTag(It.IsAny<BaseItem>(), It.IsAny<ItemImageInfo>()))
+ .Returns<BaseItem, ItemImageInfo>((_, image) => image.Path);
+
+ return new DtoService(
+ logger.Object,
+ libraryManager.Object,
+ userDataManager.Object,
+ imageProcessor.Object,
+ providerManager.Object,
+ recordingsManager.Object,
+ appHost.Object,
+ mediaSourceManager.Object,
+ new Lazy<ILiveTvManager>(() => liveTvManager.Object),
+ trickplayManager.Object,
+ chapterManager.Object);
+ }
+}