aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-01-31 23:44:07 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-01-31 23:45:38 +0100
commit09a729effe1c0eeccd3bbc7482923b4d1cdabfc1 (patch)
tree6e4cece5eac29c5e0252724fff2d868786177a78
parent2789532aa88ccc899ff8497537642e1d78b31ef5 (diff)
Fix tag checks
-rw-r--r--Jellyfin.Api/Controllers/ItemsController.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs69
-rw-r--r--Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs35
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs25
5 files changed, 81 insertions, 52 deletions
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 9674ecd092..f8c715dc86 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -530,7 +530,7 @@ public class ItemsController : BaseJellyfinApiController
return new QueryResult<BaseItemDto>(
startIndex,
result.TotalRecordCount,
- _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user));
+ _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user, skipVisibilityCheck: true));
}
/// <summary>
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 55ef5972d4..778cedaecf 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -1303,7 +1303,7 @@ public sealed class BaseItemRepository
.ToArray();
var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
{
- CleanValue = GetCleanValue(f.Value),
+ CleanValue = f.Value.GetCleanValue(),
ItemValueId = Guid.NewGuid(),
Type = f.MagicNumber,
Value = f.Value
@@ -1877,7 +1877,7 @@ public sealed class BaseItemRepository
entity.IndexNumber = dto.IndexNumber;
entity.IsLocked = dto.IsLocked;
entity.Name = dto.Name;
- entity.CleanName = GetCleanValue(dto.Name);
+ entity.CleanName = dto.Name.GetCleanValue();
entity.OfficialRating = dto.OfficialRating;
entity.Overview = dto.Overview;
entity.ParentIndexNumber = dto.ParentIndexNumber;
@@ -2308,33 +2308,6 @@ public sealed class BaseItemRepository
}
}
- /// <summary>
- /// Normalizes a value for clean comparison by removing diacritics, punctuation, and converting to lowercase.
- /// </summary>
- /// <param name="value">The value to clean.</param>
- /// <returns>The normalized value.</returns>
- public static string GetCleanValue(string value)
- {
- if (string.IsNullOrWhiteSpace(value))
- {
- return value;
- }
-
- // Remove diacritics and convert to lowercase
- var cleaned = value.RemoveDiacritics().ToLowerInvariant();
-
- // Replace all punctuation and special characters with spaces
- // This includes: periods, commas, colons, semicolons, hyphens, underscores,
- // parentheses, brackets, braces, quotes, apostrophes, exclamation marks,
- // question marks, ampersands, slashes, backslashes, em/en dashes, etc.
- cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"[^\p{L}\p{N}\s]", " ");
-
- // Collapse multiple spaces into single space and trim
- cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"\s+", " ").Trim();
-
- return cleaned;
- }
-
private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
{
var list = new List<(ItemValueType, string)>();
@@ -2664,7 +2637,7 @@ public sealed class BaseItemRepository
if (!string.IsNullOrEmpty(filter.SearchTerm))
{
- var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
+ var cleanedSearchTerm = filter.SearchTerm.GetCleanValue();
var originalSearchTerm = filter.SearchTerm;
if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
{
@@ -2893,7 +2866,7 @@ public sealed class BaseItemRepository
}
else
{
- var cleanName = GetCleanValue(filter.Name);
+ var cleanName = filter.Name.GetCleanValue();
baseQuery = baseQuery.Where(e => e.CleanName == cleanName);
}
}
@@ -3078,21 +3051,21 @@ public sealed class BaseItemRepository
if (filter.Genres.Count > 0)
{
- var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
+ var cleanGenres = filter.Genres.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
baseQuery = baseQuery
.Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(cleanGenres));
}
if (tags.Count > 0)
{
- var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
+ var cleanValues = tags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
baseQuery = baseQuery
.Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues));
}
if (excludeTags.Count > 0)
{
- var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
+ var cleanValues = excludeTags.Select(e => e.GetCleanValue()).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
baseQuery = baseQuery
.Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues));
}
@@ -3486,23 +3459,29 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0)
{
- var excludedTags = filter.ExcludeInheritedTags;
+ var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
baseQuery = baseQuery.Where(e =>
!context.ItemValuesMap.Any(f =>
f.ItemValue.Type == ItemValueType.Tags
&& excludedTags.Contains(f.ItemValue.CleanValue)
- && (f.ItemId == e.Id || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value))));
+ && (f.ItemId == e.Id
+ || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value)
+ || e.Parents!.Any(p => f.ItemId == p.ParentItemId)
+ || (e.TopParentId.HasValue && f.ItemId == e.TopParentId.Value))));
}
if (filter.IncludeInheritedTags.Length > 0)
{
- var includeTags = filter.IncludeInheritedTags;
+ var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
baseQuery = baseQuery.Where(e =>
context.ItemValuesMap.Any(f =>
f.ItemValue.Type == ItemValueType.Tags
&& includeTags.Contains(f.ItemValue.CleanValue)
- && (f.ItemId == e.Id || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value)))
+ && (f.ItemId == e.Id
+ || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value)
+ || e.Parents!.Any(p => f.ItemId == p.ParentItemId)
+ || (e.TopParentId.HasValue && f.ItemId == e.TopParentId.Value)))
// A playlist should be accessible to its owner regardless of allowed tags
|| (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
@@ -3864,23 +3843,29 @@ public sealed class BaseItemRepository
// Apply excluded tags filtering (blocked tags)
if (filter.ExcludeInheritedTags.Length > 0)
{
- var excludedTags = filter.ExcludeInheritedTags;
+ var excludedTags = filter.ExcludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
baseQuery = baseQuery.Where(e =>
!context.ItemValuesMap.Any(f =>
f.ItemValue.Type == ItemValueType.Tags
&& excludedTags.Contains(f.ItemValue.CleanValue)
- && (f.ItemId == e.Id || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value))));
+ && (f.ItemId == e.Id
+ || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value)
+ || e.Parents!.Any(p => f.ItemId == p.ParentItemId)
+ || (e.TopParentId.HasValue && f.ItemId == e.TopParentId.Value))));
}
// Apply included tags filtering (allowed tags - item must have at least one)
if (filter.IncludeInheritedTags.Length > 0)
{
- var includeTags = filter.IncludeInheritedTags;
+ var includeTags = filter.IncludeInheritedTags.Select(e => e.GetCleanValue()).ToArray();
baseQuery = baseQuery.Where(e =>
context.ItemValuesMap.Any(f =>
f.ItemValue.Type == ItemValueType.Tags
&& includeTags.Contains(f.ItemValue.CleanValue)
- && (f.ItemId == e.Id || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value))));
+ && (f.ItemId == e.Id
+ || (e.SeriesId.HasValue && f.ItemId == e.SeriesId.Value)
+ || e.Parents!.Any(p => f.ItemId == p.ParentItemId)
+ || (e.TopParentId.HasValue && f.ItemId == e.TopParentId.Value))));
}
return baseQuery;
diff --git a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs
index eadabf6776..4b5659cd6c 100644
--- a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs
+++ b/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs
@@ -61,7 +61,7 @@ public class RefreshCleanNames : IAsyncMigrationRoutine
{
try
{
- var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : BaseItemRepository.GetCleanValue(item.Name);
+ var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : item.Name.GetCleanValue();
if (!string.Equals(newCleanName, item.CleanName, StringComparison.Ordinal))
{
_logger.LogDebug(
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 13af7a6178..f1c1555842 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1671,16 +1671,26 @@ namespace MediaBrowser.Controller.Entities
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
{
- var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
- var blockedTagsPreference = user.GetPreference(PreferenceKind.BlockedTags);
- var needsTagCheck = allowedTagsPreference.Length > 0 || blockedTagsPreference.Length > 0;
- if (!needsTagCheck)
+ var blockedTags = user.GetPreference(PreferenceKind.BlockedTags);
+ var allowedTags = user.GetPreference(PreferenceKind.AllowedTags);
+
+ if (blockedTags.Length == 0 && allowedTags.Length == 0)
{
return true;
}
- var allTags = GetInheritedTags();
- if (blockedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ // Normalize tags using the same logic as database queries
+ var normalizedBlockedTags = blockedTags
+ .Where(t => !string.IsNullOrWhiteSpace(t))
+ .Select(t => t.GetCleanValue())
+ .ToHashSet(StringComparer.Ordinal);
+
+ var normalizedItemTags = GetInheritedTags()
+ .Select(t => t.GetCleanValue())
+ .ToHashSet(StringComparer.Ordinal);
+
+ // Check blocked tags - item is hidden if it has any blocked tag
+ if (normalizedBlockedTags.Overlaps(normalizedItemTags))
{
return false;
}
@@ -1691,9 +1701,18 @@ namespace MediaBrowser.Controller.Entities
return true;
}
- if (!skipAllowedTagsCheck && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
+ // Check allowed tags - item must have at least one allowed tag
+ if (!skipAllowedTagsCheck && allowedTags.Length > 0)
{
- return false;
+ var normalizedAllowedTags = allowedTags
+ .Where(t => !string.IsNullOrWhiteSpace(t))
+ .Select(t => t.GetCleanValue())
+ .ToHashSet(StringComparer.Ordinal);
+
+ if (!normalizedAllowedTags.Overlaps(normalizedItemTags))
+ {
+ return false;
+ }
}
return true;
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 60df47113a..01225cf283 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -148,5 +148,30 @@ namespace Jellyfin.Extensions
{
return string.IsNullOrEmpty(text) ? text : text.AsSpan().LeftPart('\0').ToString();
}
+
+ /// <summary>
+ /// Normalizes a string for comparison by removing diacritics, converting to lowercase,
+ /// replacing punctuation/special characters with spaces, and collapsing whitespace.
+ /// </summary>
+ /// <param name="value">The string to normalize.</param>
+ /// <returns>The normalized string, or the original if null/whitespace.</returns>
+ public static string GetCleanValue(this string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return value;
+ }
+
+ // Remove diacritics and convert to lowercase
+ var cleaned = value.RemoveDiacritics().ToLowerInvariant();
+
+ // Replace all punctuation and special characters with spaces
+ cleaned = Regex.Replace(cleaned, @"[^\p{L}\p{N}\s]", " ");
+
+ // Collapse multiple spaces into single space and trim
+ cleaned = Regex.Replace(cleaned, @"\s+", " ").Trim();
+
+ return cleaned;
+ }
}
}