diff options
| author | Shadowghost <Shadowghost@users.noreply.github.com> | 2026-09-27 16:30:49 -0400 |
|---|---|---|
| committer | Cody Robibero <cody@robibe.ro> | 2026-09-27 16:30:49 -0400 |
| commit | 0b907a9790f86960d7ee9712b9422e2a859eaabb (patch) | |
| tree | f34f20a72e3af9ab1e57433be0ed7beeedd058a5 | |
| parent | 91ac66ce0b02faac5a646bcbcdc4d505872fea43 (diff) | |
Backport pull request #18138 from jellyfin/release-12.z
Don't apply the allowed tags filter to by-name items
Original-merge: 76194d76993e048c555d10dd74f6c61b2ea3cff8
Merged-by: crobibero <cody@robibe.ro>
Backported-by: Cody Robibero <cody@robibe.ro>
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs | 7 | ||||
| -rw-r--r-- | tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryAllowedTagsByNameTests.cs | 158 |
2 files changed, 164 insertions, 1 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs index 51ac146a6f..4199f83c21 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs @@ -223,7 +223,12 @@ public sealed partial class BaseItemRepository Years = filter.Years, NameContains = filter.NameContains, SearchTerm = filter.SearchTerm, - ExcludeItemIds = filter.ExcludeItemIds + ExcludeItemIds = filter.ExcludeItemIds, + + // A genre, studio or artist carries none of the tags of the media it describes, so an + // allow list can only ever hide all of them. Reachability is settled by innerQueryFilter + // instead: a value gets this far only when an item the user may see carries it. + IncludeInheritedTags = [] }; // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) into one diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryAllowedTagsByNameTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryAllowedTagsByNameTests.cs new file mode 100644 index 0000000000..3c8bd9fd50 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryAllowedTagsByNameTests.cs @@ -0,0 +1,158 @@ +using System; +using System.Linq; +using Emby.Server.Implementations.Data; +using Jellyfin.Data; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Server.Implementations.Item; +using MediaBrowser.Controller.Entities; +using Xunit; +using BaseItemKind = Jellyfin.Data.Enums.BaseItemKind; + +namespace Jellyfin.Server.Implementations.Tests.Item; + +/// <summary> +/// A user restricted to a set of allowed tags still has to see the by-name items - genres, studios, +/// artists - of the media those tags let through. The by-name item carries none of the media's tags, +/// so the allowed-tags filter would otherwise hide every one of them. +/// </summary> +public sealed class BaseItemRepositoryAllowedTagsByNameTests : SqliteDbTestFixture +{ + private const string AllowedTag = "kids"; + + private readonly BaseItemRepository _repository; + private readonly ItemTypeLookup _itemTypeLookup; + + public BaseItemRepositoryAllowedTagsByNameTests() + { + _itemTypeLookup = new ItemTypeLookup(); + _repository = CreateBaseItemRepository(_itemTypeLookup); + Seed(); + } + + [Fact] + public void GetGenres_WithAllowedTags_ReturnsGenresOfAllowedItems() + { + var result = _repository.GetGenres(CreateQuery(AllowedTag)); + + Assert.Equal(["Comedy"], Names(result)); + } + + [Fact] + public void GetStudios_WithAllowedTags_ReturnsStudiosOfAllowedItems() + { + var result = _repository.GetStudios(CreateQuery(AllowedTag)); + + Assert.Equal(["Pixar"], Names(result)); + } + + [Fact] + public void GetAllArtists_WithAllowedTags_ReturnsArtistsOfAllowedItems() + { + var result = _repository.GetAllArtists(CreateQuery(AllowedTag)); + + Assert.Equal(["Raffi"], Names(result)); + } + + [Fact] + public void GetGenres_WithoutAllowedTags_ReturnsEveryGenre() + { + var result = _repository.GetGenres(CreateQuery()); + + Assert.Equal(["Comedy", "Horror", "Jazz"], Names(result)); + } + + private static string[] Names(MediaBrowser.Model.Querying.QueryResult<(BaseItem Item, MediaBrowser.Model.Dto.ItemCounts? ItemCounts)> result) + => result.Items.Select(i => i.Item.Name!).Order(StringComparer.Ordinal).ToArray(); + + private static InternalItemsQuery CreateQuery(params string[] allowedTags) + { + var user = new User("restricted", "auth", "reset"); + if (allowedTags.Length > 0) + { + user.SetPreference(PreferenceKind.AllowedTags, allowedTags); + } + + return new InternalItemsQuery(user); + } + + private void Seed() + { + using var context = CreateDbContext(); + + // Tagged media, plus the by-name items describing it. + var movie = CreateItem(BaseItemKind.Movie, "Allowed Movie", mediaType: "Video"); + var song = CreateItem(BaseItemKind.Audio, "Allowed Song", mediaType: "Audio"); + var genre = CreateItem(BaseItemKind.Genre, "Comedy"); + var studio = CreateItem(BaseItemKind.Studio, "Pixar"); + var artist = CreateItem(BaseItemKind.MusicArtist, "Raffi"); + + // Media the allow list keeps out, plus the by-name items only it is described by. + var blockedMovie = CreateItem(BaseItemKind.Movie, "Untagged Movie", mediaType: "Video"); + var blockedGenre = CreateItem(BaseItemKind.Genre, "Horror"); + + // A genre written on nothing but a by-name item: that item is no more visible than the + // media behind it, so the genre must not leak into a restricted user's list. + var artistOnlyGenre = CreateItem(BaseItemKind.Genre, "Jazz"); + var blockedArtist = CreateItem(BaseItemKind.MusicArtist, "Mingus"); + + var tag = CreateItemValue(ItemValueType.Tags, AllowedTag); + var comedy = CreateItemValue(ItemValueType.Genre, "Comedy"); + var horror = CreateItemValue(ItemValueType.Genre, "Horror"); + var jazz = CreateItemValue(ItemValueType.Genre, "Jazz"); + var pixar = CreateItemValue(ItemValueType.Studios, "Pixar"); + var raffi = CreateItemValue(ItemValueType.Artist, "Raffi"); + + context.BaseItems.AddRange(movie, song, genre, studio, artist, blockedMovie, blockedGenre, artistOnlyGenre, blockedArtist); + context.ItemValues.AddRange(tag, comedy, horror, jazz, pixar, raffi); + context.ItemValuesMap.AddRange( + CreateMap(movie, tag), + CreateMap(movie, comedy), + CreateMap(movie, pixar), + CreateMap(song, tag), + CreateMap(song, raffi), + CreateMap(blockedMovie, horror), + CreateMap(blockedArtist, jazz)); + context.SaveChanges(); + } + + private BaseItemEntity CreateItem(BaseItemKind kind, string name, string? mediaType = null) + { + var id = Guid.NewGuid(); + + return new BaseItemEntity + { + Id = id, + Type = _itemTypeLookup.BaseItemKindNames[kind], + Name = name, + CleanName = name.ToLowerInvariant(), + PresentationUniqueKey = id.ToString("N"), + MediaType = mediaType, + IsMovie = kind == BaseItemKind.Movie, + IsFolder = false, + IsVirtualItem = false + }; + } + + private static ItemValue CreateItemValue(ItemValueType type, string value) + { + return new ItemValue + { + ItemValueId = Guid.NewGuid(), + Type = type, + Value = value, + CleanValue = value.ToLowerInvariant() + }; + } + + private static ItemValueMap CreateMap(BaseItemEntity item, ItemValue itemValue) + { + return new ItemValueMap + { + ItemId = item.Id, + ItemValueId = itemValue.ItemValueId, + Item = item, + ItemValue = itemValue + }; + } +} |
