From ee1bdf4e222125ed7382165fd7e09599ca4bd4aa Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 8 Sep 2024 16:56:14 +0000 Subject: WIP move baseitem to jellyfin.db --- .../Data/SqliteItemRepository.cs | 1656 +------------------- 1 file changed, 5 insertions(+), 1651 deletions(-) (limited to 'Emby.Server.Implementations/Data/SqliteItemRepository.cs') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 60f5ee47a..c7a8421c6 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -63,130 +63,6 @@ namespace Emby.Server.Implementations.Data private readonly ItemFields[] _allItemFields = Enum.GetValues(); - private static readonly string[] _retrieveItemColumns = - { - "type", - "data", - "StartDate", - "EndDate", - "ChannelId", - "IsMovie", - "IsSeries", - "EpisodeTitle", - "IsRepeat", - "CommunityRating", - "CustomRating", - "IndexNumber", - "IsLocked", - "PreferredMetadataLanguage", - "PreferredMetadataCountryCode", - "Width", - "Height", - "DateLastRefreshed", - "Name", - "Path", - "PremiereDate", - "Overview", - "ParentIndexNumber", - "ProductionYear", - "OfficialRating", - "ForcedSortName", - "RunTimeTicks", - "Size", - "DateCreated", - "DateModified", - "guid", - "Genres", - "ParentId", - "Audio", - "ExternalServiceId", - "IsInMixedFolder", - "DateLastSaved", - "LockedFields", - "Studios", - "Tags", - "TrailerTypes", - "OriginalTitle", - "PrimaryVersionId", - "DateLastMediaAdded", - "Album", - "LUFS", - "NormalizationGain", - "CriticRating", - "IsVirtualItem", - "SeriesName", - "SeasonName", - "SeasonId", - "SeriesId", - "PresentationUniqueKey", - "InheritedParentalRatingValue", - "ExternalSeriesId", - "Tagline", - "ProviderIds", - "Images", - "ProductionLocations", - "ExtraIds", - "TotalBitrate", - "ExtraType", - "Artists", - "AlbumArtists", - "ExternalId", - "SeriesPresentationUniqueKey", - "ShowId", - "OwnerId" - }; - - private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid"; - - private static readonly string[] _mediaStreamSaveColumns = - { - "ItemId", - "StreamIndex", - "StreamType", - "Codec", - "Language", - "ChannelLayout", - "Profile", - "AspectRatio", - "Path", - "IsInterlaced", - "BitRate", - "Channels", - "SampleRate", - "IsDefault", - "IsForced", - "IsExternal", - "Height", - "Width", - "AverageFrameRate", - "RealFrameRate", - "Level", - "PixelFormat", - "BitDepth", - "IsAnamorphic", - "RefFrames", - "CodecTag", - "Comment", - "NalLengthSize", - "IsAvc", - "Title", - "TimeBase", - "CodecTimeBase", - "ColorPrimaries", - "ColorSpace", - "ColorTransfer", - "DvVersionMajor", - "DvVersionMinor", - "DvProfile", - "DvLevel", - "RpuPresentFlag", - "ElPresentFlag", - "BlPresentFlag", - "DvBlSignalCompatibilityId", - "IsHearingImpaired", - "Rotation" - }; - private static readonly string _mediaStreamSaveColumnsInsertQuery = $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values "; @@ -336,956 +212,14 @@ namespace Emby.Server.Implementations.Data /// protected override TempStoreMode TempStore => TempStoreMode.Memory; - /// - /// Opens the connection to the database. - /// - public override void Initialize() - { - base.Initialize(); - - const string CreateMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - - const string CreateMediaAttachmentsTableCommand - = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; - - string[] queries = - { - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", - - "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", - "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", - "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", - - "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", - - "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - - "drop index if exists idxPeopleItemId", - "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", - "create index if not exists idxPeopleName on People(Name)", - - "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - - CreateMediaStreamsTableCommand, - CreateMediaAttachmentsTableCommand, - - "pragma shrink_memory" - }; - - string[] postQueries = - { - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", - "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", - - "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", - "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", - "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", - - // covering index - "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", - - // series - "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - - // series counts - // seriesdateplayed sort order - "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", - - // live tv programs - "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", - - // covering index for getitemvalues - "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", - - // used by movie suggestions - "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", - "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", - - // latest items - "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", - "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", - - // resume - "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", - - // items by name - "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", - - // Used to update inherited tags - "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", - - "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", - "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" - }; - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - connection.Execute(string.Join(';', queries)); - - var existingColumnNames = GetColumnNames(connection, "AncestorIds"); - AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); - - AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "ItemValues"); - AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, ChaptersTableName); - AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "MediaStreams"); - AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames); - - connection.Execute(string.Join(';', postQueries)); - - transaction.Commit(); - } - } - - public void SaveImages(BaseItem item) - { - ArgumentNullException.ThrowIfNull(item); - - CheckDisposed(); - - var images = SerializeImages(item.ImageInfos); - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); - saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", images); - - saveImagesStatement.ExecuteNonQuery(); - transaction.Commit(); - } - - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// - /// or is null. - /// - public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(items); - - cancellationToken.ThrowIfCancellationRequested(); - - CheckDisposed(); - - var itemsLen = items.Count; - var tuples = new ValueTuple, BaseItem, string, List>[itemsLen]; - for (int i = 0; i < itemsLen; i++) - { - var item = items[i]; - var ancestorIds = item.SupportsAncestors ? - item.GetAncestorIds().Distinct().ToList() : - null; - - var topParent = item.GetTopParent(); - - var userdataKey = item.GetUserDataKeys().FirstOrDefault(); - var inheritedTags = item.GetInheritedTags(); - - tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); - } - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - SaveItemsInTransaction(connection, tuples); - transaction.Commit(); - } - - private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples) + private bool TypeRequiresDeserialization(Type type) { - using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) - using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) + if (_config.Configuration.SkipDeserializationForBasicTypes) { - var requiresReset = false; - foreach (var tuple in tuples) + if (type == typeof(Channel) + || type == typeof(UserRootFolder)) { - if (requiresReset) - { - saveItemStatement.Parameters.Clear(); - deleteAncestorsStatement.Parameters.Clear(); - } - - var item = tuple.Item; - var topParent = tuple.TopParent; - var userDataKey = tuple.UserDataKey; - - SaveItem(item, topParent, userDataKey, saveItemStatement); - - var inheritedTags = tuple.InheritedTags; - - if (item.SupportsAncestors) - { - UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement); - } - - UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); - - requiresReset = true; - } - } - } - - private string GetPathToSave(string path) - { - if (path is null) - { - return null; - } - - return _appHost.ReverseVirtualPath(path); - } - - private string RestorePath(string path) - { - return _appHost.ExpandVirtualPath(path); - } - - private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) - { - Type type = item.GetType(); - - saveItemStatement.TryBind("@guid", item.Id); - saveItemStatement.TryBind("@type", type.FullName); - - if (TypeRequiresDeserialization(type)) - { - saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); - } - else - { - saveItemStatement.TryBindNull("@data"); - } - - saveItemStatement.TryBind("@Path", GetPathToSave(item.Path)); - - if (item is IHasStartDate hasStartDate) - { - saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate); - } - else - { - saveItemStatement.TryBindNull("@StartDate"); - } - - if (item.EndDate.HasValue) - { - saveItemStatement.TryBind("@EndDate", item.EndDate.Value); - } - else - { - saveItemStatement.TryBindNull("@EndDate"); - } - - saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); - - if (item is IHasProgramAttributes hasProgramAttributes) - { - saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); - saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); - saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); - saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); - } - else - { - saveItemStatement.TryBindNull("@IsMovie"); - saveItemStatement.TryBindNull("@IsSeries"); - saveItemStatement.TryBindNull("@EpisodeTitle"); - saveItemStatement.TryBindNull("@IsRepeat"); - } - - saveItemStatement.TryBind("@CommunityRating", item.CommunityRating); - saveItemStatement.TryBind("@CustomRating", item.CustomRating); - saveItemStatement.TryBind("@IndexNumber", item.IndexNumber); - saveItemStatement.TryBind("@IsLocked", item.IsLocked); - saveItemStatement.TryBind("@Name", item.Name); - saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); - saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); - saveItemStatement.TryBind("@Overview", item.Overview); - saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); - saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); - saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); - - var parentId = item.ParentId; - if (parentId.IsEmpty()) - { - saveItemStatement.TryBindNull("@ParentId"); - } - else - { - saveItemStatement.TryBind("@ParentId", parentId); - } - - if (item.Genres.Length > 0) - { - saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres)); - } - else - { - saveItemStatement.TryBindNull("@Genres"); - } - - saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); - - saveItemStatement.TryBind("@SortName", item.SortName); - - saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName); - - saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); - saveItemStatement.TryBind("@Size", item.Size); - - saveItemStatement.TryBind("@DateCreated", item.DateCreated); - saveItemStatement.TryBind("@DateModified", item.DateModified); - - saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); - saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); - - if (item.Width > 0) - { - saveItemStatement.TryBind("@Width", item.Width); - } - else - { - saveItemStatement.TryBindNull("@Width"); - } - - if (item.Height > 0) - { - saveItemStatement.TryBind("@Height", item.Height); - } - else - { - saveItemStatement.TryBindNull("@Height"); - } - - if (item.DateLastRefreshed != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed); - } - else - { - saveItemStatement.TryBindNull("@DateLastRefreshed"); - } - - if (item.DateLastSaved != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved); - } - else - { - saveItemStatement.TryBindNull("@DateLastSaved"); - } - - saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); - - if (item.LockedFields.Length > 0) - { - saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields)); - } - else - { - saveItemStatement.TryBindNull("@LockedFields"); - } - - if (item.Studios.Length > 0) - { - saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios)); - } - else - { - saveItemStatement.TryBindNull("@Studios"); - } - - if (item.Audio.HasValue) - { - saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@Audio"); - } - - if (item is LiveTvChannel liveTvChannel) - { - saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName); - } - else - { - saveItemStatement.TryBindNull("@ExternalServiceId"); - } - - if (item.Tags.Length > 0) - { - saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags)); - } - else - { - saveItemStatement.TryBindNull("@Tags"); - } - - saveItemStatement.TryBind("@IsFolder", item.IsFolder); - - saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); - - if (topParent is null) - { - saveItemStatement.TryBindNull("@TopParentId"); - } - else - { - saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture)); - } - - if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) - { - saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes)); - } - else - { - saveItemStatement.TryBindNull("@TrailerTypes"); - } - - saveItemStatement.TryBind("@CriticRating", item.CriticRating); - - if (string.IsNullOrWhiteSpace(item.Name)) - { - saveItemStatement.TryBindNull("@CleanName"); - } - else - { - saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name)); - } - - saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey); - saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle); - - if (item is Video video) - { - saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId); - } - else - { - saveItemStatement.TryBindNull("@PrimaryVersionId"); - } - - if (item is Folder folder && folder.DateLastMediaAdded.HasValue) - { - saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value); - } - else - { - saveItemStatement.TryBindNull("@DateLastMediaAdded"); - } - - saveItemStatement.TryBind("@Album", item.Album); - saveItemStatement.TryBind("@LUFS", item.LUFS); - saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain); - saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); - - if (item is IHasSeries hasSeriesName) - { - saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName); - } - else - { - saveItemStatement.TryBindNull("@SeriesName"); - } - - if (string.IsNullOrWhiteSpace(userDataKey)) - { - saveItemStatement.TryBindNull("@UserDataKey"); - } - else - { - saveItemStatement.TryBind("@UserDataKey", userDataKey); - } - - if (item is Episode episode) - { - saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - - var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; - - saveItemStatement.TryBind("@SeasonId", nullableSeasonId); - } - else - { - saveItemStatement.TryBindNull("@SeasonName"); - saveItemStatement.TryBindNull("@SeasonId"); - } - - if (item is IHasSeries hasSeries) - { - var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; - - saveItemStatement.TryBind("@SeriesId", nullableSeriesId); - saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); - } - else - { - saveItemStatement.TryBindNull("@SeriesId"); - saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey"); - } - - saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); - saveItemStatement.TryBind("@Tagline", item.Tagline); - - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); - saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); - - if (item.ProductionLocations.Length > 0) - { - saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations)); - } - else - { - saveItemStatement.TryBindNull("@ProductionLocations"); - } - - if (item.ExtraIds.Length > 0) - { - saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds)); - } - else - { - saveItemStatement.TryBindNull("@ExtraIds"); - } - - saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); - if (item.ExtraType.HasValue) - { - saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@ExtraType"); - } - - string artists = null; - if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) - { - artists = string.Join('|', hasArtists.Artists); - } - - saveItemStatement.TryBind("@Artists", artists); - - string albumArtists = null; - if (item is IHasAlbumArtist hasAlbumArtists - && hasAlbumArtists.AlbumArtists.Count > 0) - { - albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists); - } - - saveItemStatement.TryBind("@AlbumArtists", albumArtists); - saveItemStatement.TryBind("@ExternalId", item.ExternalId); - - if (item is LiveTvProgram program) - { - saveItemStatement.TryBind("@ShowId", program.ShowId); - } - else - { - saveItemStatement.TryBindNull("@ShowId"); - } - - Guid ownerId = item.OwnerId; - if (ownerId.IsEmpty()) - { - saveItemStatement.TryBindNull("@OwnerId"); - } - else - { - saveItemStatement.TryBind("@OwnerId", ownerId); - } - - saveItemStatement.ExecuteNonQuery(); - } - - internal static string SerializeProviderIds(Dictionary providerIds) - { - StringBuilder str = new StringBuilder(); - foreach (var i in providerIds) - { - // Ideally we shouldn't need this IsNullOrWhiteSpace check, - // but we're seeing some cases of bad data slip through - if (string.IsNullOrWhiteSpace(i.Value)) - { - continue; - } - - str.Append(i.Key) - .Append('=') - .Append(i.Value) - .Append('|'); - } - - if (str.Length == 0) - { - return null; - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal static void DeserializeProviderIds(string value, IHasProviderIds item) - { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - foreach (var part in value.SpanSplit('|')) - { - var providerDelimiterIndex = part.IndexOf('='); - // Don't let empty values through - if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) - { - item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); - } - } - } - - internal string SerializeImages(ItemImageInfo[] images) - { - if (images.Length == 0) - { - return null; - } - - StringBuilder str = new StringBuilder(); - foreach (var i in images) - { - if (string.IsNullOrWhiteSpace(i.Path)) - { - continue; - } - - AppendItemImageInfo(str, i); - str.Append('|'); - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal ItemImageInfo[] DeserializeImages(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Array.Empty(); - } - - // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed - var valueSpan = value.AsSpan(); - var count = valueSpan.Count('|') + 1; - - var position = 0; - var result = new ItemImageInfo[count]; - foreach (var part in valueSpan.Split('|')) - { - var image = ItemImageInfoFromValueString(part); - - if (image is not null) - { - result[position++] = image; - } - } - - if (position == count) - { - return result; - } - - if (position == 0) - { - return Array.Empty(); - } - - // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. - return result[..position]; - } - - private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) - { - const char Delimiter = '*'; - - var path = image.Path ?? string.Empty; - - bldr.Append(GetPathToSave(path)) - .Append(Delimiter) - .Append(image.DateModified.Ticks) - .Append(Delimiter) - .Append(image.Type) - .Append(Delimiter) - .Append(image.Width) - .Append(Delimiter) - .Append(image.Height); - - var hash = image.BlurHash; - if (!string.IsNullOrEmpty(hash)) - { - bldr.Append(Delimiter) - // Replace delimiters with other characters. - // This can be removed when we migrate to a proper DB. - .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); - } - } - - internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value) - { - const char Delimiter = '*'; - - var nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan path = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan dateModified = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan imageType = value[..nextSegment]; - - var image = new ItemImageInfo - { - Path = RestorePath(path.ToString()) - }; - - if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) - && ticks >= DateTime.MinValue.Ticks - && ticks <= DateTime.MaxValue.Ticks) - { - image.DateModified = new DateTime(ticks, DateTimeKind.Utc); - } - else - { - return null; - } - - if (Enum.TryParse(imageType, true, out ImageType type)) - { - image.Type = type; - } - else - { - return null; - } - - // Optional parameters: width*height*blurhash - if (nextSegment + 1 < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1 || nextSegment == value.Length) - { - return image; - } - - ReadOnlySpan widthSpan = value[..nextSegment]; - - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan heightSpan = value[..nextSegment]; - - if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) - && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) - { - image.Width = width; - image.Height = height; - } - - if (nextSegment < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - var length = value.Length; - - Span blurHashSpan = stackalloc char[length]; - for (int i = 0; i < length; i++) - { - var c = value[i]; - blurHashSpan[i] = c switch - { - '/' => Delimiter, - '\\' => '|', - _ => c - }; - } - - image.BlurHash = new string(blurHashSpan); - } - } - - return image; - } - - /// - /// Internal retrieve from items or users table. - /// - /// The id. - /// BaseItem. - /// is null. - /// is . - public BaseItem RetrieveItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - CheckDisposed(); - - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) - { - statement.TryBind("@guid", id); - - foreach (var row in statement.ExecuteQuery()) - { - return GetItem(row, new InternalItemsQuery()); - } - } - - return null; - } - - private bool TypeRequiresDeserialization(Type type) - { - if (_config.Configuration.SkipDeserializationForBasicTypes) - { - if (type == typeof(Channel) - || type == typeof(UserRootFolder)) - { - return false; + return false; } } @@ -1304,586 +238,6 @@ namespace Emby.Server.Implementations.Data && type != typeof(MusicAlbum); } - private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query) - { - return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false); - } - - private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization) - { - var typeString = reader.GetString(0); - - var type = _typeMapper.GetType(typeString); - - if (type is null) - { - return null; - } - - BaseItem item = null; - - if (TypeRequiresDeserialization(type) && !skipDeserialization) - { - try - { - item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem; - } - catch (JsonException ex) - { - Logger.LogError(ex, "Error deserializing item with JSON: {Data}", reader.GetString(1)); - } - } - - if (item is null) - { - try - { - item = Activator.CreateInstance(type) as BaseItem; - } - catch - { - } - } - - if (item is null) - { - return null; - } - - var index = 2; - - if (queryHasStartDate) - { - if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate)) - { - hasStartDate.StartDate = startDate; - } - - index++; - } - - if (reader.TryReadDateTime(index++, out var endDate)) - { - item.EndDate = endDate; - } - - if (reader.TryGetGuid(index, out var guid)) - { - item.ChannelId = guid; - } - - index++; - - if (enableProgramAttributes) - { - if (item is IHasProgramAttributes hasProgramAttributes) - { - if (reader.TryGetBoolean(index++, out var isMovie)) - { - hasProgramAttributes.IsMovie = isMovie; - } - - if (reader.TryGetBoolean(index++, out var isSeries)) - { - hasProgramAttributes.IsSeries = isSeries; - } - - if (reader.TryGetString(index++, out var episodeTitle)) - { - hasProgramAttributes.EpisodeTitle = episodeTitle; - } - - if (reader.TryGetBoolean(index++, out var isRepeat)) - { - hasProgramAttributes.IsRepeat = isRepeat; - } - } - else - { - index += 4; - } - } - - if (reader.TryGetSingle(index++, out var communityRating)) - { - item.CommunityRating = communityRating; - } - - if (HasField(query, ItemFields.CustomRating)) - { - if (reader.TryGetString(index++, out var customRating)) - { - item.CustomRating = customRating; - } - } - - if (reader.TryGetInt32(index++, out var indexNumber)) - { - item.IndexNumber = indexNumber; - } - - if (HasField(query, ItemFields.Settings)) - { - if (reader.TryGetBoolean(index++, out var isLocked)) - { - item.IsLocked = isLocked; - } - - if (reader.TryGetString(index++, out var preferredMetadataLanguage)) - { - item.PreferredMetadataLanguage = preferredMetadataLanguage; - } - - if (reader.TryGetString(index++, out var preferredMetadataCountryCode)) - { - item.PreferredMetadataCountryCode = preferredMetadataCountryCode; - } - } - - if (HasField(query, ItemFields.Width)) - { - if (reader.TryGetInt32(index++, out var width)) - { - item.Width = width; - } - } - - if (HasField(query, ItemFields.Height)) - { - if (reader.TryGetInt32(index++, out var height)) - { - item.Height = height; - } - } - - if (HasField(query, ItemFields.DateLastRefreshed)) - { - if (reader.TryReadDateTime(index++, out var dateLastRefreshed)) - { - item.DateLastRefreshed = dateLastRefreshed; - } - } - - if (reader.TryGetString(index++, out var name)) - { - item.Name = name; - } - - if (reader.TryGetString(index++, out var restorePath)) - { - item.Path = RestorePath(restorePath); - } - - if (reader.TryReadDateTime(index++, out var premiereDate)) - { - item.PremiereDate = premiereDate; - } - - if (HasField(query, ItemFields.Overview)) - { - if (reader.TryGetString(index++, out var overview)) - { - item.Overview = overview; - } - } - - if (reader.TryGetInt32(index++, out var parentIndexNumber)) - { - item.ParentIndexNumber = parentIndexNumber; - } - - if (reader.TryGetInt32(index++, out var productionYear)) - { - item.ProductionYear = productionYear; - } - - if (reader.TryGetString(index++, out var officialRating)) - { - item.OfficialRating = officialRating; - } - - if (HasField(query, ItemFields.SortName)) - { - if (reader.TryGetString(index++, out var forcedSortName)) - { - item.ForcedSortName = forcedSortName; - } - } - - if (reader.TryGetInt64(index++, out var runTimeTicks)) - { - item.RunTimeTicks = runTimeTicks; - } - - if (reader.TryGetInt64(index++, out var size)) - { - item.Size = size; - } - - if (HasField(query, ItemFields.DateCreated)) - { - if (reader.TryReadDateTime(index++, out var dateCreated)) - { - item.DateCreated = dateCreated; - } - } - - if (reader.TryReadDateTime(index++, out var dateModified)) - { - item.DateModified = dateModified; - } - - item.Id = reader.GetGuid(index++); - - if (HasField(query, ItemFields.Genres)) - { - if (reader.TryGetString(index++, out var genres)) - { - item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (reader.TryGetGuid(index++, out var parentId)) - { - item.ParentId = parentId; - } - - if (reader.TryGetString(index++, out var audioString)) - { - if (Enum.TryParse(audioString, true, out ProgramAudio audio)) - { - item.Audio = audio; - } - } - - // TODO: Even if not needed by apps, the server needs it internally - // But get this excluded from contexts where it is not needed - if (hasServiceName) - { - if (item is LiveTvChannel liveTvChannel) - { - if (reader.TryGetString(index, out var serviceName)) - { - liveTvChannel.ServiceName = serviceName; - } - } - - index++; - } - - if (reader.TryGetBoolean(index++, out var isInMixedFolder)) - { - item.IsInMixedFolder = isInMixedFolder; - } - - if (HasField(query, ItemFields.DateLastSaved)) - { - if (reader.TryReadDateTime(index++, out var dateLastSaved)) - { - item.DateLastSaved = dateLastSaved; - } - } - - if (HasField(query, ItemFields.Settings)) - { - if (reader.TryGetString(index++, out var lockedFields)) - { - List fields = null; - foreach (var i in lockedFields.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out MetadataField parsedValue)) - { - (fields ??= new List()).Add(parsedValue); - } - } - - item.LockedFields = fields?.ToArray() ?? Array.Empty(); - } - } - - if (HasField(query, ItemFields.Studios)) - { - if (reader.TryGetString(index++, out var studios)) - { - item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (HasField(query, ItemFields.Tags)) - { - if (reader.TryGetString(index++, out var tags)) - { - item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (hasTrailerTypes) - { - if (item is Trailer trailer) - { - if (reader.TryGetString(index, out var trailerTypes)) - { - List types = null; - foreach (var i in trailerTypes.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out TrailerType parsedValue)) - { - (types ??= new List()).Add(parsedValue); - } - } - - trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); - } - } - - index++; - } - - if (HasField(query, ItemFields.OriginalTitle)) - { - if (reader.TryGetString(index++, out var originalTitle)) - { - item.OriginalTitle = originalTitle; - } - } - - if (item is Video video) - { - if (reader.TryGetString(index, out var primaryVersionId)) - { - video.PrimaryVersionId = primaryVersionId; - } - } - - index++; - - if (HasField(query, ItemFields.DateLastMediaAdded)) - { - if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded)) - { - folder.DateLastMediaAdded = dateLastMediaAdded; - } - - index++; - } - - if (reader.TryGetString(index++, out var album)) - { - item.Album = album; - } - - if (reader.TryGetSingle(index++, out var lUFS)) - { - item.LUFS = lUFS; - } - - if (reader.TryGetSingle(index++, out var normalizationGain)) - { - item.NormalizationGain = normalizationGain; - } - - if (reader.TryGetSingle(index++, out var criticRating)) - { - item.CriticRating = criticRating; - } - - if (reader.TryGetBoolean(index++, out var isVirtualItem)) - { - item.IsVirtualItem = isVirtualItem; - } - - if (item is IHasSeries hasSeriesName) - { - if (reader.TryGetString(index, out var seriesName)) - { - hasSeriesName.SeriesName = seriesName; - } - } - - index++; - - if (hasEpisodeAttributes) - { - if (item is Episode episode) - { - if (reader.TryGetString(index, out var seasonName)) - { - episode.SeasonName = seasonName; - } - - index++; - if (reader.TryGetGuid(index, out var seasonId)) - { - episode.SeasonId = seasonId; - } - } - else - { - index++; - } - - index++; - } - - var hasSeries = item as IHasSeries; - if (hasSeriesFields) - { - if (hasSeries is not null) - { - if (reader.TryGetGuid(index, out var seriesId)) - { - hasSeries.SeriesId = seriesId; - } - } - - index++; - } - - if (HasField(query, ItemFields.PresentationUniqueKey)) - { - if (reader.TryGetString(index++, out var presentationUniqueKey)) - { - item.PresentationUniqueKey = presentationUniqueKey; - } - } - - if (HasField(query, ItemFields.InheritedParentalRatingValue)) - { - if (reader.TryGetInt32(index++, out var parentalRating)) - { - item.InheritedParentalRatingValue = parentalRating; - } - } - - if (HasField(query, ItemFields.ExternalSeriesId)) - { - if (reader.TryGetString(index++, out var externalSeriesId)) - { - item.ExternalSeriesId = externalSeriesId; - } - } - - if (HasField(query, ItemFields.Taglines)) - { - if (reader.TryGetString(index++, out var tagLine)) - { - item.Tagline = tagLine; - } - } - - if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds)) - { - DeserializeProviderIds(providerIds, item); - } - - index++; - - if (query.DtoOptions.EnableImages) - { - if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos)) - { - item.ImageInfos = DeserializeImages(imageInfos); - } - - index++; - } - - if (HasField(query, ItemFields.ProductionLocations)) - { - if (reader.TryGetString(index++, out var productionLocations)) - { - item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (HasField(query, ItemFields.ExtraIds)) - { - if (reader.TryGetString(index++, out var extraIds)) - { - item.ExtraIds = SplitToGuids(extraIds); - } - } - - if (reader.TryGetInt32(index++, out var totalBitrate)) - { - item.TotalBitrate = totalBitrate; - } - - if (reader.TryGetString(index++, out var extraTypeString)) - { - if (Enum.TryParse(extraTypeString, true, out ExtraType extraType)) - { - item.ExtraType = extraType; - } - } - - if (hasArtistFields) - { - if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists)) - { - hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - index++; - - if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists)) - { - hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - index++; - } - - if (reader.TryGetString(index++, out var externalId)) - { - item.ExternalId = externalId; - } - - if (HasField(query, ItemFields.SeriesPresentationUniqueKey)) - { - if (hasSeries is not null) - { - if (reader.TryGetString(index, out var seriesPresentationUniqueKey)) - { - hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; - } - } - - index++; - } - - if (enableProgramAttributes) - { - if (item is LiveTvProgram program && reader.TryGetString(index, out var showId)) - { - program.ShowId = showId; - } - - index++; - } - - if (reader.TryGetGuid(index, out var ownerId)) - { - item.OwnerId = ownerId; - } - - return item; - } - - private static Guid[] SplitToGuids(string value) - { - var ids = value.Split('|'); - - var result = new Guid[ids.Length]; - - for (var i = 0; i < result.Length; i++) - { - result[i] = new Guid(ids[i]); - } - - return result; - } - /// public List GetChapters(BaseItem item) { -- cgit v1.2.3 From 6c819fe516ba742f1dcc77d61f6eedbe987cd692 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:27:27 +0000 Subject: WIP BaseItem search refactoring --- .../Data/SqliteItemRepository.cs | 1211 +---------------- Jellyfin.Data/Entities/BaseItem.cs | 6 + Jellyfin.Data/Entities/People.cs | 1 + .../Item/BaseItemManager.cs | 1364 +++++++++++++++++++- .../Item/ChapterManager.cs | 58 +- MediaBrowser.Controller/Chapters/ChapterManager.cs | 24 + .../Chapters/IChapterManager.cs | 16 + .../Persistence/IItemRepository.cs | 22 - MediaBrowser.Providers/Chapters/ChapterManager.cs | 26 - .../MediaBrowser.Providers.csproj | 2 +- 10 files changed, 1444 insertions(+), 1286 deletions(-) create mode 100644 MediaBrowser.Controller/Chapters/ChapterManager.cs delete mode 100644 MediaBrowser.Providers/Chapters/ChapterManager.cs (limited to 'Emby.Server.Implementations/Data/SqliteItemRepository.cs') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index a2aeaf0fc..94a5eba81 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -59,119 +59,6 @@ namespace Emby.Server.Implementations.Data private readonly TypeMapper _typeMapper; private readonly JsonSerializerOptions _jsonOptions; - private readonly ItemFields[] _allItemFields = Enum.GetValues(); - - private static readonly string _mediaStreamSaveColumnsInsertQuery = - $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values "; - - private static readonly string _mediaStreamSaveColumnsSelectQuery = - $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId"; - - private static readonly string[] _mediaAttachmentSaveColumns = - { - "ItemId", - "AttachmentIndex", - "Codec", - "CodecTag", - "Comment", - "Filename", - "MIMEType" - }; - - private static readonly string _mediaAttachmentSaveColumnsSelectQuery = - $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId"; - - private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix(); - - private static readonly BaseItemKind[] _programTypes = new[] - { - BaseItemKind.Program, - BaseItemKind.TvChannel, - BaseItemKind.LiveTvProgram, - BaseItemKind.LiveTvChannel - }; - - private static readonly BaseItemKind[] _programExcludeParentTypes = new[] - { - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicArtist, - BaseItemKind.PhotoAlbum - }; - - private static readonly BaseItemKind[] _serviceTypes = new[] - { - BaseItemKind.TvChannel, - BaseItemKind.LiveTvChannel - }; - - private static readonly BaseItemKind[] _startDateTypes = new[] - { - BaseItemKind.Program, - BaseItemKind.LiveTvProgram - }; - - private static readonly BaseItemKind[] _seriesTypes = new[] - { - BaseItemKind.Book, - BaseItemKind.AudioBook, - BaseItemKind.Episode, - BaseItemKind.Season - }; - - private static readonly BaseItemKind[] _artistExcludeParentTypes = new[] - { - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.PhotoAlbum - }; - - private static readonly BaseItemKind[] _artistsTypes = new[] - { - BaseItemKind.Audio, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicVideo, - BaseItemKind.AudioBook - }; - - private static readonly Dictionary _baseItemKindNames = new() - { - { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, - { BaseItemKind.Audio, typeof(Audio).FullName }, - { BaseItemKind.AudioBook, typeof(AudioBook).FullName }, - { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName }, - { BaseItemKind.Book, typeof(Book).FullName }, - { BaseItemKind.BoxSet, typeof(BoxSet).FullName }, - { BaseItemKind.Channel, typeof(Channel).FullName }, - { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName }, - { BaseItemKind.Episode, typeof(Episode).FullName }, - { BaseItemKind.Folder, typeof(Folder).FullName }, - { BaseItemKind.Genre, typeof(Genre).FullName }, - { BaseItemKind.Movie, typeof(Movie).FullName }, - { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName }, - { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName }, - { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName }, - { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName }, - { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName }, - { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName }, - { BaseItemKind.Person, typeof(Person).FullName }, - { BaseItemKind.Photo, typeof(Photo).FullName }, - { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName }, - { BaseItemKind.Playlist, typeof(Playlist).FullName }, - { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName }, - { BaseItemKind.Season, typeof(Season).FullName }, - { BaseItemKind.Series, typeof(Series).FullName }, - { BaseItemKind.Studio, typeof(Studio).FullName }, - { BaseItemKind.Trailer, typeof(Trailer).FullName }, - { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName }, - { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName }, - { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName }, - { BaseItemKind.UserView, typeof(UserView).FullName }, - { BaseItemKind.Video, typeof(Video).FullName }, - { BaseItemKind.Year, typeof(Year).FullName } - }; - /// /// Initializes a new instance of the class. /// @@ -210,957 +97,15 @@ namespace Emby.Server.Implementations.Data /// protected override TempStoreMode TempStore => TempStoreMode.Memory; - /// - /// Opens the connection to the database. - /// - public override void Initialize() - { - base.Initialize(); - - const string CreateMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - - const string CreateMediaAttachmentsTableCommand - = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; - - string[] queries = - { - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", - - "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", - "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", - "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", - - "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", - - "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - - "drop index if exists idxPeopleItemId", - "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", - "create index if not exists idxPeopleName on People(Name)", - - "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - - CreateMediaStreamsTableCommand, - CreateMediaAttachmentsTableCommand, - - "pragma shrink_memory" - }; - - string[] postQueries = - { - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", - "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", - - "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", - "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", - "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", - - // covering index - "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", - - // series - "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - - // series counts - // seriesdateplayed sort order - "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", - - // live tv programs - "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", - - // covering index for getitemvalues - "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", - - // used by movie suggestions - "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", - "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", - - // latest items - "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", - "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", - - // resume - "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", - - // items by name - "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", - - // Used to update inherited tags - "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", - - "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", - "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" - }; - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - connection.Execute(string.Join(';', queries)); - - var existingColumnNames = GetColumnNames(connection, "AncestorIds"); - AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); - - AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "ItemValues"); - AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, ChaptersTableName); - AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "MediaStreams"); - AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames); - - connection.Execute(string.Join(';', postQueries)); - - transaction.Commit(); - } - } - - /// - public void SaveImages(BaseItem item) - { - ArgumentNullException.ThrowIfNull(item); - - CheckDisposed(); - - var images = SerializeImages(item.ImageInfos); - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); - saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", images); - - saveImagesStatement.ExecuteNonQuery(); - transaction.Commit(); - } - - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// - /// or is null. - /// - public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(items); - - cancellationToken.ThrowIfCancellationRequested(); - - CheckDisposed(); - - var itemsLen = items.Count; - var tuples = new ValueTuple, BaseItem, string, List>[itemsLen]; - for (int i = 0; i < itemsLen; i++) - { - var item = items[i]; - var ancestorIds = item.SupportsAncestors ? - item.GetAncestorIds().Distinct().ToList() : - null; - - var topParent = item.GetTopParent(); - - var userdataKey = item.GetUserDataKeys().FirstOrDefault(); - var inheritedTags = item.GetInheritedTags(); - - tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); - } - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - SaveItemsInTransaction(connection, tuples); - transaction.Commit(); - } - private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples) + private bool TypeRequiresDeserialization(Type type) { - using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) - using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) + if (_config.Configuration.SkipDeserializationForBasicTypes) { - var requiresReset = false; - foreach (var tuple in tuples) + if (type == typeof(Channel) + || type == typeof(UserRootFolder)) { - if (requiresReset) - { - saveItemStatement.Parameters.Clear(); - deleteAncestorsStatement.Parameters.Clear(); - } - - var item = tuple.Item; - var topParent = tuple.TopParent; - var userDataKey = tuple.UserDataKey; - - SaveItem(item, topParent, userDataKey, saveItemStatement); - - var inheritedTags = tuple.InheritedTags; - - if (item.SupportsAncestors) - { - UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement); - } - - UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); - - requiresReset = true; - } - } - } - - private string GetPathToSave(string path) - { - if (path is null) - { - return null; - } - - return _appHost.ReverseVirtualPath(path); - } - - private string RestorePath(string path) - { - return _appHost.ExpandVirtualPath(path); - } - - private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) - { - Type type = item.GetType(); - - saveItemStatement.TryBind("@guid", item.Id); - saveItemStatement.TryBind("@type", type.FullName); - - if (TypeRequiresDeserialization(type)) - { - saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); - } - else - { - saveItemStatement.TryBindNull("@data"); - } - - saveItemStatement.TryBind("@Path", GetPathToSave(item.Path)); - - if (item is IHasStartDate hasStartDate) - { - saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate); - } - else - { - saveItemStatement.TryBindNull("@StartDate"); - } - - if (item.EndDate.HasValue) - { - saveItemStatement.TryBind("@EndDate", item.EndDate.Value); - } - else - { - saveItemStatement.TryBindNull("@EndDate"); - } - - saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); - - if (item is IHasProgramAttributes hasProgramAttributes) - { - saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); - saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); - saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); - saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); - } - else - { - saveItemStatement.TryBindNull("@IsMovie"); - saveItemStatement.TryBindNull("@IsSeries"); - saveItemStatement.TryBindNull("@EpisodeTitle"); - saveItemStatement.TryBindNull("@IsRepeat"); - } - - saveItemStatement.TryBind("@CommunityRating", item.CommunityRating); - saveItemStatement.TryBind("@CustomRating", item.CustomRating); - saveItemStatement.TryBind("@IndexNumber", item.IndexNumber); - saveItemStatement.TryBind("@IsLocked", item.IsLocked); - saveItemStatement.TryBind("@Name", item.Name); - saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); - saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); - saveItemStatement.TryBind("@Overview", item.Overview); - saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); - saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); - saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); - - var parentId = item.ParentId; - if (parentId.IsEmpty()) - { - saveItemStatement.TryBindNull("@ParentId"); - } - else - { - saveItemStatement.TryBind("@ParentId", parentId); - } - - if (item.Genres.Length > 0) - { - saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres)); - } - else - { - saveItemStatement.TryBindNull("@Genres"); - } - - saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); - - saveItemStatement.TryBind("@SortName", item.SortName); - - saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName); - - saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); - saveItemStatement.TryBind("@Size", item.Size); - - saveItemStatement.TryBind("@DateCreated", item.DateCreated); - saveItemStatement.TryBind("@DateModified", item.DateModified); - - saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); - saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); - - if (item.Width > 0) - { - saveItemStatement.TryBind("@Width", item.Width); - } - else - { - saveItemStatement.TryBindNull("@Width"); - } - - if (item.Height > 0) - { - saveItemStatement.TryBind("@Height", item.Height); - } - else - { - saveItemStatement.TryBindNull("@Height"); - } - - if (item.DateLastRefreshed != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed); - } - else - { - saveItemStatement.TryBindNull("@DateLastRefreshed"); - } - - if (item.DateLastSaved != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved); - } - else - { - saveItemStatement.TryBindNull("@DateLastSaved"); - } - - saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); - - if (item.LockedFields.Length > 0) - { - saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields)); - } - else - { - saveItemStatement.TryBindNull("@LockedFields"); - } - - if (item.Studios.Length > 0) - { - saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios)); - } - else - { - saveItemStatement.TryBindNull("@Studios"); - } - - if (item.Audio.HasValue) - { - saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@Audio"); - } - - if (item is LiveTvChannel liveTvChannel) - { - saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName); - } - else - { - saveItemStatement.TryBindNull("@ExternalServiceId"); - } - - if (item.Tags.Length > 0) - { - saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags)); - } - else - { - saveItemStatement.TryBindNull("@Tags"); - } - - saveItemStatement.TryBind("@IsFolder", item.IsFolder); - - saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); - - if (topParent is null) - { - saveItemStatement.TryBindNull("@TopParentId"); - } - else - { - saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture)); - } - - if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) - { - saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes)); - } - else - { - saveItemStatement.TryBindNull("@TrailerTypes"); - } - - saveItemStatement.TryBind("@CriticRating", item.CriticRating); - - if (string.IsNullOrWhiteSpace(item.Name)) - { - saveItemStatement.TryBindNull("@CleanName"); - } - else - { - saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name)); - } - - saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey); - saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle); - - if (item is Video video) - { - saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId); - } - else - { - saveItemStatement.TryBindNull("@PrimaryVersionId"); - } - - if (item is Folder folder && folder.DateLastMediaAdded.HasValue) - { - saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value); - } - else - { - saveItemStatement.TryBindNull("@DateLastMediaAdded"); - } - - saveItemStatement.TryBind("@Album", item.Album); - saveItemStatement.TryBind("@LUFS", item.LUFS); - saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain); - saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); - - if (item is IHasSeries hasSeriesName) - { - saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName); - } - else - { - saveItemStatement.TryBindNull("@SeriesName"); - } - - if (string.IsNullOrWhiteSpace(userDataKey)) - { - saveItemStatement.TryBindNull("@UserDataKey"); - } - else - { - saveItemStatement.TryBind("@UserDataKey", userDataKey); - } - - if (item is Episode episode) - { - saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - - var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; - - saveItemStatement.TryBind("@SeasonId", nullableSeasonId); - } - else - { - saveItemStatement.TryBindNull("@SeasonName"); - saveItemStatement.TryBindNull("@SeasonId"); - } - - if (item is IHasSeries hasSeries) - { - var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; - - saveItemStatement.TryBind("@SeriesId", nullableSeriesId); - saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); - } - else - { - saveItemStatement.TryBindNull("@SeriesId"); - saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey"); - } - - saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); - saveItemStatement.TryBind("@Tagline", item.Tagline); - - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); - saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); - - if (item.ProductionLocations.Length > 0) - { - saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations)); - } - else - { - saveItemStatement.TryBindNull("@ProductionLocations"); - } - - if (item.ExtraIds.Length > 0) - { - saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds)); - } - else - { - saveItemStatement.TryBindNull("@ExtraIds"); - } - - saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); - if (item.ExtraType.HasValue) - { - saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@ExtraType"); - } - - string artists = null; - if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) - { - artists = string.Join('|', hasArtists.Artists); - } - - saveItemStatement.TryBind("@Artists", artists); - - string albumArtists = null; - if (item is IHasAlbumArtist hasAlbumArtists - && hasAlbumArtists.AlbumArtists.Count > 0) - { - albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists); - } - - saveItemStatement.TryBind("@AlbumArtists", albumArtists); - saveItemStatement.TryBind("@ExternalId", item.ExternalId); - - if (item is LiveTvProgram program) - { - saveItemStatement.TryBind("@ShowId", program.ShowId); - } - else - { - saveItemStatement.TryBindNull("@ShowId"); - } - - Guid ownerId = item.OwnerId; - if (ownerId.IsEmpty()) - { - saveItemStatement.TryBindNull("@OwnerId"); - } - else - { - saveItemStatement.TryBind("@OwnerId", ownerId); - } - - saveItemStatement.ExecuteNonQuery(); - } - - internal static string SerializeProviderIds(Dictionary providerIds) - { - StringBuilder str = new StringBuilder(); - foreach (var i in providerIds) - { - // Ideally we shouldn't need this IsNullOrWhiteSpace check, - // but we're seeing some cases of bad data slip through - if (string.IsNullOrWhiteSpace(i.Value)) - { - continue; - } - - str.Append(i.Key) - .Append('=') - .Append(i.Value) - .Append('|'); - } - - if (str.Length == 0) - { - return null; - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal static void DeserializeProviderIds(string value, IHasProviderIds item) - { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - foreach (var part in value.SpanSplit('|')) - { - var providerDelimiterIndex = part.IndexOf('='); - // Don't let empty values through - if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) - { - item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); - } - } - } - - internal string SerializeImages(ItemImageInfo[] images) - { - if (images.Length == 0) - { - return null; - } - - StringBuilder str = new StringBuilder(); - foreach (var i in images) - { - if (string.IsNullOrWhiteSpace(i.Path)) - { - continue; - } - - AppendItemImageInfo(str, i); - str.Append('|'); - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal ItemImageInfo[] DeserializeImages(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Array.Empty(); - } - - // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed - var valueSpan = value.AsSpan(); - var count = valueSpan.Count('|') + 1; - - var position = 0; - var result = new ItemImageInfo[count]; - foreach (var part in valueSpan.Split('|')) - { - var image = ItemImageInfoFromValueString(part); - - if (image is not null) - { - result[position++] = image; - } - } - - if (position == count) - { - return result; - } - - if (position == 0) - { - return Array.Empty(); - } - - // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. - return result[..position]; - } - - private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) - { - const char Delimiter = '*'; - - var path = image.Path ?? string.Empty; - - bldr.Append(GetPathToSave(path)) - .Append(Delimiter) - .Append(image.DateModified.Ticks) - .Append(Delimiter) - .Append(image.Type) - .Append(Delimiter) - .Append(image.Width) - .Append(Delimiter) - .Append(image.Height); - - var hash = image.BlurHash; - if (!string.IsNullOrEmpty(hash)) - { - bldr.Append(Delimiter) - // Replace delimiters with other characters. - // This can be removed when we migrate to a proper DB. - .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); - } - } - - internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value) - { - const char Delimiter = '*'; - - var nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan path = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan dateModified = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan imageType = value[..nextSegment]; - - var image = new ItemImageInfo - { - Path = RestorePath(path.ToString()) - }; - - if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) - && ticks >= DateTime.MinValue.Ticks - && ticks <= DateTime.MaxValue.Ticks) - { - image.DateModified = new DateTime(ticks, DateTimeKind.Utc); - } - else - { - return null; - } - - if (Enum.TryParse(imageType, true, out ImageType type)) - { - image.Type = type; - } - else - { - return null; - } - - // Optional parameters: width*height*blurhash - if (nextSegment + 1 < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1 || nextSegment == value.Length) - { - return image; - } - - ReadOnlySpan widthSpan = value[..nextSegment]; - - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan heightSpan = value[..nextSegment]; - - if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) - && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) - { - image.Width = width; - image.Height = height; - } - - if (nextSegment < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - var length = value.Length; - - Span blurHashSpan = stackalloc char[length]; - for (int i = 0; i < length; i++) - { - var c = value[i]; - blurHashSpan[i] = c switch - { - '/' => Delimiter, - '\\' => '|', - _ => c - }; - } - - image.BlurHash = new string(blurHashSpan); - } - } - - return image; - } - - /// - /// Internal retrieve from items or users table. - /// - /// The id. - /// BaseItem. - /// is null. - /// is . - public BaseItem RetrieveItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - CheckDisposed(); - - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) - { - statement.TryBind("@guid", id); - - foreach (var row in statement.ExecuteQuery()) - { - return GetItem(row, new InternalItemsQuery()); - } - } - - return null; - } - - private bool TypeRequiresDeserialization(Type type) - { - if (_config.Configuration.SkipDeserializationForBasicTypes) - { - if (type == typeof(Channel) - || type == typeof(UserRootFolder)) - { - return false; + return false; } } @@ -1179,152 +124,6 @@ namespace Emby.Server.Implementations.Data && type != typeof(MusicAlbum); } - /// - public List GetChapters(BaseItem item) - { - CheckDisposed(); - - var chapters = new List(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) - { - statement.TryBind("@ItemId", item.Id); - - foreach (var row in statement.ExecuteQuery()) - { - chapters.Add(GetChapter(row, item)); - } - } - - return chapters; - } - - /// - public ChapterInfo GetChapter(BaseItem item, int index) - { - CheckDisposed(); - - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) - { - statement.TryBind("@ItemId", item.Id); - statement.TryBind("@ChapterIndex", index); - - foreach (var row in statement.ExecuteQuery()) - { - return GetChapter(row, item); - } - } - - return null; - } - - /// - /// Gets the chapter. - /// - /// The reader. - /// The item. - /// ChapterInfo. - private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item) - { - var chapter = new ChapterInfo - { - StartPositionTicks = reader.GetInt64(0) - }; - - if (reader.TryGetString(1, out var chapterName)) - { - chapter.Name = chapterName; - } - - if (reader.TryGetString(2, out var imagePath)) - { - chapter.ImagePath = imagePath; - chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter); - } - - if (reader.TryReadDateTime(3, out var imageDateModified)) - { - chapter.ImageDateModified = imageDateModified; - } - - return chapter; - } - - /// - /// Saves the chapters. - /// - /// The item id. - /// The chapters. - public void SaveChapters(Guid id, IReadOnlyList chapters) - { - CheckDisposed(); - - if (id.IsEmpty()) - { - throw new ArgumentNullException(nameof(id)); - } - - ArgumentNullException.ThrowIfNull(chapters); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // First delete chapters - using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); - - InsertChapters(id, chapters, connection); - transaction.Commit(); - } - - private void InsertChapters(Guid idBlob, IReadOnlyList chapters, ManagedConnection db) - { - var startIndex = 0; - var limit = 100; - var chapterIndex = 0; - - const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values "; - var insertText = new StringBuilder(StartInsertText, 256); - - while (startIndex < chapters.Count) - { - var endIndex = Math.Min(chapters.Count, startIndex + limit); - - for (var i = startIndex; i < endIndex; i++) - { - insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); - } - - insertText.Length -= 1; // Remove trailing comma - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", idBlob); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var chapter = chapters[i]; - - statement.TryBind("@ChapterIndex" + index, chapterIndex); - statement.TryBind("@StartPositionTicks" + index, chapter.StartPositionTicks); - statement.TryBind("@Name" + index, chapter.Name); - statement.TryBind("@ImagePath" + index, chapter.ImagePath); - statement.TryBind("@ImageDateModified" + index, chapter.ImageDateModified); - - chapterIndex++; - } - - statement.ExecuteNonQuery(); - } - - startIndex += limit; - insertText.Length = StartInsertText.Length; - } - } - private static bool EnableJoinUserData(InternalItemsQuery query) { if (query.User is null) diff --git a/Jellyfin.Data/Entities/BaseItem.cs b/Jellyfin.Data/Entities/BaseItem.cs index c0c88b2e6..18166f7c1 100644 --- a/Jellyfin.Data/Entities/BaseItem.cs +++ b/Jellyfin.Data/Entities/BaseItem.cs @@ -161,4 +161,10 @@ public class BaseItem public int? Height { get; set; } public long? Size { get; set; } + + public ICollection? Peoples { get; set; } + + public ICollection? UserData { get; set; } + + public ICollection? ItemValues { get; set; } } diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs index 72c39699b..014a0f1c9 100644 --- a/Jellyfin.Data/Entities/People.cs +++ b/Jellyfin.Data/Entities/People.cs @@ -7,6 +7,7 @@ namespace Jellyfin.Data.Entities; public class People { public Guid ItemId { get; set; } + public BaseItem Item { get; set; } public required string Name { get; set; } public string? Role { get; set; } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index 4ad842e0b..85dc98e09 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -3,16 +3,24 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Linq.Expressions; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; +using System.Threading.Channels; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; @@ -22,25 +30,1316 @@ namespace Jellyfin.Server.Implementations.Item; /// /// Handles all storage logic for BaseItems. /// -public class BaseItemManager +public class BaseItemManager : IItemRepository { private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationHost _appHost; - /// + + + private readonly ItemFields[] _allItemFields = Enum.GetValues(); + + private static readonly BaseItemKind[] _programTypes = new[] + { + BaseItemKind.Program, + BaseItemKind.TvChannel, + BaseItemKind.LiveTvProgram, + BaseItemKind.LiveTvChannel + }; + + private static readonly BaseItemKind[] _programExcludeParentTypes = new[] + { + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.PhotoAlbum + }; + + private static readonly BaseItemKind[] _serviceTypes = new[] + { + BaseItemKind.TvChannel, + BaseItemKind.LiveTvChannel + }; + + private static readonly BaseItemKind[] _startDateTypes = new[] + { + BaseItemKind.Program, + BaseItemKind.LiveTvProgram + }; + + private static readonly BaseItemKind[] _seriesTypes = new[] + { + BaseItemKind.Book, + BaseItemKind.AudioBook, + BaseItemKind.Episode, + BaseItemKind.Season + }; + + private static readonly BaseItemKind[] _artistExcludeParentTypes = new[] + { + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.PhotoAlbum + }; + + private static readonly BaseItemKind[] _artistsTypes = new[] + { + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicVideo, + BaseItemKind.AudioBook + }; + + private static readonly Dictionary _baseItemKindNames = new() + { + { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, + { BaseItemKind.Audio, typeof(Audio).FullName }, + { BaseItemKind.AudioBook, typeof(AudioBook).FullName }, + { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName }, + { BaseItemKind.Book, typeof(Book).FullName }, + { BaseItemKind.BoxSet, typeof(BoxSet).FullName }, + { BaseItemKind.Channel, typeof(Channel).FullName }, + { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName }, + { BaseItemKind.Episode, typeof(Episode).FullName }, + { BaseItemKind.Folder, typeof(Folder).FullName }, + { BaseItemKind.Genre, typeof(Genre).FullName }, + { BaseItemKind.Movie, typeof(Movie).FullName }, + { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName }, + { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName }, + { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName }, + { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName }, + { BaseItemKind.Person, typeof(Person).FullName }, + { BaseItemKind.Photo, typeof(Photo).FullName }, + { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName }, + { BaseItemKind.Playlist, typeof(Playlist).FullName }, + { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName }, + { BaseItemKind.Season, typeof(Season).FullName }, + { BaseItemKind.Series, typeof(Series).FullName }, + { BaseItemKind.Studio, typeof(Studio).FullName }, + { BaseItemKind.Trailer, typeof(Trailer).FullName }, + { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName }, + { BaseItemKind.UserView, typeof(UserView).FullName }, + { BaseItemKind.Video, typeof(Video).FullName }, + { BaseItemKind.Year, typeof(Year).FullName } + }; + + /// /// This holds all the types in the running assemblies /// so that we can de-serialize properly when we don't have strong types. /// private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); - /// - /// Initializes a new instance of the class. - /// - /// The db factory. - public BaseItemManager(IDbContextFactory dbProvider, IServerApplicationHost appHost) - { - _dbProvider = dbProvider; - _appHost = appHost; + /// + /// Initializes a new instance of the class. + /// + /// The db factory. + /// The Application host. + public BaseItemManager(IDbContextFactory dbProvider, IServerApplicationHost appHost) + { + _dbProvider = dbProvider; + _appHost = appHost; + } + + public int GetCount(InternalItemsQuery query) + { + ArgumentNullException.ThrowIfNull(query); + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + + if (query.IsResumable ?? false) + { + query.IsVirtualItem = false; + } + + + + } + + private IQueryable TranslateQuery( + IQueryable baseQuery, + JellyfinDbContext context, + InternalItemsQuery query) + { + var minWidth = query.MinWidth; + var maxWidth = query.MaxWidth; + var now = DateTime.UtcNow; + + if (query.IsHD.HasValue) + { + const int Threshold = 1200; + if (query.IsHD.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (query.Is4K.HasValue) + { + const int Threshold = 3800; + if (query.Is4K.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (minWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= minWidth); + } + + if (query.MinHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height >= query.MinHeight); + } + + if (maxWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= maxWidth); + } + + if (query.MaxHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height <= query.MaxHeight); + } + + if (query.IsLocked.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsLocked == query.IsLocked); + } + + var tags = query.Tags.ToList(); + var excludeTags = query.ExcludeTags.ToList(); + + if (query.IsMovie == true) + { + if (query.IncludeItemTypes.Length == 0 + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + { + baseQuery = baseQuery.Where(e => e.IsMovie); + } + } + else if (query.IsMovie.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsMovie == query.IsMovie); + } + + if (query.IsSeries.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsSeries == query.IsSeries); + } + + if (query.IsSports.HasValue) + { + if (query.IsSports.Value) + { + tags.Add("Sports"); + } + else + { + excludeTags.Add("Sports"); + } + } + + if (query.IsNews.HasValue) + { + if (query.IsNews.Value) + { + tags.Add("News"); + } + else + { + excludeTags.Add("News"); + } + } + + if (query.IsKids.HasValue) + { + if (query.IsKids.Value) + { + tags.Add("Kids"); + } + else + { + excludeTags.Add("Kids"); + } + } + + 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"); + } + + if (query.IsFolder.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsFolder == query.IsFolder); + } + + var includeTypes = query.IncludeItemTypes; + // Only specify excluded types if no included types are specified + if (query.IncludeItemTypes.Length == 0) + { + var excludeTypes = query.ExcludeItemTypes; + if (excludeTypes.Length == 1) + { + if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) + { + baseQuery = baseQuery.Where(e => e.Type != excludeTypeName); + } + } + else if (excludeTypes.Length > 1) + { + var excludeTypeName = new List(); + foreach (var excludeType in excludeTypes) + { + if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) + { + excludeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type)); + } + } + else if (includeTypes.Length == 1) + { + if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) + { + baseQuery = baseQuery.Where(e => e.Type == includeTypeName); + } + } + else if (includeTypes.Length > 1) + { + var includeTypeName = new List(); + foreach (var includeType in includeTypes) + { + if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) + { + includeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); + } + + if (query.ChannelIds.Count == 1) + { + baseQuery = baseQuery.Where(e => e.ChannelId == query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); + } + else if (query.ChannelIds.Count > 1) + { + baseQuery = baseQuery.Where(e => query.ChannelIds.Select(f => f.ToString("N", CultureInfo.InvariantCulture)).Contains(e.ChannelId)); + } + + if (!query.ParentId.IsEmpty()) + { + baseQuery = baseQuery.Where(e => e.ParentId.Equals(query.ParentId)); + } + + if (!string.IsNullOrWhiteSpace(query.Path)) + { + baseQuery = baseQuery.Where(e => e.Path == query.Path); + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == query.PresentationUniqueKey); + } + + if (query.MinCommunityRating.HasValue) + { + baseQuery = baseQuery.Where(e => e.CommunityRating >= query.MinCommunityRating); + } + + if (query.MinIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber >= query.MinIndexNumber); + } + + if (query.MinParentAndIndexNumber.HasValue) + { + baseQuery = baseQuery + .Where(e => (e.ParentIndexNumber == query.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= query.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > query.MinParentAndIndexNumber.Value.ParentIndexNumber); + } + + if (query.MinDateCreated.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateCreated >= query.MinDateCreated); + } + + if (query.MinDateLastSaved.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= query.MinDateLastSaved.Value); + } + + if (query.MinDateLastSavedForUser.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= query.MinDateLastSavedForUser.Value); + } + + if (query.IndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber == query.IndexNumber.Value); + } + + if (query.ParentIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber == query.ParentIndexNumber.Value); + } + + if (query.ParentIndexNumberNotEquals.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber != query.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); + } + + var minEndDate = query.MinEndDate; + var maxEndDate = query.MaxEndDate; + + if (query.HasAired.HasValue) + { + if (query.HasAired.Value) + { + maxEndDate = DateTime.UtcNow; + } + else + { + minEndDate = DateTime.UtcNow; + } + } + + if (minEndDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate); + } + + if (maxEndDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate); + } + + if (query.MinStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate >= query.MinStartDate.Value); + } + + if (query.MaxStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate <= query.MaxStartDate.Value); + } + + if (query.MinPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= query.MinPremiereDate.Value); + } + + if (query.MaxPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= query.MaxPremiereDate.Value); + } + + if (query.TrailerTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => query.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); + } + + if (query.IsAiring.HasValue) + { + if (query.IsAiring.Value) + { + baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now); + } + else + { + baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now); + } + } + + if (query.PersonIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => + context.Peoples.Where(w => context.BaseItems.Where(w => query.PersonIds.Contains(w.Id)).Any(f => f.Name == w.Name)) + .Any(f => f.ItemId.Equals(e.Id))); + } + + if (!string.IsNullOrWhiteSpace(query.Person)) + { + baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == query.Person)); + } + + if (!string.IsNullOrWhiteSpace(query.MinSortName)) + { + // this does not makes sense. + // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName); + // whereClauses.Add("SortName>=@MinSortName"); + // statement?.TryBind("@MinSortName", query.MinSortName); + } + + if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) + { + baseQuery = baseQuery.Where(e => e.ExternalSeriesId == query.ExternalSeriesId); + } + + if (!string.IsNullOrWhiteSpace(query.ExternalId)) + { + baseQuery = baseQuery.Where(e => e.ExternalId == query.ExternalId); + } + + if (!string.IsNullOrWhiteSpace(query.Name)) + { + var cleanName = GetCleanValue(query.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } + + // These are the same, for now + var nameContains = query.NameContains; + if (!string.IsNullOrWhiteSpace(nameContains)) + { + baseQuery = baseQuery.Where(e => + e.CleanName == query.NameContains + || e.OriginalTitle!.Contains(query.NameContains!, StringComparison.Ordinal)); + } + + if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) + { + baseQuery = baseQuery.Where(e => e.SortName!.Contains(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] > query.NameStartsWithOrGreater[0]); + } + + if (!string.IsNullOrWhiteSpace(query.NameLessThan)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] < query.NameLessThan[0]); + } + + if (query.ImageTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => query.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); + } + + if (query.IsLiked.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); + } + + if (query.IsFavoriteOrLiked.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == query.IsFavoriteOrLiked); + } + + if (query.IsFavorite.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == query.IsFavorite); + } + + if (query.IsPlayed.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played == query.IsPlayed.Value); + } + + if (query.IsResumable.HasValue) + { + if (query.IsResumable.Value) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks > 0); + } + else + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); + } + } + + if (query.ArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type <= 1 && context.BaseItems.Where(w => query.ArtistIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + } + + if (query.AlbumArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 1 && context.BaseItems.Where(w => query.ArtistIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + } + + if (query.ContributingArtistIds.Length > 0) + { + var contributingArtists = context.BaseItems.Where(e => query.ContributingArtistIds.Contains(e.Id)); + baseQuery = baseQuery.Where(e => e.ItemValues!.Any(f => f.Type == 0 && contributingArtists.Any(w => w.CleanName == f.CleanValue))); + + clauseBuilder.Append('('); + for (var i = 0; i < query.ContributingArtistIds.Length; i++) + { + clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.AlbumIds.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.AlbumIds.Length; i++) + { + clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds") + .Append(i) + .Append(") OR "); + statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.ExcludeArtistIds.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.ExcludeArtistIds.Length; i++) + { + clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId") + .Append(i) + .Append(") and Type<=1)) OR "); + statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.GenreIds.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.GenreIds.Count; i++) + { + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId") + .Append(i) + .Append(") and Type=2)) OR "); + statement?.TryBind("@GenreId" + i, query.GenreIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.Genres.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.Genres.Count; i++) + { + clauseBuilder.Append("@Genre") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR "); + statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i])); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (tags.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < tags.Count; i++) + { + clauseBuilder.Append("@Tag") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@Tag" + i, GetCleanValue(tags[i])); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (excludeTags.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < excludeTags.Count; i++) + { + clauseBuilder.Append("@ExcludeTag") + .Append(i) + .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i])); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.StudioIds.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.StudioIds.Length; i++) + { + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId") + .Append(i) + .Append(") and Type=3)) OR "); + statement?.TryBind("@StudioId" + i, query.StudioIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.OfficialRatings.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.OfficialRatings.Length; i++) + { + clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or); + statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + clauseBuilder.Append('('); + if (query.HasParentalRating ?? false) + { + clauseBuilder.Append("InheritedParentalRatingValue not null"); + if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } + + if (query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + } + else if (query.BlockUnratedItems.Length > 0) + { + const string ParamName = "@UnratedType"; + clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in ("); + + for (int i = 0; i < query.BlockUnratedItems.Length; i++) + { + clauseBuilder.Append(ParamName).Append(i).Append(','); + statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString()); + } + + // Remove trailing comma + clauseBuilder.Length--; + clauseBuilder.Append("))"); + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(" OR ("); + } + + if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } + + if (query.MaxParentalRating.HasValue) + { + if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append(" AND "); + } + + clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(')'); + } + + if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) + { + clauseBuilder.Append(" OR InheritedParentalRatingValue not null"); + } + } + else if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + + if (query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + + clauseBuilder.Append(')'); + } + else if (query.MaxParentalRating.HasValue) + { + clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + else if (!query.HasParentalRating ?? false) + { + clauseBuilder.Append("InheritedParentalRatingValue is null"); + } + + if (clauseBuilder.Length > 1) + { + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.HasOfficialRating.HasValue) + { + if (query.HasOfficialRating.Value) + { + whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')"); + } + else + { + whereClauses.Add("(OfficialRating is null OR OfficialRating='')"); + } + } + + if (query.HasOverview.HasValue) + { + if (query.HasOverview.Value) + { + whereClauses.Add("(Overview not null AND Overview<>'')"); + } + else + { + whereClauses.Add("(Overview is null OR Overview='')"); + } + } + + if (query.HasOwnerId.HasValue) + { + if (query.HasOwnerId.Value) + { + whereClauses.Add("OwnerId not null"); + } + else + { + whereClauses.Add("OwnerId is null"); + } + } + + if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); + } + + if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); + } + + if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); + } + + if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); + } + + if (query.HasSubtitles.HasValue) + { + if (query.HasSubtitles.Value) + { + whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)"); + } + else + { + whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)"); + } + } + + if (query.HasChapterImages.HasValue) + { + if (query.HasChapterImages.Value) + { + whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) not null)"); + } + else + { + whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) is null)"); + } + } + + if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value) + { + whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); + } + + if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value) + { + whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))"); + } + + if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value) + { + whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)"); + } + + if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value) + { + whereClauses.Add("Name not in (Select Name From People)"); + } + + if (query.Years.Length == 1) + { + whereClauses.Add("ProductionYear=@Years"); + statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); + } + else if (query.Years.Length > 1) + { + var val = string.Join(',', query.Years); + whereClauses.Add("ProductionYear in (" + val + ")"); + } + + var isVirtualItem = query.IsVirtualItem ?? query.IsMissing; + if (isVirtualItem.HasValue) + { + whereClauses.Add("IsVirtualItem=@IsVirtualItem"); + statement?.TryBind("@IsVirtualItem", isVirtualItem.Value); + } + + if (query.IsSpecialSeason.HasValue) + { + if (query.IsSpecialSeason.Value) + { + whereClauses.Add("IndexNumber = 0"); + } + else + { + whereClauses.Add("IndexNumber <> 0"); + } + } + + if (query.IsUnaired.HasValue) + { + if (query.IsUnaired.Value) + { + whereClauses.Add("PremiereDate >= DATETIME('now')"); + } + else + { + whereClauses.Add("PremiereDate < DATETIME('now')"); + } + } + + if (query.MediaTypes.Length == 1) + { + whereClauses.Add("MediaType=@MediaTypes"); + statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString()); + } + else if (query.MediaTypes.Length > 1) + { + var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'")); + whereClauses.Add("MediaType in (" + val + ")"); + } + + if (query.ItemIds.Length > 0) + { + var includeIds = new List(); + var index = 0; + foreach (var id in query.ItemIds) + { + includeIds.Add("Guid = @IncludeId" + index); + statement?.TryBind("@IncludeId" + index, id); + index++; + } + + whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")"); + } + + if (query.ExcludeItemIds.Length > 0) + { + var excludeIds = new List(); + var index = 0; + foreach (var id in query.ExcludeItemIds) + { + excludeIds.Add("Guid <> @ExcludeId" + index); + statement?.TryBind("@ExcludeId" + index, id); + index++; + } + + whereClauses.Add(string.Join(" AND ", excludeIds)); + } + + if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0) + { + var excludeIds = new List(); + + var index = 0; + foreach (var pair in query.ExcludeProviderIds) + { + if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var paramName = "@ExcludeProviderId" + index; + excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")"); + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); + index++; + + break; + } + + if (excludeIds.Count > 0) + { + whereClauses.Add(string.Join(" AND ", excludeIds)); + } + } + + if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0) + { + var hasProviderIds = new List(); + + var index = 0; + foreach (var pair in query.HasAnyProviderId) + { + if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // TODO this seems to be an idea for a better schema where ProviderIds are their own table + // but this is not implemented + // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); + + // TODO this is a really BAD way to do it since the pair: + // Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567 + // and maybe even NotTmdb=1234. + + // this is a placeholder for this specific pair to correlate it in the bigger query + var paramName = "@HasAnyProviderId" + index; + + // this is a search for the placeholder + hasProviderIds.Add("ProviderIds like " + paramName); + + // this replaces the placeholder with a value, here: %key=val% + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); + index++; + + break; + } + + if (hasProviderIds.Count > 0) + { + whereClauses.Add("(" + string.Join(" OR ", hasProviderIds) + ")"); + } + } + + if (query.HasImdbId.HasValue) + { + whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb")); + } + + if (query.HasTmdbId.HasValue) + { + whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb")); + } + + if (query.HasTvdbId.HasValue) + { + whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); + } + + var queryTopParentIds = query.TopParentIds; + + if (queryTopParentIds.Length > 0) + { + var includedItemByNameTypes = GetItemByNameTypesInQuery(query); + var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; + + if (queryTopParentIds.Length == 1) + { + 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)); + } + else if (queryTopParentIds.Length > 1) + { + 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 + ")"); + } + } + } + + if (query.AncestorIds.Length == 1) + { + whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); + statement?.TryBind("@AncestorId", query.AncestorIds[0]); + } + + if (query.AncestorIds.Length > 1) + { + var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); + } + + 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); + } + + if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) + { + whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); + statement?.TryBind("@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])); + } + } + } + + if (query.IncludeInheritedTags.Length > 0) + { + var paramName = "@IncludeInheritedTags"; + if (statement is null) + { + 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) + """); + } + + // 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)"); + } + } + 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")}"%"""); + } + } + } + + 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) + ")"); + } + + 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) + ")"); + } + + 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) + ")"); + } + + if (query.Is3D.HasValue) + { + if (query.Is3D.Value) + { + whereClauses.Add("data like '%Video3DFormat%'"); + } + else + { + whereClauses.Add("data not like '%Video3DFormat%'"); + } + } + + if (query.IsPlaceHolder.HasValue) + { + if (query.IsPlaceHolder.Value) + { + whereClauses.Add("data like '%\"IsPlaceHolder\":true%'"); + } + else + { + whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')"); + } + } + + if (query.HasSpecialFeature.HasValue) + { + 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"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + + if (query.HasThemeVideo.HasValue) + { + if (query.HasThemeVideo.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } } /// @@ -58,14 +1357,26 @@ public class BaseItemManager .FirstOrDefault(t => t is not null)); } - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// - /// or is null. - /// + /// + public void SaveImages(BaseItem item) + { + ArgumentNullException.ThrowIfNull(item); + + var images = SerializeImages(item.ImageInfos); + using var db = _dbProvider.CreateDbContext(); + + db.BaseItems + .Where(e => e.Id.Equals(item.Id)) + .ExecuteUpdate(e => e.SetProperty(f => f.Images, images)); + } + + /// + public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) + { + UpdateOrInsertItems(items, cancellationToken); + } + + /// public void UpdateOrInsertItems(IReadOnlyList items, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(items); @@ -124,7 +1435,8 @@ public class BaseItemManager context.SaveChanges(true); } - public BaseItemDto? GetSingle(Guid id) + /// + public BaseItemDto? RetrieveItem(Guid id) { if (id.IsEmpty()) { @@ -141,13 +1453,6 @@ public class BaseItemManager return DeserialiseBaseItem(item); } - private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) - { - var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); - var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type.");; - return Map(baseItemEntity, dto); - } - /// /// Maps a Entity to the DTO. /// @@ -462,6 +1767,13 @@ public class BaseItemManager return entity; } + private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) + { + var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); ; + return Map(baseItemEntity, dto); + } + private string GetCleanValue(string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/Jellyfin.Server.Implementations/Item/ChapterManager.cs b/Jellyfin.Server.Implementations/Item/ChapterManager.cs index 273cc96ba..7b0f98fde 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterManager.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterManager.cs @@ -1,26 +1,74 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; -using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; namespace Jellyfin.Server.Implementations.Item; -public class ChapterManager +/// +/// The Chapter manager. +/// +public class ChapterManager : IChapterManager { private readonly IDbContextFactory _dbProvider; + private readonly IImageProcessor _imageProcessor; - public ChapterManager(IDbContextFactory dbProvider) + /// + /// Initializes a new instance of the class. + /// + /// The EFCore provider. + /// The Image Processor. + public ChapterManager(IDbContextFactory dbProvider, IImageProcessor imageProcessor) { _dbProvider = dbProvider; + _imageProcessor = imageProcessor; } - public IReadOnlyList GetChapters(BaseItemDto baseItemDto) + /// + public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) { using var context = _dbProvider.CreateDbContext(); - return context.Chapters.Where(e => e.ItemId.Equals(baseItemDto.Id)).Select(Map).ToList(); + var chapter = context.Chapters.FirstOrDefault(e => e.ItemId.Equals(baseItem.Id) && e.ChapterIndex == index); + if (chapter is not null) + { + return Map(chapter, baseItem); + } + + return null; + } + + /// + public IReadOnlyList GetChapters(BaseItemDto baseItem) + { + using var context = _dbProvider.CreateDbContext(); + return context.Chapters.Where(e => e.ItemId.Equals(baseItem.Id)) + .ToList() + .Select(e => Map(e, baseItem)) + .ToImmutableArray(); + } + + /// + public void SaveChapters(Guid itemId, IReadOnlyList chapters) + { + using var context = _dbProvider.CreateDbContext(); + using (var transaction = context.Database.BeginTransaction()) + { + context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + for (var i = 0; i < chapters.Count; i++) + { + var chapter = chapters[i]; + context.Chapters.Add(Map(chapter, i, itemId)); + } + + context.SaveChanges(); + transaction.Commit(); + } } private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId) diff --git a/MediaBrowser.Controller/Chapters/ChapterManager.cs b/MediaBrowser.Controller/Chapters/ChapterManager.cs new file mode 100644 index 000000000..a9e11f603 --- /dev/null +++ b/MediaBrowser.Controller/Chapters/ChapterManager.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Chapters +{ + public class ChapterManager : IChapterManager + { + public ChapterManager(IDbContextFactory dbProvider) + { + _itemRepo = itemRepo; + } + + /// + public void SaveChapters(Guid itemId, IReadOnlyList chapters) + { + _itemRepo.SaveChapters(itemId, chapters); + } + } +} diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index c049bb97e..55762c7fc 100644 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Chapters @@ -15,5 +16,20 @@ namespace MediaBrowser.Controller.Chapters /// The item. /// The set of chapters. void SaveChapters(Guid itemId, IReadOnlyList chapters); + + /// + /// Gets all chapters associated with the baseItem. + /// + /// The baseitem. + /// A readonly list of chapter instances. + IReadOnlyList GetChapters(BaseItemDto baseItem); + + /// + /// Gets a single chapter of a BaseItem on a specific index. + /// + /// The baseitem. + /// The index of that chapter. + /// A chapter instance. + ChapterInfo? GetChapter(BaseItemDto baseItem, int index); } } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 2c52b2b45..21b9ee4b7 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -39,28 +39,6 @@ namespace MediaBrowser.Controller.Persistence /// BaseItem. BaseItem RetrieveItem(Guid id); - /// - /// Gets chapters for an item. - /// - /// The item. - /// The list of chapter info. - List GetChapters(BaseItem item); - - /// - /// Gets a single chapter for an item. - /// - /// The item. - /// The chapter index. - /// The chapter info at the specified index. - ChapterInfo GetChapter(BaseItem item, int index); - - /// - /// Saves the chapters. - /// - /// The item id. - /// The list of chapters to save. - void SaveChapters(Guid id, IReadOnlyList chapters); - /// /// Gets the media streams. /// diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs deleted file mode 100644 index 3cbfe7d4d..000000000 --- a/MediaBrowser.Providers/Chapters/ChapterManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Providers.Chapters -{ - public class ChapterManager : IChapterManager - { - private readonly IItemRepository _itemRepo; - - public ChapterManager(IItemRepository itemRepo) - { - _itemRepo = itemRepo; - } - - /// - public void SaveChapters(Guid itemId, IReadOnlyList chapters) - { - _itemRepo.SaveChapters(itemId, chapters); - } - } -} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9a65852f0..f2df731c0 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -23,7 +23,7 @@ - + -- cgit v1.2.3 From ea81db67f412dee6203e3f18798e449dce7c06f9 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:27:47 +0000 Subject: Added Sorting and Grouping --- .../Data/SqliteItemRepository.cs | 1816 +------------------- Jellyfin.Data/Enums/ItemSortBy.cs | 10 - .../Item/BaseItemManager.cs | 306 +++- 3 files changed, 301 insertions(+), 1831 deletions(-) (limited to 'Emby.Server.Implementations/Data/SqliteItemRepository.cs') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 94a5eba81..26255e6aa 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -529,159 +529,6 @@ namespace Emby.Server.Implementations.Data return string.Empty; } - /// - public int GetCount(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) - { - query.Limit = query.Limit.Value + 4; - } - - var columns = new List { "count(distinct PresentationUniqueKey)" }; - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 256) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - if (whereClauses.Count != 0) - { - commandTextBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses); - } - - var commandText = commandTextBuilder.ToString(); - - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - return statement.SelectScalarInt(); - } - } - - /// - public List GetItemList(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) - { - query.Limit = query.Limit.Value + 4; - } - - var columns = _retrieveItemColumns.ToList(); - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 1024) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - - if (whereClauses.Count != 0) - { - commandTextBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses); - } - - commandTextBuilder.Append(GetGroupBy(query)) - .Append(GetOrderByText(query)); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandTextBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - commandTextBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var commandText = commandTextBuilder.ToString(); - var items = new List(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasServiceName = HasServiceName(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization); - if (item is not null) - { - items.Add(item); - } - } - } - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.EnableGroupByMetadataKey) - { - var limit = query.Limit ?? int.MaxValue; - limit -= 4; - var newList = new List(); - - foreach (var item in items) - { - AddItem(newList, item); - - if (newList.Count >= limit) - { - break; - } - } - - items = newList; - } - - return items; - } private string FixUnicodeChars(string buffer) { @@ -703,204 +550,6 @@ namespace Emby.Server.Implementations.Data return buffer.Replace('\u00B4', '\''); // acute accent } - private void AddItem(List items, BaseItem newItem) - { - for (var i = 0; i < items.Count; i++) - { - var item = items[i]; - - foreach (var providerId in newItem.ProviderIds) - { - if (string.Equals(providerId.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.Ordinal)) - { - continue; - } - - if (string.Equals(item.GetProviderId(providerId.Key), providerId.Value, StringComparison.Ordinal)) - { - if (newItem.SourceType == SourceType.Library) - { - items[i] = newItem; - } - - return; - } - } - } - - items.Add(newItem); - } - - /// - public QueryResult GetItems(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) - { - var returnList = GetItemList(query); - return new QueryResult( - query.StartIndex, - returnList.Count, - returnList); - } - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) - { - query.Limit = query.Limit.Value + 4; - } - - var columns = _retrieveItemColumns.ToList(); - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 512) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - - var whereText = whereClauses.Count == 0 ? - string.Empty : - string.Join(" AND ", whereClauses); - - if (!string.IsNullOrEmpty(whereText)) - { - commandTextBuilder.Append(" where ") - .Append(whereText); - } - - commandTextBuilder.Append(GetGroupBy(query)) - .Append(GetOrderByText(query)); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandTextBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - commandTextBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - var itemQuery = string.Empty; - var totalRecordCountQuery = string.Empty; - if (!isReturningZeroItems) - { - itemQuery = commandTextBuilder.ToString(); - } - - if (query.EnableTotalRecordCount) - { - commandTextBuilder.Clear(); - - commandTextBuilder.Append(" select "); - - List columnsToSelect; - if (EnableGroupByPresentationUniqueKey(query)) - { - columnsToSelect = new List { "count (distinct PresentationUniqueKey)" }; - } - else if (query.GroupBySeriesPresentationUniqueKey) - { - columnsToSelect = new List { "count (distinct SeriesPresentationUniqueKey)" }; - } - else - { - columnsToSelect = new List { "count (guid)" }; - } - - SetFinalColumnsToSelect(query, columnsToSelect); - - commandTextBuilder.AppendJoin(',', columnsToSelect) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - if (!string.IsNullOrEmpty(whereText)) - { - commandTextBuilder.Append(" where ") - .Append(whereText); - } - - totalRecordCountQuery = commandTextBuilder.ToString(); - } - - var list = new List(); - var result = new QueryResult(); - using var connection = GetConnection(true); - using var transaction = connection.BeginTransaction(); - if (!isReturningZeroItems) - { - using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) - using (var statement = PrepareStatement(connection, itemQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasServiceName = HasServiceName(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false); - if (item is not null) - { - list.Add(item); - } - } - } - } - - if (query.EnableTotalRecordCount) - { - using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) - using (var statement = PrepareStatement(connection, totalRecordCountQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - result.TotalRecordCount = statement.SelectScalarInt(); - } - } - - transaction.Commit(); - - result.StartIndex = query.StartIndex ?? 0; - result.Items = list; - return result; - } - private string GetOrderByText(InternalItemsQuery query) { var orderBy = query.OrderBy; @@ -1066,1433 +715,19 @@ namespace Emby.Server.Implementations.Data return IsAlphaNumeric(value); } -#nullable enable - private List GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement) + private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) { - if (query.IsResumable ?? false) + if (query.ExcludeItemTypes.Contains(type)) { - query.IsVirtualItem = false; + return false; } - var minWidth = query.MinWidth; - var maxWidth = query.MaxWidth; + return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); + } - if (query.IsHD.HasValue) - { - const int Threshold = 1200; - if (query.IsHD.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - if (query.Is4K.HasValue) - { - const int Threshold = 3800; - if (query.Is4K.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - var whereClauses = new List(); - - if (minWidth.HasValue) - { - whereClauses.Add("Width>=@MinWidth"); - statement?.TryBind("@MinWidth", minWidth); - } - - if (query.MinHeight.HasValue) - { - whereClauses.Add("Height>=@MinHeight"); - statement?.TryBind("@MinHeight", query.MinHeight); - } - - if (maxWidth.HasValue) - { - whereClauses.Add("Width<=@MaxWidth"); - statement?.TryBind("@MaxWidth", maxWidth); - } - - if (query.MaxHeight.HasValue) - { - whereClauses.Add("Height<=@MaxHeight"); - statement?.TryBind("@MaxHeight", query.MaxHeight); - } - - if (query.IsLocked.HasValue) - { - whereClauses.Add("IsLocked=@IsLocked"); - statement?.TryBind("@IsLocked", query.IsLocked); - } - - var tags = query.Tags.ToList(); - var excludeTags = query.ExcludeTags.ToList(); - - if (query.IsMovie == true) - { - if (query.IncludeItemTypes.Length == 0 - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.Trailer)) - { - whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); - } - else - { - whereClauses.Add("IsMovie=@IsMovie"); - } - - statement?.TryBind("@IsMovie", true); - } - else if (query.IsMovie.HasValue) - { - whereClauses.Add("IsMovie=@IsMovie"); - statement?.TryBind("@IsMovie", query.IsMovie); - } - - if (query.IsSeries.HasValue) - { - whereClauses.Add("IsSeries=@IsSeries"); - statement?.TryBind("@IsSeries", query.IsSeries); - } - - if (query.IsSports.HasValue) - { - if (query.IsSports.Value) - { - tags.Add("Sports"); - } - else - { - excludeTags.Add("Sports"); - } - } - - if (query.IsNews.HasValue) - { - if (query.IsNews.Value) - { - tags.Add("News"); - } - else - { - excludeTags.Add("News"); - } - } - - if (query.IsKids.HasValue) - { - if (query.IsKids.Value) - { - tags.Add("Kids"); - } - else - { - excludeTags.Add("Kids"); - } - } - - if (query.SimilarTo is not null && query.MinSimilarityScore > 0) - { - whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture)); - } - - if (!string.IsNullOrEmpty(query.SearchTerm)) - { - whereClauses.Add("SearchScore > 0"); - } - - if (query.IsFolder.HasValue) - { - whereClauses.Add("IsFolder=@IsFolder"); - statement?.TryBind("@IsFolder", query.IsFolder); - } - - var includeTypes = query.IncludeItemTypes; - // Only specify excluded types if no included types are specified - if (query.IncludeItemTypes.Length == 0) - { - var excludeTypes = query.ExcludeItemTypes; - if (excludeTypes.Length == 1) - { - if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) - { - whereClauses.Add("type<>@type"); - statement?.TryBind("@type", excludeTypeName); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeTypes[0]); - } - } - else if (excludeTypes.Length > 1) - { - var whereBuilder = new StringBuilder("type not in ("); - foreach (var excludeType in excludeTypes) - { - if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) - { - whereBuilder - .Append('\'') - .Append(baseItemKindName) - .Append("',"); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeType); - } - } - - // Remove trailing comma. - whereBuilder.Length--; - whereBuilder.Append(')'); - whereClauses.Add(whereBuilder.ToString()); - } - } - else if (includeTypes.Length == 1) - { - if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) - { - whereClauses.Add("type=@type"); - statement?.TryBind("@type", includeTypeName); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeTypes[0]); - } - } - else if (includeTypes.Length > 1) - { - var whereBuilder = new StringBuilder("type in ("); - foreach (var includeType in includeTypes) - { - if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) - { - whereBuilder - .Append('\'') - .Append(baseItemKindName) - .Append("',"); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeType); - } - } - - // Remove trailing comma. - whereBuilder.Length--; - whereBuilder.Append(')'); - whereClauses.Add(whereBuilder.ToString()); - } - - if (query.ChannelIds.Count == 1) - { - whereClauses.Add("ChannelId=@ChannelId"); - statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); - } - else if (query.ChannelIds.Count > 1) - { - var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - whereClauses.Add($"ChannelId in ({inClause})"); - } - - if (!query.ParentId.IsEmpty()) - { - whereClauses.Add("ParentId=@ParentId"); - statement?.TryBind("@ParentId", query.ParentId); - } - - if (!string.IsNullOrWhiteSpace(query.Path)) - { - whereClauses.Add("Path=@Path"); - statement?.TryBind("@Path", GetPathToSave(query.Path)); - } - - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) - { - whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey"); - statement?.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey); - } - - if (query.MinCommunityRating.HasValue) - { - whereClauses.Add("CommunityRating>=@MinCommunityRating"); - statement?.TryBind("@MinCommunityRating", query.MinCommunityRating.Value); - } - - if (query.MinIndexNumber.HasValue) - { - whereClauses.Add("IndexNumber>=@MinIndexNumber"); - statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value); - } - - if (query.MinParentAndIndexNumber.HasValue) - { - whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)"); - statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber); - statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber); - } - - if (query.MinDateCreated.HasValue) - { - whereClauses.Add("DateCreated>=@MinDateCreated"); - statement?.TryBind("@MinDateCreated", query.MinDateCreated.Value); - } - - if (query.MinDateLastSaved.HasValue) - { - whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)"); - statement?.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value); - } - - if (query.MinDateLastSavedForUser.HasValue) - { - whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)"); - statement?.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value); - } - - if (query.IndexNumber.HasValue) - { - whereClauses.Add("IndexNumber=@IndexNumber"); - statement?.TryBind("@IndexNumber", query.IndexNumber.Value); - } - - if (query.ParentIndexNumber.HasValue) - { - whereClauses.Add("ParentIndexNumber=@ParentIndexNumber"); - statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value); - } - - if (query.ParentIndexNumberNotEquals.HasValue) - { - whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)"); - statement?.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value); - } - - var minEndDate = query.MinEndDate; - var maxEndDate = query.MaxEndDate; - - if (query.HasAired.HasValue) - { - if (query.HasAired.Value) - { - maxEndDate = DateTime.UtcNow; - } - else - { - minEndDate = DateTime.UtcNow; - } - } - - if (minEndDate.HasValue) - { - whereClauses.Add("EndDate>=@MinEndDate"); - statement?.TryBind("@MinEndDate", minEndDate.Value); - } - - if (maxEndDate.HasValue) - { - whereClauses.Add("EndDate<=@MaxEndDate"); - statement?.TryBind("@MaxEndDate", maxEndDate.Value); - } - - if (query.MinStartDate.HasValue) - { - whereClauses.Add("StartDate>=@MinStartDate"); - statement?.TryBind("@MinStartDate", query.MinStartDate.Value); - } - - if (query.MaxStartDate.HasValue) - { - whereClauses.Add("StartDate<=@MaxStartDate"); - statement?.TryBind("@MaxStartDate", query.MaxStartDate.Value); - } - - if (query.MinPremiereDate.HasValue) - { - whereClauses.Add("PremiereDate>=@MinPremiereDate"); - statement?.TryBind("@MinPremiereDate", query.MinPremiereDate.Value); - } - - if (query.MaxPremiereDate.HasValue) - { - whereClauses.Add("PremiereDate<=@MaxPremiereDate"); - statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value); - } - - StringBuilder clauseBuilder = new StringBuilder(); - const string Or = " OR "; - - var trailerTypes = query.TrailerTypes; - int trailerTypesLen = trailerTypes.Length; - if (trailerTypesLen > 0) - { - clauseBuilder.Append('('); - - for (int i = 0; i < trailerTypesLen; i++) - { - var paramName = "@TrailerTypes" + i; - clauseBuilder.Append("TrailerTypes like ") - .Append(paramName) - .Append(Or); - statement?.TryBind(paramName, "%" + trailerTypes[i] + "%"); - } - - clauseBuilder.Length -= Or.Length; - clauseBuilder.Append(')'); - - whereClauses.Add(clauseBuilder.ToString()); - - clauseBuilder.Length = 0; - } - - if (query.IsAiring.HasValue) - { - if (query.IsAiring.Value) - { - whereClauses.Add("StartDate<=@MaxStartDate"); - statement?.TryBind("@MaxStartDate", DateTime.UtcNow); - - whereClauses.Add("EndDate>=@MinEndDate"); - statement?.TryBind("@MinEndDate", DateTime.UtcNow); - } - else - { - whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)"); - statement?.TryBind("@IsAiringDate", DateTime.UtcNow); - } - } - - int personIdsLen = query.PersonIds.Length; - if (personIdsLen > 0) - { - // TODO: Should this query with CleanName ? - - clauseBuilder.Append('('); - - Span idBytes = stackalloc byte[16]; - for (int i = 0; i < personIdsLen; i++) - { - string paramName = "@PersonId" + i; - clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=") - .Append(paramName) - .Append("))) OR "); - - statement?.TryBind(paramName, query.PersonIds[i]); - } - - clauseBuilder.Length -= Or.Length; - clauseBuilder.Append(')'); - - whereClauses.Add(clauseBuilder.ToString()); - - clauseBuilder.Length = 0; - } - - if (!string.IsNullOrWhiteSpace(query.Person)) - { - whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)"); - statement?.TryBind("@PersonName", query.Person); - } - - if (!string.IsNullOrWhiteSpace(query.MinSortName)) - { - whereClauses.Add("SortName>=@MinSortName"); - statement?.TryBind("@MinSortName", query.MinSortName); - } - - if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) - { - whereClauses.Add("ExternalSeriesId=@ExternalSeriesId"); - statement?.TryBind("@ExternalSeriesId", query.ExternalSeriesId); - } - - if (!string.IsNullOrWhiteSpace(query.ExternalId)) - { - whereClauses.Add("ExternalId=@ExternalId"); - statement?.TryBind("@ExternalId", query.ExternalId); - } - - if (!string.IsNullOrWhiteSpace(query.Name)) - { - whereClauses.Add("CleanName=@Name"); - statement?.TryBind("@Name", GetCleanValue(query.Name)); - } - - // These are the same, for now - var nameContains = query.NameContains; - if (!string.IsNullOrWhiteSpace(nameContains)) - { - whereClauses.Add("(CleanName like @NameContains or OriginalTitle like @NameContains)"); - if (statement is not null) - { - nameContains = FixUnicodeChars(nameContains); - statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%"); - } - } - - if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) - { - whereClauses.Add("SortName like @NameStartsWith"); - statement?.TryBind("@NameStartsWith", query.NameStartsWith + "%"); - } - - if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) - { - whereClauses.Add("SortName >= @NameStartsWithOrGreater"); - // lowercase this because SortName is stored as lowercase - statement?.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant()); - } - - if (!string.IsNullOrWhiteSpace(query.NameLessThan)) - { - whereClauses.Add("SortName < @NameLessThan"); - // lowercase this because SortName is stored as lowercase - statement?.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant()); - } - - if (query.ImageTypes.Length > 0) - { - foreach (var requiredImage in query.ImageTypes) - { - whereClauses.Add("Images like '%" + requiredImage + "%'"); - } - } - - if (query.IsLiked.HasValue) - { - if (query.IsLiked.Value) - { - whereClauses.Add("rating>=@UserRating"); - statement?.TryBind("@UserRating", UserItemData.MinLikeValue); - } - else - { - whereClauses.Add("(rating is null or rating<@UserRating)"); - statement?.TryBind("@UserRating", UserItemData.MinLikeValue); - } - } - - if (query.IsFavoriteOrLiked.HasValue) - { - if (query.IsFavoriteOrLiked.Value) - { - whereClauses.Add("IsFavorite=@IsFavoriteOrLiked"); - } - else - { - whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)"); - } - - statement?.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value); - } - - if (query.IsFavorite.HasValue) - { - if (query.IsFavorite.Value) - { - whereClauses.Add("IsFavorite=@IsFavorite"); - } - else - { - whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)"); - } - - statement?.TryBind("@IsFavorite", query.IsFavorite.Value); - } - - if (EnableJoinUserData(query)) - { - if (query.IsPlayed.HasValue) - { - // We should probably figure this out for all folders, but for right now, this is the only place where we need it - if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.Series) - { - if (query.IsPlayed.Value) - { - whereClauses.Add("PresentationUniqueKey not in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)"); - } - else - { - whereClauses.Add("PresentationUniqueKey in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)"); - } - } - else - { - if (query.IsPlayed.Value) - { - whereClauses.Add("(played=@IsPlayed)"); - } - else - { - whereClauses.Add("(played is null or played=@IsPlayed)"); - } - - statement?.TryBind("@IsPlayed", query.IsPlayed.Value); - } - } - } - - if (query.IsResumable.HasValue) - { - if (query.IsResumable.Value) - { - whereClauses.Add("playbackPositionTicks > 0"); - } - else - { - whereClauses.Add("(playbackPositionTicks is null or playbackPositionTicks = 0)"); - } - } - - if (query.ArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.ArtistIds.Length; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") and Type<=1)) OR "); - statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.AlbumArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.AlbumArtistIds.Length; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") and Type=1)) OR "); - statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.ContributingArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.ContributingArtistIds.Length; i++) - { - clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR "); - statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.AlbumIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.AlbumIds.Length; i++) - { - clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds") - .Append(i) - .Append(") OR "); - statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.ExcludeArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.ExcludeArtistIds.Length; i++) - { - clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId") - .Append(i) - .Append(") and Type<=1)) OR "); - statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.GenreIds.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.GenreIds.Count; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId") - .Append(i) - .Append(") and Type=2)) OR "); - statement?.TryBind("@GenreId" + i, query.GenreIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.Genres.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.Genres.Count; i++) - { - clauseBuilder.Append("@Genre") - .Append(i) - .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR "); - statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i])); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (tags.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < tags.Count; i++) - { - clauseBuilder.Append("@Tag") - .Append(i) - .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); - statement?.TryBind("@Tag" + i, GetCleanValue(tags[i])); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (excludeTags.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < excludeTags.Count; i++) - { - clauseBuilder.Append("@ExcludeTag") - .Append(i) - .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); - statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i])); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.StudioIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.StudioIds.Length; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId") - .Append(i) - .Append(") and Type=3)) OR "); - statement?.TryBind("@StudioId" + i, query.StudioIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.OfficialRatings.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.OfficialRatings.Length; i++) - { - clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or); - statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - clauseBuilder.Append('('); - if (query.HasParentalRating ?? false) - { - clauseBuilder.Append("InheritedParentalRatingValue not null"); - if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); - statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); - } - - if (query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - } - else if (query.BlockUnratedItems.Length > 0) - { - const string ParamName = "@UnratedType"; - clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in ("); - - for (int i = 0; i < query.BlockUnratedItems.Length; i++) - { - clauseBuilder.Append(ParamName).Append(i).Append(','); - statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString()); - } - - // Remove trailing comma - clauseBuilder.Length--; - clauseBuilder.Append("))"); - - if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(" OR ("); - } - - if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); - statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); - } - - if (query.MaxParentalRating.HasValue) - { - if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append(" AND "); - } - - clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - - if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(')'); - } - - if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) - { - clauseBuilder.Append(" OR InheritedParentalRatingValue not null"); - } - } - else if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); - statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); - - if (query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - - clauseBuilder.Append(')'); - } - else if (query.MaxParentalRating.HasValue) - { - clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - else if (!query.HasParentalRating ?? false) - { - clauseBuilder.Append("InheritedParentalRatingValue is null"); - } - - if (clauseBuilder.Length > 1) - { - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.HasOfficialRating.HasValue) - { - if (query.HasOfficialRating.Value) - { - whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')"); - } - else - { - whereClauses.Add("(OfficialRating is null OR OfficialRating='')"); - } - } - - if (query.HasOverview.HasValue) - { - if (query.HasOverview.Value) - { - whereClauses.Add("(Overview not null AND Overview<>'')"); - } - else - { - whereClauses.Add("(Overview is null OR Overview='')"); - } - } - - if (query.HasOwnerId.HasValue) - { - if (query.HasOwnerId.Value) - { - whereClauses.Add("OwnerId not null"); - } - else - { - whereClauses.Add("OwnerId is null"); - } - } - - if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); - } - - if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); - } - - if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); - } - - if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); - } - - if (query.HasSubtitles.HasValue) - { - if (query.HasSubtitles.Value) - { - whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)"); - } - else - { - whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)"); - } - } - - if (query.HasChapterImages.HasValue) - { - if (query.HasChapterImages.Value) - { - whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) not null)"); - } - else - { - whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) is null)"); - } - } - - if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value) - { - whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); - } - - if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value) - { - whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))"); - } - - if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value) - { - whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)"); - } - - if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value) - { - whereClauses.Add("Name not in (Select Name From People)"); - } - - if (query.Years.Length == 1) - { - whereClauses.Add("ProductionYear=@Years"); - statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); - } - else if (query.Years.Length > 1) - { - var val = string.Join(',', query.Years); - whereClauses.Add("ProductionYear in (" + val + ")"); - } - - var isVirtualItem = query.IsVirtualItem ?? query.IsMissing; - if (isVirtualItem.HasValue) - { - whereClauses.Add("IsVirtualItem=@IsVirtualItem"); - statement?.TryBind("@IsVirtualItem", isVirtualItem.Value); - } - - if (query.IsSpecialSeason.HasValue) - { - if (query.IsSpecialSeason.Value) - { - whereClauses.Add("IndexNumber = 0"); - } - else - { - whereClauses.Add("IndexNumber <> 0"); - } - } - - if (query.IsUnaired.HasValue) - { - if (query.IsUnaired.Value) - { - whereClauses.Add("PremiereDate >= DATETIME('now')"); - } - else - { - whereClauses.Add("PremiereDate < DATETIME('now')"); - } - } - - if (query.MediaTypes.Length == 1) - { - whereClauses.Add("MediaType=@MediaTypes"); - statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString()); - } - else if (query.MediaTypes.Length > 1) - { - var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'")); - whereClauses.Add("MediaType in (" + val + ")"); - } - - if (query.ItemIds.Length > 0) - { - var includeIds = new List(); - var index = 0; - foreach (var id in query.ItemIds) - { - includeIds.Add("Guid = @IncludeId" + index); - statement?.TryBind("@IncludeId" + index, id); - index++; - } - - whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")"); - } - - if (query.ExcludeItemIds.Length > 0) - { - var excludeIds = new List(); - var index = 0; - foreach (var id in query.ExcludeItemIds) - { - excludeIds.Add("Guid <> @ExcludeId" + index); - statement?.TryBind("@ExcludeId" + index, id); - index++; - } - - whereClauses.Add(string.Join(" AND ", excludeIds)); - } - - if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0) - { - var excludeIds = new List(); - - var index = 0; - foreach (var pair in query.ExcludeProviderIds) - { - if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var paramName = "@ExcludeProviderId" + index; - excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")"); - statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - index++; - - break; - } - - if (excludeIds.Count > 0) - { - whereClauses.Add(string.Join(" AND ", excludeIds)); - } - } - - if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0) - { - var hasProviderIds = new List(); - - var index = 0; - foreach (var pair in query.HasAnyProviderId) - { - if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - // TODO this seems to be an idea for a better schema where ProviderIds are their own table - // but this is not implemented - // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); - - // TODO this is a really BAD way to do it since the pair: - // Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567 - // and maybe even NotTmdb=1234. - - // this is a placeholder for this specific pair to correlate it in the bigger query - var paramName = "@HasAnyProviderId" + index; - - // this is a search for the placeholder - hasProviderIds.Add("ProviderIds like " + paramName); - - // this replaces the placeholder with a value, here: %key=val% - statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - index++; - - break; - } - - if (hasProviderIds.Count > 0) - { - whereClauses.Add("(" + string.Join(" OR ", hasProviderIds) + ")"); - } - } - - if (query.HasImdbId.HasValue) - { - whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb")); - } - - if (query.HasTmdbId.HasValue) - { - whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb")); - } - - if (query.HasTvdbId.HasValue) - { - whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); - } - - var queryTopParentIds = query.TopParentIds; - - if (queryTopParentIds.Length > 0) - { - var includedItemByNameTypes = GetItemByNameTypesInQuery(query); - var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; - - if (queryTopParentIds.Length == 1) - { - 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)); - } - else if (queryTopParentIds.Length > 1) - { - 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 + ")"); - } - } - } - - if (query.AncestorIds.Length == 1) - { - whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); - statement?.TryBind("@AncestorId", query.AncestorIds[0]); - } - - if (query.AncestorIds.Length > 1) - { - var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); - } - - 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); - } - - if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) - { - whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); - statement?.TryBind("@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])); - } - } - } - - if (query.IncludeInheritedTags.Length > 0) - { - var paramName = "@IncludeInheritedTags"; - if (statement is null) - { - 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) - """); - } - - // 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)"); - } - } - 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")}"%"""); - } - } - } - - 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) + ")"); - } - - 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) + ")"); - } - - 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) + ")"); - } - - if (query.Is3D.HasValue) - { - if (query.Is3D.Value) - { - whereClauses.Add("data like '%Video3DFormat%'"); - } - else - { - whereClauses.Add("data not like '%Video3DFormat%'"); - } - } - - if (query.IsPlaceHolder.HasValue) - { - if (query.IsPlaceHolder.Value) - { - whereClauses.Add("data like '%\"IsPlaceHolder\":true%'"); - } - else - { - whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')"); - } - } - - if (query.HasSpecialFeature.HasValue) - { - 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"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - if (query.HasThemeVideo.HasValue) - { - if (query.HasThemeVideo.Value) - { - whereClauses.Add("ExtraIds not null"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - return whereClauses; - } - - /// - /// Formats a where clause for the specified provider. - /// - /// Whether or not to include items with this provider's ids. - /// Provider name. - /// Formatted SQL clause. - private string GetProviderIdClause(bool includeResults, string provider) - { - return string.Format( - CultureInfo.InvariantCulture, - "ProviderIds {0} like '%{1}=%'", - includeResults ? string.Empty : "not", - provider); - } - -#nullable disable - 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); - } - - private string GetCleanValue(string value) - { - if (string.IsNullOrWhiteSpace(value)) + private string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) { return value; } @@ -2500,41 +735,6 @@ namespace Emby.Server.Implementations.Data return value.RemoveDiacritics().ToLowerInvariant(); } - private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) - { - if (!query.GroupByPresentationUniqueKey) - { - return false; - } - - if (query.GroupBySeriesPresentationUniqueKey) - { - return false; - } - - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) - { - return false; - } - - if (query.User is null) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Contains(BaseItemKind.Episode) - || query.IncludeItemTypes.Contains(BaseItemKind.Video) - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) - || query.IncludeItemTypes.Contains(BaseItemKind.Series) - || query.IncludeItemTypes.Contains(BaseItemKind.Season); - } - /// public void UpdateInheritedValues() { diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs index 17bf1166d..ef7650294 100644 --- a/Jellyfin.Data/Enums/ItemSortBy.cs +++ b/Jellyfin.Data/Enums/ItemSortBy.cs @@ -154,14 +154,4 @@ public enum ItemSortBy /// The index number. /// IndexNumber = 29, - - /// - /// The similarity score. - /// - SimilarityScore = 30, - - /// - /// The search score. - /// - SearchScore = 31, } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index 8f3c9636e..f2d6b6261 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Linq.Expressions; @@ -36,8 +37,6 @@ public class BaseItemManager : IItemRepository private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationHost _appHost; - - private readonly ItemFields[] _allItemFields = Enum.GetValues(); private static readonly BaseItemKind[] _programTypes = new[] @@ -146,22 +145,284 @@ public class BaseItemManager : IItemRepository _appHost = appHost; } - public int GetCount(InternalItemsQuery query) + private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) + { + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + query = query.Skip(offset); + } + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + } + + return query; + } + + private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + { +#pragma warning disable CS8603 // Possible null reference return. + return sortBy switch + { + ItemSortBy.AirTime => e => e.SortName, // TODO + ItemSortBy.Runtime => e => e.RunTimeTicks, + ItemSortBy.Random => e => EF.Functions.Random(), + ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate, + ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount, + ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite, + ItemSortBy.IsFolder => e => e.IsFolder, + ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, + ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue), + ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue), + ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), + ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => e => e.SeriesName, + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => e => e.Album, + ItemSortBy.DateCreated => e => e.DateCreated, + ItemSortBy.PremiereDate => e => e.PremiereDate, + ItemSortBy.StartDate => e => e.StartDate, + ItemSortBy.Name => e => e.Name, + ItemSortBy.CommunityRating => e => e.CommunityRating, + ItemSortBy.ProductionYear => e => e.ProductionYear, + ItemSortBy.CriticRating => e => e.CriticRating, + ItemSortBy.VideoBitRate => e => e.TotalBitrate, + ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, + ItemSortBy.IndexNumber => e => e.IndexNumber, + _ => e => e.SortName + }; +#pragma warning restore CS8603 // Possible null reference return. + + } + + private IQueryable MapOrderByField(IQueryable dbQuery, ItemSortBy sortBy, InternalItemsQuery query) + { + return sortBy switch + { + ItemSortBy.AirTime => dbQuery.OrderBy(e => e.SortName), // TODO + ItemSortBy.Runtime => dbQuery.OrderBy(e => e.RunTimeTicks), + ItemSortBy.Random => dbQuery.OrderBy(e => EF.Functions.Random()), + ItemSortBy.DatePlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate), + ItemSortBy.PlayCount => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount), + ItemSortBy.IsFavoriteOrLiked => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite), + ItemSortBy.IsFolder => dbQuery.OrderBy(e => e.IsFolder), + ItemSortBy.IsPlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), + ItemSortBy.IsUnplayed => dbQuery.OrderBy(e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), + ItemSortBy.DateLastContentAdded => dbQuery.OrderBy(e => e.DateLastMediaAdded), + ItemSortBy.Artist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue)), + ItemSortBy.AlbumArtist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue)), + ItemSortBy.Studio => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue)), + ItemSortBy.OfficialRating => dbQuery.OrderBy(e => e.InheritedParentalRatingValue), + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => dbQuery.OrderBy(e => e.SeriesName), + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => dbQuery.OrderBy(e => e.Album), + ItemSortBy.DateCreated => dbQuery.OrderBy(e => e.DateCreated), + ItemSortBy.PremiereDate => dbQuery.OrderBy(e => e.PremiereDate), + ItemSortBy.StartDate => dbQuery.OrderBy(e => e.StartDate), + ItemSortBy.Name => dbQuery.OrderBy(e => e.Name), + ItemSortBy.CommunityRating => dbQuery.OrderBy(e => e.CommunityRating), + ItemSortBy.ProductionYear => dbQuery.OrderBy(e => e.ProductionYear), + ItemSortBy.CriticRating => dbQuery.OrderBy(e => e.CriticRating), + ItemSortBy.VideoBitRate => dbQuery.OrderBy(e => e.TotalBitrate), + ItemSortBy.ParentIndexNumber => dbQuery.OrderBy(e => e.ParentIndexNumber), + ItemSortBy.IndexNumber => dbQuery.OrderBy(e => e.IndexNumber), + _ => dbQuery.OrderBy(e => e.SortName) + }; + } + + private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) + { + var orderBy = filter.OrderBy; + bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + + if (hasSearch) + { + List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); + if (hasSearch) + { + prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); + } + + orderBy = filter.OrderBy = [.. prepend, .. orderBy]; + } + else if (orderBy.Count == 0) + { + return query; + } + + foreach (var item in orderBy) + { + var expression = MapOrderByField(item.OrderBy, filter); + if (item.SortOrder == SortOrder.Ascending) + { + query = query.OrderBy(expression); + } + else + { + query = query.OrderByDescending(expression); + } + } + + return query; + } + + public IReadOnlyList GetItemIdsList(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + PrepareFilterQuery(filter); + + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter) + .DistinctBy(e => e.Id); + + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); + if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).SelectMany(e => e); + } + + if (enableGroupByPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).SelectMany(e => e); + } + + if (filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).SelectMany(e => e); + } + + dbQuery = ApplyOrder(dbQuery, filter); + + return Pageinate(dbQuery, filter).Select(e => e.Id).ToImmutableArray(); + } + + private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + { + if (!query.GroupByPresentationUniqueKey) + { + return false; + } + + if (query.GroupBySeriesPresentationUniqueKey) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + return false; + } + + if (query.User is null) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + return query.IncludeItemTypes.Contains(BaseItemKind.Episode) + || query.IncludeItemTypes.Contains(BaseItemKind.Video) + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) + || query.IncludeItemTypes.Contains(BaseItemKind.Series) + || query.IncludeItemTypes.Contains(BaseItemKind.Season); + } + + /// + public QueryResult GetItems(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { - query.Limit = query.Limit.Value + 4; + var returnList = GetItemList(query); + return new QueryResult( + query.StartIndex, + returnList.Count, + returnList); } - if (query.IsResumable ?? false) + PrepareFilterQuery(query); + var result = new QueryResult(); + + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, query) + .DistinctBy(e => e.Id); + if (query.EnableTotalRecordCount) { - query.IsVirtualItem = false; + result.TotalRecordCount = dbQuery.Count(); + } + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (query.Limit.HasValue) + { + dbQuery = dbQuery.Take(query.Limit.Value); + } + } + + result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + result.StartIndex = query.StartIndex ?? 0; + return result; + } + + /// + public IReadOnlyList GetItemList(InternalItemsQuery query) + { + ArgumentNullException.ThrowIfNull(query); + PrepareFilterQuery(query); + + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, query) + .DistinctBy(e => e.Id); + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (query.Limit.HasValue) + { + dbQuery = dbQuery.Take(query.Limit.Value); + } } + return dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + } + + /// + public int GetCount(InternalItemsQuery query) + { + ArgumentNullException.ThrowIfNull(query); + // Hack for right now since we currently don't support filtering out these duplicates within a query + PrepareFilterQuery(query); + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, query); + return dbQuery.Count(); } private IQueryable TranslateQuery( @@ -1049,6 +1310,8 @@ public class BaseItemManager : IItemRepository .Where(e => e.ExtraIds == null); } } + + return baseQuery; } /// @@ -1212,9 +1475,9 @@ public class BaseItemManager : IItemRepository dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : Guid.Parse(entity.OwnerId); dto.Width = entity.Width.GetValueOrDefault(); dto.Height = entity.Height.GetValueOrDefault(); - if (entity.ProviderIds is not null) + if (entity.Provider is not null) { - DeserializeProviderIds(entity.ProviderIds, dto); + dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); } if (entity.ExtraType is not null) @@ -1386,7 +1649,12 @@ public class BaseItemManager : IItemRepository entity.OwnerId = dto.OwnerId.ToString(); entity.Width = dto.Width; entity.Height = dto.Height; - entity.ProviderIds = SerializeProviderIds(dto.ProviderIds); + entity.Provider = dto.ProviderIds.Select(e => new Data.Entities.BaseItemProvider() + { + Item = entity, + ProviderId = e.Key, + ProviderValue = e.Value + }).ToList(); entity.Audio = dto.Audio?.ToString(); entity.ExtraType = dto.ExtraType?.ToString(); @@ -1479,10 +1747,23 @@ public class BaseItemManager : IItemRepository private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) { var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); - var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); ; + var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); return Map(baseItemEntity, dto); } + private static void PrepareFilterQuery(InternalItemsQuery query) + { + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + + if (query.IsResumable ?? false) + { + query.IsVirtualItem = false; + } + } + private string GetCleanValue(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -1813,5 +2094,4 @@ public class BaseItemManager : IItemRepository return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); } - } -- cgit v1.2.3 From 6acd146d17691d1fd58e8a110425cf1d7e2cdc44 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:11:31 +0000 Subject: WIP migration sqlite item repository to efcore --- .../Data/SqliteItemRepository.cs | 1935 +------------------- Jellyfin.Data/Entities/PeopleKind.cs | 133 ++ .../Item/BaseItemManager.cs | 434 +++-- .../Item/MediaStreamManager.cs | 201 ++ .../Item/PeopleManager.cs | 164 ++ 5 files changed, 861 insertions(+), 2006 deletions(-) create mode 100644 Jellyfin.Data/Entities/PeopleKind.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaStreamManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/PeopleManager.cs (limited to 'Emby.Server.Implementations/Data/SqliteItemRepository.cs') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 26255e6aa..a650f9555 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -146,1901 +146,111 @@ namespace Emby.Server.Implementations.Data || query.IsLiked.HasValue; } - private bool HasField(InternalItemsQuery query, ItemFields name) - { - switch (name) - { - case ItemFields.Tags: - return query.DtoOptions.ContainsField(name) || HasProgramAttributes(query); - case ItemFields.CustomRating: - case ItemFields.ProductionLocations: - case ItemFields.Settings: - case ItemFields.OriginalTitle: - case ItemFields.Taglines: - case ItemFields.SortName: - case ItemFields.Studios: - case ItemFields.ExtraIds: - case ItemFields.DateCreated: - case ItemFields.Overview: - case ItemFields.Genres: - case ItemFields.DateLastMediaAdded: - case ItemFields.PresentationUniqueKey: - case ItemFields.InheritedParentalRatingValue: - case ItemFields.ExternalSeriesId: - case ItemFields.SeriesPresentationUniqueKey: - case ItemFields.DateLastRefreshed: - case ItemFields.DateLastSaved: - return query.DtoOptions.ContainsField(name); - case ItemFields.ServiceName: - return HasServiceName(query); - default: - return true; - } - } - - private bool HasProgramAttributes(InternalItemsQuery query) - { - if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _programTypes.Contains(x)); - } - - private bool HasServiceName(InternalItemsQuery query) - { - if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x)); - } - - private bool HasStartDate(InternalItemsQuery query) - { - if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _startDateTypes.Contains(x)); - } - - private bool HasEpisodeAttributes(InternalItemsQuery query) - { - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Contains(BaseItemKind.Episode); - } - - private bool HasTrailerTypes(InternalItemsQuery query) - { - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Contains(BaseItemKind.Trailer); - } - - private bool HasArtistFields(InternalItemsQuery query) - { - if (query.ParentType is not null && _artistExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x)); - } - - private bool HasSeriesFields(InternalItemsQuery query) - { - if (query.ParentType == BaseItemKind.PhotoAlbum) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x)); - } - - private void SetFinalColumnsToSelect(InternalItemsQuery query, List columns) - { - foreach (var field in _allItemFields) - { - if (!HasField(query, field)) - { - switch (field) - { - case ItemFields.Settings: - columns.Remove("IsLocked"); - columns.Remove("PreferredMetadataCountryCode"); - columns.Remove("PreferredMetadataLanguage"); - columns.Remove("LockedFields"); - break; - case ItemFields.ServiceName: - columns.Remove("ExternalServiceId"); - break; - case ItemFields.SortName: - columns.Remove("ForcedSortName"); - break; - case ItemFields.Taglines: - columns.Remove("Tagline"); - break; - case ItemFields.Tags: - columns.Remove("Tags"); - break; - case ItemFields.IsHD: - // do nothing - break; - default: - columns.Remove(field.ToString()); - break; - } - } - } - - if (!HasProgramAttributes(query)) - { - columns.Remove("IsMovie"); - columns.Remove("IsSeries"); - columns.Remove("EpisodeTitle"); - columns.Remove("IsRepeat"); - columns.Remove("ShowId"); - } - - if (!HasEpisodeAttributes(query)) - { - columns.Remove("SeasonName"); - columns.Remove("SeasonId"); - } - - if (!HasStartDate(query)) - { - columns.Remove("StartDate"); - } - - if (!HasTrailerTypes(query)) - { - columns.Remove("TrailerTypes"); - } - - if (!HasArtistFields(query)) - { - columns.Remove("AlbumArtists"); - columns.Remove("Artists"); - } - - if (!HasSeriesFields(query)) - { - columns.Remove("SeriesId"); - } - - if (!HasEpisodeAttributes(query)) - { - columns.Remove("SeasonName"); - columns.Remove("SeasonId"); - } - - if (!query.DtoOptions.EnableImages) - { - columns.Remove("Images"); - } - - if (EnableJoinUserData(query)) - { - columns.Add("UserDatas.UserId"); - columns.Add("UserDatas.lastPlayedDate"); - columns.Add("UserDatas.playbackPositionTicks"); - columns.Add("UserDatas.playcount"); - columns.Add("UserDatas.isFavorite"); - columns.Add("UserDatas.played"); - columns.Add("UserDatas.rating"); - } - - if (query.SimilarTo is not null) - { - var item = query.SimilarTo; - - var builder = new StringBuilder(); - builder.Append('('); - - if (item.InheritedParentalRatingValue == 0) - { - builder.Append("((InheritedParentalRatingValue=0) * 10)"); - } - else - { - builder.Append( - @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0 - THEN 0 - ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue)) - END)"); - } - - if (item.ProductionYear.HasValue) - { - builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 10 Else 0 End )"); - builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )"); - } - - // genres, tags, studios, person, year? - builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))"); - builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))"); - - if (item is MusicArtist) - { - // Match albums where the artist is AlbumArtist against other albums. - // It is assumed that similar albums => similar artists. - builder.Append( - @"+ (WITH artistValues AS ( - SELECT DISTINCT albumValues.CleanValue - FROM ItemValues albumValues - INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId - INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId - ), similarArtist AS ( - SELECT albumValues.ItemId - FROM ItemValues albumValues - INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId - INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid - ) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))"); - } - - builder.Append(") as SimilarityScore"); - - columns.Add(builder.ToString()); - - query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds]; - query.ExcludeProviderIds = item.ProviderIds; - } - - if (!string.IsNullOrEmpty(query.SearchTerm)) - { - var builder = new StringBuilder(); - builder.Append('('); - - builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); - builder.Append("+ ((CleanName = @SearchTermStartsWith COLLATE NOCASE or (OriginalTitle not null and OriginalTitle = @SearchTermStartsWith COLLATE NOCASE)) * 10)"); - - if (query.SearchTerm.Length > 1) - { - builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); - } - - builder.Append(") as SearchScore"); - - columns.Add(builder.ToString()); - } - } - - private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement) - { - var searchTerm = query.SearchTerm; - - if (string.IsNullOrEmpty(searchTerm)) - { - return; - } - - searchTerm = FixUnicodeChars(searchTerm); - searchTerm = GetCleanValue(searchTerm); - - var commandText = statement.CommandText; - if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SearchTermStartsWith", searchTerm + "%"); - } - - if (commandText.Contains("@SearchTermContains", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); - } - } - - private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement) - { - var item = query.SimilarTo; - - if (item is null) - { - return; - } - - var commandText = statement.CommandText; - - if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@ItemOfficialRating", item.OfficialRating); - } - - if (commandText.Contains("@ItemProductionYear", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0); - } - - if (commandText.Contains("@SimilarItemId", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SimilarItemId", item.Id); - } - - if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); - } - } - - private string GetJoinUserDataText(InternalItemsQuery query) - { - if (!EnableJoinUserData(query)) - { - return string.Empty; - } - - return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)"; - } - - private string GetGroupBy(InternalItemsQuery query) - { - var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query); - if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey) - { - return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey"; - } - - if (enableGroupByPresentationUniqueKey) - { - return " Group by PresentationUniqueKey"; - } - - if (query.GroupBySeriesPresentationUniqueKey) - { - return " Group by SeriesPresentationUniqueKey"; - } - - return string.Empty; - } - - - private string FixUnicodeChars(string buffer) - { - buffer = buffer.Replace('\u2013', '-'); // en dash - buffer = buffer.Replace('\u2014', '-'); // em dash - buffer = buffer.Replace('\u2015', '-'); // horizontal bar - buffer = buffer.Replace('\u2017', '_'); // double low line - buffer = buffer.Replace('\u2018', '\''); // left single quotation mark - buffer = buffer.Replace('\u2019', '\''); // right single quotation mark - buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark - buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark - buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark - buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark - buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark - buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis - buffer = buffer.Replace('\u2032', '\''); // prime - buffer = buffer.Replace('\u2033', '\"'); // double prime - buffer = buffer.Replace('\u0060', '\''); // grave accent - return buffer.Replace('\u00B4', '\''); // acute accent - } - - private string GetOrderByText(InternalItemsQuery query) - { - var orderBy = query.OrderBy; - bool hasSimilar = query.SimilarTo is not null; - bool hasSearch = !string.IsNullOrEmpty(query.SearchTerm); - - if (hasSimilar || hasSearch) - { - List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); - if (hasSearch) - { - prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending)); - prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); - } - - if (hasSimilar) - { - prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending)); - prepend.Add((ItemSortBy.Random, SortOrder.Ascending)); - } - - orderBy = query.OrderBy = [.. prepend, .. orderBy]; - } - else if (orderBy.Count == 0) - { - return string.Empty; - } - - return " ORDER BY " + string.Join(',', orderBy.Select(i => - { - var sortBy = MapOrderByField(i.OrderBy, query); - var sortOrder = i.SortOrder == SortOrder.Ascending ? "ASC" : "DESC"; - return sortBy + " " + sortOrder; - })); - } - - private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) - { - return sortBy switch - { - ItemSortBy.AirTime => "SortName", // TODO - ItemSortBy.Runtime => "RuntimeTicks", - ItemSortBy.Random => "RANDOM()", - ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)", - ItemSortBy.DatePlayed => "LastPlayedDate", - ItemSortBy.PlayCount => "PlayCount", - ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )", - ItemSortBy.IsFolder => "IsFolder", - ItemSortBy.IsPlayed => "played", - ItemSortBy.IsUnplayed => "played", - ItemSortBy.DateLastContentAdded => "DateLastMediaAdded", - ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)", - ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)", - ItemSortBy.OfficialRating => "InheritedParentalRatingValue", - ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)", - ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => "SeriesName", - ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => "Album", - ItemSortBy.DateCreated => "DateCreated", - ItemSortBy.PremiereDate => "PremiereDate", - ItemSortBy.StartDate => "StartDate", - ItemSortBy.Name => "Name", - ItemSortBy.CommunityRating => "CommunityRating", - ItemSortBy.ProductionYear => "ProductionYear", - ItemSortBy.CriticRating => "CriticRating", - ItemSortBy.VideoBitRate => "VideoBitRate", - ItemSortBy.ParentIndexNumber => "ParentIndexNumber", - ItemSortBy.IndexNumber => "IndexNumber", - ItemSortBy.SimilarityScore => "SimilarityScore", - ItemSortBy.SearchScore => "SearchScore", - _ => "SortName" - }; - } - - /// - public List GetItemIdsList(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - var columns = new List { "guid" }; - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 256) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - if (whereClauses.Count != 0) - { - commandTextBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses); - } - - commandTextBuilder.Append(GetGroupBy(query)) - .Append(GetOrderByText(query)); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandTextBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - commandTextBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var commandText = commandTextBuilder.ToString(); - var list = new List(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row.GetGuid(0)); - } - } - - return list; - } - - private bool IsAlphaNumeric(string str) - { - if (string.IsNullOrWhiteSpace(str)) - { - return false; - } - - for (int i = 0; i < str.Length; i++) - { - if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) - { - return false; - } - } - - return true; - } - - private bool IsValidPersonType(string value) - { - return IsAlphaNumeric(value); - } - - private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) - { - if (query.ExcludeItemTypes.Contains(type)) - { - return false; - } - - return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); - } - - private string GetCleanValue(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return value; - } - - return value.RemoveDiacritics().ToLowerInvariant(); - } - - /// - public void UpdateInheritedValues() - { - const string Statements = """ -delete from ItemValues where type = 6; -insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4; -insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue -FROM AncestorIds -LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId) -where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4; -"""; - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - connection.Execute(Statements); - transaction.Commit(); - } - - /// - public void DeleteItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentNullException(nameof(id)); - } - - CheckDisposed(); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // Delete people - ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id); - - // Delete chapters - ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id); - - // Delete media streams - ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id); - - // Delete ancestors - ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id); - - // Delete item values - ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id); - - // Delete the item - ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id); - - transaction.Commit(); - } - - private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value) - { - using (var statement = PrepareStatement(db, query)) - { - statement.TryBind("@Id", value); - - statement.ExecuteNonQuery(); - } - } - - /// - public List GetPeopleNames(InternalPeopleQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - var commandText = new StringBuilder("select Distinct p.Name from People p"); - - var whereClauses = GetPeopleWhereClauses(query, null); - - if (whereClauses.Count != 0) - { - commandText.Append(" where ").AppendJoin(" AND ", whereClauses); - } - - commandText.Append(" order by ListOrder"); - - if (query.Limit > 0) - { - commandText.Append(" LIMIT ").Append(query.Limit); - } - - var list = new List(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText.ToString())) - { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row.GetString(0)); - } - } - - return list; - } - - /// - public List GetPeople(InternalPeopleQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p"); - - var whereClauses = GetPeopleWhereClauses(query, null); - - if (whereClauses.Count != 0) - { - commandText.Append(" where ").AppendJoin(" AND ", whereClauses); - } - - commandText.Append(" order by ListOrder"); - - if (query.Limit > 0) - { - commandText.Append(" LIMIT ").Append(query.Limit); - } - - var list = new List(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText.ToString())) - { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetPerson(row)); - } - } - - return list; - } - - private List GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement) - { - var whereClauses = new List(); - - if (query.User is not null && query.IsFavorite.HasValue) - { - whereClauses.Add(@"p.Name IN ( -SELECT Name FROM TypedBaseItems WHERE UserDataKey IN ( -SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId) -AND Type = @InternalPersonType)"); - statement?.TryBind("@IsFavorite", query.IsFavorite.Value); - statement?.TryBind("@InternalPersonType", typeof(Person).FullName); - statement?.TryBind("@UserId", query.User.InternalId); - } - - if (!query.ItemId.IsEmpty()) - { - whereClauses.Add("ItemId=@ItemId"); - statement?.TryBind("@ItemId", query.ItemId); - } - - if (!query.AppearsInItemId.IsEmpty()) - { - whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)"); - statement?.TryBind("@AppearsInItemId", query.AppearsInItemId); - } - - var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); - - if (queryPersonTypes.Count == 1) - { - whereClauses.Add("PersonType=@PersonType"); - statement?.TryBind("@PersonType", queryPersonTypes[0]); - } - else if (queryPersonTypes.Count > 1) - { - var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'")); - - whereClauses.Add("PersonType in (" + val + ")"); - } - - var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList(); - - if (queryExcludePersonTypes.Count == 1) - { - whereClauses.Add("PersonType<>@PersonType"); - statement?.TryBind("@PersonType", queryExcludePersonTypes[0]); - } - else if (queryExcludePersonTypes.Count > 1) - { - var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'")); - - whereClauses.Add("PersonType not in (" + val + ")"); - } - - if (query.MaxListOrder.HasValue) - { - whereClauses.Add("ListOrder<=@MaxListOrder"); - statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value); - } - - if (!string.IsNullOrWhiteSpace(query.NameContains)) - { - whereClauses.Add("p.Name like @NameContains"); - statement?.TryBind("@NameContains", "%" + query.NameContains + "%"); - } - - return whereClauses; - } - - private void UpdateAncestors(Guid itemId, List ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement) - { - if (itemId.IsEmpty()) - { - throw new ArgumentNullException(nameof(itemId)); - } - - ArgumentNullException.ThrowIfNull(ancestorIds); - - CheckDisposed(); - - // First delete - deleteAncestorsStatement.TryBind("@ItemId", itemId); - deleteAncestorsStatement.ExecuteNonQuery(); - - if (ancestorIds.Count == 0) - { - return; - } - - var insertText = new StringBuilder("insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values "); - - for (var i = 0; i < ancestorIds.Count; i++) - { - insertText.AppendFormat( - CultureInfo.InvariantCulture, - "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),", - i.ToString(CultureInfo.InvariantCulture)); - } - - // Remove trailing comma - insertText.Length--; - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", itemId); - - for (var i = 0; i < ancestorIds.Count; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var ancestorId = ancestorIds[i]; - - statement.TryBind("@AncestorId" + index, ancestorId); - statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.ExecuteNonQuery(); - } - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); - } - - /// - public List GetStudioNames() - { - return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); - } - - /// - public List GetAllArtistNames() - { - return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); - } - - /// - public List GetMusicGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - new string[] - { - typeof(Audio).FullName, - typeof(MusicVideo).FullName, - typeof(MusicAlbum).FullName, - typeof(MusicArtist).FullName - }, - Array.Empty()); - } - - /// - public List GetGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - Array.Empty(), - new string[] - { - typeof(Audio).FullName, - typeof(MusicVideo).FullName, - typeof(MusicAlbum).FullName, - typeof(MusicArtist).FullName - }); - } - - private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) - { - CheckDisposed(); - - var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); - if (itemValueTypes.Length == 1) - { - stringBuilder.Append('=') - .Append(itemValueTypes[0]); - } - else - { - stringBuilder.Append(" in (") - .AppendJoin(',', itemValueTypes) - .Append(')'); - } - - if (withItemTypes.Count > 0) - { - stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (") - .AppendJoinInSingleQuotes(',', withItemTypes) - .Append("))"); - } - - if (excludeItemTypes.Count > 0) - { - stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (") - .AppendJoinInSingleQuotes(',', excludeItemTypes) - .Append("))"); - } - - stringBuilder.Append(" Group By CleanValue"); - var commandText = stringBuilder.ToString(); - - var list = new List(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - foreach (var row in statement.ExecuteQuery()) - { - if (row.TryGetString(0, out var result)) - { - list.Add(result); - } - } - } - - return list; - } - - private QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) - { - ArgumentNullException.ThrowIfNull(query); - - if (!query.Limit.HasValue) - { - query.EnableTotalRecordCount = false; - } - - CheckDisposed(); - - var typeClause = itemValueTypes.Length == 1 ? - ("Type=" + itemValueTypes[0]) : - ("Type in (" + string.Join(',', itemValueTypes) + ")"); - - InternalItemsQuery typeSubQuery = null; - - string itemCountColumns = null; - - var stringBuilder = new StringBuilder(1024); - var typesToCount = query.IncludeItemTypes; - - if (typesToCount.Length > 0) - { - stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B"); - - typeSubQuery = new InternalItemsQuery(query.User) - { - ExcludeItemTypes = query.ExcludeItemTypes, - IncludeItemTypes = query.IncludeItemTypes, - MediaTypes = query.MediaTypes, - AncestorIds = query.AncestorIds, - ExcludeItemIds = query.ExcludeItemIds, - ItemIds = query.ItemIds, - TopParentIds = query.TopParentIds, - ParentId = query.ParentId, - IsPlayed = query.IsPlayed - }; - var whereClauses = GetWhereClauses(typeSubQuery, null); - - stringBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses) - .Append(" AND ") - .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ") - .Append(typeClause) - .Append(")) as itemTypes"); - - itemCountColumns = stringBuilder.ToString(); - stringBuilder.Clear(); - } - - List columns = _retrieveItemColumns.ToList(); - // Unfortunately we need to add it to columns to ensure the order of the columns in the select - if (!string.IsNullOrEmpty(itemCountColumns)) - { - columns.Add(itemCountColumns); - } - - // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo - var innerQuery = new InternalItemsQuery(query.User) - { - ExcludeItemTypes = query.ExcludeItemTypes, - IncludeItemTypes = query.IncludeItemTypes, - MediaTypes = query.MediaTypes, - AncestorIds = query.AncestorIds, - ItemIds = query.ItemIds, - TopParentIds = query.TopParentIds, - ParentId = query.ParentId, - IsAiring = query.IsAiring, - IsMovie = query.IsMovie, - IsSports = query.IsSports, - IsKids = query.IsKids, - IsNews = query.IsNews, - IsSeries = query.IsSeries - }; - - SetFinalColumnsToSelect(query, columns); - - var innerWhereClauses = GetWhereClauses(innerQuery, null); - - stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ") - .Append(typeClause) - .Append(" AND ItemId in (select guid from TypedBaseItems"); - if (innerWhereClauses.Count > 0) - { - stringBuilder.Append(" where ") - .AppendJoin(" AND ", innerWhereClauses); - } - - stringBuilder.Append("))"); - - var outerQuery = new InternalItemsQuery(query.User) - { - IsPlayed = query.IsPlayed, - IsFavorite = query.IsFavorite, - IsFavoriteOrLiked = query.IsFavoriteOrLiked, - IsLiked = query.IsLiked, - IsLocked = query.IsLocked, - NameLessThan = query.NameLessThan, - NameStartsWith = query.NameStartsWith, - NameStartsWithOrGreater = query.NameStartsWithOrGreater, - Tags = query.Tags, - OfficialRatings = query.OfficialRatings, - StudioIds = query.StudioIds, - GenreIds = query.GenreIds, - Genres = query.Genres, - Years = query.Years, - NameContains = query.NameContains, - SearchTerm = query.SearchTerm, - SimilarTo = query.SimilarTo, - ExcludeItemIds = query.ExcludeItemIds - }; - - var outerWhereClauses = GetWhereClauses(outerQuery, null); - if (outerWhereClauses.Count != 0) - { - stringBuilder.Append(" AND ") - .AppendJoin(" AND ", outerWhereClauses); - } - - var whereText = stringBuilder.ToString(); - stringBuilder.Clear(); - - stringBuilder.Append("select ") - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)) - .Append(whereText) - .Append(" group by PresentationUniqueKey"); - - if (query.OrderBy.Count != 0 - || query.SimilarTo is not null - || !string.IsNullOrEmpty(query.SearchTerm)) - { - stringBuilder.Append(GetOrderByText(query)); - } - else - { - stringBuilder.Append(" order by SortName"); - } - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - stringBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - stringBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - string commandText = string.Empty; - - if (!isReturningZeroItems) - { - commandText = stringBuilder.ToString(); - } - - string countText = string.Empty; - if (query.EnableTotalRecordCount) - { - stringBuilder.Clear(); - var columnsToSelect = new List { "count (distinct PresentationUniqueKey)" }; - SetFinalColumnsToSelect(query, columnsToSelect); - stringBuilder.Append("select ") - .AppendJoin(',', columnsToSelect) - .Append(FromText) - .Append(GetJoinUserDataText(query)) - .Append(whereText); - - countText = stringBuilder.ToString(); - } - - var list = new List<(BaseItem, ItemCounts)>(); - var result = new QueryResult<(BaseItem, ItemCounts)>(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var transaction = connection.BeginTransaction()) - { - if (!isReturningZeroItems) - { - using (var statement = PrepareStatement(connection, commandText)) - { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasServiceName = HasServiceName(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false); - if (item is not null) - { - var countStartColumn = columns.Count - 1; - - list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); - } - } - } - } - - if (query.EnableTotalRecordCount) - { - using (var statement = PrepareStatement(connection, countText)) - { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - result.TotalRecordCount = statement.SelectScalarInt(); - } - } - - transaction.Commit(); - } - - if (result.TotalRecordCount == 0) - { - result.TotalRecordCount = list.Count; - } - - result.StartIndex = query.StartIndex ?? 0; - result.Items = list; - - return result; - } - - private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount) - { - var counts = new ItemCounts(); - - if (typesToCount.Length == 0) - { - return counts; - } - - if (!reader.TryGetString(countStartColumn, out var typeString)) - { - return counts; - } - - foreach (var typeName in typeString.AsSpan().Split('|')) - { - if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.SeriesCount++; - } - else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.EpisodeCount++; - } - else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.MovieCount++; - } - else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.AlbumCount++; - } - else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.ArtistCount++; - } - else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.SongCount++; - } - else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.TrailerCount++; - } - - counts.ItemCount++; - } - - return counts; - } - - private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List inheritedTags) - { - var list = new List<(int, string)>(); - - if (item is IHasArtist hasArtist) - { - list.AddRange(hasArtist.Artists.Select(i => (0, i))); - } - - if (item is IHasAlbumArtist hasAlbumArtist) - { - list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); - } - - list.AddRange(item.Genres.Select(i => (2, i))); - list.AddRange(item.Studios.Select(i => (3, i))); - list.AddRange(item.Tags.Select(i => (4, i))); - - // keywords was 5 - - list.AddRange(inheritedTags.Select(i => (6, i))); - - // Remove all invalid values. - list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2)); - - return list; - } - - private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db) - { - if (itemId.IsEmpty()) - { - throw new ArgumentNullException(nameof(itemId)); - } - - ArgumentNullException.ThrowIfNull(values); - - CheckDisposed(); - - // First delete - using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id"); - command.TryBind("@Id", itemId); - command.ExecuteNonQuery(); - - InsertItemValues(itemId, values, db); - } - - private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db) - { - const int Limit = 100; - var startIndex = 0; - - const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values "; - var insertText = new StringBuilder(StartInsertText); - while (startIndex < values.Count) - { - var endIndex = Math.Min(values.Count, startIndex + Limit); - - for (var i = startIndex; i < endIndex; i++) - { - insertText.AppendFormat( - CultureInfo.InvariantCulture, - "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0}),", - i); - } - - // Remove trailing comma - insertText.Length--; - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", id); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var currentValueInfo = values[i]; - - var itemValue = currentValueInfo.Value; - - statement.TryBind("@Type" + index, currentValueInfo.MagicNumber); - statement.TryBind("@Value" + index, itemValue); - statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue)); - } - - statement.ExecuteNonQuery(); - } - - startIndex += Limit; - insertText.Length = StartInsertText.Length; - } - } - - /// - public void UpdatePeople(Guid itemId, List people) - { - if (itemId.IsEmpty()) - { - throw new ArgumentNullException(nameof(itemId)); - } - - CheckDisposed(); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // Delete all existing people first - using var command = connection.CreateCommand(); - command.CommandText = "delete from People where ItemId=@ItemId"; - command.TryBind("@ItemId", itemId); - command.ExecuteNonQuery(); - - if (people is not null) - { - InsertPeople(itemId, people, connection); - } - - transaction.Commit(); - } - - private void InsertPeople(Guid id, List people, ManagedConnection db) - { - const int Limit = 100; - var startIndex = 0; - var listIndex = 0; - - const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values "; - var insertText = new StringBuilder(StartInsertText); - while (startIndex < people.Count) - { - var endIndex = Math.Min(people.Count, startIndex + Limit); - for (var i = startIndex; i < endIndex; i++) - { - insertText.AppendFormat( - CultureInfo.InvariantCulture, - "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", - i.ToString(CultureInfo.InvariantCulture)); - } - - // Remove trailing comma - insertText.Length--; - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", id); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var person = people[i]; - - statement.TryBind("@Name" + index, person.Name); - statement.TryBind("@Role" + index, person.Role); - statement.TryBind("@PersonType" + index, person.Type.ToString()); - statement.TryBind("@SortOrder" + index, person.SortOrder); - statement.TryBind("@ListOrder" + index, listIndex); - - listIndex++; - } - - statement.ExecuteNonQuery(); - } - - startIndex += Limit; - insertText.Length = StartInsertText.Length; - } - } - - private PersonInfo GetPerson(SqliteDataReader reader) - { - var item = new PersonInfo - { - ItemId = reader.GetGuid(0), - Name = reader.GetString(1) - }; - - if (reader.TryGetString(2, out var role)) - { - item.Role = role; - } - - if (reader.TryGetString(3, out var type) - && Enum.TryParse(type, true, out PersonKind personKind)) - { - item.Type = personKind; - } - - if (reader.TryGetInt32(4, out var sortOrder)) + private string GetJoinUserDataText(InternalItemsQuery query) + { + if (!EnableJoinUserData(query)) { - item.SortOrder = sortOrder; + return string.Empty; } - return item; + return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)"; } /// - public List GetMediaStreams(MediaStreamQuery query) + public List GetStudioNames() { - CheckDisposed(); - - ArgumentNullException.ThrowIfNull(query); - - var cmdText = _mediaStreamSaveColumnsSelectQuery; - - if (query.Type.HasValue) - { - cmdText += " AND StreamType=@StreamType"; - } - - if (query.Index.HasValue) - { - cmdText += " AND StreamIndex=@StreamIndex"; - } - - cmdText += " order by StreamIndex ASC"; - - using (var connection = GetConnection(true)) - { - var list = new List(); - - using (var statement = PrepareStatement(connection, cmdText)) - { - statement.TryBind("@ItemId", query.ItemId); - - if (query.Type.HasValue) - { - statement.TryBind("@StreamType", query.Type.Value.ToString()); - } - - if (query.Index.HasValue) - { - statement.TryBind("@StreamIndex", query.Index.Value); - } - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetMediaStream(row)); - } - } - - return list; - } + return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); } /// - public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) + public List GetAllArtistNames() { - CheckDisposed(); - - if (id.IsEmpty()) - { - throw new ArgumentNullException(nameof(id)); - } - - ArgumentNullException.ThrowIfNull(streams); - - cancellationToken.ThrowIfCancellationRequested(); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // Delete existing mediastreams - using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId"); - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); - - InsertMediaStreams(id, streams, connection); - - transaction.Commit(); + return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); } - private void InsertMediaStreams(Guid id, IReadOnlyList streams, ManagedConnection db) + /// + public List GetMusicGenreNames() { - const int Limit = 10; - var startIndex = 0; - - var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery); - while (startIndex < streams.Count) - { - var endIndex = Math.Min(streams.Count, startIndex + Limit); - - for (var i = startIndex; i < endIndex; i++) + return GetItemValueNames( + new[] { 2 }, + new string[] { - if (i != startIndex) - { - insertText.Append(','); - } - - var index = i.ToString(CultureInfo.InvariantCulture); - insertText.Append("(@ItemId, "); - - foreach (var column in _mediaStreamSaveColumns.Skip(1)) - { - insertText.Append('@').Append(column).Append(index).Append(','); - } - - insertText.Length -= 1; // Remove the last comma - - insertText.Append(')'); - } + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }, + Array.Empty()); + } - using (var statement = PrepareStatement(db, insertText.ToString())) + /// + public List GetGenreNames() + { + return GetItemValueNames( + new[] { 2 }, + Array.Empty(), + new string[] { - statement.TryBind("@ItemId", id); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var stream = streams[i]; - - statement.TryBind("@StreamIndex" + index, stream.Index); - statement.TryBind("@StreamType" + index, stream.Type.ToString()); - statement.TryBind("@Codec" + index, stream.Codec); - statement.TryBind("@Language" + index, stream.Language); - statement.TryBind("@ChannelLayout" + index, stream.ChannelLayout); - statement.TryBind("@Profile" + index, stream.Profile); - statement.TryBind("@AspectRatio" + index, stream.AspectRatio); - statement.TryBind("@Path" + index, GetPathToSave(stream.Path)); - - statement.TryBind("@IsInterlaced" + index, stream.IsInterlaced); - statement.TryBind("@BitRate" + index, stream.BitRate); - statement.TryBind("@Channels" + index, stream.Channels); - statement.TryBind("@SampleRate" + index, stream.SampleRate); - - statement.TryBind("@IsDefault" + index, stream.IsDefault); - statement.TryBind("@IsForced" + index, stream.IsForced); - statement.TryBind("@IsExternal" + index, stream.IsExternal); - - // Yes these are backwards due to a mistake - statement.TryBind("@Width" + index, stream.Height); - statement.TryBind("@Height" + index, stream.Width); - - statement.TryBind("@AverageFrameRate" + index, stream.AverageFrameRate); - statement.TryBind("@RealFrameRate" + index, stream.RealFrameRate); - statement.TryBind("@Level" + index, stream.Level); - - statement.TryBind("@PixelFormat" + index, stream.PixelFormat); - statement.TryBind("@BitDepth" + index, stream.BitDepth); - statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic); - statement.TryBind("@IsExternal" + index, stream.IsExternal); - statement.TryBind("@RefFrames" + index, stream.RefFrames); - - statement.TryBind("@CodecTag" + index, stream.CodecTag); - statement.TryBind("@Comment" + index, stream.Comment); - statement.TryBind("@NalLengthSize" + index, stream.NalLengthSize); - statement.TryBind("@IsAvc" + index, stream.IsAVC); - statement.TryBind("@Title" + index, stream.Title); - - statement.TryBind("@TimeBase" + index, stream.TimeBase); - statement.TryBind("@CodecTimeBase" + index, stream.CodecTimeBase); - - statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries); - statement.TryBind("@ColorSpace" + index, stream.ColorSpace); - statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer); - - statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor); - statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor); - statement.TryBind("@DvProfile" + index, stream.DvProfile); - statement.TryBind("@DvLevel" + index, stream.DvLevel); - statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag); - statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag); - statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag); - statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId); - - statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired); - - statement.TryBind("@Rotation" + index, stream.Rotation); - } - - statement.ExecuteNonQuery(); - } - - startIndex += Limit; - insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length; - } + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }); } - /// - /// Gets the media stream. - /// - /// The reader. - /// MediaStream. - private MediaStream GetMediaStream(SqliteDataReader reader) + private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) { - var item = new MediaStream - { - Index = reader.GetInt32(1), - Type = Enum.Parse(reader.GetString(2), true) - }; - - if (reader.TryGetString(3, out var codec)) - { - item.Codec = codec; - } - - if (reader.TryGetString(4, out var language)) - { - item.Language = language; - } - - if (reader.TryGetString(5, out var channelLayout)) - { - item.ChannelLayout = channelLayout; - } - - if (reader.TryGetString(6, out var profile)) - { - item.Profile = profile; - } - - if (reader.TryGetString(7, out var aspectRatio)) - { - item.AspectRatio = aspectRatio; - } - - if (reader.TryGetString(8, out var path)) - { - item.Path = RestorePath(path); - } - - item.IsInterlaced = reader.GetBoolean(9); - - if (reader.TryGetInt32(10, out var bitrate)) - { - item.BitRate = bitrate; - } - - if (reader.TryGetInt32(11, out var channels)) - { - item.Channels = channels; - } - - if (reader.TryGetInt32(12, out var sampleRate)) - { - item.SampleRate = sampleRate; - } - - item.IsDefault = reader.GetBoolean(13); - item.IsForced = reader.GetBoolean(14); - item.IsExternal = reader.GetBoolean(15); - - if (reader.TryGetInt32(16, out var width)) - { - item.Width = width; - } - - if (reader.TryGetInt32(17, out var height)) - { - item.Height = height; - } - - if (reader.TryGetSingle(18, out var averageFrameRate)) - { - item.AverageFrameRate = averageFrameRate; - } - - if (reader.TryGetSingle(19, out var realFrameRate)) - { - item.RealFrameRate = realFrameRate; - } - - if (reader.TryGetSingle(20, out var level)) - { - item.Level = level; - } - - if (reader.TryGetString(21, out var pixelFormat)) - { - item.PixelFormat = pixelFormat; - } - - if (reader.TryGetInt32(22, out var bitDepth)) - { - item.BitDepth = bitDepth; - } - - if (reader.TryGetBoolean(23, out var isAnamorphic)) - { - item.IsAnamorphic = isAnamorphic; - } - - if (reader.TryGetInt32(24, out var refFrames)) - { - item.RefFrames = refFrames; - } - - if (reader.TryGetString(25, out var codecTag)) - { - item.CodecTag = codecTag; - } - - if (reader.TryGetString(26, out var comment)) - { - item.Comment = comment; - } - - if (reader.TryGetString(27, out var nalLengthSize)) - { - item.NalLengthSize = nalLengthSize; - } - - if (reader.TryGetBoolean(28, out var isAVC)) - { - item.IsAVC = isAVC; - } - - if (reader.TryGetString(29, out var title)) - { - item.Title = title; - } - - if (reader.TryGetString(30, out var timeBase)) - { - item.TimeBase = timeBase; - } - - if (reader.TryGetString(31, out var codecTimeBase)) - { - item.CodecTimeBase = codecTimeBase; - } - - if (reader.TryGetString(32, out var colorPrimaries)) - { - item.ColorPrimaries = colorPrimaries; - } - - if (reader.TryGetString(33, out var colorSpace)) - { - item.ColorSpace = colorSpace; - } - - if (reader.TryGetString(34, out var colorTransfer)) - { - item.ColorTransfer = colorTransfer; - } - - if (reader.TryGetInt32(35, out var dvVersionMajor)) - { - item.DvVersionMajor = dvVersionMajor; - } - - if (reader.TryGetInt32(36, out var dvVersionMinor)) - { - item.DvVersionMinor = dvVersionMinor; - } - - if (reader.TryGetInt32(37, out var dvProfile)) - { - item.DvProfile = dvProfile; - } - - if (reader.TryGetInt32(38, out var dvLevel)) - { - item.DvLevel = dvLevel; - } + CheckDisposed(); - if (reader.TryGetInt32(39, out var rpuPresentFlag)) + var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); + if (itemValueTypes.Length == 1) { - item.RpuPresentFlag = rpuPresentFlag; + stringBuilder.Append('=') + .Append(itemValueTypes[0]); } - - if (reader.TryGetInt32(40, out var elPresentFlag)) + else { - item.ElPresentFlag = elPresentFlag; + stringBuilder.Append(" in (") + .AppendJoin(',', itemValueTypes) + .Append(')'); } - if (reader.TryGetInt32(41, out var blPresentFlag)) + if (withItemTypes.Count > 0) { - item.BlPresentFlag = blPresentFlag; + stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', withItemTypes) + .Append("))"); } - if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId)) + if (excludeItemTypes.Count > 0) { - item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId; + stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', excludeItemTypes) + .Append("))"); } - item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result; - - if (reader.TryGetInt32(44, out var rotation)) - { - item.Rotation = rotation; - } + stringBuilder.Append(" Group By CleanValue"); + var commandText = stringBuilder.ToString(); - if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) + var list = new List(); + using (new QueryTimeLogger(Logger, commandText)) + using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - item.LocalizedDefault = _localization.GetLocalizedString("Default"); - item.LocalizedExternal = _localization.GetLocalizedString("External"); - - if (item.Type is MediaStreamType.Subtitle) + foreach (var row in statement.ExecuteQuery()) { - item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); - item.LocalizedForced = _localization.GetLocalizedString("Forced"); - item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); + if (row.TryGetString(0, out var result)) + { + list.Add(result); + } } } - return item; + return list; } + + /// public List GetMediaAttachments(MediaAttachmentQuery query) { @@ -2205,21 +415,6 @@ AND Type = @InternalPersonType)"); return item; } - private static string BuildMediaAttachmentInsertPrefix() - { - var queryPrefixText = new StringBuilder(); - queryPrefixText.Append("insert into mediaattachments ("); - foreach (var column in _mediaAttachmentSaveColumns) - { - queryPrefixText.Append(column) - .Append(','); - } - - queryPrefixText.Length -= 1; - queryPrefixText.Append(") values "); - return queryPrefixText.ToString(); - } - #nullable enable private readonly struct QueryTimeLogger : IDisposable diff --git a/Jellyfin.Data/Entities/PeopleKind.cs b/Jellyfin.Data/Entities/PeopleKind.cs new file mode 100644 index 000000000..967f7c11f --- /dev/null +++ b/Jellyfin.Data/Entities/PeopleKind.cs @@ -0,0 +1,133 @@ +namespace Jellyfin.Data.Entities; + +/// +/// The person kind. +/// +public enum PeopleKind +{ + /// + /// An unknown person kind. + /// + Unknown, + + /// + /// A person whose profession is acting on the stage, in films, or on television. + /// + Actor, + + /// + /// A person who supervises the actors and other staff in a film, play, or similar production. + /// + Director, + + /// + /// A person who writes music, especially as a professional occupation. + /// + Composer, + + /// + /// A writer of a book, article, or document. Can also be used as a generic term for music writer if there is a lack of specificity. + /// + Writer, + + /// + /// A well-known actor or other performer who appears in a work in which they do not have a regular role. + /// + GuestStar, + + /// + /// A person responsible for the financial and managerial aspects of the making of a film or broadcast or for staging a play, opera, etc. + /// + Producer, + + /// + /// A person who directs the performance of an orchestra or choir. + /// + Conductor, + + /// + /// A person who writes the words to a song or musical. + /// + Lyricist, + + /// + /// A person who adapts a musical composition for performance. + /// + Arranger, + + /// + /// An audio engineer who performed a general engineering role. + /// + Engineer, + + /// + /// An engineer responsible for using a mixing console to mix a recorded track into a single piece of music suitable for release. + /// + Mixer, + + /// + /// A person who remixed a recording by taking one or more other tracks, substantially altering them and mixing them together with other material. + /// + Remixer, + + /// + /// A person who created the material. + /// + Creator, + + /// + /// A person who was the artist. + /// + Artist, + + /// + /// A person who was the album artist. + /// + AlbumArtist, + + /// + /// A person who was the author. + /// + Author, + + /// + /// A person who was the illustrator. + /// + Illustrator, + + /// + /// A person responsible for drawing the art. + /// + Penciller, + + /// + /// A person responsible for inking the pencil art. + /// + Inker, + + /// + /// A person responsible for applying color to drawings. + /// + Colorist, + + /// + /// A person responsible for drawing text and speech bubbles. + /// + Letterer, + + /// + /// A person responsible for drawing the cover art. + /// + CoverArtist, + + /// + /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter. + /// An editor may also prepare a resource for production, publication, or distribution. + /// + Editor, + + /// + /// A person who renders a text from one language into another. + /// + Translator +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index f2d6b6261..022f26cd7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -19,10 +19,12 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; @@ -145,8 +147,72 @@ public class BaseItemManager : IItemRepository _appHost = appHost; } - private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) + private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType) { + ArgumentNullException.ThrowIfNull(filter); + + if (!filter.Limit.HasValue) + { + filter.EnableTotalRecordCount = false; + } + + using var context = _dbProvider.CreateDbContext(); + + var innerQuery = new InternalItemsQuery(filter.User) + { + ExcludeItemTypes = filter.ExcludeItemTypes, + IncludeItemTypes = filter.IncludeItemTypes, + MediaTypes = filter.MediaTypes, + AncestorIds = filter.AncestorIds, + ItemIds = filter.ItemIds, + TopParentIds = filter.TopParentIds, + ParentId = filter.ParentId, + IsAiring = filter.IsAiring, + IsMovie = filter.IsMovie, + IsSports = filter.IsSports, + IsKids = filter.IsKids, + IsNews = filter.IsNews, + IsSeries = filter.IsSeries + }; + var query = TranslateQuery(context.BaseItems, context, innerQuery); + + query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Contains(f.Type))); + + var outerQuery = new InternalItemsQuery(filter.User) + { + IsPlayed = filter.IsPlayed, + IsFavorite = filter.IsFavorite, + IsFavoriteOrLiked = filter.IsFavoriteOrLiked, + IsLiked = filter.IsLiked, + IsLocked = filter.IsLocked, + NameLessThan = filter.NameLessThan, + NameStartsWith = filter.NameStartsWith, + NameStartsWithOrGreater = filter.NameStartsWithOrGreater, + Tags = filter.Tags, + OfficialRatings = filter.OfficialRatings, + StudioIds = filter.StudioIds, + GenreIds = filter.GenreIds, + Genres = filter.Genres, + Years = filter.Years, + NameContains = filter.NameContains, + SearchTerm = filter.SearchTerm, + SimilarTo = filter.SimilarTo, + ExcludeItemIds = filter.ExcludeItemIds + }; + query = TranslateQuery(query, context, outerQuery) + .OrderBy(e => e.PresentationUniqueKey); + + if (filter.OrderBy.Count != 0 + || filter.SimilarTo is not null + || !string.IsNullOrEmpty(filter.SearchTerm)) + { + query = ApplyOrder(query, filter); + } + else + { + query = query.OrderBy(e => e.SortName); + } + if (filter.Limit.HasValue || filter.StartIndex.HasValue) { var offset = filter.StartIndex ?? 0; @@ -158,131 +224,96 @@ public class BaseItemManager : IItemRepository if (filter.Limit.HasValue) { - query = query.Take(filter.Limit.Value); + query.Take(filter.Limit.Value); } } - return query; - } - - private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) - { -#pragma warning disable CS8603 // Possible null reference return. - return sortBy switch - { - ItemSortBy.AirTime => e => e.SortName, // TODO - ItemSortBy.Runtime => e => e.RunTimeTicks, - ItemSortBy.Random => e => EF.Functions.Random(), - ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate, - ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount, - ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite, - ItemSortBy.IsFolder => e => e.IsFolder, - ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, - ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, - ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, - ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue), - ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue), - ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), - ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => e => e.SeriesName, - // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => e => e.Album, - ItemSortBy.DateCreated => e => e.DateCreated, - ItemSortBy.PremiereDate => e => e.PremiereDate, - ItemSortBy.StartDate => e => e.StartDate, - ItemSortBy.Name => e => e.Name, - ItemSortBy.CommunityRating => e => e.CommunityRating, - ItemSortBy.ProductionYear => e => e.ProductionYear, - ItemSortBy.CriticRating => e => e.CriticRating, - ItemSortBy.VideoBitRate => e => e.TotalBitrate, - ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, - ItemSortBy.IndexNumber => e => e.IndexNumber, - _ => e => e.SortName - }; -#pragma warning restore CS8603 // Possible null reference return. - - } - - private IQueryable MapOrderByField(IQueryable dbQuery, ItemSortBy sortBy, InternalItemsQuery query) - { - return sortBy switch + var result = new QueryResult<(BaseItem, ItemCounts)>(); + string countText = string.Empty; + if (filter.EnableTotalRecordCount) { - ItemSortBy.AirTime => dbQuery.OrderBy(e => e.SortName), // TODO - ItemSortBy.Runtime => dbQuery.OrderBy(e => e.RunTimeTicks), - ItemSortBy.Random => dbQuery.OrderBy(e => EF.Functions.Random()), - ItemSortBy.DatePlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate), - ItemSortBy.PlayCount => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount), - ItemSortBy.IsFavoriteOrLiked => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite), - ItemSortBy.IsFolder => dbQuery.OrderBy(e => e.IsFolder), - ItemSortBy.IsPlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), - ItemSortBy.IsUnplayed => dbQuery.OrderBy(e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), - ItemSortBy.DateLastContentAdded => dbQuery.OrderBy(e => e.DateLastMediaAdded), - ItemSortBy.Artist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue)), - ItemSortBy.AlbumArtist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue)), - ItemSortBy.Studio => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue)), - ItemSortBy.OfficialRating => dbQuery.OrderBy(e => e.InheritedParentalRatingValue), - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => dbQuery.OrderBy(e => e.SeriesName), - // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => dbQuery.OrderBy(e => e.Album), - ItemSortBy.DateCreated => dbQuery.OrderBy(e => e.DateCreated), - ItemSortBy.PremiereDate => dbQuery.OrderBy(e => e.PremiereDate), - ItemSortBy.StartDate => dbQuery.OrderBy(e => e.StartDate), - ItemSortBy.Name => dbQuery.OrderBy(e => e.Name), - ItemSortBy.CommunityRating => dbQuery.OrderBy(e => e.CommunityRating), - ItemSortBy.ProductionYear => dbQuery.OrderBy(e => e.ProductionYear), - ItemSortBy.CriticRating => dbQuery.OrderBy(e => e.CriticRating), - ItemSortBy.VideoBitRate => dbQuery.OrderBy(e => e.TotalBitrate), - ItemSortBy.ParentIndexNumber => dbQuery.OrderBy(e => e.ParentIndexNumber), - ItemSortBy.IndexNumber => dbQuery.OrderBy(e => e.IndexNumber), - _ => dbQuery.OrderBy(e => e.SortName) - }; - } - - private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) - { - var orderBy = filter.OrderBy; - bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count(); + } - if (hasSearch) + var resultQuery = query.Select(e => new { - List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); - if (hasSearch) + item = e, + itemCount = new ItemCounts() { - prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); + SeriesCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Series), + EpisodeCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Episode), + MovieCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Movie), + AlbumCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), + ArtistCount = e.ItemValues!.Count(e => e.Type == 0 || e.Type == 1), + SongCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), + TrailerCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Trailer), } + }); - orderBy = filter.OrderBy = [.. prepend, .. orderBy]; - } - else if (orderBy.Count == 0) + result.StartIndex = filter.StartIndex ?? 0; + result.Items = resultQuery.ToImmutableArray().Select(e => { - return query; - } + return (DeserialiseBaseItem(e.item), e.itemCount); + }).ToImmutableArray(); - foreach (var item in orderBy) - { - var expression = MapOrderByField(item.OrderBy, filter); - if (item.SortOrder == SortOrder.Ascending) - { - query = query.OrderBy(expression); - } - else - { - query = query.OrderByDescending(expression); - } - } + return result; + } - return query; + /// + public void DeleteItem(Guid id) + { + ArgumentNullException.ThrowIfNull(id.IsEmpty() ? null : id); + + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + context.Peoples.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.Chapters.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.AncestorIds.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.ItemValues.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.BaseItems.Where(e => e.Id.Equals(id)).ExecuteDelete(); + context.SaveChanges(); + transaction.Commit(); + } + + /// + public void UpdateInheritedValues() + { + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.ItemValues.Where(e => e.Type == 6).ExecuteDelete(); + context.ItemValues.AddRange(context.ItemValues.Where(e => e.Type == 4).Select(e => new Data.Entities.ItemValue() + { + CleanValue = e.CleanValue, + ItemId = e.ItemId, + Type = 6, + Value = e.Value, + Item = null! + })); + + context.ItemValues.AddRange( + context.AncestorIds.Where(e => e.AncestorIdText != null).Join(context.ItemValues.Where(e => e.Value != null && e.Type == 4), e => e.Id, e => e.ItemId, (e, f) => new Data.Entities.ItemValue() + { + CleanValue = f.CleanValue, + ItemId = e.ItemId, + Item = null!, + Type = 6, + Value = f.Value + })); + context.SaveChanges(); + + transaction.Commit(); } + /// public IReadOnlyList GetItemIdsList(InternalItemsQuery filter) { ArgumentNullException.ThrowIfNull(filter); PrepareFilterQuery(filter); using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) + var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter) .DistinctBy(e => e.Id); var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); @@ -306,56 +337,57 @@ public class BaseItemManager : IItemRepository return Pageinate(dbQuery, filter).Select(e => e.Id).ToImmutableArray(); } - private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) { - if (!query.GroupByPresentationUniqueKey) - { - return false; - } + return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); + } - if (query.GroupBySeriesPresentationUniqueKey) - { - return false; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); + } - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) - { - return false; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); + } - if (query.User is null) - { - return false; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); + } - if (query.IncludeItemTypes.Length == 0) - { - return true; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); + } - return query.IncludeItemTypes.Contains(BaseItemKind.Episode) - || query.IncludeItemTypes.Contains(BaseItemKind.Video) - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) - || query.IncludeItemTypes.Contains(BaseItemKind.Series) - || query.IncludeItemTypes.Contains(BaseItemKind.Season); + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); } /// - public QueryResult GetItems(InternalItemsQuery query) + public QueryResult GetItems(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { var returnList = GetItemList(query); - return new QueryResult( + return new QueryResult( query.StartIndex, returnList.Count, returnList); } PrepareFilterQuery(query); - var result = new QueryResult(); + var result = new QueryResult(); using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.BaseItems, context, query) @@ -2094,4 +2126,134 @@ public class BaseItemManager : IItemRepository return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); } + + private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) + { + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + query = query.Skip(offset); + } + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + } + + return query; + } + + private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + { +#pragma warning disable CS8603 // Possible null reference return. + return sortBy switch + { + ItemSortBy.AirTime => e => e.SortName, // TODO + ItemSortBy.Runtime => e => e.RunTimeTicks, + ItemSortBy.Random => e => EF.Functions.Random(), + ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate, + ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount, + ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite, + ItemSortBy.IsFolder => e => e.IsFolder, + ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, + ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue), + ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue), + ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), + ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => e => e.SeriesName, + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => e => e.Album, + ItemSortBy.DateCreated => e => e.DateCreated, + ItemSortBy.PremiereDate => e => e.PremiereDate, + ItemSortBy.StartDate => e => e.StartDate, + ItemSortBy.Name => e => e.Name, + ItemSortBy.CommunityRating => e => e.CommunityRating, + ItemSortBy.ProductionYear => e => e.ProductionYear, + ItemSortBy.CriticRating => e => e.CriticRating, + ItemSortBy.VideoBitRate => e => e.TotalBitrate, + ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, + ItemSortBy.IndexNumber => e => e.IndexNumber, + _ => e => e.SortName + }; +#pragma warning restore CS8603 // Possible null reference return. + + } + + private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + { + if (!query.GroupByPresentationUniqueKey) + { + return false; + } + + if (query.GroupBySeriesPresentationUniqueKey) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + return false; + } + + if (query.User is null) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + return query.IncludeItemTypes.Contains(BaseItemKind.Episode) + || query.IncludeItemTypes.Contains(BaseItemKind.Video) + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) + || query.IncludeItemTypes.Contains(BaseItemKind.Series) + || query.IncludeItemTypes.Contains(BaseItemKind.Season); + } + + private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) + { + var orderBy = filter.OrderBy; + bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + + if (hasSearch) + { + List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); + if (hasSearch) + { + prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); + } + + orderBy = filter.OrderBy = [.. prepend, .. orderBy]; + } + else if (orderBy.Count == 0) + { + return query; + } + + foreach (var item in orderBy) + { + var expression = MapOrderByField(item.OrderBy, filter); + if (item.SortOrder == SortOrder.Ascending) + { + query = query.OrderBy(expression); + } + else + { + query = query.OrderByDescending(expression); + } + } + + return query; + } } diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs b/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs new file mode 100644 index 000000000..e609cdc1e --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Initializes a new instance of the class. +/// +/// +/// +/// +public class MediaStreamManager(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) +{ + /// + public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) + { + using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.MediaStreamInfos.AddRange(streams.Select(f => Map(f, id))); + context.SaveChanges(); + + transaction.Commit(); + } + + /// + public IReadOnlyList GetMediaStreams(MediaStreamQuery filter) + { + using var context = dbProvider.CreateDbContext(); + return TranslateQuery(context.MediaStreamInfos, filter).ToList().Select(Map).ToImmutableArray(); + } + + private string? GetPathToSave(string? path) + { + if (path is null) + { + return null; + } + + return serverApplicationHost.ReverseVirtualPath(path); + } + + private string? RestorePath(string? path) + { + if (path is null) + { + return null; + } + + return serverApplicationHost.ExpandVirtualPath(path); + } + + private IQueryable TranslateQuery(IQueryable query, MediaStreamQuery filter) + { + query = query.Where(e => e.ItemId.Equals(filter.ItemId)); + if (filter.Index.HasValue) + { + query = query.Where(e => e.StreamIndex == filter.Index); + } + + if (filter.Type.HasValue) + { + query = query.Where(e => e.StreamType == filter.Type.ToString()); + } + + return query; + } + + private MediaStream Map(MediaStreamInfo entity) + { + var dto = new MediaStream(); + dto.Index = entity.StreamIndex; + if (entity.StreamType != null) + { + dto.Type = Enum.Parse(entity.StreamType); + } + + dto.IsAVC = entity.IsAvc; + dto.Codec = entity.Codec; + dto.Language = entity.Language; + dto.ChannelLayout = entity.ChannelLayout; + dto.Profile = entity.Profile; + dto.AspectRatio = entity.AspectRatio; + dto.Path = RestorePath(entity.Path); + dto.IsInterlaced = entity.IsInterlaced; + dto.BitRate = entity.BitRate; + dto.Channels = entity.Channels; + dto.SampleRate = entity.SampleRate; + dto.IsDefault = entity.IsDefault; + dto.IsForced = entity.IsForced; + dto.IsExternal = entity.IsExternal; + dto.Height = entity.Height; + dto.Width = entity.Width; + dto.AverageFrameRate = entity.AverageFrameRate; + dto.RealFrameRate = entity.RealFrameRate; + dto.Level = entity.Level; + dto.PixelFormat = entity.PixelFormat; + dto.BitDepth = entity.BitDepth; + dto.IsAnamorphic = entity.IsAnamorphic; + dto.RefFrames = entity.RefFrames; + dto.CodecTag = entity.CodecTag; + dto.Comment = entity.Comment; + dto.NalLengthSize = entity.NalLengthSize; + dto.Title = entity.Title; + dto.TimeBase = entity.TimeBase; + dto.CodecTimeBase = entity.CodecTimeBase; + dto.ColorPrimaries = entity.ColorPrimaries; + dto.ColorSpace = entity.ColorSpace; + dto.ColorTransfer = entity.ColorTransfer; + dto.DvVersionMajor = entity.DvVersionMajor; + dto.DvVersionMinor = entity.DvVersionMinor; + dto.DvProfile = entity.DvProfile; + dto.DvLevel = entity.DvLevel; + dto.RpuPresentFlag = entity.RpuPresentFlag; + dto.ElPresentFlag = entity.ElPresentFlag; + dto.BlPresentFlag = entity.BlPresentFlag; + dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId; + dto.IsHearingImpaired = entity.IsHearingImpaired; + dto.Rotation = entity.Rotation; + + if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) + { + dto.LocalizedDefault = localization.GetLocalizedString("Default"); + dto.LocalizedExternal = localization.GetLocalizedString("External"); + + if (dto.Type is MediaStreamType.Subtitle) + { + dto.LocalizedUndefined = localization.GetLocalizedString("Undefined"); + dto.LocalizedForced = localization.GetLocalizedString("Forced"); + dto.LocalizedHearingImpaired = localization.GetLocalizedString("HearingImpaired"); + } + } + + return dto; + } + + private MediaStreamInfo Map(MediaStream dto, Guid itemId) + { + var entity = new MediaStreamInfo + { + Item = null!, + ItemId = itemId, + StreamIndex = dto.Index, + StreamType = dto.Type.ToString(), + IsAvc = dto.IsAVC.GetValueOrDefault(), + + Codec = dto.Codec, + Language = dto.Language, + ChannelLayout = dto.ChannelLayout, + Profile = dto.Profile, + AspectRatio = dto.AspectRatio, + Path = GetPathToSave(dto.Path), + IsInterlaced = dto.IsInterlaced, + BitRate = dto.BitRate.GetValueOrDefault(0), + Channels = dto.Channels.GetValueOrDefault(0), + SampleRate = dto.SampleRate.GetValueOrDefault(0), + IsDefault = dto.IsDefault, + IsForced = dto.IsForced, + IsExternal = dto.IsExternal, + Height = dto.Height.GetValueOrDefault(0), + Width = dto.Width.GetValueOrDefault(0), + AverageFrameRate = dto.AverageFrameRate.GetValueOrDefault(0), + RealFrameRate = dto.RealFrameRate.GetValueOrDefault(0), + Level = (float)dto.Level.GetValueOrDefault(), + PixelFormat = dto.PixelFormat, + BitDepth = dto.BitDepth.GetValueOrDefault(0), + IsAnamorphic = dto.IsAnamorphic.GetValueOrDefault(0), + RefFrames = dto.RefFrames.GetValueOrDefault(0), + CodecTag = dto.CodecTag, + Comment = dto.Comment, + NalLengthSize = dto.NalLengthSize, + Title = dto.Title, + TimeBase = dto.TimeBase, + CodecTimeBase = dto.CodecTimeBase, + ColorPrimaries = dto.ColorPrimaries, + ColorSpace = dto.ColorSpace, + ColorTransfer = dto.ColorTransfer, + DvVersionMajor = dto.DvVersionMajor.GetValueOrDefault(0), + DvVersionMinor = dto.DvVersionMinor.GetValueOrDefault(0), + DvProfile = dto.DvProfile.GetValueOrDefault(0), + DvLevel = dto.DvLevel.GetValueOrDefault(0), + RpuPresentFlag = dto.RpuPresentFlag.GetValueOrDefault(0), + ElPresentFlag = dto.ElPresentFlag.GetValueOrDefault(0), + BlPresentFlag = dto.BlPresentFlag.GetValueOrDefault(0), + DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId.GetValueOrDefault(0), + IsHearingImpaired = dto.IsHearingImpaired, + Rotation = dto.Rotation.GetValueOrDefault(0) + }; + return entity; + } +} diff --git a/Jellyfin.Server.Implementations/Item/PeopleManager.cs b/Jellyfin.Server.Implementations/Item/PeopleManager.cs new file mode 100644 index 000000000..0f1760cbd --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/PeopleManager.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +public class PeopleManager +{ + private readonly IDbContextFactory _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The EFCore Context factory. + public PeopleManager(IDbContextFactory dbProvider) + { + _dbProvider = dbProvider; + } + + public IReadOnlyList GetPeople(InternalPeopleQuery filter) + { + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); + + dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.Limit > 0) + { + dbQuery = dbQuery.Take(filter.Limit); + } + + return dbQuery.ToList().Select(Map).ToImmutableArray(); + } + + public IReadOnlyList GetPeopleNames(InternalPeopleQuery filter) + { + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); + + dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.Limit > 0) + { + dbQuery = dbQuery.Take(filter.Limit); + } + + return dbQuery.Select(e => e.Name).ToImmutableArray(); + } + + /// + public void UpdatePeople(Guid itemId, IReadOnlyList people) + { + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.Peoples.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + context.Peoples.AddRange(people.Select(Map)); + context.SaveChanges(); + transaction.Commit(); + } + + private PersonInfo Map(People people) + { + var personInfo = new PersonInfo() + { + ItemId = people.ItemId, + Name = people.Name, + Role = people.Role, + SortOrder = people.SortOrder, + }; + if (Enum.TryParse(people.PersonType, out var kind)) + { + personInfo.Type = kind; + } + + return personInfo; + } + + private People Map(PersonInfo people) + { + var personInfo = new People() + { + ItemId = people.ItemId, + Name = people.Name, + Role = people.Role, + SortOrder = people.SortOrder, + PersonType = people.Type.ToString() + }; + + return personInfo; + } + + private IQueryable TranslateQuery(IQueryable query, JellyfinDbContext context, InternalPeopleQuery filter) + { + if (filter.User is not null && filter.IsFavorite.HasValue) + { + query = query.Where(e => e.PersonType == typeof(Person).FullName) + .Where(e => context.BaseItems.Where(d => context.UserData.Where(e => e.IsFavorite == filter.IsFavorite && e.UserId.Equals(filter.User.Id)).Any(f => f.Key == d.UserDataKey)) + .Select(f => f.Name).Contains(e.Name)); + } + + if (!filter.ItemId.IsEmpty()) + { + query = query.Where(e => e.ItemId.Equals(filter.ItemId)); + } + + if (!filter.AppearsInItemId.IsEmpty()) + { + query = query.Where(e => context.Peoples.Where(f => f.ItemId.Equals(filter.AppearsInItemId)).Select(e => e.Name).Contains(e.Name)); + } + + var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList(); + if (queryPersonTypes.Count > 0) + { + query = query.Where(e => queryPersonTypes.Contains(e.PersonType)); + } + + var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList(); + + if (queryExcludePersonTypes.Count > 0) + { + query = query.Where(e => !queryPersonTypes.Contains(e.PersonType)); + } + + if (filter.MaxListOrder.HasValue) + { + query = query.Where(e => e.ListOrder <= filter.MaxListOrder.Value); + } + + if (!string.IsNullOrWhiteSpace(filter.NameContains)) + { + query = query.Where(e => e.Name.Contains(filter.NameContains)); + } + + return query; + } + + private bool IsAlphaNumeric(string str) + { + if (string.IsNullOrWhiteSpace(str)) + { + return false; + } + + for (int i = 0; i < str.Length; i++) + { + if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) + { + return false; + } + } + + return true; + } + + private bool IsValidPersonType(string value) + { + return IsAlphaNumeric(value); + } +} -- cgit v1.2.3 From 15bf43e3adc69fc0ec5413e81a20b1f0d5dccd5c Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:53:26 +0000 Subject: Removed BaseSqliteRepository --- .../Data/BaseSqliteRepository.cs | 269 --------- .../Data/ManagedConnection.cs | 62 --- .../Data/SqliteItemRepository.cs | 461 ---------------- .../Data/SynchronousMode.cs | 30 - Emby.Server.Implementations/Data/TempStoreMode.cs | 23 - Jellyfin.Data/Entities/AttachmentStreamInfo.cs | 2 + .../Item/BaseItemManager.cs | 604 ++++++++++++--------- .../Item/MediaAttachmentManager.cs | 73 +++ .../Item/MediaStreamManager.cs | 2 +- .../Item/PeopleManager.cs | 22 +- .../Persistence/IItemRepository.cs | 200 +++---- .../Persistence/IMediaAttachmentManager.cs | 29 + .../Persistence/IMediaStreamManager.cs | 28 + .../Persistence/IPeopleManager.cs | 34 ++ 14 files changed, 591 insertions(+), 1248 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/BaseSqliteRepository.cs delete mode 100644 Emby.Server.Implementations/Data/ManagedConnection.cs delete mode 100644 Emby.Server.Implementations/Data/SqliteItemRepository.cs delete mode 100644 Emby.Server.Implementations/Data/SynchronousMode.cs delete mode 100644 Emby.Server.Implementations/Data/TempStoreMode.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaStreamManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IPeopleManager.cs (limited to 'Emby.Server.Implementations/Data/SqliteItemRepository.cs') diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs deleted file mode 100644 index 8ed72c208..000000000 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ /dev/null @@ -1,269 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Threading; -using Jellyfin.Extensions; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Data -{ - public abstract class BaseSqliteRepository : IDisposable - { - private bool _disposed = false; - private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); - private SqliteConnection _writeConnection; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - protected BaseSqliteRepository(ILogger logger) - { - Logger = logger; - } - - /// - /// Gets or sets the path to the DB file. - /// - protected string DbFilePath { get; set; } - - /// - /// Gets the logger. - /// - /// The logger. - protected ILogger Logger { get; } - - /// - /// Gets the cache size. - /// - /// The cache size or null. - protected virtual int? CacheSize => null; - - /// - /// Gets the locking mode. . - /// - protected virtual string LockingMode => "NORMAL"; - - /// - /// Gets the journal mode. . - /// - /// The journal mode. - protected virtual string JournalMode => "WAL"; - - /// - /// Gets the journal size limit. . - /// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users. - /// - /// The journal size limit. - protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB - - /// - /// Gets the page size. - /// - /// The page size or null. - protected virtual int? PageSize => null; - - /// - /// Gets the temp store mode. - /// - /// The temp store mode. - /// - protected virtual TempStoreMode TempStore => TempStoreMode.Memory; - - /// - /// Gets the synchronous mode. - /// - /// The synchronous mode or null. - /// - protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal; - - public virtual void Initialize() - { - // Configuration and pragmas can affect VACUUM so it needs to be last. - using (var connection = GetConnection()) - { - connection.Execute("VACUUM"); - } - } - - protected ManagedConnection GetConnection(bool readOnly = false) - { - if (!readOnly) - { - _writeLock.Wait(); - if (_writeConnection is not null) - { - return new ManagedConnection(_writeConnection, _writeLock); - } - - var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False"); - writeConnection.Open(); - - if (CacheSize.HasValue) - { - writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); - } - - if (!string.IsNullOrWhiteSpace(LockingMode)) - { - writeConnection.Execute("PRAGMA locking_mode=" + LockingMode); - } - - if (!string.IsNullOrWhiteSpace(JournalMode)) - { - writeConnection.Execute("PRAGMA journal_mode=" + JournalMode); - } - - if (JournalSizeLimit.HasValue) - { - writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); - } - - if (Synchronous.HasValue) - { - writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); - } - - if (PageSize.HasValue) - { - writeConnection.Execute("PRAGMA page_size=" + PageSize.Value); - } - - writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - - return new ManagedConnection(_writeConnection = writeConnection, _writeLock); - } - - var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly"); - connection.Open(); - - if (CacheSize.HasValue) - { - connection.Execute("PRAGMA cache_size=" + CacheSize.Value); - } - - if (!string.IsNullOrWhiteSpace(LockingMode)) - { - connection.Execute("PRAGMA locking_mode=" + LockingMode); - } - - if (!string.IsNullOrWhiteSpace(JournalMode)) - { - connection.Execute("PRAGMA journal_mode=" + JournalMode); - } - - if (JournalSizeLimit.HasValue) - { - connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value); - } - - if (Synchronous.HasValue) - { - connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); - } - - if (PageSize.HasValue) - { - connection.Execute("PRAGMA page_size=" + PageSize.Value); - } - - connection.Execute("PRAGMA temp_store=" + (int)TempStore); - - return new ManagedConnection(connection, null); - } - - public SqliteCommand PrepareStatement(ManagedConnection connection, string sql) - { - var command = connection.CreateCommand(); - command.CommandText = sql; - return command; - } - - protected bool TableExists(ManagedConnection connection, string name) - { - using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master"); - foreach (var row in statement.ExecuteQuery()) - { - if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - protected List GetColumnNames(ManagedConnection connection, string table) - { - var columnNames = new List(); - - foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) - { - if (row.TryGetString(1, out var columnName)) - { - columnNames.Add(columnName); - } - } - - return columnNames; - } - - protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List existingColumnNames) - { - if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL"); - } - - protected void CheckDisposed() - { - ObjectDisposedException.ThrowIf(_disposed, this); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (_disposed) - { - return; - } - - if (dispose) - { - _writeLock.Wait(); - try - { - _writeConnection.Dispose(); - } - finally - { - _writeLock.Release(); - } - - _writeLock.Dispose(); - } - - _writeConnection = null; - _writeLock = null; - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs deleted file mode 100644 index 860950b30..000000000 --- a/Emby.Server.Implementations/Data/ManagedConnection.cs +++ /dev/null @@ -1,62 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Threading; -using Microsoft.Data.Sqlite; - -namespace Emby.Server.Implementations.Data; - -public sealed class ManagedConnection : IDisposable -{ - private readonly SemaphoreSlim? _writeLock; - - private SqliteConnection _db; - - private bool _disposed = false; - - public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock) - { - _db = db; - _writeLock = writeLock; - } - - public SqliteTransaction BeginTransaction() - => _db.BeginTransaction(); - - public SqliteCommand CreateCommand() - => _db.CreateCommand(); - - public void Execute(string commandText) - => _db.Execute(commandText); - - public SqliteCommand PrepareStatement(string sql) - => _db.PrepareStatement(sql); - - public IEnumerable Query(string commandText) - => _db.Query(commandText); - - public void Dispose() - { - if (_disposed) - { - return; - } - - if (_writeLock is null) - { - // Read connections are managed with an internal pool - _db.Dispose(); - } - else - { - // Write lock is managed by BaseSqliteRepository - // Don't dispose here - _writeLock.Release(); - } - - _db = null!; - - _disposed = true; - } -} diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs deleted file mode 100644 index a650f9555..000000000 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ /dev/null @@ -1,461 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using Emby.Server.Implementations.Playlists; -using Jellyfin.Data.Enums; -using Jellyfin.Extensions; -using Jellyfin.Extensions.Json; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Playlists; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Querying; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Data -{ - /// - /// Class SQLiteItemRepository. - /// - public class SqliteItemRepository : BaseSqliteRepository, IItemRepository - { - private const string FromText = " from TypedBaseItems A"; - private const string ChaptersTableName = "Chapters2"; - - private const string SaveItemCommandText = - @"replace into TypedBaseItems - (guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId) - values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)"; - - private readonly IServerConfigurationManager _config; - private readonly IServerApplicationHost _appHost; - private readonly ILocalizationManager _localization; - // TODO: Remove this dependency. GetImageCacheTag() is the only method used and it can be converted to a static helper method - private readonly IImageProcessor _imageProcessor; - - private readonly TypeMapper _typeMapper; - private readonly JsonSerializerOptions _jsonOptions; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// config is null. - public SqliteItemRepository( - IServerConfigurationManager config, - IServerApplicationHost appHost, - ILogger logger, - ILocalizationManager localization, - IImageProcessor imageProcessor, - IConfiguration configuration) - : base(logger) - { - _config = config; - _appHost = appHost; - _localization = localization; - _imageProcessor = imageProcessor; - - _typeMapper = new TypeMapper(); - _jsonOptions = JsonDefaults.Options; - - DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); - - CacheSize = configuration.GetSqliteCacheSize(); - } - - /// - protected override int? CacheSize { get; } - - /// - protected override TempStoreMode TempStore => TempStoreMode.Memory; - - - private bool TypeRequiresDeserialization(Type type) - { - if (_config.Configuration.SkipDeserializationForBasicTypes) - { - if (type == typeof(Channel) - || type == typeof(UserRootFolder)) - { - return false; - } - } - - return type != typeof(Season) - && type != typeof(MusicArtist) - && type != typeof(Person) - && type != typeof(MusicGenre) - && type != typeof(Genre) - && type != typeof(Studio) - && type != typeof(PlaylistsFolder) - && type != typeof(PhotoAlbum) - && type != typeof(Year) - && type != typeof(Book) - && type != typeof(LiveTvProgram) - && type != typeof(AudioBook) - && type != typeof(MusicAlbum); - } - - private static bool EnableJoinUserData(InternalItemsQuery query) - { - if (query.User is null) - { - return false; - } - - var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy)); - - return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked) - || sortingFields.Contains(ItemSortBy.IsPlayed) - || sortingFields.Contains(ItemSortBy.IsUnplayed) - || sortingFields.Contains(ItemSortBy.PlayCount) - || sortingFields.Contains(ItemSortBy.DatePlayed) - || sortingFields.Contains(ItemSortBy.SeriesDatePlayed) - || query.IsFavoriteOrLiked.HasValue - || query.IsFavorite.HasValue - || query.IsResumable.HasValue - || query.IsPlayed.HasValue - || query.IsLiked.HasValue; - } - - private string GetJoinUserDataText(InternalItemsQuery query) - { - if (!EnableJoinUserData(query)) - { - return string.Empty; - } - - return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)"; - } - - /// - public List GetStudioNames() - { - return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); - } - - /// - public List GetAllArtistNames() - { - return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); - } - - /// - public List GetMusicGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - new string[] - { - typeof(Audio).FullName, - typeof(MusicVideo).FullName, - typeof(MusicAlbum).FullName, - typeof(MusicArtist).FullName - }, - Array.Empty()); - } - - /// - public List GetGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - Array.Empty(), - new string[] - { - typeof(Audio).FullName, - typeof(MusicVideo).FullName, - typeof(MusicAlbum).FullName, - typeof(MusicArtist).FullName - }); - } - - private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) - { - CheckDisposed(); - - var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); - if (itemValueTypes.Length == 1) - { - stringBuilder.Append('=') - .Append(itemValueTypes[0]); - } - else - { - stringBuilder.Append(" in (") - .AppendJoin(',', itemValueTypes) - .Append(')'); - } - - if (withItemTypes.Count > 0) - { - stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (") - .AppendJoinInSingleQuotes(',', withItemTypes) - .Append("))"); - } - - if (excludeItemTypes.Count > 0) - { - stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (") - .AppendJoinInSingleQuotes(',', excludeItemTypes) - .Append("))"); - } - - stringBuilder.Append(" Group By CleanValue"); - var commandText = stringBuilder.ToString(); - - var list = new List(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - foreach (var row in statement.ExecuteQuery()) - { - if (row.TryGetString(0, out var result)) - { - list.Add(result); - } - } - } - - return list; - } - - - - /// - public List GetMediaAttachments(MediaAttachmentQuery query) - { - CheckDisposed(); - - ArgumentNullException.ThrowIfNull(query); - - var cmdText = _mediaAttachmentSaveColumnsSelectQuery; - - if (query.Index.HasValue) - { - cmdText += " AND AttachmentIndex=@AttachmentIndex"; - } - - cmdText += " order by AttachmentIndex ASC"; - - var list = new List(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, cmdText)) - { - statement.TryBind("@ItemId", query.ItemId); - - if (query.Index.HasValue) - { - statement.TryBind("@AttachmentIndex", query.Index.Value); - } - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetMediaAttachment(row)); - } - } - - return list; - } - - /// - public void SaveMediaAttachments( - Guid id, - IReadOnlyList attachments, - CancellationToken cancellationToken) - { - CheckDisposed(); - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty.", nameof(id)); - } - - ArgumentNullException.ThrowIfNull(attachments); - - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId")) - { - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); - - InsertMediaAttachments(id, attachments, connection, cancellationToken); - - transaction.Commit(); - } - } - - private void InsertMediaAttachments( - Guid id, - IReadOnlyList attachments, - ManagedConnection db, - CancellationToken cancellationToken) - { - const int InsertAtOnce = 10; - - var insertText = new StringBuilder(_mediaAttachmentInsertPrefix); - for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce) - { - var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce); - - for (var i = startIndex; i < endIndex; i++) - { - insertText.Append("(@ItemId, "); - - foreach (var column in _mediaAttachmentSaveColumns.Skip(1)) - { - insertText.Append('@') - .Append(column) - .Append(i) - .Append(','); - } - - insertText.Length -= 1; - - insertText.Append("),"); - } - - insertText.Length--; - - cancellationToken.ThrowIfCancellationRequested(); - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", id); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var attachment = attachments[i]; - - statement.TryBind("@AttachmentIndex" + index, attachment.Index); - statement.TryBind("@Codec" + index, attachment.Codec); - statement.TryBind("@CodecTag" + index, attachment.CodecTag); - statement.TryBind("@Comment" + index, attachment.Comment); - statement.TryBind("@Filename" + index, attachment.FileName); - statement.TryBind("@MIMEType" + index, attachment.MimeType); - } - - statement.ExecuteNonQuery(); - } - - insertText.Length = _mediaAttachmentInsertPrefix.Length; - } - } - - /// - /// Gets the attachment. - /// - /// The reader. - /// MediaAttachment. - private MediaAttachment GetMediaAttachment(SqliteDataReader reader) - { - var item = new MediaAttachment - { - Index = reader.GetInt32(1) - }; - - if (reader.TryGetString(2, out var codec)) - { - item.Codec = codec; - } - - if (reader.TryGetString(3, out var codecTag)) - { - item.CodecTag = codecTag; - } - - if (reader.TryGetString(4, out var comment)) - { - item.Comment = comment; - } - - if (reader.TryGetString(5, out var fileName)) - { - item.FileName = fileName; - } - - if (reader.TryGetString(6, out var mimeType)) - { - item.MimeType = mimeType; - } - - return item; - } - -#nullable enable - - private readonly struct QueryTimeLogger : IDisposable - { - private readonly ILogger _logger; - private readonly string _commandText; - private readonly string _methodName; - private readonly long _startTimestamp; - - public QueryTimeLogger(ILogger logger, string commandText, [CallerMemberName] string methodName = "") - { - _logger = logger; - _commandText = commandText; - _methodName = methodName; - _startTimestamp = logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : -1; - } - - public void Dispose() - { - if (_startTimestamp == -1) - { - return; - } - - var elapsedMs = Stopwatch.GetElapsedTime(_startTimestamp).TotalMilliseconds; - -#if DEBUG - const int SlowThreshold = 100; -#else - const int SlowThreshold = 10; -#endif - - if (elapsedMs >= SlowThreshold) - { - _logger.LogDebug( - "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}", - _methodName, - elapsedMs, - _commandText); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Data/SynchronousMode.cs b/Emby.Server.Implementations/Data/SynchronousMode.cs deleted file mode 100644 index cde524e2e..000000000 --- a/Emby.Server.Implementations/Data/SynchronousMode.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Emby.Server.Implementations.Data; - -/// -/// The disk synchronization mode, controls how aggressively SQLite will write data -/// all the way out to physical storage. -/// -public enum SynchronousMode -{ - /// - /// SQLite continues without syncing as soon as it has handed data off to the operating system. - /// - Off = 0, - - /// - /// SQLite database engine will still sync at the most critical moments. - /// - Normal = 1, - - /// - /// SQLite database engine will use the xSync method of the VFS - /// to ensure that all content is safely written to the disk surface prior to continuing. - /// - Full = 2, - - /// - /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal - /// is synced after that journal is unlinked to commit a transaction in DELETE mode. - /// - Extra = 3 -} diff --git a/Emby.Server.Implementations/Data/TempStoreMode.cs b/Emby.Server.Implementations/Data/TempStoreMode.cs deleted file mode 100644 index d2427ce47..000000000 --- a/Emby.Server.Implementations/Data/TempStoreMode.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Emby.Server.Implementations.Data; - -/// -/// Storage mode used by temporary database files. -/// -public enum TempStoreMode -{ - /// - /// The compile-time C preprocessor macro SQLITE_TEMP_STORE - /// is used to determine where temporary tables and indices are stored. - /// - Default = 0, - - /// - /// Temporary tables and indices are stored in a file. - /// - File = 1, - - /// - /// Temporary tables and indices are kept in as if they were pure in-memory databases memory. - /// - Memory = 2 -} diff --git a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs index d2483548b..858465424 100644 --- a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs +++ b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs @@ -7,6 +7,8 @@ public class AttachmentStreamInfo { public required Guid ItemId { get; set; } + public required BaseItem Item { get; set; } + public required int Index { get; set; } public required string Codec { get; set; } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index 022f26cd7..66cc765f3 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -34,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Item; /// /// Handles all storage logic for BaseItems. /// -public class BaseItemManager : IItemRepository +public sealed class BaseItemManager : IItemRepository, IDisposable { private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationHost _appHost; @@ -135,6 +135,7 @@ public class BaseItemManager : IItemRepository /// so that we can de-serialize properly when we don't have strong types. /// private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); + private bool _disposed; /// /// Initializes a new instance of the class. @@ -147,6 +148,17 @@ public class BaseItemManager : IItemRepository _appHost = appHost; } + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + } + private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType) { ArgumentNullException.ThrowIfNull(filter); @@ -338,106 +350,148 @@ public class BaseItemManager : IItemRepository } /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, new[] { 0, 1 }, typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, new[] { 0 }, typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, new[] { 1 }, typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter) { - return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); + return GetItemValues(filter, new[] { 3 }, typeof(Studio).FullName!); } /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) { - return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); + return GetItemValues(filter, new[] { 2 }, typeof(Genre).FullName!); } /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) { - return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); + return GetItemValues(filter, new[] { 2 }, typeof(MusicGenre).FullName!); } /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) + public IReadOnlyList GetStudioNames() { - return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); + return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); } /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) + public IReadOnlyList GetAllArtistNames() { - return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); + return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); } /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) + public IReadOnlyList GetMusicGenreNames() { - return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); + return GetItemValueNames( + new[] { 2 }, + new string[] + { + typeof(Audio).FullName!, + typeof(MusicVideo).FullName!, + typeof(MusicAlbum).FullName!, + typeof(MusicArtist).FullName! + }, + Array.Empty()); + } + + /// + public IReadOnlyList GetGenreNames() + { + return GetItemValueNames( + new[] { 2 }, + Array.Empty(), + new string[] + { + typeof(Audio).FullName!, + typeof(MusicVideo).FullName!, + typeof(MusicAlbum).FullName!, + typeof(MusicArtist).FullName! + }); } /// - public QueryResult GetItems(InternalItemsQuery query) + public QueryResult GetItems(InternalItemsQuery filter) { - ArgumentNullException.ThrowIfNull(query); - if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) + ArgumentNullException.ThrowIfNull(filter); + if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0)) { - var returnList = GetItemList(query); + var returnList = GetItemList(filter); return new QueryResult( - query.StartIndex, + filter.StartIndex, returnList.Count, returnList); } - PrepareFilterQuery(query); + PrepareFilterQuery(filter); var result = new QueryResult(); using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, query) + var dbQuery = TranslateQuery(context.BaseItems, context, filter) .DistinctBy(e => e.Id); - if (query.EnableTotalRecordCount) + if (filter.EnableTotalRecordCount) { result.TotalRecordCount = dbQuery.Count(); } - if (query.Limit.HasValue || query.StartIndex.HasValue) + if (filter.Limit.HasValue || filter.StartIndex.HasValue) { - var offset = query.StartIndex ?? 0; + var offset = filter.StartIndex ?? 0; if (offset > 0) { dbQuery = dbQuery.Skip(offset); } - if (query.Limit.HasValue) + if (filter.Limit.HasValue) { - dbQuery = dbQuery.Take(query.Limit.Value); + dbQuery = dbQuery.Take(filter.Limit.Value); } } result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); - result.StartIndex = query.StartIndex ?? 0; + result.StartIndex = filter.StartIndex ?? 0; return result; } /// - public IReadOnlyList GetItemList(InternalItemsQuery query) + public IReadOnlyList GetItemList(InternalItemsQuery filter) { - ArgumentNullException.ThrowIfNull(query); - PrepareFilterQuery(query); + ArgumentNullException.ThrowIfNull(filter); + PrepareFilterQuery(filter); using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, query) + var dbQuery = TranslateQuery(context.BaseItems, context, filter) .DistinctBy(e => e.Id); - if (query.Limit.HasValue || query.StartIndex.HasValue) + if (filter.Limit.HasValue || filter.StartIndex.HasValue) { - var offset = query.StartIndex ?? 0; + var offset = filter.StartIndex ?? 0; if (offset > 0) { dbQuery = dbQuery.Skip(offset); } - if (query.Limit.HasValue) + if (filter.Limit.HasValue) { - dbQuery = dbQuery.Take(query.Limit.Value); + dbQuery = dbQuery.Take(filter.Limit.Value); } } @@ -445,14 +499,14 @@ public class BaseItemManager : IItemRepository } /// - public int GetCount(InternalItemsQuery query) + public int GetCount(InternalItemsQuery filter) { - ArgumentNullException.ThrowIfNull(query); + ArgumentNullException.ThrowIfNull(filter); // Hack for right now since we currently don't support filtering out these duplicates within a query - PrepareFilterQuery(query); + PrepareFilterQuery(filter); using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, query); + var dbQuery = TranslateQuery(context.BaseItems, context, filter); return dbQuery.Count(); } @@ -460,16 +514,16 @@ public class BaseItemManager : IItemRepository private IQueryable TranslateQuery( IQueryable baseQuery, JellyfinDbContext context, - InternalItemsQuery query) + InternalItemsQuery filter) { - var minWidth = query.MinWidth; - var maxWidth = query.MaxWidth; + var minWidth = filter.MinWidth; + var maxWidth = filter.MaxWidth; var now = DateTime.UtcNow; - if (query.IsHD.HasValue) + if (filter.IsHD.HasValue) { const int Threshold = 1200; - if (query.IsHD.Value) + if (filter.IsHD.Value) { minWidth = Threshold; } @@ -479,10 +533,10 @@ public class BaseItemManager : IItemRepository } } - if (query.Is4K.HasValue) + if (filter.Is4K.HasValue) { const int Threshold = 3800; - if (query.Is4K.Value) + if (filter.Is4K.Value) { minWidth = Threshold; } @@ -497,9 +551,9 @@ public class BaseItemManager : IItemRepository baseQuery = baseQuery.Where(e => e.Width >= minWidth); } - if (query.MinHeight.HasValue) + if (filter.MinHeight.HasValue) { - baseQuery = baseQuery.Where(e => e.Height >= query.MinHeight); + baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight); } if (maxWidth.HasValue) @@ -507,41 +561,41 @@ public class BaseItemManager : IItemRepository baseQuery = baseQuery.Where(e => e.Width >= maxWidth); } - if (query.MaxHeight.HasValue) + if (filter.MaxHeight.HasValue) { - baseQuery = baseQuery.Where(e => e.Height <= query.MaxHeight); + baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight); } - if (query.IsLocked.HasValue) + if (filter.IsLocked.HasValue) { - baseQuery = baseQuery.Where(e => e.IsLocked == query.IsLocked); + baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked); } - var tags = query.Tags.ToList(); - var excludeTags = query.ExcludeTags.ToList(); + var tags = filter.Tags.ToList(); + var excludeTags = filter.ExcludeTags.ToList(); - if (query.IsMovie == true) + if (filter.IsMovie == true) { - if (query.IncludeItemTypes.Length == 0 - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + if (filter.IncludeItemTypes.Length == 0 + || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) + || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) { baseQuery = baseQuery.Where(e => e.IsMovie); } } - else if (query.IsMovie.HasValue) + else if (filter.IsMovie.HasValue) { - baseQuery = baseQuery.Where(e => e.IsMovie == query.IsMovie); + baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); } - if (query.IsSeries.HasValue) + if (filter.IsSeries.HasValue) { - baseQuery = baseQuery.Where(e => e.IsSeries == query.IsSeries); + baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries); } - if (query.IsSports.HasValue) + if (filter.IsSports.HasValue) { - if (query.IsSports.Value) + if (filter.IsSports.Value) { tags.Add("Sports"); } @@ -551,9 +605,9 @@ public class BaseItemManager : IItemRepository } } - if (query.IsNews.HasValue) + if (filter.IsNews.HasValue) { - if (query.IsNews.Value) + if (filter.IsNews.Value) { tags.Add("News"); } @@ -563,9 +617,9 @@ public class BaseItemManager : IItemRepository } } - if (query.IsKids.HasValue) + if (filter.IsKids.HasValue) { - if (query.IsKids.Value) + if (filter.IsKids.Value) { tags.Add("Kids"); } @@ -575,21 +629,21 @@ public class BaseItemManager : IItemRepository } } - if (!string.IsNullOrEmpty(query.SearchTerm)) + if (!string.IsNullOrEmpty(filter.SearchTerm)) { - baseQuery = baseQuery.Where(e => e.CleanName!.Contains(query.SearchTerm, StringComparison.InvariantCultureIgnoreCase) || (e.OriginalTitle != null && e.OriginalTitle.Contains(query.SearchTerm, StringComparison.InvariantCultureIgnoreCase))); + baseQuery = baseQuery.Where(e => e.CleanName!.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase) || (e.OriginalTitle != null && e.OriginalTitle.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase))); } - if (query.IsFolder.HasValue) + if (filter.IsFolder.HasValue) { - baseQuery = baseQuery.Where(e => e.IsFolder == query.IsFolder); + baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder); } - var includeTypes = query.IncludeItemTypes; + var includeTypes = filter.IncludeItemTypes; // Only specify excluded types if no included types are specified - if (query.IncludeItemTypes.Length == 0) + if (filter.IncludeItemTypes.Length == 0) { - var excludeTypes = query.ExcludeItemTypes; + var excludeTypes = filter.ExcludeItemTypes; if (excludeTypes.Length == 1) { if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) @@ -632,82 +686,82 @@ public class BaseItemManager : IItemRepository baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); } - if (query.ChannelIds.Count == 1) + if (filter.ChannelIds.Count == 1) { - baseQuery = baseQuery.Where(e => e.ChannelId == query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); + baseQuery = baseQuery.Where(e => e.ChannelId == filter.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); } - else if (query.ChannelIds.Count > 1) + else if (filter.ChannelIds.Count > 1) { - baseQuery = baseQuery.Where(e => query.ChannelIds.Select(f => f.ToString("N", CultureInfo.InvariantCulture)).Contains(e.ChannelId)); + baseQuery = baseQuery.Where(e => filter.ChannelIds.Select(f => f.ToString("N", CultureInfo.InvariantCulture)).Contains(e.ChannelId)); } - if (!query.ParentId.IsEmpty()) + if (!filter.ParentId.IsEmpty()) { - baseQuery = baseQuery.Where(e => e.ParentId.Equals(query.ParentId)); + baseQuery = baseQuery.Where(e => e.ParentId.Equals(filter.ParentId)); } - if (!string.IsNullOrWhiteSpace(query.Path)) + if (!string.IsNullOrWhiteSpace(filter.Path)) { - baseQuery = baseQuery.Where(e => e.Path == query.Path); + baseQuery = baseQuery.Where(e => e.Path == filter.Path); } - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) { - baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == query.PresentationUniqueKey); + baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey); } - if (query.MinCommunityRating.HasValue) + if (filter.MinCommunityRating.HasValue) { - baseQuery = baseQuery.Where(e => e.CommunityRating >= query.MinCommunityRating); + baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating); } - if (query.MinIndexNumber.HasValue) + if (filter.MinIndexNumber.HasValue) { - baseQuery = baseQuery.Where(e => e.IndexNumber >= query.MinIndexNumber); + baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber); } - if (query.MinParentAndIndexNumber.HasValue) + if (filter.MinParentAndIndexNumber.HasValue) { baseQuery = baseQuery - .Where(e => (e.ParentIndexNumber == query.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= query.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > query.MinParentAndIndexNumber.Value.ParentIndexNumber); + .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber); } - if (query.MinDateCreated.HasValue) + if (filter.MinDateCreated.HasValue) { - baseQuery = baseQuery.Where(e => e.DateCreated >= query.MinDateCreated); + baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated); } - if (query.MinDateLastSaved.HasValue) + if (filter.MinDateLastSaved.HasValue) { - baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= query.MinDateLastSaved.Value); + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value); } - if (query.MinDateLastSavedForUser.HasValue) + if (filter.MinDateLastSavedForUser.HasValue) { - baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= query.MinDateLastSavedForUser.Value); + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value); } - if (query.IndexNumber.HasValue) + if (filter.IndexNumber.HasValue) { - baseQuery = baseQuery.Where(e => e.IndexNumber == query.IndexNumber.Value); + baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value); } - if (query.ParentIndexNumber.HasValue) + if (filter.ParentIndexNumber.HasValue) { - baseQuery = baseQuery.Where(e => e.ParentIndexNumber == query.ParentIndexNumber.Value); + baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value); } - if (query.ParentIndexNumberNotEquals.HasValue) + if (filter.ParentIndexNumberNotEquals.HasValue) { - baseQuery = baseQuery.Where(e => e.ParentIndexNumber != query.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); + baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); } - var minEndDate = query.MinEndDate; - var maxEndDate = query.MaxEndDate; + var minEndDate = filter.MinEndDate; + var maxEndDate = filter.MaxEndDate; - if (query.HasAired.HasValue) + if (filter.HasAired.HasValue) { - if (query.HasAired.Value) + if (filter.HasAired.Value) { maxEndDate = DateTime.UtcNow; } @@ -727,34 +781,34 @@ public class BaseItemManager : IItemRepository baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate); } - if (query.MinStartDate.HasValue) + if (filter.MinStartDate.HasValue) { - baseQuery = baseQuery.Where(e => e.StartDate >= query.MinStartDate.Value); + baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value); } - if (query.MaxStartDate.HasValue) + if (filter.MaxStartDate.HasValue) { - baseQuery = baseQuery.Where(e => e.StartDate <= query.MaxStartDate.Value); + baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value); } - if (query.MinPremiereDate.HasValue) + if (filter.MinPremiereDate.HasValue) { - baseQuery = baseQuery.Where(e => e.PremiereDate <= query.MinPremiereDate.Value); + baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value); } - if (query.MaxPremiereDate.HasValue) + if (filter.MaxPremiereDate.HasValue) { - baseQuery = baseQuery.Where(e => e.PremiereDate <= query.MaxPremiereDate.Value); + baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value); } - if (query.TrailerTypes.Length > 0) + if (filter.TrailerTypes.Length > 0) { - baseQuery = baseQuery.Where(e => query.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); + baseQuery = baseQuery.Where(e => filter.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); } - if (query.IsAiring.HasValue) + if (filter.IsAiring.HasValue) { - if (query.IsAiring.Value) + if (filter.IsAiring.Value) { baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now); } @@ -764,20 +818,20 @@ public class BaseItemManager : IItemRepository } } - if (query.PersonIds.Length > 0) + if (filter.PersonIds.Length > 0) { baseQuery = baseQuery .Where(e => - context.Peoples.Where(w => context.BaseItems.Where(w => query.PersonIds.Contains(w.Id)).Any(f => f.Name == w.Name)) + context.Peoples.Where(w => context.BaseItems.Where(w => filter.PersonIds.Contains(w.Id)).Any(f => f.Name == w.Name)) .Any(f => f.ItemId.Equals(e.Id))); } - if (!string.IsNullOrWhiteSpace(query.Person)) + if (!string.IsNullOrWhiteSpace(filter.Person)) { - baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == query.Person)); + baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == filter.Person)); } - if (!string.IsNullOrWhiteSpace(query.MinSortName)) + if (!string.IsNullOrWhiteSpace(filter.MinSortName)) { // this does not makes sense. // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName); @@ -785,132 +839,132 @@ public class BaseItemManager : IItemRepository // statement?.TryBind("@MinSortName", query.MinSortName); } - if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) + if (!string.IsNullOrWhiteSpace(filter.ExternalSeriesId)) { - baseQuery = baseQuery.Where(e => e.ExternalSeriesId == query.ExternalSeriesId); + baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId); } - if (!string.IsNullOrWhiteSpace(query.ExternalId)) + if (!string.IsNullOrWhiteSpace(filter.ExternalId)) { - baseQuery = baseQuery.Where(e => e.ExternalId == query.ExternalId); + baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId); } - if (!string.IsNullOrWhiteSpace(query.Name)) + if (!string.IsNullOrWhiteSpace(filter.Name)) { - var cleanName = GetCleanValue(query.Name); + var cleanName = GetCleanValue(filter.Name); baseQuery = baseQuery.Where(e => e.CleanName == cleanName); } // These are the same, for now - var nameContains = query.NameContains; + var nameContains = filter.NameContains; if (!string.IsNullOrWhiteSpace(nameContains)) { baseQuery = baseQuery.Where(e => - e.CleanName == query.NameContains - || e.OriginalTitle!.Contains(query.NameContains!, StringComparison.Ordinal)); + e.CleanName == filter.NameContains + || e.OriginalTitle!.Contains(filter.NameContains!, StringComparison.Ordinal)); } - if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) + if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) { - baseQuery = baseQuery.Where(e => e.SortName!.Contains(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + baseQuery = baseQuery.Where(e => e.SortName!.Contains(filter.NameStartsWith, StringComparison.OrdinalIgnoreCase)); } - if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) + if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) { // i hate this - baseQuery = baseQuery.Where(e => e.SortName![0] > query.NameStartsWithOrGreater[0]); + baseQuery = baseQuery.Where(e => e.SortName![0] > filter.NameStartsWithOrGreater[0]); } - if (!string.IsNullOrWhiteSpace(query.NameLessThan)) + if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) { // i hate this - baseQuery = baseQuery.Where(e => e.SortName![0] < query.NameLessThan[0]); + baseQuery = baseQuery.Where(e => e.SortName![0] < filter.NameLessThan[0]); } - if (query.ImageTypes.Length > 0) + if (filter.ImageTypes.Length > 0) { - baseQuery = baseQuery.Where(e => query.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); + baseQuery = baseQuery.Where(e => filter.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); } - if (query.IsLiked.HasValue) + if (filter.IsLiked.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); } - if (query.IsFavoriteOrLiked.HasValue) + if (filter.IsFavoriteOrLiked.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == query.IsFavoriteOrLiked); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavoriteOrLiked); } - if (query.IsFavorite.HasValue) + if (filter.IsFavorite.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == query.IsFavorite); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavorite); } - if (query.IsPlayed.HasValue) + if (filter.IsPlayed.HasValue) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played == query.IsPlayed.Value); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Played == filter.IsPlayed.Value); } - if (query.IsResumable.HasValue) + if (filter.IsResumable.HasValue) { - if (query.IsResumable.Value) + if (filter.IsResumable.Value) { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks > 0); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks > 0); } else { baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); } } - var artistQuery = context.BaseItems.Where(w => query.ArtistIds.Contains(w.Id)); + var artistQuery = context.BaseItems.Where(w => filter.ArtistIds.Contains(w.Id)); - if (query.ArtistIds.Length > 0) + if (filter.ArtistIds.Length > 0) { baseQuery = baseQuery .Where(e => e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); } - if (query.AlbumArtistIds.Length > 0) + if (filter.AlbumArtistIds.Length > 0) { baseQuery = baseQuery .Where(e => e.ItemValues!.Any(f => f.Type == 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); } - if (query.ContributingArtistIds.Length > 0) + if (filter.ContributingArtistIds.Length > 0) { - var contributingArtists = context.BaseItems.Where(e => query.ContributingArtistIds.Contains(e.Id)); + var contributingArtists = context.BaseItems.Where(e => filter.ContributingArtistIds.Contains(e.Id)); baseQuery = baseQuery.Where(e => e.ItemValues!.Any(f => f.Type == 0 && contributingArtists.Any(w => w.CleanName == f.CleanValue))); } - if (query.AlbumIds.Length > 0) + if (filter.AlbumIds.Length > 0) { - baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => query.AlbumIds.Contains(e.Id)).Any(f => f.Name == e.Album)); + baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => filter.AlbumIds.Contains(e.Id)).Any(f => f.Name == e.Album)); } - if (query.ExcludeArtistIds.Length > 0) + if (filter.ExcludeArtistIds.Length > 0) { - var excludeArtistQuery = context.BaseItems.Where(w => query.ExcludeArtistIds.Contains(w.Id)); + var excludeArtistQuery = context.BaseItems.Where(w => filter.ExcludeArtistIds.Contains(w.Id)); baseQuery = baseQuery .Where(e => !e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); } - if (query.GenreIds.Count > 0) + if (filter.GenreIds.Count > 0) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 2 && context.BaseItems.Where(w => query.GenreIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.Type == 2 && context.BaseItems.Where(w => filter.GenreIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); } - if (query.Genres.Count > 0) + if (filter.Genres.Count > 0) { - var cleanGenres = query.Genres.Select(e => GetCleanValue(e)).ToArray(); + var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray(); baseQuery = baseQuery .Where(e => e.ItemValues!.Any(f => f.Type == 2 && cleanGenres.Contains(f.CleanValue))); } @@ -929,82 +983,82 @@ public class BaseItemManager : IItemRepository .Where(e => !e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); } - if (query.StudioIds.Length > 0) + if (filter.StudioIds.Length > 0) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 3 && context.BaseItems.Where(w => query.StudioIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.Type == 3 && context.BaseItems.Where(w => filter.StudioIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); } - if (query.OfficialRatings.Length > 0) + if (filter.OfficialRatings.Length > 0) { baseQuery = baseQuery - .Where(e => query.OfficialRatings.Contains(e.OfficialRating)); + .Where(e => filter.OfficialRatings.Contains(e.OfficialRating)); } - if (query.HasParentalRating ?? false) + if (filter.HasParentalRating ?? false) { - if (query.MinParentalRating.HasValue) + if (filter.MinParentalRating.HasValue) { baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue >= query.MinParentalRating.Value); + .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); } - if (query.MaxParentalRating.HasValue) + if (filter.MaxParentalRating.HasValue) { baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue < query.MaxParentalRating.Value); + .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value); } } - else if (query.BlockUnratedItems.Length > 0) + else if (filter.BlockUnratedItems.Length > 0) { - if (query.MinParentalRating.HasValue) + if (filter.MinParentalRating.HasValue) { - if (query.MaxParentalRating.HasValue) + if (filter.MaxParentalRating.HasValue) { baseQuery = baseQuery - .Where(e => (e.InheritedParentalRatingValue == null && !query.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) - || (e.InheritedParentalRatingValue >= query.MinParentalRating && e.InheritedParentalRatingValue <= query.MaxParentalRating)); + .Where(e => (e.InheritedParentalRatingValue == null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) + || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating)); } else { baseQuery = baseQuery - .Where(e => (e.InheritedParentalRatingValue == null && !query.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) - || e.InheritedParentalRatingValue >= query.MinParentalRating); + .Where(e => (e.InheritedParentalRatingValue == null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) + || e.InheritedParentalRatingValue >= filter.MinParentalRating); } } else { baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && !query.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)); + .Where(e => e.InheritedParentalRatingValue != null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)); } } - else if (query.MinParentalRating.HasValue) + else if (filter.MinParentalRating.HasValue) { - if (query.MaxParentalRating.HasValue) + if (filter.MaxParentalRating.HasValue) { baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= query.MinParentalRating.Value && e.InheritedParentalRatingValue <= query.MaxParentalRating.Value); + .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value); } else { baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= query.MinParentalRating.Value); + .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); } } - else if (query.MaxParentalRating.HasValue) + else if (filter.MaxParentalRating.HasValue) { baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= query.MaxParentalRating.Value); + .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value); } - else if (!query.HasParentalRating ?? false) + else if (!filter.HasParentalRating ?? false) { baseQuery = baseQuery .Where(e => e.InheritedParentalRatingValue == null); } - if (query.HasOfficialRating.HasValue) + if (filter.HasOfficialRating.HasValue) { - if (query.HasOfficialRating.Value) + if (filter.HasOfficialRating.Value) { baseQuery = baseQuery .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty); @@ -1016,9 +1070,9 @@ public class BaseItemManager : IItemRepository } } - if (query.HasOverview.HasValue) + if (filter.HasOverview.HasValue) { - if (query.HasOverview.Value) + if (filter.HasOverview.Value) { baseQuery = baseQuery .Where(e => e.Overview != null && e.Overview != string.Empty); @@ -1030,9 +1084,9 @@ public class BaseItemManager : IItemRepository } } - if (query.HasOwnerId.HasValue) + if (filter.HasOwnerId.HasValue) { - if (query.HasOwnerId.Value) + if (filter.HasOwnerId.Value) { baseQuery = baseQuery .Where(e => e.OwnerId != null); @@ -1044,87 +1098,87 @@ public class BaseItemManager : IItemRepository } } - if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) + if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage)) { baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Audio" && e.Language == query.HasNoAudioTrackWithLanguage)); + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Audio" && e.Language == filter.HasNoAudioTrackWithLanguage)); } - if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage)) + if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage)) { baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && !e.IsExternal && e.Language == query.HasNoInternalSubtitleTrackWithLanguage)); + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && !e.IsExternal && e.Language == filter.HasNoInternalSubtitleTrackWithLanguage)); } - if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage)) + if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage)) { baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.IsExternal && e.Language == query.HasNoExternalSubtitleTrackWithLanguage)); + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.IsExternal && e.Language == filter.HasNoExternalSubtitleTrackWithLanguage)); } - if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage)) + if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage)) { baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.Language == query.HasNoSubtitleTrackWithLanguage)); + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.Language == filter.HasNoSubtitleTrackWithLanguage)); } - if (query.HasSubtitles.HasValue) + if (filter.HasSubtitles.HasValue) { baseQuery = baseQuery - .Where(e => e.MediaStreams!.Any(e => e.StreamType == "Subtitle") == query.HasSubtitles.Value); + .Where(e => e.MediaStreams!.Any(e => e.StreamType == "Subtitle") == filter.HasSubtitles.Value); } - if (query.HasChapterImages.HasValue) + if (filter.HasChapterImages.HasValue) { baseQuery = baseQuery - .Where(e => e.Chapters!.Any(e => e.ImagePath != null) == query.HasChapterImages.Value); + .Where(e => e.Chapters!.Any(e => e.ImagePath != null) == filter.HasChapterImages.Value); } - if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value) + if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value) { baseQuery = baseQuery .Where(e => e.ParentId.HasValue && context.BaseItems.Any(f => f.Id.Equals(e.ParentId.Value))); } - if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value) + if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value) { baseQuery = baseQuery .Where(e => e.ItemValues!.Any(f => (f.Type == 0 || f.Type == 1) && f.CleanValue == e.CleanName)); } - if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value) + if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value) { baseQuery = baseQuery .Where(e => e.ItemValues!.Any(f => f.Type == 3 && f.CleanValue == e.CleanName)); } - if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value) + if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) { baseQuery = baseQuery .Where(e => !e.Peoples!.Any(f => f.Name == e.Name)); } - if (query.Years.Length == 1) + if (filter.Years.Length == 1) { baseQuery = baseQuery - .Where(e => e.ProductionYear == query.Years[0]); + .Where(e => e.ProductionYear == filter.Years[0]); } - else if (query.Years.Length > 1) + else if (filter.Years.Length > 1) { baseQuery = baseQuery - .Where(e => query.Years.Any(f => f == e.ProductionYear)); + .Where(e => filter.Years.Any(f => f == e.ProductionYear)); } - var isVirtualItem = query.IsVirtualItem ?? query.IsMissing; + var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing; if (isVirtualItem.HasValue) { baseQuery = baseQuery .Where(e => e.IsVirtualItem == isVirtualItem.Value); } - if (query.IsSpecialSeason.HasValue) + if (filter.IsSpecialSeason.HasValue) { - if (query.IsSpecialSeason.Value) + if (filter.IsSpecialSeason.Value) { baseQuery = baseQuery .Where(e => e.IndexNumber == 0); @@ -1136,9 +1190,9 @@ public class BaseItemManager : IItemRepository } } - if (query.IsUnaired.HasValue) + if (filter.IsUnaired.HasValue) { - if (query.IsUnaired.Value) + if (filter.IsUnaired.Value) { baseQuery = baseQuery .Where(e => e.PremiereDate >= now); @@ -1150,60 +1204,60 @@ public class BaseItemManager : IItemRepository } } - if (query.MediaTypes.Length == 1) + if (filter.MediaTypes.Length == 1) { baseQuery = baseQuery - .Where(e => e.MediaType == query.MediaTypes[0].ToString()); + .Where(e => e.MediaType == filter.MediaTypes[0].ToString()); } - else if (query.MediaTypes.Length > 1) + else if (filter.MediaTypes.Length > 1) { baseQuery = baseQuery - .Where(e => query.MediaTypes.Select(f => f.ToString()).Contains(e.MediaType)); + .Where(e => filter.MediaTypes.Select(f => f.ToString()).Contains(e.MediaType)); } - if (query.ItemIds.Length > 0) + if (filter.ItemIds.Length > 0) { baseQuery = baseQuery - .Where(e => query.ItemIds.Contains(e.Id)); + .Where(e => filter.ItemIds.Contains(e.Id)); } - if (query.ExcludeItemIds.Length > 0) + if (filter.ExcludeItemIds.Length > 0) { baseQuery = baseQuery - .Where(e => !query.ItemIds.Contains(e.Id)); + .Where(e => !filter.ItemIds.Contains(e.Id)); } - if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0) + if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0) { - baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !query.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); + baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); } - if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0) + if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0) { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !query.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); } - if (query.HasImdbId.HasValue) + if (filter.HasImdbId.HasValue) { baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); } - if (query.HasTmdbId.HasValue) + if (filter.HasTmdbId.HasValue) { baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); } - if (query.HasTvdbId.HasValue) + if (filter.HasTvdbId.HasValue) { baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); } - var queryTopParentIds = query.TopParentIds; + var queryTopParentIds = filter.TopParentIds; if (queryTopParentIds.Length > 0) { - var includedItemByNameTypes = GetItemByNameTypesInQuery(query); - var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; + var includedItemByNameTypes = GetItemByNameTypesInQuery(filter); + var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; if (enableItemsByName && includedItemByNameTypes.Count > 0) { baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); @@ -1214,31 +1268,31 @@ public class BaseItemManager : IItemRepository } } - if (query.AncestorIds.Length > 0) + if (filter.AncestorIds.Length > 0) { - baseQuery = baseQuery.Where(e => e.AncestorIds!.Any(f => query.AncestorIds.Contains(f.Id))); + baseQuery = baseQuery.Where(e => e.AncestorIds!.Any(f => filter.AncestorIds.Contains(f.Id))); } - if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) + if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) { baseQuery = baseQuery - .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == query.AncestorWithPresentationUniqueKey).Any(f => f.AncestorIds!.Any(w => w.ItemId.Equals(f.Id)))); + .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.AncestorIds!.Any(w => w.ItemId.Equals(f.Id)))); } - if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) + if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) { baseQuery = baseQuery - .Where(e => e.SeriesPresentationUniqueKey == query.SeriesPresentationUniqueKey); + .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey); } - if (query.ExcludeInheritedTags.Length > 0) + if (filter.ExcludeInheritedTags.Length > 0) { baseQuery = baseQuery .Where(e => !e.ItemValues!.Where(e => e.Type == 6) - .Any(f => query.ExcludeInheritedTags.Contains(f.CleanValue))); + .Any(f => filter.ExcludeInheritedTags.Contains(f.CleanValue))); } - if (query.IncludeInheritedTags.Length > 0) + if (filter.IncludeInheritedTags.Length > 0) { // 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. @@ -1246,10 +1300,10 @@ public class BaseItemManager : IItemRepository { baseQuery = baseQuery .Where(e => e.ItemValues!.Where(e => e.Type == 6) - .Any(f => query.IncludeInheritedTags.Contains(f.CleanValue)) + .Any(f => filter.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)))); + .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)))); } // A playlist should be accessible to its owner regardless of allowed tags. @@ -1257,39 +1311,39 @@ public class BaseItemManager : IItemRepository { 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}\"")); + .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); // d ^^ this is stupid it hate this. } else { baseQuery = baseQuery .Where(e => e.ItemValues!.Where(e => e.Type == 6) - .Any(f => query.IncludeInheritedTags.Contains(f.CleanValue))); + .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue))); } } - if (query.SeriesStatuses.Length > 0) + if (filter.SeriesStatuses.Length > 0) { baseQuery = baseQuery - .Where(e => query.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); + .Where(e => filter.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); } - if (query.BoxSetLibraryFolders.Length > 0) + if (filter.BoxSetLibraryFolders.Length > 0) { baseQuery = baseQuery - .Where(e => query.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); + .Where(e => filter.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); } - if (query.VideoTypes.Length > 0) + if (filter.VideoTypes.Length > 0) { - var videoTypeBs = query.VideoTypes.Select(e => $"\"VideoType\":\"" + e + "\""); + var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"" + e + "\""); baseQuery = baseQuery .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f, StringComparison.InvariantCultureIgnoreCase))); } - if (query.Is3D.HasValue) + if (filter.Is3D.HasValue) { - if (query.Is3D.Value) + if (filter.Is3D.Value) { baseQuery = baseQuery .Where(e => e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); @@ -1301,9 +1355,9 @@ public class BaseItemManager : IItemRepository } } - if (query.IsPlaceHolder.HasValue) + if (filter.IsPlaceHolder.HasValue) { - if (query.IsPlaceHolder.Value) + if (filter.IsPlaceHolder.Value) { baseQuery = baseQuery .Where(e => e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); @@ -1315,9 +1369,9 @@ public class BaseItemManager : IItemRepository } } - if (query.HasSpecialFeature.HasValue) + if (filter.HasSpecialFeature.HasValue) { - if (query.HasSpecialFeature.Value) + if (filter.HasSpecialFeature.Value) { baseQuery = baseQuery .Where(e => e.ExtraIds != null); @@ -1329,9 +1383,9 @@ public class BaseItemManager : IItemRepository } } - if (query.HasTrailer.HasValue || query.HasThemeSong.HasValue || query.HasThemeVideo.HasValue) + if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue) { - if (query.HasTrailer.GetValueOrDefault() || query.HasThemeSong.GetValueOrDefault() || query.HasThemeVideo.GetValueOrDefault()) + if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault()) { baseQuery = baseQuery .Where(e => e.ExtraIds != null); @@ -1776,6 +1830,26 @@ public class BaseItemManager : IItemRepository return entity; } + private IReadOnlyList GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) + { + using var context = _dbProvider.CreateDbContext(); + + var query = context.ItemValues + .Where(e => itemValueTypes.Contains(e.Type)); + if (withItemTypes.Count > 0) + { + query = query.Where(e => context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId.Equals(e.ItemId)))); + } + + if (excludeItemTypes.Count > 0) + { + query = query.Where(e => !context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId.Equals(e.ItemId)))); + } + + query = query.DistinctBy(e => e.CleanValue); + return query.Select(e => e.CleanValue).ToImmutableArray(); + } + private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) { var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs new file mode 100644 index 000000000..288b1943e --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Manager for handling Media Attachments. +/// +/// Efcore Factory. +public class MediaAttachmentManager(IDbContextFactory dbProvider) : IMediaAttachmentManager +{ + /// + public void SaveMediaAttachments( + Guid id, + IReadOnlyList attachments, + CancellationToken cancellationToken) + { + using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.AttachmentStreamInfos.AddRange(attachments.Select(e => Map(e, id))); + context.SaveChanges(); + transaction.Commit(); + } + + /// + public IReadOnlyList GetMediaAttachments(MediaAttachmentQuery filter) + { + using var context = dbProvider.CreateDbContext(); + var query = context.AttachmentStreamInfos.Where(e => e.ItemId.Equals(filter.ItemId)); + if (filter.Index.HasValue) + { + query = query.Where(e => e.Index == filter.Index); + } + + return query.ToList().Select(Map).ToImmutableArray(); + } + + private MediaAttachment Map(AttachmentStreamInfo attachment) + { + return new MediaAttachment() + { + Codec = attachment.Codec, + CodecTag = attachment.CodecTag, + Comment = attachment.Comment, + FileName = attachment.Filename, + Index = attachment.Index, + MimeType = attachment.MimeType, + }; + } + + private AttachmentStreamInfo Map(MediaAttachment attachment, Guid id) + { + return new AttachmentStreamInfo() + { + Codec = attachment.Codec, + CodecTag = attachment.CodecTag, + Comment = attachment.Comment, + Filename = attachment.FileName, + Index = attachment.Index, + MimeType = attachment.MimeType, + ItemId = id, + Item = null! + }; + } +} diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs b/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs index e609cdc1e..b7124283a 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs @@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations.Item; /// /// /// -public class MediaStreamManager(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) +public class MediaStreamManager(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) : IMediaStreamManager { /// public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) diff --git a/Jellyfin.Server.Implementations/Item/PeopleManager.cs b/Jellyfin.Server.Implementations/Item/PeopleManager.cs index 0f1760cbd..d29d8b143 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleManager.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleManager.cs @@ -6,22 +6,22 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Item; -public class PeopleManager +/// +/// Manager for handling people. +/// +/// Efcore Factory. +/// +/// Initializes a new instance of the class. +/// +/// The EFCore Context factory. +public class PeopleManager(IDbContextFactory dbProvider) : IPeopleManager { - private readonly IDbContextFactory _dbProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The EFCore Context factory. - public PeopleManager(IDbContextFactory dbProvider) - { - _dbProvider = dbProvider; - } + private readonly IDbContextFactory _dbProvider = dbProvider; public IReadOnlyList GetPeople(InternalPeopleQuery filter) { diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 21b9ee4b7..313b1459a 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -7,135 +7,83 @@ using System.Collections.Generic; using System.Threading; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -namespace MediaBrowser.Controller.Persistence +namespace MediaBrowser.Controller.Persistence; + +/// +/// Provides an interface to implement an Item repository. +/// +public interface IItemRepository : IDisposable { /// - /// Provides an interface to implement an Item repository. + /// Deletes the item. + /// + /// The identifier. + void DeleteItem(Guid id); + + /// + /// Saves the items. + /// + /// The items. + /// The cancellation token. + void SaveItems(IReadOnlyList items, CancellationToken cancellationToken); + + void SaveImages(BaseItem item); + + /// + /// Retrieves the item. + /// + /// The id. + /// BaseItem. + BaseItem RetrieveItem(Guid id); + + /// + /// Gets the items. + /// + /// The query. + /// QueryResult<BaseItem>. + QueryResult GetItems(InternalItemsQuery filter); + + /// + /// Gets the item ids list. + /// + /// The query. + /// List<Guid>. + IReadOnlyList GetItemIdsList(InternalItemsQuery filter); + + + /// + /// Gets the item list. + /// + /// The query. + /// List<BaseItem>. + IReadOnlyList GetItemList(InternalItemsQuery filter); + + /// + /// Updates the inherited values. /// - public interface IItemRepository : IDisposable - { - /// - /// Deletes the item. - /// - /// The identifier. - void DeleteItem(Guid id); - - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - void SaveItems(IReadOnlyList items, CancellationToken cancellationToken); - - void SaveImages(BaseItem item); - - /// - /// Retrieves the item. - /// - /// The id. - /// BaseItem. - BaseItem RetrieveItem(Guid id); - - /// - /// Gets the media streams. - /// - /// The query. - /// IEnumerable{MediaStream}. - List GetMediaStreams(MediaStreamQuery query); - - /// - /// Saves the media streams. - /// - /// The identifier. - /// The streams. - /// The cancellation token. - void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken); - - /// - /// Gets the media attachments. - /// - /// The query. - /// IEnumerable{MediaAttachment}. - List GetMediaAttachments(MediaAttachmentQuery query); - - /// - /// Saves the media attachments. - /// - /// The identifier. - /// The attachments. - /// The cancellation token. - void SaveMediaAttachments(Guid id, IReadOnlyList attachments, CancellationToken cancellationToken); - - /// - /// Gets the items. - /// - /// The query. - /// QueryResult<BaseItem>. - QueryResult GetItems(InternalItemsQuery query); - - /// - /// Gets the item ids list. - /// - /// The query. - /// List<Guid>. - List GetItemIdsList(InternalItemsQuery query); - - /// - /// Gets the people. - /// - /// The query. - /// List<PersonInfo>. - List GetPeople(InternalPeopleQuery query); - - /// - /// Updates the people. - /// - /// The item identifier. - /// The people. - void UpdatePeople(Guid itemId, List people); - - /// - /// Gets the people names. - /// - /// The query. - /// List<System.String>. - List GetPeopleNames(InternalPeopleQuery query); - - /// - /// Gets the item list. - /// - /// The query. - /// List<BaseItem>. - List GetItemList(InternalItemsQuery query); - - /// - /// Updates the inherited values. - /// - void UpdateInheritedValues(); - - int GetCount(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query); - - List GetMusicGenreNames(); - - List GetStudioNames(); - - List GetGenreNames(); - - List GetAllArtistNames(); - } + void UpdateInheritedValues(); + + int GetCount(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter); + + IReadOnlyList GetMusicGenreNames(); + + IReadOnlyList GetStudioNames(); + + IReadOnlyList GetGenreNames(); + + IReadOnlyList GetAllArtistNames(); } diff --git a/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs b/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs new file mode 100644 index 000000000..210d80afa --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs @@ -0,0 +1,29 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IMediaAttachmentManager +{ + + /// + /// Gets the media attachments. + /// + /// The query. + /// IEnumerable{MediaAttachment}. + IReadOnlyList GetMediaAttachments(MediaAttachmentQuery filter); + + /// + /// Saves the media attachments. + /// + /// The identifier. + /// The attachments. + /// The cancellation token. + void SaveMediaAttachments(Guid id, IReadOnlyList attachments, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamManager.cs b/MediaBrowser.Controller/Persistence/IMediaStreamManager.cs new file mode 100644 index 000000000..ec7c72935 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaStreamManager.cs @@ -0,0 +1,28 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IMediaStreamManager +{ + /// + /// Gets the media streams. + /// + /// The query. + /// IEnumerable{MediaStream}. + List GetMediaStreams(MediaStreamQuery filter); + + /// + /// Saves the media streams. + /// + /// The identifier. + /// The streams. + /// The cancellation token. + void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Persistence/IPeopleManager.cs b/MediaBrowser.Controller/Persistence/IPeopleManager.cs new file mode 100644 index 000000000..84e503fef --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IPeopleManager.cs @@ -0,0 +1,34 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IPeopleManager +{ + /// + /// Gets the people. + /// + /// The query. + /// List<PersonInfo>. + IReadOnlyList GetPeople(InternalPeopleQuery filter); + + /// + /// Updates the people. + /// + /// The item identifier. + /// The people. + void UpdatePeople(Guid itemId, IReadOnlyList people); + + /// + /// Gets the people names. + /// + /// The query. + /// List<System.String>. + IReadOnlyList GetPeopleNames(InternalPeopleQuery filter); + +} -- cgit v1.2.3