diff --git a/Jellyfin.Data/Entities/BaseItem.cs b/Jellyfin.Data/Entities/BaseItem.cs index 4ce6523427..0e67a7ca45 100644 --- a/Jellyfin.Data/Entities/BaseItem.cs +++ b/Jellyfin.Data/Entities/BaseItem.cs @@ -14,7 +14,7 @@ public class BaseItem public required string Type { get; set; } - public IReadOnlyList? Data { get; set; } + public string? Data { get; set; } public Guid? ParentId { get; set; } @@ -94,7 +94,7 @@ public class BaseItem public string? UnratedType { get; set; } - public string? TopParentId { get; set; } + public Guid? TopParentId { get; set; } public string? TrailerTypes { get; set; } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index 5116b13d46..8f3c9636ee 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; @@ -281,16 +282,9 @@ public class BaseItemManager : IItemRepository } } - if (query.SimilarTo is not null && query.MinSimilarityScore > 0) - { - // TODO support similarty score via CTE - baseQuery = baseQuery.Where(e => e.Sim == query.IsSeries); - whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture)); - } - if (!string.IsNullOrEmpty(query.SearchTerm)) { - whereClauses.Add("SearchScore > 0"); + baseQuery = baseQuery.Where(e => e.CleanName!.Contains(query.SearchTerm, StringComparison.InvariantCultureIgnoreCase) || (e.OriginalTitle != null && e.OriginalTitle.Contains(query.SearchTerm, StringComparison.InvariantCultureIgnoreCase))); } if (query.IsFolder.HasValue) @@ -917,44 +911,13 @@ public class BaseItemManager : IItemRepository { var includedItemByNameTypes = GetItemByNameTypesInQuery(query); var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; - - if (queryTopParentIds.Length == 1) + if (enableItemsByName && includedItemByNameTypes.Count > 0) { - if (enableItemsByName && includedItemByNameTypes.Count == 1) - { - whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); - statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); - } - else - { - whereClauses.Add("(TopParentId=@TopParentId)"); - } - - statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); + baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); } - else if (queryTopParentIds.Length > 1) + else { - var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - - if (enableItemsByName && includedItemByNameTypes.Count == 1) - { - whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); - statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); - } - else - { - whereClauses.Add("TopParentId in (" + val + ")"); - } + baseQuery = baseQuery.Where(e => queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); } } @@ -965,124 +928,83 @@ public class BaseItemManager : IItemRepository if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) { - var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; - whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); - statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); + baseQuery = baseQuery + .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == query.AncestorWithPresentationUniqueKey).Any(f => f.AncestorIds!.Any(w => w.ItemId.Equals(f.Id)))); } if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) { - whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); - statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); + baseQuery = baseQuery + .Where(e => e.SeriesPresentationUniqueKey == query.SeriesPresentationUniqueKey); } if (query.ExcludeInheritedTags.Length > 0) { - var paramName = "@ExcludeInheritedTags"; - if (statement is null) - { - int index = 0; - string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++)); - whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)"); - } - else - { - for (int index = 0; index < query.ExcludeInheritedTags.Length; index++) - { - statement.TryBind(paramName + index, GetCleanValue(query.ExcludeInheritedTags[index])); - } - } + baseQuery = baseQuery + .Where(e => !e.ItemValues!.Where(e => e.Type == 6) + .Any(f => query.ExcludeInheritedTags.Contains(f.CleanValue))); } if (query.IncludeInheritedTags.Length > 0) { - var paramName = "@IncludeInheritedTags"; - if (statement is null) + // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. + // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. + if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) { - int index = 0; - string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++)); - // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. - // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. - if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) - { - whereClauses.Add($""" - ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null - OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null) - """); - } + baseQuery = baseQuery + .Where(e => e.ItemValues!.Where(e => e.Type == 6) + .Any(f => query.IncludeInheritedTags.Contains(f.CleanValue)) + || + (e.ParentId.HasValue && context.ItemValues.Where(w => w.ItemId.Equals(e.ParentId.Value))!.Where(e => e.Type == 6) + .Any(f => query.IncludeInheritedTags.Contains(f.CleanValue)))); + } - // A playlist should be accessible to its owner regardless of allowed tags. - else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) - { - whereClauses.Add($""" - ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null - OR data like @PlaylistOwnerUserId) - """); - } - else - { - whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)"); - } + // A playlist should be accessible to its owner regardless of allowed tags. + else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Where(e => e.Type == 6) + .Any(f => query.IncludeInheritedTags.Contains(f.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{query.User!.Id:N}\"")); + // d ^^ this is stupid it hate this. } else { - for (int index = 0; index < query.IncludeInheritedTags.Length; index++) - { - statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); - } - - if (query.User is not null) - { - statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%"""); - } + baseQuery = baseQuery + .Where(e => e.ItemValues!.Where(e => e.Type == 6) + .Any(f => query.IncludeInheritedTags.Contains(f.CleanValue))); } } if (query.SeriesStatuses.Length > 0) { - var statuses = new List(); - - foreach (var seriesStatus in query.SeriesStatuses) - { - statuses.Add("data like '%" + seriesStatus + "%'"); - } - - whereClauses.Add("(" + string.Join(" OR ", statuses) + ")"); + baseQuery = baseQuery + .Where(e => query.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); } if (query.BoxSetLibraryFolders.Length > 0) { - var folderIdQueries = new List(); - - foreach (var folderId in query.BoxSetLibraryFolders) - { - folderIdQueries.Add("data like '%" + folderId.ToString("N", CultureInfo.InvariantCulture) + "%'"); - } - - whereClauses.Add("(" + string.Join(" OR ", folderIdQueries) + ")"); + baseQuery = baseQuery + .Where(e => query.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); } if (query.VideoTypes.Length > 0) { - var videoTypes = new List(); - - foreach (var videoType in query.VideoTypes) - { - videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'"); - } - - whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")"); + var videoTypeBs = query.VideoTypes.Select(e => $"\"VideoType\":\"" + e + "\""); + baseQuery = baseQuery + .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f, StringComparison.InvariantCultureIgnoreCase))); } if (query.Is3D.HasValue) { if (query.Is3D.Value) { - whereClauses.Add("data like '%Video3DFormat%'"); + baseQuery = baseQuery + .Where(e => e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); } else { - whereClauses.Add("data not like '%Video3DFormat%'"); + baseQuery = baseQuery + .Where(e => !e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); } } @@ -1090,11 +1012,13 @@ public class BaseItemManager : IItemRepository { if (query.IsPlaceHolder.Value) { - whereClauses.Add("data like '%\"IsPlaceHolder\":true%'"); + baseQuery = baseQuery + .Where(e => e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); } else { - whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')"); + baseQuery = baseQuery + .Where(e => !e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); } } @@ -1102,47 +1026,27 @@ public class BaseItemManager : IItemRepository { if (query.HasSpecialFeature.Value) { - whereClauses.Add("ExtraIds not null"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - if (query.HasTrailer.HasValue) - { - if (query.HasTrailer.Value) - { - whereClauses.Add("ExtraIds not null"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - if (query.HasThemeSong.HasValue) - { - if (query.HasThemeSong.Value) - { - whereClauses.Add("ExtraIds not null"); + baseQuery = baseQuery + .Where(e => e.ExtraIds != null); } else { - whereClauses.Add("ExtraIds is null"); + baseQuery = baseQuery + .Where(e => e.ExtraIds == null); } } - if (query.HasThemeVideo.HasValue) + if (query.HasTrailer.HasValue || query.HasThemeSong.HasValue || query.HasThemeVideo.HasValue) { - if (query.HasThemeVideo.Value) + if (query.HasTrailer.GetValueOrDefault() || query.HasThemeSong.GetValueOrDefault() || query.HasThemeVideo.GetValueOrDefault()) { - whereClauses.Add("ExtraIds not null"); + baseQuery = baseQuery + .Where(e => e.ExtraIds != null); } else { - whereClauses.Add("ExtraIds is null"); + baseQuery = baseQuery + .Where(e => e.ExtraIds == null); } } } @@ -1867,4 +1771,47 @@ public class BaseItemManager : IItemRepository return image; } + + private List GetItemByNameTypesInQuery(InternalItemsQuery query) + { + var list = new List(); + + if (IsTypeInQuery(BaseItemKind.Person, query)) + { + list.Add(typeof(Person).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.Genre, query)) + { + list.Add(typeof(Genre).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.MusicGenre, query)) + { + list.Add(typeof(MusicGenre).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.MusicArtist, query)) + { + list.Add(typeof(MusicArtist).FullName!); + } + + if (IsTypeInQuery(BaseItemKind.Studio, query)) + { + list.Add(typeof(Studio).FullName!); + } + + return list; + } + + private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) + { + if (query.ExcludeItemTypes.Contains(type)) + { + return false; + } + + return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); + } + }