From 07a802d8fa93460c9f2a7f42da7a1f14a893a322 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 3 May 2026 23:33:56 +0200 Subject: Implement search providers --- .../Library/IExternalSearchProvider.cs | 20 ++++++++ .../Library/IInternalSearchProvider.cs | 8 +++ MediaBrowser.Controller/Library/ISearchEngine.cs | 18 ------- MediaBrowser.Controller/Library/ISearchManager.cs | 48 +++++++++++++++++ MediaBrowser.Controller/Library/ISearchProvider.cs | 44 ++++++++++++++++ .../Library/SearchProviderQuery.cs | 45 ++++++++++++++++ MediaBrowser.Controller/Library/SearchResult.cs | 60 ++++++++++++++++++++++ 7 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 MediaBrowser.Controller/Library/IExternalSearchProvider.cs create mode 100644 MediaBrowser.Controller/Library/IInternalSearchProvider.cs delete mode 100644 MediaBrowser.Controller/Library/ISearchEngine.cs create mode 100644 MediaBrowser.Controller/Library/ISearchManager.cs create mode 100644 MediaBrowser.Controller/Library/ISearchProvider.cs create mode 100644 MediaBrowser.Controller/Library/SearchProviderQuery.cs create mode 100644 MediaBrowser.Controller/Library/SearchResult.cs (limited to 'MediaBrowser.Controller/Library') diff --git a/MediaBrowser.Controller/Library/IExternalSearchProvider.cs b/MediaBrowser.Controller/Library/IExternalSearchProvider.cs new file mode 100644 index 0000000000..bded8ba3a3 --- /dev/null +++ b/MediaBrowser.Controller/Library/IExternalSearchProvider.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading; + +namespace MediaBrowser.Controller.Library; + +/// +/// Interface for external search providers that offer enhanced search capabilities. +/// +public interface IExternalSearchProvider : ISearchProvider +{ + /// + /// Searches for items matching the query. + /// + /// The search query. + /// Cancellation token. + /// Async enumerable of search results with relevance scores. + new IAsyncEnumerable SearchAsync( + SearchProviderQuery query, + CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Library/IInternalSearchProvider.cs b/MediaBrowser.Controller/Library/IInternalSearchProvider.cs new file mode 100644 index 0000000000..f87931395d --- /dev/null +++ b/MediaBrowser.Controller/Library/IInternalSearchProvider.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Controller.Library; + +/// +/// Marker interface for internal search providers that typically query the local database directly. +/// +public interface IInternalSearchProvider : ISearchProvider +{ +} diff --git a/MediaBrowser.Controller/Library/ISearchEngine.cs b/MediaBrowser.Controller/Library/ISearchEngine.cs deleted file mode 100644 index 31dcbba5bd..0000000000 --- a/MediaBrowser.Controller/Library/ISearchEngine.cs +++ /dev/null @@ -1,18 +0,0 @@ -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Search; - -namespace MediaBrowser.Controller.Library -{ - /// - /// Interface ILibrarySearchEngine. - /// - public interface ISearchEngine - { - /// - /// Gets the search hints. - /// - /// The query. - /// Task{IEnumerable{SearchHintInfo}}. - QueryResult GetSearchHints(SearchQuery query); - } -} diff --git a/MediaBrowser.Controller/Library/ISearchManager.cs b/MediaBrowser.Controller/Library/ISearchManager.cs new file mode 100644 index 0000000000..4f763829a7 --- /dev/null +++ b/MediaBrowser.Controller/Library/ISearchManager.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Search; + +namespace MediaBrowser.Controller.Library; + +/// +/// Orchestrates search operations across registered search providers. +/// +public interface ISearchManager +{ + /// + /// Searches for items and returns hints suitable for autocomplete/typeahead UI. + /// Results are ordered by relevance score from search providers. + /// + /// The search query including filters and pagination. + /// Cancellation token. + /// Paginated search hints with item metadata for display. + Task> GetSearchHintsAsync( + SearchQuery query, + CancellationToken cancellationToken = default); + + /// + /// Gets ranked search results from registered providers. Returns only item IDs and + /// relevance scores; callers are responsible for loading items and applying user-access filtering. + /// + /// The search provider query with type/media filters. + /// Cancellation token. + /// Search results containing item IDs and relevance scores. + Task> GetSearchResultsAsync( + SearchProviderQuery query, + CancellationToken cancellationToken = default); + + /// + /// Registers search providers discovered through dependency injection. + /// Called during application startup. + /// + /// The search providers to register. + void AddParts(IEnumerable providers); + + /// + /// Gets all registered search providers ordered by priority. + /// + /// The list of search providers including the SQL fallback provider. + IReadOnlyList GetProviders(); +} diff --git a/MediaBrowser.Controller/Library/ISearchProvider.cs b/MediaBrowser.Controller/Library/ISearchProvider.cs new file mode 100644 index 0000000000..3b300ed38b --- /dev/null +++ b/MediaBrowser.Controller/Library/ISearchProvider.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Controller.Library; + +/// +/// Interface for search providers. +/// +public interface ISearchProvider +{ + /// + /// Gets the name of the provider. + /// + string Name { get; } + + /// + /// Gets the type of the provider. + /// + MetadataPluginType Type { get; } + + /// + /// Gets the priority of the provider. Lower values execute first. + /// + int Priority { get; } + + /// + /// Searches for items matching the query. + /// + /// The search query. + /// Cancellation token. + /// Ranked list of candidate item IDs with scores. + Task> SearchAsync( + SearchProviderQuery query, + CancellationToken cancellationToken); + + /// + /// Determines whether this provider can handle the given query. + /// + /// The search query to evaluate. + /// True if this provider can search for the query; otherwise, false. + bool CanSearch(SearchProviderQuery query); +} diff --git a/MediaBrowser.Controller/Library/SearchProviderQuery.cs b/MediaBrowser.Controller/Library/SearchProviderQuery.cs new file mode 100644 index 0000000000..845588c872 --- /dev/null +++ b/MediaBrowser.Controller/Library/SearchProviderQuery.cs @@ -0,0 +1,45 @@ +using System; +using Jellyfin.Data.Enums; + +namespace MediaBrowser.Controller.Library; + +/// +/// Query object for search providers. +/// +public class SearchProviderQuery +{ + /// + /// Gets the search term. + /// + public required string SearchTerm { get; init; } + + /// + /// Gets the user ID for user-specific searches. + /// + public Guid? UserId { get; init; } + + /// + /// Gets the item types to include in the search. + /// + public BaseItemKind[] IncludeItemTypes { get; init; } = []; + + /// + /// Gets the item types to exclude from the search. + /// + public BaseItemKind[] ExcludeItemTypes { get; init; } = []; + + /// + /// Gets the media types to include in the search. + /// + public MediaType[] MediaTypes { get; init; } = []; + + /// + /// Gets the maximum number of results to return. + /// + public int? Limit { get; init; } + + /// + /// Gets the parent ID to scope the search. + /// + public Guid? ParentId { get; init; } +} diff --git a/MediaBrowser.Controller/Library/SearchResult.cs b/MediaBrowser.Controller/Library/SearchResult.cs new file mode 100644 index 0000000000..e6f145e979 --- /dev/null +++ b/MediaBrowser.Controller/Library/SearchResult.cs @@ -0,0 +1,60 @@ +using System; + +namespace MediaBrowser.Controller.Library; + +/// +/// Represents an item matched by a search query with its relevance score. +/// +public readonly struct SearchResult : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The item ID. + /// The relevance score. + public SearchResult(Guid itemId, float score) + { + ItemId = itemId; + Score = score; + } + + /// + /// Gets the ID of the matching item. + /// + public Guid ItemId { get; init; } + + /// + /// Gets the relevance score. Higher values indicate more relevant results. + /// + public float Score { get; init; } + + /// + /// Compares two instances for equality. + /// + /// The left operand. + /// The right operand. + /// True if the instances are equal; otherwise, false. + public static bool operator ==(SearchResult left, SearchResult right) + => left.Equals(right); + + /// + /// Compares two instances for inequality. + /// + /// The left operand. + /// The right operand. + /// True if the instances are not equal; otherwise, false. + public static bool operator !=(SearchResult left, SearchResult right) + => !left.Equals(right); + + /// + public override bool Equals(object? obj) + => obj is SearchResult other && Equals(other); + + /// + public bool Equals(SearchResult other) + => ItemId.Equals(other.ItemId) && Score.Equals(other.Score); + + /// + public override int GetHashCode() + => HashCode.Combine(ItemId, Score); +} -- cgit v1.2.3 From cb9d6e9884d3b952321736392801743198b0ccd9 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 24 May 2026 18:26:21 +0200 Subject: Add batch method for people names --- .../Library/LibraryManager.cs | 6 +++ .../Item/PeopleRepository.cs | 58 ++++++++++++++++++++++ MediaBrowser.Controller/Library/ILibraryManager.cs | 8 +++ .../Persistence/IPeopleRepository.cs | 10 ++++ 4 files changed, 82 insertions(+) (limited to 'MediaBrowser.Controller/Library') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 30ff1bd333..662e28ec1d 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3394,6 +3394,12 @@ namespace Emby.Server.Implementations.Library return _peopleRepository.GetPeopleNames(query); } + /// + public IReadOnlyDictionary> GetPeopleNamesByItem(IReadOnlyList itemIds, IReadOnlyList personTypes) + { + return _peopleRepository.GetPeopleNamesByItem(itemIds, personTypes); + } + public void UpdatePeople(BaseItem item, List people) { UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index b612112d49..d84a59850d 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -165,6 +165,64 @@ public class PeopleRepository(IDbContextFactory dbProvider, I transaction.Commit(); } + /// + public IReadOnlyDictionary> GetPeopleNamesByItem(IReadOnlyList itemIds, IReadOnlyList personTypes) + { + if (itemIds.Count == 0) + { + return new Dictionary>(); + } + + using var context = _dbProvider.CreateDbContext(); + var query = context.PeopleBaseItemMap + .AsNoTracking() + .Where(m => itemIds.Contains(m.ItemId)); + + if (personTypes.Count > 0) + { + query = query.Where(m => personTypes.Contains(m.People.PersonType)); + } + + // One round-trip: pull (ItemId, ListOrder, Name) sorted by ItemId+ListOrder, group in memory. + var rows = query + .OrderBy(m => m.ItemId) + .ThenBy(m => m.ListOrder) + .Select(m => new { m.ItemId, m.People.Name }) + .ToArray(); + + var result = new Dictionary>(); + List? current = null; + var currentId = Guid.Empty; + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var row in rows) + { + if (row.ItemId != currentId) + { + if (current is { Count: > 0 }) + { + result[currentId] = current; + } + + currentId = row.ItemId; + current = new List(); + seen.Clear(); + } + + if (!string.IsNullOrWhiteSpace(row.Name) && seen.Add(row.Name)) + { + current!.Add(row.Name); + } + } + + if (current is { Count: > 0 }) + { + result[currentId] = current; + } + + return result; + } + private PersonInfo Map(People people) { var mapping = people.BaseItems?.FirstOrDefault(); diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index f4c2196400..d794205f00 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -597,6 +597,14 @@ namespace MediaBrowser.Controller.Library /// List<System.String>. IReadOnlyList GetPeopleNames(InternalPeopleQuery query); + /// + /// Gets the people names per item for a batch of item IDs in a single DB round-trip. + /// + /// The item IDs to look up. + /// Optional person types to include. Empty for all. + /// Dictionary keyed by item id; values are the per-item people names. Items with no people are absent. + IReadOnlyDictionary> GetPeopleNamesByItem(IReadOnlyList itemIds, IReadOnlyList personTypes); + /// /// Queries the items. /// diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs index a89f3ef9ee..3a3b2bfb1f 100644 --- a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs +++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs @@ -32,4 +32,14 @@ public interface IPeopleRepository /// The query. /// The list of people names matching the filter. IReadOnlyList GetPeopleNames(InternalPeopleQuery filter); + + /// + /// Gets the people names per item for a batch of item IDs, preserving per-item list order. + /// One database round-trip for the whole batch; grouped by item id in memory. + /// Items with no people are omitted from the returned dictionary. + /// + /// The item IDs to get people for. + /// Optional person types to include (e.g. "Actor", "Director"). Empty for all. + /// Dictionary keyed by item id; values are the per-item people names. + IReadOnlyDictionary> GetPeopleNamesByItem(IReadOnlyList itemIds, IReadOnlyList personTypes); } -- cgit v1.2.3