aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-08-08 10:59:37 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-08-08 10:59:37 +0200
commite63c05137a236acc9fc09d69ebe3862449c43759 (patch)
tree414fb7e6c1b9b441f6bd0150ab68b2fe893f283e
parent70ffd25b505fff4423d7aab5386d46e327713708 (diff)
Fix missing ItemRemoved events and search fallback after access filtering
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs13
-rw-r--r--Emby.Server.Implementations/Library/Search/SearchManager.cs38
-rw-r--r--MediaBrowser.Controller/Library/SearchProviderQuery.cs12
3 files changed, 39 insertions, 24 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 19371f68d7..8f8f62425f 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -412,6 +412,13 @@ namespace Emby.Server.Implementations.Library
}
_persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
+
+ // Evict the deleted items from the cache and announce each removal.
+ foreach (var (item, _, _) in pathMaps)
+ {
+ _cache.TryRemove(item.Id, out _);
+ ReportItemRemoved(item, item.GetOwner() ?? item.GetParent());
+ }
}
public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
@@ -611,6 +618,12 @@ namespace Emby.Server.Implementations.Library
folder.UserData = null;
}
+ // Announce the descendants before the item itself.
+ foreach (var child in children)
+ {
+ ReportItemRemoved(child, item);
+ }
+
ReportItemRemoved(item, parent);
}
diff --git a/Emby.Server.Implementations/Library/Search/SearchManager.cs b/Emby.Server.Implementations/Library/Search/SearchManager.cs
index 01f9062734..0e180753a6 100644
--- a/Emby.Server.Implementations/Library/Search/SearchManager.cs
+++ b/Emby.Server.Implementations/Library/Search/SearchManager.cs
@@ -92,37 +92,33 @@ public class SearchManager : ISearchManager
await Task.WhenAll(externalTask, internalTask).ConfigureAwait(false);
var externalResults = await externalTask.ConfigureAwait(false);
- var fromExternal = externalResults.Count > 0;
- IReadOnlyList<SearchResult> results;
- if (fromExternal)
- {
- results = externalResults;
- }
- else
- {
- results = await internalTask.ConfigureAwait(false);
- if (_internalProviders.Length > 0)
- {
- _logger.LogDebug("No results from external providers, using internal provider results");
- }
- }
// Internal providers apply user-access filtering inline in their queries. External
// providers don't know about user permissions, so they may return IDs from hidden
- // libraries or items the user is otherwise blocked from. Run the post-filter only
- // when results came from externals to close that gap. The Items controller's second
- // roundtrip via folder.GetItems applies most of these again, but it does not restrict
- // by TopParentIds when ItemIds is set.
- if (fromExternal && results.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
+ // libraries or items the user is otherwise blocked from. Filter them here to close
+ // that gap. The Items controller's second roundtrip via folder.GetItems applies most
+ // of these again, but it does not restrict by TopParentIds when ItemIds is set.
+ if (externalResults.Count > 0 && query.UserId.HasValue && !query.UserId.Value.IsEmpty())
{
var user = _userManager.GetUserById(query.UserId.Value);
if (user is not null)
{
- results = await FilterByUserAccessAsync(results, user, query, cancellationToken).ConfigureAwait(false);
+ externalResults = await FilterByUserAccessAsync(externalResults, user, query, cancellationToken).ConfigureAwait(false);
}
}
- return results;
+ if (externalResults.Count > 0)
+ {
+ return externalResults;
+ }
+
+ var internalResults = await internalTask.ConfigureAwait(false);
+ if (_internalProviders.Length > 0)
+ {
+ _logger.LogDebug("No results from external providers, using internal provider results");
+ }
+
+ return internalResults;
}
private async Task<IReadOnlyList<SearchResult>> FilterByUserAccessAsync(
diff --git a/MediaBrowser.Controller/Library/SearchProviderQuery.cs b/MediaBrowser.Controller/Library/SearchProviderQuery.cs
index 845588c872..b1ff800fa0 100644
--- a/MediaBrowser.Controller/Library/SearchProviderQuery.cs
+++ b/MediaBrowser.Controller/Library/SearchProviderQuery.cs
@@ -19,7 +19,9 @@ public class SearchProviderQuery
public Guid? UserId { get; init; }
/// <summary>
- /// Gets the item types to include in the search.
+ /// Gets the item types to include in the search. An empty array means every type is eligible.
+ /// When this is non-empty it is the authoritative type filter and <see cref="ExcludeItemTypes"/>
+ /// does not apply; excludes only take effect when no include types were requested.
/// </summary>
public BaseItemKind[] IncludeItemTypes { get; init; } = [];
@@ -29,7 +31,9 @@ public class SearchProviderQuery
public BaseItemKind[] ExcludeItemTypes { get; init; } = [];
/// <summary>
- /// Gets the media types to include in the search.
+ /// Gets the media types to include in the search. This is an additional constraint rather than
+ /// an alternative one: a provider must return only items that match both the requested media
+ /// types and the requested item types, not the union of the two.
/// </summary>
public MediaType[] MediaTypes { get; init; } = [];
@@ -39,7 +43,9 @@ public class SearchProviderQuery
public int? Limit { get; init; }
/// <summary>
- /// Gets the parent ID to scope the search.
+ /// Gets the parent ID to scope the search. This scopes to the whole subtree, not just direct
+ /// children - callers routinely pass a library folder id and expect items nested arbitrarily
+ /// deep beneath it (an episode under a season under a series) to match.
/// </summary>
public Guid? ParentId { get; init; }
}