diff options
| author | Niels van Velzen <nielsvanvelzen@users.noreply.github.com> | 2026-05-03 21:56:34 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-03 21:56:34 +0200 |
| commit | 6e22075a63432aae48859cf9c67fde158dc80d2e (patch) | |
| tree | c3a33238cc56857d8e3daa56db01f290118c9215 /Emby.Server.Implementations/Library/LibraryManager.cs | |
| parent | d9ced0d6399c82ddad9e983605bb0d828a608e63 (diff) | |
| parent | d68d0fa96267ad96eaa5a0ba37e072f59a71442a (diff) | |
Merge pull request #16062 from Shadowghost/perf-rebased
Query Performance Improvements
Diffstat (limited to 'Emby.Server.Implementations/Library/LibraryManager.cs')
| -rw-r--r-- | Emby.Server.Implementations/Library/LibraryManager.cs | 436 |
1 files changed, 403 insertions, 33 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index eee87c4d8b..2bcb10e9e1 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -34,14 +34,11 @@ using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -77,6 +74,10 @@ namespace Emby.Server.Implementations.Library private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly IItemRepository _itemRepository; + private readonly IItemPersistenceService _persistenceService; + private readonly INextUpService _nextUpService; + private readonly IItemCountService _countService; + private readonly ILinkedChildrenService _linkedChildrenService; private readonly IImageProcessor _imageProcessor; private readonly NamingOptions _namingOptions; private readonly IPeopleRepository _peopleRepository; @@ -115,6 +116,10 @@ namespace Emby.Server.Implementations.Library /// <param name="userViewManagerFactory">The user view manager.</param> /// <param name="mediaEncoder">The media encoder.</param> /// <param name="itemRepository">The item repository.</param> + /// <param name="persistenceService">The item persistence service.</param> + /// <param name="nextUpService">The next up service.</param> + /// <param name="countService">The item count service.</param> + /// <param name="linkedChildrenService">The linked children service.</param> /// <param name="imageProcessor">The image processor.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="directoryService">The directory service.</param> @@ -133,6 +138,10 @@ namespace Emby.Server.Implementations.Library Lazy<IUserViewManager> userViewManagerFactory, IMediaEncoder mediaEncoder, IItemRepository itemRepository, + IItemPersistenceService persistenceService, + INextUpService nextUpService, + IItemCountService countService, + ILinkedChildrenService linkedChildrenService, IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService, @@ -151,6 +160,10 @@ namespace Emby.Server.Implementations.Library _userViewManagerFactory = userViewManagerFactory; _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; + _persistenceService = persistenceService; + _nextUpService = nextUpService; + _countService = countService; + _linkedChildrenService = linkedChildrenService; _imageProcessor = imageProcessor; _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize); @@ -327,9 +340,17 @@ namespace Emby.Server.Implementations.Library DeleteItem(item, options, parent, notifyParentItem); } - public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items) + public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false) { - var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDeletePaths())).ToArray(); + if (items.Count == 0) + { + return; + } + + var pathMaps = items.Select(e => + (Item: e, + InternalPath: GetInternalMetadataPaths(e), + DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray(); foreach (var (item, internalPaths, pathsToDelete) in pathMaps) { @@ -363,7 +384,7 @@ namespace Emby.Server.Implementations.Library } } - _itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]); + _persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]); } public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem) @@ -406,6 +427,99 @@ namespace Emby.Server.Implementations.Library item.Id); } + // If deleting a primary version video, clear PrimaryVersionId from alternate versions + // OwnerId check: items with OwnerId set are alternate versions or extras, not primaries + if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty()) + { + var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet(); + var allAlternateVersions = localAlternateIds + .Concat(GetLinkedAlternateVersions(video).Select(v => v.Id)) + .Distinct() + .Select(id => GetItemById(id)) + .OfType<Video>() + .ToList(); + + // Partition alternates by whether their files still exist on disk + var alternateVersions = new List<Video>(); + var missingAlternates = new List<Video>(); + foreach (var alt in allAlternateVersions) + { + if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path)) + { + missingAlternates.Add(alt); + } + else + { + alternateVersions.Add(alt); + } + } + + // Delete alternates whose files no longer exist to avoid ghost items. + // Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted. + foreach (var missing in missingAlternates) + { + _logger.LogInformation( + "Deleting missing alternate version {Name} ({Path})", + missing.Name ?? "Unknown name", + missing.Path ?? string.Empty); + missing.SetPrimaryVersionId(null); + missing.OwnerId = Guid.Empty; + missing.LocalAlternateVersions = []; + missing.LinkedAlternateVersions = []; + DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false); + } + + if (alternateVersions.Count > 0) + { + _logger.LogInformation( + "Clearing PrimaryVersionId from {Count} alternate versions of {Name}", + alternateVersions.Count, + item.Name ?? "Unknown name"); + + // Promote the first alternate version to be the new primary + var newPrimary = alternateVersions[0]; + newPrimary.SetPrimaryVersionId(null); + newPrimary.OwnerId = Guid.Empty; + + // Transfer alternate version arrays from old primary to new primary + // so UpdateToRepositoryAsync creates correct LinkedChildren entries + newPrimary.LocalAlternateVersions = video.LocalAlternateVersions + .Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions + .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id)) + .ToArray(); + + newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); + + // Re-route playlist/collection references from deleted primary to new primary + RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult(); + + // Update remaining alternates to point to new primary + foreach (var alternate in alternateVersions.Skip(1)) + { + alternate.SetPrimaryVersionId(newPrimary.Id); + // Only set OwnerId for local alternates; linked alternates are independent items + alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty; + alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); + } + } + } + else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue) + { + // If deleting an alternate version, re-route references to its primary + RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter().GetResult(); + + // Remove deleted alternate from primary's LinkedAlternateVersions + if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo) + { + primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions + .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id)) + .ToArray(); + primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); + } + } + var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false) : []; @@ -450,7 +564,7 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - _itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]); + _persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]); _cache.TryRemove(item.Id, out _); foreach (var child in children) { @@ -576,6 +690,9 @@ namespace Emby.Server.Implementations.Library // Trickplay list.Add(_pathManager.GetTrickplayDirectory(video)); + // Chapter Images + list.Add(_pathManager.GetChapterImageFolderPath(video)); + // Subtitles and attachments foreach (var mediaSource in item.GetMediaSources(false)) { @@ -657,8 +774,63 @@ namespace Emby.Server.Implementations.Library return key.GetMD5(); } - public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null) - => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent); + public BaseItem? ResolvePath( + FileSystemMetadata fileInfo, + Folder? parent = null, + IDirectoryService? directoryService = null, + CollectionType? collectionType = null) + => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType); + + /// <inheritdoc /> + public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? collectionType) + { + // Clean up any existing item saved with wrong type (e.g. Video instead of Movie). + // This happens when items were previously resolved without proper type context + // in mixed-content libraries where collectionType is null. + var expectedId = GetNewItemId(path, expectedVideoType); + if (expectedVideoType != typeof(Video)) + { + var wrongTypeId = GetNewItemId(path, typeof(Video)); + if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem) + { + _logger.LogInformation( + "Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}", + wrongTypeItem.GetType().Name, + expectedVideoType.Name, + path); + DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false }); + } + } + + var resolved = ResolvePath( + _fileSystem.GetFileSystemInfo(path), + parent, + collectionType: collectionType) as Video; + + if (resolved is null) + { + return null; + } + + // Ensure the alternate version has the same concrete type as the primary video. + // ResolvePath may return a generic Video for files in mixed-content libraries + // where collectionType is null, even though the primary is a Movie/Episode/etc. + if (resolved.GetType() != expectedVideoType) + { + if (Activator.CreateInstance(expectedVideoType) is Video correctVideo) + { + correctVideo.Path = resolved.Path; + correctVideo.Name = resolved.Name; + correctVideo.VideoType = resolved.VideoType; + correctVideo.ProductionYear = resolved.ProductionYear; + correctVideo.ExtraType = resolved.ExtraType; + resolved = correctVideo; + } + } + + resolved.Id = expectedId; + return resolved; + } private BaseItem? ResolvePath( FileSystemMetadata fileInfo, @@ -1041,7 +1213,7 @@ namespace Emby.Server.Implementations.Library public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names) { - return _itemRepository.FindArtists(names); + return _linkedChildrenService.FindArtists(names); } public MusicArtist GetArtist(string name, DtoOptions options) @@ -1186,7 +1358,7 @@ namespace Emby.Server.Implementations.Library if (toDelete.Count > 0) { - _itemRepository.DeleteItem(toDelete.ToArray()); + _persistenceService.DeleteItem(toDelete.ToArray()); } } @@ -1262,7 +1434,7 @@ namespace Emby.Server.Implementations.Library progress.Report(percent * 100); } - _itemRepository.UpdateInheritedValues(); + _persistenceService.UpdateInheritedValues(); progress.Report(100); } @@ -1421,14 +1593,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User, allowExternalContent); } - var itemList = _itemRepository.GetItemList(query); - var user = query.User; - if (user is not null) - { - return itemList.Where(i => i.IsVisible(user)).ToList(); - } - - return itemList; + return _itemRepository.GetItemList(query); } public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query) @@ -1452,7 +1617,7 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User); } - return _itemRepository.GetCount(query); + return _countService.GetCount(query); } public ItemCounts GetItemCounts(InternalItemsQuery query) @@ -1471,7 +1636,30 @@ namespace Emby.Server.Implementations.Library AddUserToQuery(query, query.User); } - return _itemRepository.GetItemCounts(query); + return _countService.GetItemCounts(query); + } + + /// <inheritdoc/> + public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? user) + { + var query = new InternalItemsQuery(user); + if (user is not null) + { + AddUserToQuery(query, user); + } + + return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query); + } + + public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId) + { + return _countService.GetChildCountBatch(parentIds, userId); + } + + /// <inheritdoc/> + public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User user) + { + return _countService.GetPlayedAndTotalCountBatch(folderIds, user); } public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents) @@ -1516,7 +1704,17 @@ namespace Emby.Server.Implementations.Library } } - return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff); + return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff); + } + + /// <inheritdoc /> + public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch( + InternalItemsQuery query, + IReadOnlyList<string> seriesKeys, + bool includeSpecials, + bool includeWatchedForRewatching) + { + return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching); } public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) @@ -1700,6 +1898,11 @@ namespace Emby.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { + if (query.User is null) + { + query.SetUser(user); + } + if (query.AncestorIds.Length == 0 && query.ParentId.IsEmpty() && query.ChannelIds.Count == 0 && @@ -1725,6 +1928,15 @@ namespace Emby.Server.Implementations.Library } } + /// <inheritdoc/> + public void ConfigureUserAccess(InternalItemsQuery query, User user) + { + ArgumentNullException.ThrowIfNull(query); + ArgumentNullException.ThrowIfNull(user); + + AddUserToQuery(query, user); + } + private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user) { if (item is UserView view) @@ -1890,6 +2102,44 @@ namespace Emby.Server.Implementations.Library } /// <inheritdoc /> + public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video) + { + ArgumentNullException.ThrowIfNull(video); + + var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LocalAlternateVersion); + if (linkedIds.Count > 0) + { + return linkedIds; + } + + return []; + } + + /// <inheritdoc /> + public IEnumerable<Video> GetLinkedAlternateVersions(Video video) + { + ArgumentNullException.ThrowIfNull(video); + + var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LinkedAlternateVersion); + if (linkedIds.Count > 0) + { + return linkedIds + .Select(id => GetItemById(id)) + .Where(i => i is not null) + .OfType<Video>() + .OrderBy(i => i.SortName); + } + + return []; + } + + /// <inheritdoc /> + public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType childType) + { + _linkedChildrenService.UpsertLinkedChild(parentId, childId, childType); + } + + /// <inheritdoc /> public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder) { IOrderedEnumerable<BaseItem>? orderedItems = null; @@ -1993,10 +2243,45 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken) { - _itemRepository.SaveItems(items, cancellationToken); - + // Resolve and add any local alternate version items that don't exist yet + // This ensures they exist in the database when LinkedChildren are processed + var allItems = new List<BaseItem>(items); + var parentFolder = parent as Folder; + var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null; foreach (var item in items) { + if (item is Video video && video.LocalAlternateVersions.Length > 0) + { + var videoType = video.GetType(); + foreach (var path in video.LocalAlternateVersions) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + + // Use the primary video's type for ID calculation to ensure consistency + var altId = GetNewItemId(path, videoType); + if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId))) + { + // Alternate version doesn't exist, resolve and create it + // ensuring it has the same type as the primary video + var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType); + if (altVideo is not null) + { + altVideo.OwnerId = video.Id; + altVideo.SetPrimaryVersionId(video.Id); + allItems.Add(altVideo); + } + } + } + } + } + + _persistenceService.SaveItems(allItems, cancellationToken); + + foreach (var item in allItems) + { RegisterItem(item); } @@ -2144,7 +2429,7 @@ namespace Emby.Server.Implementations.Library item.ValidateImages(); - await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false); + await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false); RegisterItem(item); } @@ -2161,7 +2446,50 @@ namespace Emby.Server.Implementations.Library item.DateLastSaved = DateTime.UtcNow; } - _itemRepository.SaveItems(items, cancellationToken); + // Resolve and add any local alternate version items that don't exist yet + // This ensures they exist in the database when LinkedChildren are processed + var allItems = new List<BaseItem>(items); + var parentFolder = parent as Folder; + var parentCollectionType = GetTopFolderContentType(parent); + foreach (var item in items) + { + if (item is Video video && video.LocalAlternateVersions.Length > 0) + { + var videoType = video.GetType(); + foreach (var path in video.LocalAlternateVersions) + { + if (string.IsNullOrEmpty(path)) + { + continue; + } + + // Use the primary video's type for ID calculation to ensure consistency + var altId = GetNewItemId(path, videoType); + if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId))) + { + // Alternate version doesn't exist, resolve and create it + // ensuring it has the same type as the primary video + var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType); + if (altVideo is not null) + { + altVideo.OwnerId = video.Id; + altVideo.SetPrimaryVersionId(video.Id); + allItems.Add(altVideo); + } + } + } + } + } + + _persistenceService.SaveItems(allItems, cancellationToken); + + foreach (var item in allItems) + { + if (!items.Contains(item)) + { + RegisterItem(item); + } + } if (parent is Folder folder) { @@ -2205,7 +2533,7 @@ namespace Emby.Server.Implementations.Library /// <inheritdoc /> public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken) { - await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false); + await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false); } public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) @@ -2834,7 +3162,8 @@ namespace Emby.Server.Implementations.Library { // Apply .ignore rules var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList(); - var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath); + var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd)); + var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath); if (ownerVideoInfo is null) { yield break; @@ -2896,10 +3225,16 @@ namespace Emby.Server.Implementations.Library extra.ExtraType = extraType; } - extra.ParentId = Guid.Empty; - extra.OwnerId = owner.Id; - extra.IsInMixedFolder = isInMixedFolder; - return extra; + // Only return items that are actual extras (have ExtraType set) + // Note: OwnerId and ParentId are set by RefreshExtras, not here, + // so that RefreshExtras can detect when they need updating and set ForceSave. + if (extra.ExtraType is not null) + { + extra.IsInMixedFolder = isInMixedFolder; + return extra; + } + + return null; } } @@ -3385,5 +3720,40 @@ namespace Emby.Server.Implementations.Library _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path)); RemoveContentTypeOverrides(path); } + + /// <inheritdoc /> + public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId) + { + var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId); + + // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents + foreach (var parentId in affectedParentIds) + { + if (GetItemById(parentId) is Folder parent) + { + foreach (var lc in parent.LinkedChildren) + { + if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId)) + { + lc.ItemId = toChildId; + } + } + + await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false); + } + } + } + + /// <inheritdoc /> + public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return _itemRepository.GetQueryFiltersLegacy(query); + } } } |
