aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
authorPiotr Niełacny <piotr.nielacny@gmail.com>2026-03-10 16:53:12 +0100
committerPiotr Niełacny <piotr.nielacny@gmail.com>2026-07-29 10:41:41 +0200
commit9693f11fe5507a005fc7fcd0e30ef918ab1f214d (patch)
treead3eba15cc221d3e9d0fc531b4e82e2fc6b40244 /MediaBrowser.Controller
parentafb716566d04102fa6c8e1b12977d175db301081 (diff)
Fix NameStartsWith filter not matching numeric item names
The NameStartsWith, NameStartsWithOrGreater, and NameLessThan API filters compare user input against the internal SortName, which zero-pads numbers to 10 digits for natural sorting. This caused numeric filters like NameStartsWith=1 to fail because "1" doesn't match the padded sort name "0000000012...". Normalize filter input through ModifySortChunks before comparison so that padding is applied consistently. For NameStartsWith, add a TrimStart('0') fallback so that numeric prefixes like "1" correctly match sort names such as "0000000012 years a slave". Add NormalizeSortNameFilter helper on BaseItem and make ModifySortChunks public. Add unit tests covering numeric, alphabetic, and mixed prefix matching for all three filter types. Fixes #1470
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs17
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs11
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs10
3 files changed, 31 insertions, 7 deletions
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 209feac702..bdc236ffdb 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -982,7 +982,22 @@ namespace MediaBrowser.Controller.Entities
return ModifySortChunks(sortable);
}
- internal static string ModifySortChunks(ReadOnlySpan<char> name)
+ /// <summary>
+ /// Normalizes a user-provided name filter value to match against <see cref="SortName"/>.
+ /// Applies the same zero-padding, diacritics removal, and transliteration as sort name generation.
+ /// </summary>
+ /// <param name="input">The raw filter value from the API.</param>
+ /// <returns>The normalized value suitable for comparison against SortName.</returns>
+ public static string NormalizeSortNameFilter(string input)
+ => ModifySortChunks(input).ToLowerInvariant();
+
+ /// <summary>
+ /// Processes a name by zero-padding numeric chunks to 10 digits for natural sort order,
+ /// removing diacritics, and transliterating non-ASCII characters.
+ /// </summary>
+ /// <param name="name">The input name to process.</param>
+ /// <returns>The processed string with padded numeric chunks.</returns>
+ public static string ModifySortChunks(ReadOnlySpan<char> name)
{
static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
{
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index b1f7f29bad..70f877b421 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1100,17 +1100,22 @@ namespace MediaBrowser.Controller.Entities
{
if (!string.IsNullOrWhiteSpace(query.NameStartsWith))
{
- items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase));
+ var paddedPrefix = BaseItem.NormalizeSortNameFilter(query.NameStartsWith);
+ items = items.Where(i =>
+ i.SortName.StartsWith(paddedPrefix, StringComparison.OrdinalIgnoreCase)
+ || i.SortName.TrimStart('0').StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater))
{
- items = items.Where(i => string.Compare(i.SortName, query.NameStartsWithOrGreater, StringComparison.OrdinalIgnoreCase) >= 0);
+ var paddedValue = BaseItem.NormalizeSortNameFilter(query.NameStartsWithOrGreater);
+ items = items.Where(i => string.Compare(i.SortName, paddedValue, StringComparison.OrdinalIgnoreCase) >= 0);
}
if (!string.IsNullOrWhiteSpace(query.NameLessThan))
{
- items = items.Where(i => string.Compare(i.SortName, query.NameLessThan, StringComparison.OrdinalIgnoreCase) < 0);
+ var paddedValue = BaseItem.NormalizeSortNameFilter(query.NameLessThan);
+ items = items.Where(i => string.Compare(i.SortName, paddedValue, StringComparison.OrdinalIgnoreCase) < 0);
}
return items;
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 9ba103cc8b..2ccf898c1b 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -514,18 +514,22 @@ namespace MediaBrowser.Controller.Entities
IUserDataManager userDataManager,
ILibraryManager libraryManager)
{
- if (!string.IsNullOrEmpty(query.NameStartsWith) && !item.SortName.StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase))
+ if (!string.IsNullOrEmpty(query.NameStartsWith)
+ && !item.SortName.StartsWith(BaseItem.NormalizeSortNameFilter(query.NameStartsWith), StringComparison.InvariantCultureIgnoreCase)
+ && !item.SortName.TrimStart('0').StartsWith(query.NameStartsWith, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
#pragma warning disable CA1309 // Use ordinal string comparison
- if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater) && string.Compare(query.NameStartsWithOrGreater, item.SortName, StringComparison.InvariantCultureIgnoreCase) == 1)
+ if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)
+ && string.Compare(BaseItem.NormalizeSortNameFilter(query.NameStartsWithOrGreater), item.SortName, StringComparison.InvariantCultureIgnoreCase) == 1)
{
return false;
}
- if (!string.IsNullOrEmpty(query.NameLessThan) && string.Compare(query.NameLessThan, item.SortName, StringComparison.InvariantCultureIgnoreCase) != 1)
+ if (!string.IsNullOrEmpty(query.NameLessThan)
+ && string.Compare(BaseItem.NormalizeSortNameFilter(query.NameLessThan), item.SortName, StringComparison.InvariantCultureIgnoreCase) != 1)
#pragma warning restore CA1309 // Use ordinal string comparison
{
return false;