Merge pull request #17579 from Shadowghost/fix-removal-notification

Fix missing ItemRemoved events and search fallback after access filtering
This commit is contained in:
Cody Robibero
2026-08-10 18:28:22 -04:00
committed by GitHub
3 changed files with 39 additions and 24 deletions
@@ -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);
}
@@ -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(
@@ -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; }
}