Merge pull request #17898 from Shadowghost/filters2

Populate missing tags array in Filters2
This commit is contained in:
Cody Robibero authored and GitHub committed 2026-09-12 10:44:11 -04:00
commit 3d52fbe84e
6 files changed
+120

No files matched your search

@@ -4127,6 +4127,18 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetQueryFiltersLegacy(query);
}
/// <inheritdoc />
public IReadOnlyList<string> GetTagNames(InternalItemsQuery query)
{
if (query.User is not null)
{
AddUserToQuery(query, query.User);
}
SetTopParentOrAncestorIds(query);
return _itemRepository.GetTagNames(query);
}
/// <inheritdoc />
public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
{
@@ -178,16 +178,35 @@ public class FilterController : BaseJellyfinApiController
IsSeries = isSeries
};
var tagQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = includeItemTypes,
DtoOptions = new DtoOptions
{
Fields = Array.Empty<ItemFields>(),
EnableImages = false,
EnableUserData = false
},
IsAiring = isAiring,
IsMovie = isMovie,
IsSports = isSports,
IsKids = isKids,
IsNews = isNews,
IsSeries = isSeries
};
if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
{
var ancestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
genreQuery.AncestorIds = ancestorIds;
streamLanguageQuery.AncestorIds = ancestorIds;
tagQuery.AncestorIds = ancestorIds;
}
else
{
genreQuery.Parent = parentItem;
streamLanguageQuery.Parent = parentItem;
tagQuery.Parent = parentItem;
}
if ((includeItemTypes.Contains(BaseItemKind.Series) || includeItemTypes.Contains(BaseItemKind.Season))
@@ -218,6 +237,8 @@ public class FilterController : BaseJellyfinApiController
}).ToArray();
}
filters.Tags = _libraryManager.GetTagNames(tagQuery);
if (includeItemTypes.Contains(BaseItemKind.Movie)
|| includeItemTypes.Contains(BaseItemKind.Series)
|| includeItemTypes.Contains(BaseItemKind.Season)
@@ -117,6 +117,35 @@ public sealed partial class BaseItemRepository
.ToArray();
}
/// <inheritdoc />
public IReadOnlyList<string> GetTagNames(InternalItemsQuery filter)
{
ArgumentNullException.ThrowIfNull(filter);
PrepareFilterQuery(filter);
using var context = _dbProvider.CreateDbContext();
var baseQuery = PrepareItemQuery(context, filter);
baseQuery = TranslateQuery(baseQuery, context, filter);
var matchingItemIds = baseQuery.Select(e => e.Id);
// Project the join before grouping. Grouping over the ItemValue navigation instead makes EF
// re-resolve the aggregate as a correlated subquery per group, which is orders of magnitude slower.
return context.ItemValuesMap
.AsNoTracking()
.Join(
context.ItemValues,
ivm => ivm.ItemValueId,
iv => iv.ItemValueId,
(ivm, iv) => new { ivm.ItemId, iv.Type, iv.CleanValue, iv.Value })
.Where(iv => iv.Type == ItemValueType.Tags)
.Where(iv => matchingItemIds.Contains(iv.ItemId))
.GroupBy(iv => iv.CleanValue)
.Select(g => g.Min(iv => iv.Value)!)
.OrderBy(t => t)
.ToArray();
}
private string[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
{
using var context = _dbProvider.CreateDbContext();
@@ -832,6 +832,13 @@ namespace MediaBrowser.Controller.Library
/// <returns>Aggregated filter values.</returns>
QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query);
/// <summary>
/// Gets a list of all distinct tags of the matching items.
/// </summary>
/// <param name="query">The query filter.</param>
/// <returns>List of tags.</returns>
IReadOnlyList<string> GetTagNames(InternalItemsQuery query);
/// <summary>
/// Gets a list of all language codes of the provided stream type.
/// </summary>
@@ -128,6 +128,13 @@ public interface IItemRepository
/// <returns>List of language codes.</returns>
public IReadOnlyList<string> GetMediaStreamLanguages(InternalItemsQuery filter, MediaStreamType mediaStreamType);
/// <summary>
/// Gets all distinct tags of the matching base items.
/// </summary>
/// <param name="filter">The query filter.</param>
/// <returns>The list of tags.</returns>
IReadOnlyList<string> GetTagNames(InternalItemsQuery filter);
/// <summary>
/// Gets all artist names.
/// </summary>
@@ -103,6 +103,50 @@ public sealed class BaseItemRepositoryItemValueTests : SqliteDbTestFixture
Assert.Equal(["Genre Leak"], result.Genres);
}
[Fact]
public void GetTagNames_GroupsAndFiltersItemValues()
{
var movie = CreateMovieEntity(Guid.NewGuid(), "Movie");
var otherMovie = CreateMovieEntity(Guid.NewGuid(), "Other Movie");
var audio = new BaseItemEntity
{
Id = Guid.NewGuid(),
Type = _audioTypeName,
Name = "Excluded Audio",
MediaType = "Audio",
IsMovie = false,
IsFolder = false,
IsVirtualItem = false
};
var tag = CreateItemValue(ItemValueType.Tags, "Alpha", "alpha");
var duplicateTag = CreateItemValue(ItemValueType.Tags, "alpha", "alpha");
var otherTag = CreateItemValue(ItemValueType.Tags, "Beta", "beta");
var inheritedTag = CreateItemValue(ItemValueType.InheritedTags, "Inherited", "inherited");
var genre = CreateItemValue(ItemValueType.Genre, "Genre Leak", "genre leak");
var excludedTag = CreateItemValue(ItemValueType.Tags, "Excluded Tag", "excluded tag");
using (var context = CreateDbContext())
{
context.BaseItems.AddRange(movie, otherMovie, audio);
context.ItemValues.AddRange(tag, duplicateTag, otherTag, inheritedTag, genre, excludedTag);
context.ItemValuesMap.AddRange(
CreateMap(movie, tag),
CreateMap(movie, duplicateTag),
CreateMap(otherMovie, otherTag),
CreateMap(movie, inheritedTag),
CreateMap(movie, genre),
CreateMap(audio, excludedTag));
context.SaveChanges();
}
var result = _repository.GetTagNames(new InternalItemsQuery(new Database.Implementations.Entities.User("test", "auth", "reset"))
{
IncludeItemTypes = [BaseItemKind.Movie]
});
Assert.Equal(["Alpha", "Beta"], result);
}
[Fact]
public void GetGenreNames_GroupsAndFiltersMappedItemValues()
{