From d0b4b2ddb31a54f0705303ab8461be1125d66eab Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 7 Sep 2024 19:07:34 +0000 Subject: Migrated UserData from library sqlite db to jellyfin.db --- .../Persistence/IUserDataRepository.cs | 55 ---------------------- 1 file changed, 55 deletions(-) delete mode 100644 MediaBrowser.Controller/Persistence/IUserDataRepository.cs (limited to 'MediaBrowser.Controller/Persistence') diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs deleted file mode 100644 index f2fb2826a..000000000 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// - /// Provides an interface to implement a UserData repository. - /// - public interface IUserDataRepository : IDisposable - { - /// - /// Saves the user data. - /// - /// The user id. - /// The key. - /// The user data. - /// The cancellation token. - void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken); - - /// - /// Gets the user data. - /// - /// The user id. - /// The key. - /// The user data. - UserItemData GetUserData(long userId, string key); - - /// - /// Gets the user data. - /// - /// The user id. - /// The keys. - /// The user data. - UserItemData GetUserData(long userId, List keys); - - /// - /// Return all user data associated with the given user. - /// - /// The user id. - /// The list of user item data. - List GetAllUserData(long userId); - - /// - /// Save all user data associated with the given user. - /// - /// The user id. - /// The user item data. - /// The cancellation token. - void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken); - } -} -- 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 'MediaBrowser.Controller/Persistence') 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 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 'MediaBrowser.Controller/Persistence') 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 From be48cdd9e90ed147c5526ef3fed0624bcbad7741 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:53:39 +0000 Subject: Naming refactoring and WIP porting of new interface repositories --- Emby.Server.Implementations/ApplicationHost.cs | 13 +- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 139 ++ .../MediaEncoder/EncodingManager.cs | 4 +- Jellyfin.Data/Entities/AncestorId.cs | 2 +- Jellyfin.Data/Entities/AttachmentStreamInfo.cs | 2 +- Jellyfin.Data/Entities/BaseItem.cs | 176 -- Jellyfin.Data/Entities/BaseItemEntity.cs | 177 ++ Jellyfin.Data/Entities/BaseItemProvider.cs | 23 +- Jellyfin.Data/Entities/Chapter.cs | 2 +- Jellyfin.Data/Entities/ItemValue.cs | 23 +- Jellyfin.Data/Entities/MediaStreamInfo.cs | 2 +- Jellyfin.Data/Entities/People.cs | 34 +- .../Item/BaseItemManager.cs | 2333 -------------------- .../Item/BaseItemRepository.cs | 2233 +++++++++++++++++++ .../Item/ChapterManager.cs | 99 - .../Item/ChapterRepository.cs | 123 ++ .../Item/MediaAttachmentManager.cs | 73 - .../Item/MediaAttachmentRepository.cs | 73 + .../Item/MediaStreamManager.cs | 201 -- .../Item/MediaStreamRepository.cs | 201 ++ .../Item/PeopleManager.cs | 164 -- .../Item/PeopleRepository.cs | 165 ++ .../JellyfinDbContext.cs | 2 +- .../ModelConfiguration/BaseItemConfiguration.cs | 4 +- .../BaseItemProviderConfiguration.cs | 2 +- MediaBrowser.Controller/Chapters/ChapterManager.cs | 24 - .../Chapters/IChapterManager.cs | 35 - .../Chapters/IChapterRepository.cs | 49 + MediaBrowser.Controller/Drawing/IImageProcessor.cs | 25 + MediaBrowser.Controller/Entities/BaseItem.cs | 7 +- .../Persistence/IItemRepository.cs | 1 - .../Persistence/IItemTypeLookup.cs | 57 + .../Persistence/IMediaAttachmentManager.cs | 29 - .../Persistence/IMediaAttachmentRepository.cs | 28 + .../Persistence/IMediaStreamManager.cs | 28 - .../Persistence/IMediaStreamRepository.cs | 31 + .../Persistence/IPeopleManager.cs | 34 - .../Persistence/IPeopleRepository.cs | 33 + .../MediaInfo/FFProbeVideoInfo.cs | 4 +- MediaBrowser.Providers/MediaInfo/ProbeProvider.cs | 4 +- src/Jellyfin.Drawing/ImageProcessor.cs | 25 + 41 files changed, 3459 insertions(+), 3225 deletions(-) create mode 100644 Emby.Server.Implementations/Data/ItemTypeLookup.cs delete mode 100644 Jellyfin.Data/Entities/BaseItem.cs create mode 100644 Jellyfin.Data/Entities/BaseItemEntity.cs delete mode 100644 Jellyfin.Server.Implementations/Item/BaseItemManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/BaseItemRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/ChapterManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/ChapterRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/MediaStreamManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs delete mode 100644 Jellyfin.Server.Implementations/Item/PeopleManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/PeopleRepository.cs delete mode 100644 MediaBrowser.Controller/Chapters/ChapterManager.cs delete mode 100644 MediaBrowser.Controller/Chapters/IChapterManager.cs create mode 100644 MediaBrowser.Controller/Chapters/IChapterRepository.cs create mode 100644 MediaBrowser.Controller/Persistence/IItemTypeLookup.cs delete mode 100644 MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs delete mode 100644 MediaBrowser.Controller/Persistence/IMediaStreamManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs delete mode 100644 MediaBrowser.Controller/Persistence/IPeopleManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IPeopleRepository.cs (limited to 'MediaBrowser.Controller/Persistence') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index bdf013b5d..fbec4726f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -40,6 +40,7 @@ using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Manager; using Jellyfin.Networking.Udp; using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Item; using Jellyfin.Server.Implementations.MediaSegments; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -83,7 +84,6 @@ using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; -using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; @@ -494,7 +494,12 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -539,8 +544,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -578,8 +581,6 @@ namespace Emby.Server.Implementations } } - ((SqliteItemRepository)Resolve()).Initialize(); - var localizationManager = (LocalizationManager)Resolve(); await localizationManager.LoadAll().ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs new file mode 100644 index 000000000..14dc68a32 --- /dev/null +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Threading.Channels; +using Emby.Server.Implementations.Playlists; +using Jellyfin.Data.Enums; +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.Querying; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Provides static topic based lookups for the BaseItemKind. +/// +public class ItemTypeLookup : IItemTypeLookup +{ + /// + /// Gets all values of the ItemFields type. + /// + public IReadOnlyList AllItemFields { get; } = Enum.GetValues(); + + /// + /// Gets all BaseItemKinds that are considered Programs. + /// + public IReadOnlyList ProgramTypes { get; } = + [ + BaseItemKind.Program, + BaseItemKind.TvChannel, + BaseItemKind.LiveTvProgram, + BaseItemKind.LiveTvChannel + ]; + + /// + /// Gets all BaseItemKinds that should be excluded from parent lookup. + /// + public IReadOnlyList ProgramExcludeParentTypes { get; } = + [ + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.PhotoAlbum + ]; + + /// + /// Gets all BaseItemKinds that are considered to be provided by services. + /// + public IReadOnlyList ServiceTypes { get; } = + [ + BaseItemKind.TvChannel, + BaseItemKind.LiveTvChannel + ]; + + /// + /// Gets all BaseItemKinds that have a StartDate. + /// + public IReadOnlyList StartDateTypes { get; } = + [ + BaseItemKind.Program, + BaseItemKind.LiveTvProgram + ]; + + /// + /// Gets all BaseItemKinds that are considered Series. + /// + public IReadOnlyList SeriesTypes { get; } = + [ + BaseItemKind.Book, + BaseItemKind.AudioBook, + BaseItemKind.Episode, + BaseItemKind.Season + ]; + + /// + /// Gets all BaseItemKinds that are not to be evaluated for Artists. + /// + public IReadOnlyList ArtistExcludeParentTypes { get; } = + [ + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.PhotoAlbum + ]; + + /// + /// Gets all BaseItemKinds that are considered Artists. + /// + public IReadOnlyList ArtistsTypes { get; } = + [ + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicVideo, + BaseItemKind.AudioBook + ]; + + /// + /// Gets mapping for all BaseItemKinds and their expected serialisaition target. + /// + public IDictionary BaseItemKindNames { get; } = new Dictionary() + { + { 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 } + }.AsReadOnly(); +} diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index eb55e32c5..ea7896861 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.MediaEncoder private readonly IFileSystem _fileSystem; private readonly ILogger _logger; private readonly IMediaEncoder _encoder; - private readonly IChapterManager _chapterManager; + private readonly IChapterRepository _chapterManager; private readonly ILibraryManager _libraryManager; /// @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.MediaEncoder ILogger logger, IFileSystem fileSystem, IMediaEncoder encoder, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager) { _logger = logger; diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Data/Entities/AncestorId.cs index dc83b763e..3839b1ae4 100644 --- a/Jellyfin.Data/Entities/AncestorId.cs +++ b/Jellyfin.Data/Entities/AncestorId.cs @@ -13,7 +13,7 @@ public class AncestorId public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public string? AncestorIdText { get; set; } } diff --git a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs index 858465424..056d5b05e 100644 --- a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs +++ b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs @@ -7,7 +7,7 @@ public class AttachmentStreamInfo { public required Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public required int Index { get; set; } diff --git a/Jellyfin.Data/Entities/BaseItem.cs b/Jellyfin.Data/Entities/BaseItem.cs deleted file mode 100644 index 0e67a7ca4..000000000 --- a/Jellyfin.Data/Entities/BaseItem.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities; - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -public class BaseItem -{ - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - - public Guid Id { get; set; } - - public required string Type { get; set; } - - public string? Data { get; set; } - - public Guid? ParentId { get; set; } - - public string? Path { get; set; } - - public DateTime StartDate { get; set; } - - public DateTime EndDate { get; set; } - - public string? ChannelId { get; set; } - - public bool IsMovie { get; set; } - - public float? CommunityRating { get; set; } - - public string? CustomRating { get; set; } - - public int? IndexNumber { get; set; } - - public bool IsLocked { get; set; } - - public string? Name { get; set; } - - public string? OfficialRating { get; set; } - - public string? MediaType { get; set; } - - public string? Overview { get; set; } - - public int? ParentIndexNumber { get; set; } - - public DateTime? PremiereDate { get; set; } - - public int? ProductionYear { get; set; } - - public string? Genres { get; set; } - - public string? SortName { get; set; } - - public string? ForcedSortName { get; set; } - - public long? RunTimeTicks { get; set; } - - public DateTime? DateCreated { get; set; } - - public DateTime? DateModified { get; set; } - - public bool IsSeries { get; set; } - - public string? EpisodeTitle { get; set; } - - public bool IsRepeat { get; set; } - - public string? PreferredMetadataLanguage { get; set; } - - public string? PreferredMetadataCountryCode { get; set; } - - public DateTime? DateLastRefreshed { get; set; } - - public DateTime? DateLastSaved { get; set; } - - public bool IsInMixedFolder { get; set; } - - public string? LockedFields { get; set; } - - public string? Studios { get; set; } - - public string? Audio { get; set; } - - public string? ExternalServiceId { get; set; } - - public string? Tags { get; set; } - - public bool IsFolder { get; set; } - - public int? InheritedParentalRatingValue { get; set; } - - public string? UnratedType { get; set; } - - public Guid? TopParentId { get; set; } - - public string? TrailerTypes { get; set; } - - public float? CriticRating { get; set; } - - public string? CleanName { get; set; } - - public string? PresentationUniqueKey { get; set; } - - public string? OriginalTitle { get; set; } - - public string? PrimaryVersionId { get; set; } - - public DateTime? DateLastMediaAdded { get; set; } - - public string? Album { get; set; } - - public float? LUFS { get; set; } - - public float? NormalizationGain { get; set; } - - public bool IsVirtualItem { get; set; } - - public string? SeriesName { get; set; } - - public string? UserDataKey { get; set; } - - public string? SeasonName { get; set; } - - public Guid? SeasonId { get; set; } - - public Guid? SeriesId { get; set; } - - public string? ExternalSeriesId { get; set; } - - public string? Tagline { get; set; } - - public string? Images { get; set; } - - public string? ProductionLocations { get; set; } - - public string? ExtraIds { get; set; } - - public int? TotalBitrate { get; set; } - - public string? ExtraType { get; set; } - - public string? Artists { get; set; } - - public string? AlbumArtists { get; set; } - - public string? ExternalId { get; set; } - - public string? SeriesPresentationUniqueKey { get; set; } - - public string? ShowId { get; set; } - - public string? OwnerId { get; set; } - - public int? Width { get; set; } - - 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; } - - public ICollection? MediaStreams { get; set; } - - public ICollection? Chapters { get; set; } - - public ICollection? Provider { get; set; } - - public ICollection? AncestorIds { get; set; } -} diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs new file mode 100644 index 000000000..92b5caf05 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItemEntity.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class BaseItemEntity +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + + public Guid Id { get; set; } + + public required string Type { get; set; } + + public string? Data { get; set; } + + public Guid? ParentId { get; set; } + + public string? Path { get; set; } + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } + + public string? ChannelId { get; set; } + + public bool IsMovie { get; set; } + + public float? CommunityRating { get; set; } + + public string? CustomRating { get; set; } + + public int? IndexNumber { get; set; } + + public bool IsLocked { get; set; } + + public string? Name { get; set; } + + public string? OfficialRating { get; set; } + + public string? MediaType { get; set; } + + public string? Overview { get; set; } + + public int? ParentIndexNumber { get; set; } + + public DateTime? PremiereDate { get; set; } + + public int? ProductionYear { get; set; } + + public string? Genres { get; set; } + + public string? SortName { get; set; } + + public string? ForcedSortName { get; set; } + + public long? RunTimeTicks { get; set; } + + public DateTime? DateCreated { get; set; } + + public DateTime? DateModified { get; set; } + + public bool IsSeries { get; set; } + + public string? EpisodeTitle { get; set; } + + public bool IsRepeat { get; set; } + + public string? PreferredMetadataLanguage { get; set; } + + public string? PreferredMetadataCountryCode { get; set; } + + public DateTime? DateLastRefreshed { get; set; } + + public DateTime? DateLastSaved { get; set; } + + public bool IsInMixedFolder { get; set; } + + public string? LockedFields { get; set; } + + public string? Studios { get; set; } + + public string? Audio { get; set; } + + public string? ExternalServiceId { get; set; } + + public string? Tags { get; set; } + + public bool IsFolder { get; set; } + + public int? InheritedParentalRatingValue { get; set; } + + public string? UnratedType { get; set; } + + public Guid? TopParentId { get; set; } + + public string? TrailerTypes { get; set; } + + public float? CriticRating { get; set; } + + public string? CleanName { get; set; } + + public string? PresentationUniqueKey { get; set; } + + public string? OriginalTitle { get; set; } + + public string? PrimaryVersionId { get; set; } + + public DateTime? DateLastMediaAdded { get; set; } + + public string? Album { get; set; } + + public float? LUFS { get; set; } + + public float? NormalizationGain { get; set; } + + public bool IsVirtualItem { get; set; } + + public string? SeriesName { get; set; } + + public string? UserDataKey { get; set; } + + public string? SeasonName { get; set; } + + public Guid? SeasonId { get; set; } + + public Guid? SeriesId { get; set; } + + public string? ExternalSeriesId { get; set; } + + public string? Tagline { get; set; } + + public string? Images { get; set; } + + public string? ProductionLocations { get; set; } + + public string? ExtraIds { get; set; } + + public int? TotalBitrate { get; set; } + + public string? ExtraType { get; set; } + + public string? Artists { get; set; } + + public string? AlbumArtists { get; set; } + + public string? ExternalId { get; set; } + + public string? SeriesPresentationUniqueKey { get; set; } + + public string? ShowId { get; set; } + + public string? OwnerId { get; set; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public long? Size { get; set; } + +#pragma warning disable CA2227 // Collection properties should be read only + public ICollection? Peoples { get; set; } + + public ICollection? UserData { get; set; } + + public ICollection? ItemValues { get; set; } + + public ICollection? MediaStreams { get; set; } + + public ICollection? Chapters { get; set; } + + public ICollection? Provider { get; set; } + + public ICollection? AncestorIds { get; set; } +} diff --git a/Jellyfin.Data/Entities/BaseItemProvider.cs b/Jellyfin.Data/Entities/BaseItemProvider.cs index 6f8e1c39b..1fc721d6a 100644 --- a/Jellyfin.Data/Entities/BaseItemProvider.cs +++ b/Jellyfin.Data/Entities/BaseItemProvider.cs @@ -5,11 +5,28 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; +/// +/// Represents an Key-Value relaten of an BaseItem's provider. +/// public class BaseItemProvider { + /// + /// Gets or Sets the reference ItemId. + /// public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } - public string ProviderId { get; set; } - public string ProviderValue { get; set; } + /// + /// Gets or Sets the reference BaseItem. + /// + public required BaseItemEntity Item { get; set; } + + /// + /// Gets or Sets the ProvidersId. + /// + public required string ProviderId { get; set; } + + /// + /// Gets or Sets the Providers Value. + /// + public required string ProviderValue { get; set; } } diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs index ad119d1c6..be353b5da 100644 --- a/Jellyfin.Data/Entities/Chapter.cs +++ b/Jellyfin.Data/Entities/Chapter.cs @@ -10,7 +10,7 @@ public class Chapter { public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public required int ChapterIndex { get; set; } diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Data/Entities/ItemValue.cs index a3c0908bb..1063aaa8b 100644 --- a/Jellyfin.Data/Entities/ItemValue.cs +++ b/Jellyfin.Data/Entities/ItemValue.cs @@ -5,12 +5,33 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; +/// +/// Represents an ItemValue for a BaseItem. +/// public class ItemValue { + /// + /// Gets or Sets the reference ItemId. + /// public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + /// + /// Gets or Sets the referenced BaseItem. + /// + public required BaseItemEntity Item { get; set; } + + /// + /// Gets or Sets the Type. + /// public required int Type { get; set; } + + /// + /// Gets or Sets the Value. + /// public required string Value { get; set; } + + /// + /// Gets or Sets the sanatised Value. + /// public required string CleanValue { get; set; } } diff --git a/Jellyfin.Data/Entities/MediaStreamInfo.cs b/Jellyfin.Data/Entities/MediaStreamInfo.cs index 3b89ca62f..992f33ecf 100644 --- a/Jellyfin.Data/Entities/MediaStreamInfo.cs +++ b/Jellyfin.Data/Entities/MediaStreamInfo.cs @@ -7,7 +7,7 @@ public class MediaStreamInfo { public Guid ItemId { get; set; } - public required BaseItem Item { get; set; } + public required BaseItemEntity Item { get; set; } public int StreamIndex { get; set; } diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs index 014a0f1c9..8eb23f5e4 100644 --- a/Jellyfin.Data/Entities/People.cs +++ b/Jellyfin.Data/Entities/People.cs @@ -4,14 +4,44 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; + +/// +/// People entity. +/// public class People { - public Guid ItemId { get; set; } - public BaseItem Item { get; set; } + /// + /// Gets or Sets The ItemId. + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or Sets Reference Item. + /// + public required BaseItemEntity Item { get; set; } + /// + /// Gets or Sets the Persons Name. + /// public required string Name { get; set; } + + /// + /// Gets or Sets the Role. + /// public string? Role { get; set; } + + /// + /// Gets or Sets the Type. + /// public string? PersonType { get; set; } + + /// + /// Gets or Sets the SortOrder. + /// public int? SortOrder { get; set; } + + /// + /// Gets or Sets the ListOrder. + /// public int? ListOrder { get; set; } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs deleted file mode 100644 index 66cc765f3..000000000 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ /dev/null @@ -1,2333 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -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.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; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// Handles all storage logic for BaseItems. -/// -public sealed class BaseItemManager : IItemRepository, IDisposable -{ - 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(); - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The db factory. - /// The Application host. - public BaseItemManager(IDbContextFactory dbProvider, IServerApplicationHost appHost) - { - _dbProvider = dbProvider; - _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); - - 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; - - if (offset > 0) - { - query = query.Skip(offset); - } - - if (filter.Limit.HasValue) - { - query.Take(filter.Limit.Value); - } - } - - var result = new QueryResult<(BaseItem, ItemCounts)>(); - string countText = string.Empty; - if (filter.EnableTotalRecordCount) - { - result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count(); - } - - var resultQuery = query.Select(e => new - { - item = e, - itemCount = new ItemCounts() - { - 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), - } - }); - - result.StartIndex = filter.StartIndex ?? 0; - result.Items = resultQuery.ToImmutableArray().Select(e => - { - return (DeserialiseBaseItem(e.item), e.itemCount); - }).ToImmutableArray(); - - return result; - } - - /// - 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.AsNoTracking(), 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(); - } - - /// - 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(filter, new[] { 3 }, typeof(Studio).FullName!); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 2 }, typeof(Genre).FullName!); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) - { - return GetItemValues(filter, new[] { 2 }, typeof(MusicGenre).FullName!); - } - - /// - public IReadOnlyList GetStudioNames() - { - return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); - } - - /// - public IReadOnlyList GetAllArtistNames() - { - return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); - } - - /// - public IReadOnlyList GetMusicGenreNames() - { - 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 filter) - { - ArgumentNullException.ThrowIfNull(filter); - if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0)) - { - var returnList = GetItemList(filter); - return new QueryResult( - filter.StartIndex, - returnList.Count, - returnList); - } - - PrepareFilterQuery(filter); - var result = new QueryResult(); - - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) - .DistinctBy(e => e.Id); - if (filter.EnableTotalRecordCount) - { - result.TotalRecordCount = dbQuery.Count(); - } - - if (filter.Limit.HasValue || filter.StartIndex.HasValue) - { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - dbQuery = dbQuery.Skip(offset); - } - - if (filter.Limit.HasValue) - { - dbQuery = dbQuery.Take(filter.Limit.Value); - } - } - - result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); - result.StartIndex = filter.StartIndex ?? 0; - return result; - } - - /// - public IReadOnlyList GetItemList(InternalItemsQuery filter) - { - ArgumentNullException.ThrowIfNull(filter); - PrepareFilterQuery(filter); - - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) - .DistinctBy(e => e.Id); - if (filter.Limit.HasValue || filter.StartIndex.HasValue) - { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - dbQuery = dbQuery.Skip(offset); - } - - if (filter.Limit.HasValue) - { - dbQuery = dbQuery.Take(filter.Limit.Value); - } - } - - return dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); - } - - /// - public int GetCount(InternalItemsQuery filter) - { - ArgumentNullException.ThrowIfNull(filter); - // Hack for right now since we currently don't support filtering out these duplicates within a query - PrepareFilterQuery(filter); - - using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter); - - return dbQuery.Count(); - } - - private IQueryable TranslateQuery( - IQueryable baseQuery, - JellyfinDbContext context, - InternalItemsQuery filter) - { - var minWidth = filter.MinWidth; - var maxWidth = filter.MaxWidth; - var now = DateTime.UtcNow; - - if (filter.IsHD.HasValue) - { - const int Threshold = 1200; - if (filter.IsHD.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - if (filter.Is4K.HasValue) - { - const int Threshold = 3800; - if (filter.Is4K.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - if (minWidth.HasValue) - { - baseQuery = baseQuery.Where(e => e.Width >= minWidth); - } - - if (filter.MinHeight.HasValue) - { - baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight); - } - - if (maxWidth.HasValue) - { - baseQuery = baseQuery.Where(e => e.Width >= maxWidth); - } - - if (filter.MaxHeight.HasValue) - { - baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight); - } - - if (filter.IsLocked.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked); - } - - var tags = filter.Tags.ToList(); - var excludeTags = filter.ExcludeTags.ToList(); - - if (filter.IsMovie == true) - { - if (filter.IncludeItemTypes.Length == 0 - || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) - || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) - { - baseQuery = baseQuery.Where(e => e.IsMovie); - } - } - else if (filter.IsMovie.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); - } - - if (filter.IsSeries.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries); - } - - if (filter.IsSports.HasValue) - { - if (filter.IsSports.Value) - { - tags.Add("Sports"); - } - else - { - excludeTags.Add("Sports"); - } - } - - if (filter.IsNews.HasValue) - { - if (filter.IsNews.Value) - { - tags.Add("News"); - } - else - { - excludeTags.Add("News"); - } - } - - if (filter.IsKids.HasValue) - { - if (filter.IsKids.Value) - { - tags.Add("Kids"); - } - else - { - excludeTags.Add("Kids"); - } - } - - if (!string.IsNullOrEmpty(filter.SearchTerm)) - { - baseQuery = baseQuery.Where(e => e.CleanName!.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase) || (e.OriginalTitle != null && e.OriginalTitle.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase))); - } - - if (filter.IsFolder.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder); - } - - var includeTypes = filter.IncludeItemTypes; - // Only specify excluded types if no included types are specified - if (filter.IncludeItemTypes.Length == 0) - { - var excludeTypes = filter.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 (filter.ChannelIds.Count == 1) - { - baseQuery = baseQuery.Where(e => e.ChannelId == filter.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); - } - else if (filter.ChannelIds.Count > 1) - { - baseQuery = baseQuery.Where(e => filter.ChannelIds.Select(f => f.ToString("N", CultureInfo.InvariantCulture)).Contains(e.ChannelId)); - } - - if (!filter.ParentId.IsEmpty()) - { - baseQuery = baseQuery.Where(e => e.ParentId.Equals(filter.ParentId)); - } - - if (!string.IsNullOrWhiteSpace(filter.Path)) - { - baseQuery = baseQuery.Where(e => e.Path == filter.Path); - } - - if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) - { - baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey); - } - - if (filter.MinCommunityRating.HasValue) - { - baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating); - } - - if (filter.MinIndexNumber.HasValue) - { - baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber); - } - - if (filter.MinParentAndIndexNumber.HasValue) - { - baseQuery = baseQuery - .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber); - } - - if (filter.MinDateCreated.HasValue) - { - baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated); - } - - if (filter.MinDateLastSaved.HasValue) - { - baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value); - } - - if (filter.MinDateLastSavedForUser.HasValue) - { - baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value); - } - - if (filter.IndexNumber.HasValue) - { - baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value); - } - - if (filter.ParentIndexNumber.HasValue) - { - baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value); - } - - if (filter.ParentIndexNumberNotEquals.HasValue) - { - baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); - } - - var minEndDate = filter.MinEndDate; - var maxEndDate = filter.MaxEndDate; - - if (filter.HasAired.HasValue) - { - if (filter.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 (filter.MinStartDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value); - } - - if (filter.MaxStartDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value); - } - - if (filter.MinPremiereDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value); - } - - if (filter.MaxPremiereDate.HasValue) - { - baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value); - } - - if (filter.TrailerTypes.Length > 0) - { - baseQuery = baseQuery.Where(e => filter.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); - } - - if (filter.IsAiring.HasValue) - { - if (filter.IsAiring.Value) - { - baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now); - } - else - { - baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now); - } - } - - if (filter.PersonIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => - 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(filter.Person)) - { - baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == filter.Person)); - } - - if (!string.IsNullOrWhiteSpace(filter.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(filter.ExternalSeriesId)) - { - baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId); - } - - if (!string.IsNullOrWhiteSpace(filter.ExternalId)) - { - baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId); - } - - if (!string.IsNullOrWhiteSpace(filter.Name)) - { - var cleanName = GetCleanValue(filter.Name); - baseQuery = baseQuery.Where(e => e.CleanName == cleanName); - } - - // These are the same, for now - var nameContains = filter.NameContains; - if (!string.IsNullOrWhiteSpace(nameContains)) - { - baseQuery = baseQuery.Where(e => - e.CleanName == filter.NameContains - || e.OriginalTitle!.Contains(filter.NameContains!, StringComparison.Ordinal)); - } - - if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) - { - baseQuery = baseQuery.Where(e => e.SortName!.Contains(filter.NameStartsWith, StringComparison.OrdinalIgnoreCase)); - } - - if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) - { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName![0] > filter.NameStartsWithOrGreater[0]); - } - - if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) - { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName![0] < filter.NameLessThan[0]); - } - - if (filter.ImageTypes.Length > 0) - { - baseQuery = baseQuery.Where(e => filter.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); - } - - if (filter.IsLiked.HasValue) - { - baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); - } - - if (filter.IsFavoriteOrLiked.HasValue) - { - baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavoriteOrLiked); - } - - if (filter.IsFavorite.HasValue) - { - baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavorite); - } - - if (filter.IsPlayed.HasValue) - { - baseQuery = baseQuery - .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Played == filter.IsPlayed.Value); - } - - if (filter.IsResumable.HasValue) - { - if (filter.IsResumable.Value) - { - baseQuery = baseQuery - .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(filter.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); - } - } - - var artistQuery = context.BaseItems.Where(w => filter.ArtistIds.Contains(w.Id)); - - if (filter.ArtistIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.AlbumArtistIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); - } - - if (filter.ContributingArtistIds.Length > 0) - { - 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 (filter.AlbumIds.Length > 0) - { - baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => filter.AlbumIds.Contains(e.Id)).Any(f => f.Name == e.Album)); - } - - if (filter.ExcludeArtistIds.Length > 0) - { - 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 (filter.GenreIds.Count > 0) - { - baseQuery = baseQuery - .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 (filter.Genres.Count > 0) - { - var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray(); - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 2 && cleanGenres.Contains(f.CleanValue))); - } - - if (tags.Count > 0) - { - var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray(); - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); - } - - if (excludeTags.Count > 0) - { - var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray(); - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); - } - - if (filter.StudioIds.Length > 0) - { - baseQuery = baseQuery - .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 (filter.OfficialRatings.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.OfficialRatings.Contains(e.OfficialRating)); - } - - if (filter.HasParentalRating ?? false) - { - if (filter.MinParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); - } - - if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value); - } - } - else if (filter.BlockUnratedItems.Length > 0) - { - if (filter.MinParentalRating.HasValue) - { - if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .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 && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) - || e.InheritedParentalRatingValue >= filter.MinParentalRating); - } - } - else - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)); - } - } - else if (filter.MinParentalRating.HasValue) - { - if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .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 >= filter.MinParentalRating.Value); - } - } - else if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value); - } - else if (!filter.HasParentalRating ?? false) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue == null); - } - - if (filter.HasOfficialRating.HasValue) - { - if (filter.HasOfficialRating.Value) - { - baseQuery = baseQuery - .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty); - } - else - { - baseQuery = baseQuery - .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty); - } - } - - if (filter.HasOverview.HasValue) - { - if (filter.HasOverview.Value) - { - baseQuery = baseQuery - .Where(e => e.Overview != null && e.Overview != string.Empty); - } - else - { - baseQuery = baseQuery - .Where(e => e.Overview == null || e.Overview == string.Empty); - } - } - - if (filter.HasOwnerId.HasValue) - { - if (filter.HasOwnerId.Value) - { - baseQuery = baseQuery - .Where(e => e.OwnerId != null); - } - else - { - baseQuery = baseQuery - .Where(e => e.OwnerId == null); - } - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Audio" && e.Language == filter.HasNoAudioTrackWithLanguage)); - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && !e.IsExternal && e.Language == filter.HasNoInternalSubtitleTrackWithLanguage)); - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.IsExternal && e.Language == filter.HasNoExternalSubtitleTrackWithLanguage)); - } - - if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage)) - { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.Language == filter.HasNoSubtitleTrackWithLanguage)); - } - - if (filter.HasSubtitles.HasValue) - { - baseQuery = baseQuery - .Where(e => e.MediaStreams!.Any(e => e.StreamType == "Subtitle") == filter.HasSubtitles.Value); - } - - if (filter.HasChapterImages.HasValue) - { - baseQuery = baseQuery - .Where(e => e.Chapters!.Any(e => e.ImagePath != null) == filter.HasChapterImages.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 (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 (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == 3 && f.CleanValue == e.CleanName)); - } - - if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) - { - baseQuery = baseQuery - .Where(e => !e.Peoples!.Any(f => f.Name == e.Name)); - } - - if (filter.Years.Length == 1) - { - baseQuery = baseQuery - .Where(e => e.ProductionYear == filter.Years[0]); - } - else if (filter.Years.Length > 1) - { - baseQuery = baseQuery - .Where(e => filter.Years.Any(f => f == e.ProductionYear)); - } - - var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing; - if (isVirtualItem.HasValue) - { - baseQuery = baseQuery - .Where(e => e.IsVirtualItem == isVirtualItem.Value); - } - - if (filter.IsSpecialSeason.HasValue) - { - if (filter.IsSpecialSeason.Value) - { - baseQuery = baseQuery - .Where(e => e.IndexNumber == 0); - } - else - { - baseQuery = baseQuery - .Where(e => e.IndexNumber != 0); - } - } - - if (filter.IsUnaired.HasValue) - { - if (filter.IsUnaired.Value) - { - baseQuery = baseQuery - .Where(e => e.PremiereDate >= now); - } - else - { - baseQuery = baseQuery - .Where(e => e.PremiereDate < now); - } - } - - if (filter.MediaTypes.Length == 1) - { - baseQuery = baseQuery - .Where(e => e.MediaType == filter.MediaTypes[0].ToString()); - } - else if (filter.MediaTypes.Length > 1) - { - baseQuery = baseQuery - .Where(e => filter.MediaTypes.Select(f => f.ToString()).Contains(e.MediaType)); - } - - if (filter.ItemIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.ItemIds.Contains(e.Id)); - } - - if (filter.ExcludeItemIds.Length > 0) - { - baseQuery = baseQuery - .Where(e => !filter.ItemIds.Contains(e.Id)); - } - - if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0) - { - baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); - } - - if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); - } - - if (filter.HasImdbId.HasValue) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); - } - - if (filter.HasTmdbId.HasValue) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); - } - - if (filter.HasTvdbId.HasValue) - { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); - } - - var queryTopParentIds = filter.TopParentIds; - - if (queryTopParentIds.Length > 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))); - } - else - { - baseQuery = baseQuery.Where(e => queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); - } - } - - if (filter.AncestorIds.Length > 0) - { - baseQuery = baseQuery.Where(e => e.AncestorIds!.Any(f => filter.AncestorIds.Contains(f.Id))); - } - - if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) - { - baseQuery = baseQuery - .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.AncestorIds!.Any(w => w.ItemId.Equals(f.Id)))); - } - - if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) - { - baseQuery = baseQuery - .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey); - } - - if (filter.ExcludeInheritedTags.Length > 0) - { - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Where(e => e.Type == 6) - .Any(f => filter.ExcludeInheritedTags.Contains(f.CleanValue))); - } - - 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. - if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(e => e.Type == 6) - .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 => filter.IncludeInheritedTags.Contains(f.CleanValue)))); - } - - // A playlist should be accessible to its owner regardless of allowed tags. - else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) - { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(e => e.Type == 6) - .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 => filter.IncludeInheritedTags.Contains(f.CleanValue))); - } - } - - if (filter.SeriesStatuses.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); - } - - if (filter.BoxSetLibraryFolders.Length > 0) - { - baseQuery = baseQuery - .Where(e => filter.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); - } - - if (filter.VideoTypes.Length > 0) - { - var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"" + e + "\""); - baseQuery = baseQuery - .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f, StringComparison.InvariantCultureIgnoreCase))); - } - - if (filter.Is3D.HasValue) - { - if (filter.Is3D.Value) - { - baseQuery = baseQuery - .Where(e => e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); - } - else - { - baseQuery = baseQuery - .Where(e => !e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); - } - } - - if (filter.IsPlaceHolder.HasValue) - { - if (filter.IsPlaceHolder.Value) - { - baseQuery = baseQuery - .Where(e => e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); - } - else - { - baseQuery = baseQuery - .Where(e => !e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); - } - } - - if (filter.HasSpecialFeature.HasValue) - { - if (filter.HasSpecialFeature.Value) - { - baseQuery = baseQuery - .Where(e => e.ExtraIds != null); - } - else - { - baseQuery = baseQuery - .Where(e => e.ExtraIds == null); - } - } - - if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue) - { - if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault()) - { - baseQuery = baseQuery - .Where(e => e.ExtraIds != null); - } - else - { - baseQuery = baseQuery - .Where(e => e.ExtraIds == null); - } - } - - return baseQuery; - } - - /// - /// Gets the type. - /// - /// Name of the type. - /// Type. - /// typeName is null. - private static Type? GetType(string typeName) - { - ArgumentException.ThrowIfNullOrEmpty(typeName); - - return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() - .Select(a => a.GetType(k)) - .FirstOrDefault(t => t is not 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); - cancellationToken.ThrowIfCancellationRequested(); - - var itemsLen = items.Count; - var tuples = new (BaseItemDto Item, List? AncestorIds, BaseItemDto TopParent, string? UserDataKey, List InheritedTags)[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 context = _dbProvider.CreateDbContext(); - foreach (var item in tuples) - { - var entity = Map(item.Item); - context.BaseItems.Add(entity); - - if (item.Item.SupportsAncestors && item.AncestorIds != null) - { - foreach (var ancestorId in item.AncestorIds) - { - context.AncestorIds.Add(new Data.Entities.AncestorId() - { - Item = entity, - AncestorIdText = ancestorId.ToString(), - Id = ancestorId - }); - } - } - - var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); - context.ItemValues.Where(e => e.ItemId.Equals(entity.Id)).ExecuteDelete(); - foreach (var itemValue in itemValues) - { - context.ItemValues.Add(new() - { - Item = entity, - Type = itemValue.MagicNumber, - Value = itemValue.Value, - CleanValue = GetCleanValue(itemValue.Value) - }); - } - } - - context.SaveChanges(true); - } - - /// - public BaseItemDto? RetrieveItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - using var context = _dbProvider.CreateDbContext(); - var item = context.BaseItems.FirstOrDefault(e => e.Id.Equals(id)); - if (item is null) - { - return null; - } - - return DeserialiseBaseItem(item); - } - - /// - /// Maps a Entity to the DTO. - /// - /// The entity. - /// The dto base instance. - /// The dto to map. - public BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto) - { - dto.Id = entity.Id; - dto.ParentId = entity.ParentId.GetValueOrDefault(); - dto.Path = entity.Path; - dto.EndDate = entity.EndDate; - dto.CommunityRating = entity.CommunityRating; - dto.CustomRating = entity.CustomRating; - dto.IndexNumber = entity.IndexNumber; - dto.IsLocked = entity.IsLocked; - dto.Name = entity.Name; - dto.OfficialRating = entity.OfficialRating; - dto.Overview = entity.Overview; - dto.ParentIndexNumber = entity.ParentIndexNumber; - dto.PremiereDate = entity.PremiereDate; - dto.ProductionYear = entity.ProductionYear; - dto.SortName = entity.SortName; - dto.ForcedSortName = entity.ForcedSortName; - dto.RunTimeTicks = entity.RunTimeTicks; - dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage; - dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode; - dto.IsInMixedFolder = entity.IsInMixedFolder; - dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue; - dto.CriticRating = entity.CriticRating; - dto.PresentationUniqueKey = entity.PresentationUniqueKey; - dto.OriginalTitle = entity.OriginalTitle; - dto.Album = entity.Album; - dto.LUFS = entity.LUFS; - dto.NormalizationGain = entity.NormalizationGain; - dto.IsVirtualItem = entity.IsVirtualItem; - dto.ExternalSeriesId = entity.ExternalSeriesId; - dto.Tagline = entity.Tagline; - dto.TotalBitrate = entity.TotalBitrate; - dto.ExternalId = entity.ExternalId; - dto.Size = entity.Size; - dto.Genres = entity.Genres?.Split('|'); - dto.DateCreated = entity.DateCreated.GetValueOrDefault(); - dto.DateModified = entity.DateModified.GetValueOrDefault(); - dto.ChannelId = string.IsNullOrWhiteSpace(entity.ChannelId) ? Guid.Empty : Guid.Parse(entity.ChannelId); - dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault(); - dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault(); - dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : Guid.Parse(entity.OwnerId); - dto.Width = entity.Width.GetValueOrDefault(); - dto.Height = entity.Height.GetValueOrDefault(); - if (entity.Provider is not null) - { - dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); - } - - if (entity.ExtraType is not null) - { - dto.ExtraType = Enum.Parse(entity.ExtraType); - } - - if (entity.LockedFields is not null) - { - List? fields = null; - foreach (var i in entity.LockedFields.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out MetadataField parsedValue)) - { - (fields ??= new List()).Add(parsedValue); - } - } - - dto.LockedFields = fields?.ToArray() ?? Array.Empty(); - } - - if (entity.Audio is not null) - { - dto.Audio = Enum.Parse(entity.Audio); - } - - dto.ExtraIds = entity.ExtraIds?.Split('|').Select(e => Guid.Parse(e)).ToArray(); - dto.ProductionLocations = entity.ProductionLocations?.Split('|'); - dto.Studios = entity.Studios?.Split('|'); - dto.Tags = entity.Tags?.Split('|'); - - if (dto is IHasProgramAttributes hasProgramAttributes) - { - hasProgramAttributes.IsMovie = entity.IsMovie; - hasProgramAttributes.IsSeries = entity.IsSeries; - hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle; - hasProgramAttributes.IsRepeat = entity.IsRepeat; - } - - if (dto is LiveTvChannel liveTvChannel) - { - liveTvChannel.ServiceName = entity.ExternalServiceId; - } - - if (dto is Trailer trailer) - { - List? types = null; - foreach (var i in entity.TrailerTypes.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out TrailerType parsedValue)) - { - (types ??= new List()).Add(parsedValue); - } - } - - trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); - } - - if (dto is Video video) - { - video.PrimaryVersionId = entity.PrimaryVersionId; - } - - if (dto is IHasSeries hasSeriesName) - { - hasSeriesName.SeriesName = entity.SeriesName; - hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault(); - hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey; - } - - if (dto is Episode episode) - { - episode.SeasonName = entity.SeasonName; - episode.SeasonId = entity.SeasonId.GetValueOrDefault(); - } - - if (dto is IHasArtist hasArtists) - { - hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - if (dto is IHasAlbumArtist hasAlbumArtists) - { - hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - if (dto is LiveTvProgram program) - { - program.ShowId = entity.ShowId; - } - - if (entity.Images is not null) - { - dto.ImageInfos = DeserializeImages(entity.Images); - } - - // dto.Type = entity.Type; - // dto.Data = entity.Data; - // dto.MediaType = entity.MediaType; - if (dto is IHasStartDate hasStartDate) - { - hasStartDate.StartDate = entity.StartDate; - } - - // Fields that are present in the DB but are never actually used - // dto.UnratedType = entity.UnratedType; - // dto.TopParentId = entity.TopParentId; - // dto.CleanName = entity.CleanName; - // dto.UserDataKey = entity.UserDataKey; - - if (dto is Folder folder) - { - folder.DateLastMediaAdded = entity.DateLastMediaAdded; - } - - return dto; - } - - /// - /// Maps a Entity to the DTO. - /// - /// The entity. - /// The dto to map. - public BaseItemEntity Map(BaseItemDto dto) - { - var entity = new BaseItemEntity() - { - Type = dto.GetType().ToString(), - }; - entity.Id = dto.Id; - entity.ParentId = dto.ParentId; - entity.Path = GetPathToSave(dto.Path); - entity.EndDate = dto.EndDate.GetValueOrDefault(); - entity.CommunityRating = dto.CommunityRating; - entity.CustomRating = dto.CustomRating; - entity.IndexNumber = dto.IndexNumber; - entity.IsLocked = dto.IsLocked; - entity.Name = dto.Name; - entity.OfficialRating = dto.OfficialRating; - entity.Overview = dto.Overview; - entity.ParentIndexNumber = dto.ParentIndexNumber; - entity.PremiereDate = dto.PremiereDate; - entity.ProductionYear = dto.ProductionYear; - entity.SortName = dto.SortName; - entity.ForcedSortName = dto.ForcedSortName; - entity.RunTimeTicks = dto.RunTimeTicks; - entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage; - entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode; - entity.IsInMixedFolder = dto.IsInMixedFolder; - entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue; - entity.CriticRating = dto.CriticRating; - entity.PresentationUniqueKey = dto.PresentationUniqueKey; - entity.OriginalTitle = dto.OriginalTitle; - entity.Album = dto.Album; - entity.LUFS = dto.LUFS; - entity.NormalizationGain = dto.NormalizationGain; - entity.IsVirtualItem = dto.IsVirtualItem; - entity.ExternalSeriesId = dto.ExternalSeriesId; - entity.Tagline = dto.Tagline; - entity.TotalBitrate = dto.TotalBitrate; - entity.ExternalId = dto.ExternalId; - entity.Size = dto.Size; - entity.Genres = string.Join('|', dto.Genres); - entity.DateCreated = dto.DateCreated; - entity.DateModified = dto.DateModified; - entity.ChannelId = dto.ChannelId.ToString(); - entity.DateLastRefreshed = dto.DateLastRefreshed; - entity.DateLastSaved = dto.DateLastSaved; - entity.OwnerId = dto.OwnerId.ToString(); - entity.Width = dto.Width; - entity.Height = dto.Height; - 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(); - - entity.ExtraIds = string.Join('|', dto.ExtraIds); - entity.ProductionLocations = string.Join('|', dto.ProductionLocations); - entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null; - entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null; - entity.LockedFields = dto.LockedFields is not null ? string.Join('|', dto.LockedFields) : null; - - if (dto is IHasProgramAttributes hasProgramAttributes) - { - entity.IsMovie = hasProgramAttributes.IsMovie; - entity.IsSeries = hasProgramAttributes.IsSeries; - entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle; - entity.IsRepeat = hasProgramAttributes.IsRepeat; - } - - if (dto is LiveTvChannel liveTvChannel) - { - entity.ExternalServiceId = liveTvChannel.ServiceName; - } - - if (dto is Trailer trailer) - { - entity.LockedFields = trailer.LockedFields is not null ? string.Join('|', trailer.LockedFields) : null; - } - - if (dto is Video video) - { - entity.PrimaryVersionId = video.PrimaryVersionId; - } - - if (dto is IHasSeries hasSeriesName) - { - entity.SeriesName = hasSeriesName.SeriesName; - entity.SeriesId = hasSeriesName.SeriesId; - entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey; - } - - if (dto is Episode episode) - { - entity.SeasonName = episode.SeasonName; - entity.SeasonId = episode.SeasonId; - } - - if (dto is IHasArtist hasArtists) - { - entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null; - } - - if (dto is IHasAlbumArtist hasAlbumArtists) - { - entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null; - } - - if (dto is LiveTvProgram program) - { - entity.ShowId = program.ShowId; - } - - if (dto.ImageInfos is not null) - { - entity.Images = SerializeImages(dto.ImageInfos); - } - - // dto.Type = entity.Type; - // dto.Data = entity.Data; - // dto.MediaType = entity.MediaType; - if (dto is IHasStartDate hasStartDate) - { - entity.StartDate = hasStartDate.StartDate; - } - - // Fields that are present in the DB but are never actually used - // dto.UnratedType = entity.UnratedType; - // dto.TopParentId = entity.TopParentId; - // dto.CleanName = entity.CleanName; - // dto.UserDataKey = entity.UserDataKey; - - if (dto is Folder folder) - { - entity.DateLastMediaAdded = folder.DateLastMediaAdded; - entity.IsFolder = folder.IsFolder; - } - - 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."); - 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)) - { - return value; - } - - return value.RemoveDiacritics().ToLowerInvariant(); - } - - 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; - } - - 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('|', '\\')); - } - } - - private string? GetPathToSave(string path) - { - if (path is null) - { - return null; - } - - return _appHost.ReverseVirtualPath(path); - } - - private string RestorePath(string path) - { - return _appHost.ExpandVirtualPath(path); - } - - 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; - } - - 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 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/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs new file mode 100644 index 000000000..a3e617a21 --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -0,0 +1,2233 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +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.Dto; +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.BaseItemEntity; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Handles all storage logic for BaseItems. +/// +/// +/// Initializes a new instance of the class. +/// +/// The db factory. +/// The Application host. +/// The static type lookup. +public sealed class BaseItemRepository(IDbContextFactory dbProvider, IServerApplicationHost appHost, IItemTypeLookup itemTypeLookup) + : IItemRepository, IDisposable +{ + /// + /// 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(); + private bool _disposed; + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + /// + 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.AsNoTracking(), 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(); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, [0, 1], typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, [0], typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) + { + return GetItemValues(filter, [1], typeof(MusicArtist).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter) + { + return GetItemValues(filter, [3], typeof(Studio).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) + { + return GetItemValues(filter, [2], typeof(Genre).FullName!); + } + + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) + { + return GetItemValues(filter, [2], typeof(MusicGenre).FullName!); + } + + /// + public IReadOnlyList GetStudioNames() + { + return GetItemValueNames([3], Array.Empty(), Array.Empty()); + } + + /// + public IReadOnlyList GetAllArtistNames() + { + return GetItemValueNames([0, 1], Array.Empty(), Array.Empty()); + } + + /// + public IReadOnlyList GetMusicGenreNames() + { + return GetItemValueNames( + [2], + new string[] + { + typeof(Audio).FullName!, + typeof(MusicVideo).FullName!, + typeof(MusicAlbum).FullName!, + typeof(MusicArtist).FullName! + }, + Array.Empty()); + } + + /// + public IReadOnlyList GetGenreNames() + { + return GetItemValueNames( + [2], + Array.Empty(), + new string[] + { + typeof(Audio).FullName!, + typeof(MusicVideo).FullName!, + typeof(MusicAlbum).FullName!, + typeof(MusicArtist).FullName! + }); + } + + /// + public QueryResult GetItems(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0)) + { + var returnList = GetItemList(filter); + return new QueryResult( + filter.StartIndex, + returnList.Count, + returnList); + } + + PrepareFilterQuery(filter); + var result = new QueryResult(); + + using var context = dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter) + .DistinctBy(e => e.Id); + if (filter.EnableTotalRecordCount) + { + result.TotalRecordCount = dbQuery.Count(); + } + + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (filter.Limit.HasValue) + { + dbQuery = dbQuery.Take(filter.Limit.Value); + } + } + + result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + result.StartIndex = filter.StartIndex ?? 0; + return result; + } + + /// + public IReadOnlyList GetItemList(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + PrepareFilterQuery(filter); + + using var context = dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter) + .DistinctBy(e => e.Id); + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (filter.Limit.HasValue) + { + dbQuery = dbQuery.Take(filter.Limit.Value); + } + } + + return dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + } + + /// + public int GetCount(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + // Hack for right now since we currently don't support filtering out these duplicates within a query + PrepareFilterQuery(filter); + + using var context = dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter); + + return dbQuery.Count(); + } + + private IQueryable TranslateQuery( + IQueryable baseQuery, + JellyfinDbContext context, + InternalItemsQuery filter) + { + var minWidth = filter.MinWidth; + var maxWidth = filter.MaxWidth; + var now = DateTime.UtcNow; + + if (filter.IsHD.HasValue) + { + const int Threshold = 1200; + if (filter.IsHD.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (filter.Is4K.HasValue) + { + const int Threshold = 3800; + if (filter.Is4K.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (minWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= minWidth); + } + + if (filter.MinHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight); + } + + if (maxWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= maxWidth); + } + + if (filter.MaxHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight); + } + + if (filter.IsLocked.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsLocked == filter.IsLocked); + } + + var tags = filter.Tags.ToList(); + var excludeTags = filter.ExcludeTags.ToList(); + + if (filter.IsMovie == true) + { + if (filter.IncludeItemTypes.Length == 0 + || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) + || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + { + baseQuery = baseQuery.Where(e => e.IsMovie); + } + } + else if (filter.IsMovie.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); + } + + if (filter.IsSeries.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsSeries == filter.IsSeries); + } + + if (filter.IsSports.HasValue) + { + if (filter.IsSports.Value) + { + tags.Add("Sports"); + } + else + { + excludeTags.Add("Sports"); + } + } + + if (filter.IsNews.HasValue) + { + if (filter.IsNews.Value) + { + tags.Add("News"); + } + else + { + excludeTags.Add("News"); + } + } + + if (filter.IsKids.HasValue) + { + if (filter.IsKids.Value) + { + tags.Add("Kids"); + } + else + { + excludeTags.Add("Kids"); + } + } + + if (!string.IsNullOrEmpty(filter.SearchTerm)) + { + baseQuery = baseQuery.Where(e => e.CleanName!.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase) || (e.OriginalTitle != null && e.OriginalTitle.Contains(filter.SearchTerm, StringComparison.InvariantCultureIgnoreCase))); + } + + if (filter.IsFolder.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder); + } + + var includeTypes = filter.IncludeItemTypes; + // Only specify excluded types if no included types are specified + if (filter.IncludeItemTypes.Length == 0) + { + var excludeTypes = filter.ExcludeItemTypes; + if (excludeTypes.Length == 1) + { + if (itemTypeLookup.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 (itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) + { + excludeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type)); + } + } + else if (includeTypes.Length == 1) + { + if (itemTypeLookup.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 (itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) + { + includeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); + } + + if (filter.ChannelIds.Count == 1) + { + baseQuery = baseQuery.Where(e => e.ChannelId == filter.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); + } + else if (filter.ChannelIds.Count > 1) + { + baseQuery = baseQuery.Where(e => filter.ChannelIds.Select(f => f.ToString("N", CultureInfo.InvariantCulture)).Contains(e.ChannelId)); + } + + if (!filter.ParentId.IsEmpty()) + { + baseQuery = baseQuery.Where(e => e.ParentId.Equals(filter.ParentId)); + } + + if (!string.IsNullOrWhiteSpace(filter.Path)) + { + baseQuery = baseQuery.Where(e => e.Path == filter.Path); + } + + if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) + { + baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == filter.PresentationUniqueKey); + } + + if (filter.MinCommunityRating.HasValue) + { + baseQuery = baseQuery.Where(e => e.CommunityRating >= filter.MinCommunityRating); + } + + if (filter.MinIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber >= filter.MinIndexNumber); + } + + if (filter.MinParentAndIndexNumber.HasValue) + { + baseQuery = baseQuery + .Where(e => (e.ParentIndexNumber == filter.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= filter.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > filter.MinParentAndIndexNumber.Value.ParentIndexNumber); + } + + if (filter.MinDateCreated.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateCreated >= filter.MinDateCreated); + } + + if (filter.MinDateLastSaved.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSaved.Value); + } + + if (filter.MinDateLastSavedForUser.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= filter.MinDateLastSavedForUser.Value); + } + + if (filter.IndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber == filter.IndexNumber.Value); + } + + if (filter.ParentIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber == filter.ParentIndexNumber.Value); + } + + if (filter.ParentIndexNumberNotEquals.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber != filter.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); + } + + var minEndDate = filter.MinEndDate; + var maxEndDate = filter.MaxEndDate; + + if (filter.HasAired.HasValue) + { + if (filter.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 (filter.MinStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate >= filter.MinStartDate.Value); + } + + if (filter.MaxStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate <= filter.MaxStartDate.Value); + } + + if (filter.MinPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value); + } + + if (filter.MaxPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MaxPremiereDate.Value); + } + + if (filter.TrailerTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => filter.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); + } + + if (filter.IsAiring.HasValue) + { + if (filter.IsAiring.Value) + { + baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now); + } + else + { + baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now); + } + } + + if (filter.PersonIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => + 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(filter.Person)) + { + baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == filter.Person)); + } + + if (!string.IsNullOrWhiteSpace(filter.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(filter.ExternalSeriesId)) + { + baseQuery = baseQuery.Where(e => e.ExternalSeriesId == filter.ExternalSeriesId); + } + + if (!string.IsNullOrWhiteSpace(filter.ExternalId)) + { + baseQuery = baseQuery.Where(e => e.ExternalId == filter.ExternalId); + } + + if (!string.IsNullOrWhiteSpace(filter.Name)) + { + var cleanName = GetCleanValue(filter.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } + + // These are the same, for now + var nameContains = filter.NameContains; + if (!string.IsNullOrWhiteSpace(nameContains)) + { + baseQuery = baseQuery.Where(e => + e.CleanName == filter.NameContains + || e.OriginalTitle!.Contains(filter.NameContains!, StringComparison.Ordinal)); + } + + if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) + { + baseQuery = baseQuery.Where(e => e.SortName!.Contains(filter.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] > filter.NameStartsWithOrGreater[0]); + } + + if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] < filter.NameLessThan[0]); + } + + if (filter.ImageTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => filter.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); + } + + if (filter.IsLiked.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); + } + + if (filter.IsFavoriteOrLiked.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavoriteOrLiked); + } + + if (filter.IsFavorite.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == filter.IsFavorite); + } + + if (filter.IsPlayed.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(filter.User!.Id) && f.Key == e.UserDataKey)!.Played == filter.IsPlayed.Value); + } + + if (filter.IsResumable.HasValue) + { + if (filter.IsResumable.Value) + { + baseQuery = baseQuery + .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(filter.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); + } + } + + var artistQuery = context.BaseItems.Where(w => filter.ArtistIds.Contains(w.Id)); + + if (filter.ArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type <= 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.AlbumArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 1 && artistQuery.Any(w => w.CleanName == f.CleanValue))); + } + + if (filter.ContributingArtistIds.Length > 0) + { + 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 (filter.AlbumIds.Length > 0) + { + baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => filter.AlbumIds.Contains(e.Id)).Any(f => f.Name == e.Album)); + } + + if (filter.ExcludeArtistIds.Length > 0) + { + 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 (filter.GenreIds.Count > 0) + { + baseQuery = baseQuery + .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 (filter.Genres.Count > 0) + { + var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray(); + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 2 && cleanGenres.Contains(f.CleanValue))); + } + + if (tags.Count > 0) + { + var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray(); + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); + } + + if (excludeTags.Count > 0) + { + var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray(); + baseQuery = baseQuery + .Where(e => !e.ItemValues!.Any(f => f.Type == 4 && cleanValues.Contains(f.CleanValue))); + } + + if (filter.StudioIds.Length > 0) + { + baseQuery = baseQuery + .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 (filter.OfficialRatings.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.OfficialRatings.Contains(e.OfficialRating)); + } + + if (filter.HasParentalRating ?? false) + { + if (filter.MinParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); + } + + if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value); + } + } + else if (filter.BlockUnratedItems.Length > 0) + { + if (filter.MinParentalRating.HasValue) + { + if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .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 && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)) + || e.InheritedParentalRatingValue >= filter.MinParentalRating); + } + } + else + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue != null && !filter.BlockUnratedItems.Select(e => e.ToString()).Contains(e.UnratedType)); + } + } + else if (filter.MinParentalRating.HasValue) + { + if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .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 >= filter.MinParentalRating.Value); + } + } + else if (filter.MaxParentalRating.HasValue) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value); + } + else if (!filter.HasParentalRating ?? false) + { + baseQuery = baseQuery + .Where(e => e.InheritedParentalRatingValue == null); + } + + if (filter.HasOfficialRating.HasValue) + { + if (filter.HasOfficialRating.Value) + { + baseQuery = baseQuery + .Where(e => e.OfficialRating != null && e.OfficialRating != string.Empty); + } + else + { + baseQuery = baseQuery + .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty); + } + } + + if (filter.HasOverview.HasValue) + { + if (filter.HasOverview.Value) + { + baseQuery = baseQuery + .Where(e => e.Overview != null && e.Overview != string.Empty); + } + else + { + baseQuery = baseQuery + .Where(e => e.Overview == null || e.Overview == string.Empty); + } + } + + if (filter.HasOwnerId.HasValue) + { + if (filter.HasOwnerId.Value) + { + baseQuery = baseQuery + .Where(e => e.OwnerId != null); + } + else + { + baseQuery = baseQuery + .Where(e => e.OwnerId == null); + } + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Audio" && e.Language == filter.HasNoAudioTrackWithLanguage)); + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && !e.IsExternal && e.Language == filter.HasNoInternalSubtitleTrackWithLanguage)); + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.IsExternal && e.Language == filter.HasNoExternalSubtitleTrackWithLanguage)); + } + + if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage)) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(e => e.StreamType == "Subtitle" && e.Language == filter.HasNoSubtitleTrackWithLanguage)); + } + + if (filter.HasSubtitles.HasValue) + { + baseQuery = baseQuery + .Where(e => e.MediaStreams!.Any(e => e.StreamType == "Subtitle") == filter.HasSubtitles.Value); + } + + if (filter.HasChapterImages.HasValue) + { + baseQuery = baseQuery + .Where(e => e.Chapters!.Any(e => e.ImagePath != null) == filter.HasChapterImages.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 (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 (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 3 && f.CleanValue == e.CleanName)); + } + + if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) + { + baseQuery = baseQuery + .Where(e => !e.Peoples!.Any(f => f.Name == e.Name)); + } + + if (filter.Years.Length == 1) + { + baseQuery = baseQuery + .Where(e => e.ProductionYear == filter.Years[0]); + } + else if (filter.Years.Length > 1) + { + baseQuery = baseQuery + .Where(e => filter.Years.Any(f => f == e.ProductionYear)); + } + + var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing; + if (isVirtualItem.HasValue) + { + baseQuery = baseQuery + .Where(e => e.IsVirtualItem == isVirtualItem.Value); + } + + if (filter.IsSpecialSeason.HasValue) + { + if (filter.IsSpecialSeason.Value) + { + baseQuery = baseQuery + .Where(e => e.IndexNumber == 0); + } + else + { + baseQuery = baseQuery + .Where(e => e.IndexNumber != 0); + } + } + + if (filter.IsUnaired.HasValue) + { + if (filter.IsUnaired.Value) + { + baseQuery = baseQuery + .Where(e => e.PremiereDate >= now); + } + else + { + baseQuery = baseQuery + .Where(e => e.PremiereDate < now); + } + } + + if (filter.MediaTypes.Length == 1) + { + baseQuery = baseQuery + .Where(e => e.MediaType == filter.MediaTypes[0].ToString()); + } + else if (filter.MediaTypes.Length > 1) + { + baseQuery = baseQuery + .Where(e => filter.MediaTypes.Select(f => f.ToString()).Contains(e.MediaType)); + } + + if (filter.ItemIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.ItemIds.Contains(e.Id)); + } + + if (filter.ExcludeItemIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => !filter.ItemIds.Contains(e.Id)); + } + + if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0) + { + baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); + } + + if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value))); + } + + if (filter.HasImdbId.HasValue) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); + } + + if (filter.HasTmdbId.HasValue) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); + } + + if (filter.HasTvdbId.HasValue) + { + baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); + } + + var queryTopParentIds = filter.TopParentIds; + + if (queryTopParentIds.Length > 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))); + } + else + { + baseQuery = baseQuery.Where(e => queryTopParentIds.Any(w => w.Equals(e.TopParentId!.Value))); + } + } + + if (filter.AncestorIds.Length > 0) + { + baseQuery = baseQuery.Where(e => e.AncestorIds!.Any(f => filter.AncestorIds.Contains(f.Id))); + } + + if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) + { + baseQuery = baseQuery + .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.AncestorIds!.Any(w => w.ItemId.Equals(f.Id)))); + } + + if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) + { + baseQuery = baseQuery + .Where(e => e.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey); + } + + if (filter.ExcludeInheritedTags.Length > 0) + { + baseQuery = baseQuery + .Where(e => !e.ItemValues!.Where(e => e.Type == 6) + .Any(f => filter.ExcludeInheritedTags.Contains(f.CleanValue))); + } + + 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. + if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Where(e => e.Type == 6) + .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 => filter.IncludeInheritedTags.Contains(f.CleanValue)))); + } + + // A playlist should be accessible to its owner regardless of allowed tags. + else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Where(e => e.Type == 6) + .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 => filter.IncludeInheritedTags.Contains(f.CleanValue))); + } + } + + if (filter.SeriesStatuses.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.SeriesStatuses.Any(f => e.Data!.Contains(f.ToString(), StringComparison.InvariantCultureIgnoreCase))); + } + + if (filter.BoxSetLibraryFolders.Length > 0) + { + baseQuery = baseQuery + .Where(e => filter.BoxSetLibraryFolders.Any(f => e.Data!.Contains(f.ToString("N", CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))); + } + + if (filter.VideoTypes.Length > 0) + { + var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"" + e + "\""); + baseQuery = baseQuery + .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f, StringComparison.InvariantCultureIgnoreCase))); + } + + if (filter.Is3D.HasValue) + { + if (filter.Is3D.Value) + { + baseQuery = baseQuery + .Where(e => e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); + } + else + { + baseQuery = baseQuery + .Where(e => !e.Data!.Contains("Video3DFormat", StringComparison.InvariantCultureIgnoreCase)); + } + } + + if (filter.IsPlaceHolder.HasValue) + { + if (filter.IsPlaceHolder.Value) + { + baseQuery = baseQuery + .Where(e => e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); + } + else + { + baseQuery = baseQuery + .Where(e => !e.Data!.Contains("IsPlaceHolder\":true", StringComparison.InvariantCultureIgnoreCase)); + } + } + + if (filter.HasSpecialFeature.HasValue) + { + if (filter.HasSpecialFeature.Value) + { + baseQuery = baseQuery + .Where(e => e.ExtraIds != null); + } + else + { + baseQuery = baseQuery + .Where(e => e.ExtraIds == null); + } + } + + if (filter.HasTrailer.HasValue || filter.HasThemeSong.HasValue || filter.HasThemeVideo.HasValue) + { + if (filter.HasTrailer.GetValueOrDefault() || filter.HasThemeSong.GetValueOrDefault() || filter.HasThemeVideo.GetValueOrDefault()) + { + baseQuery = baseQuery + .Where(e => e.ExtraIds != null); + } + else + { + baseQuery = baseQuery + .Where(e => e.ExtraIds == null); + } + } + + return baseQuery; + } + + /// + /// Gets the type. + /// + /// Name of the type. + /// Type. + /// typeName is null. + private static Type? GetType(string typeName) + { + ArgumentException.ThrowIfNullOrEmpty(typeName); + + return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() + .Select(a => a.GetType(k)) + .FirstOrDefault(t => t is not 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); + cancellationToken.ThrowIfCancellationRequested(); + + var itemsLen = items.Count; + var tuples = new (BaseItemDto Item, List? AncestorIds, BaseItemDto TopParent, string? UserDataKey, List InheritedTags)[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 context = dbProvider.CreateDbContext(); + foreach (var item in tuples) + { + var entity = Map(item.Item); + context.BaseItems.Add(entity); + + if (item.Item.SupportsAncestors && item.AncestorIds != null) + { + foreach (var ancestorId in item.AncestorIds) + { + context.AncestorIds.Add(new Data.Entities.AncestorId() + { + Item = entity, + AncestorIdText = ancestorId.ToString(), + Id = ancestorId + }); + } + } + + var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); + context.ItemValues.Where(e => e.ItemId.Equals(entity.Id)).ExecuteDelete(); + foreach (var itemValue in itemValues) + { + context.ItemValues.Add(new() + { + Item = entity, + Type = itemValue.MagicNumber, + Value = itemValue.Value, + CleanValue = GetCleanValue(itemValue.Value) + }); + } + } + + context.SaveChanges(true); + } + + /// + public BaseItemDto? RetrieveItem(Guid id) + { + if (id.IsEmpty()) + { + throw new ArgumentException("Guid can't be empty", nameof(id)); + } + + using var context = dbProvider.CreateDbContext(); + var item = context.BaseItems.FirstOrDefault(e => e.Id.Equals(id)); + if (item is null) + { + return null; + } + + return DeserialiseBaseItem(item); + } + + /// + /// Maps a Entity to the DTO. + /// + /// The entity. + /// The dto base instance. + /// The dto to map. + public BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto) + { + dto.Id = entity.Id; + dto.ParentId = entity.ParentId.GetValueOrDefault(); + dto.Path = entity.Path; + dto.EndDate = entity.EndDate; + dto.CommunityRating = entity.CommunityRating; + dto.CustomRating = entity.CustomRating; + dto.IndexNumber = entity.IndexNumber; + dto.IsLocked = entity.IsLocked; + dto.Name = entity.Name; + dto.OfficialRating = entity.OfficialRating; + dto.Overview = entity.Overview; + dto.ParentIndexNumber = entity.ParentIndexNumber; + dto.PremiereDate = entity.PremiereDate; + dto.ProductionYear = entity.ProductionYear; + dto.SortName = entity.SortName; + dto.ForcedSortName = entity.ForcedSortName; + dto.RunTimeTicks = entity.RunTimeTicks; + dto.PreferredMetadataLanguage = entity.PreferredMetadataLanguage; + dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode; + dto.IsInMixedFolder = entity.IsInMixedFolder; + dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue; + dto.CriticRating = entity.CriticRating; + dto.PresentationUniqueKey = entity.PresentationUniqueKey; + dto.OriginalTitle = entity.OriginalTitle; + dto.Album = entity.Album; + dto.LUFS = entity.LUFS; + dto.NormalizationGain = entity.NormalizationGain; + dto.IsVirtualItem = entity.IsVirtualItem; + dto.ExternalSeriesId = entity.ExternalSeriesId; + dto.Tagline = entity.Tagline; + dto.TotalBitrate = entity.TotalBitrate; + dto.ExternalId = entity.ExternalId; + dto.Size = entity.Size; + dto.Genres = entity.Genres?.Split('|'); + dto.DateCreated = entity.DateCreated.GetValueOrDefault(); + dto.DateModified = entity.DateModified.GetValueOrDefault(); + dto.ChannelId = string.IsNullOrWhiteSpace(entity.ChannelId) ? Guid.Empty : Guid.Parse(entity.ChannelId); + dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault(); + dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault(); + dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : Guid.Parse(entity.OwnerId); + dto.Width = entity.Width.GetValueOrDefault(); + dto.Height = entity.Height.GetValueOrDefault(); + if (entity.Provider is not null) + { + dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); + } + + if (entity.ExtraType is not null) + { + dto.ExtraType = Enum.Parse(entity.ExtraType); + } + + if (entity.LockedFields is not null) + { + List? fields = null; + foreach (var i in entity.LockedFields.AsSpan().Split('|')) + { + if (Enum.TryParse(i, true, out MetadataField parsedValue)) + { + (fields ??= new List()).Add(parsedValue); + } + } + + dto.LockedFields = fields?.ToArray() ?? Array.Empty(); + } + + if (entity.Audio is not null) + { + dto.Audio = Enum.Parse(entity.Audio); + } + + dto.ExtraIds = entity.ExtraIds?.Split('|').Select(e => Guid.Parse(e)).ToArray(); + dto.ProductionLocations = entity.ProductionLocations?.Split('|'); + dto.Studios = entity.Studios?.Split('|'); + dto.Tags = entity.Tags?.Split('|'); + + if (dto is IHasProgramAttributes hasProgramAttributes) + { + hasProgramAttributes.IsMovie = entity.IsMovie; + hasProgramAttributes.IsSeries = entity.IsSeries; + hasProgramAttributes.EpisodeTitle = entity.EpisodeTitle; + hasProgramAttributes.IsRepeat = entity.IsRepeat; + } + + if (dto is LiveTvChannel liveTvChannel) + { + liveTvChannel.ServiceName = entity.ExternalServiceId; + } + + if (dto is Trailer trailer) + { + List? types = null; + foreach (var i in entity.TrailerTypes.AsSpan().Split('|')) + { + if (Enum.TryParse(i, true, out TrailerType parsedValue)) + { + (types ??= new List()).Add(parsedValue); + } + } + + trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); + } + + if (dto is Video video) + { + video.PrimaryVersionId = entity.PrimaryVersionId; + } + + if (dto is IHasSeries hasSeriesName) + { + hasSeriesName.SeriesName = entity.SeriesName; + hasSeriesName.SeriesId = entity.SeriesId.GetValueOrDefault(); + hasSeriesName.SeriesPresentationUniqueKey = entity.SeriesPresentationUniqueKey; + } + + if (dto is Episode episode) + { + episode.SeasonName = entity.SeasonName; + episode.SeasonId = entity.SeasonId.GetValueOrDefault(); + } + + if (dto is IHasArtist hasArtists) + { + hasArtists.Artists = entity.Artists?.Split('|', StringSplitOptions.RemoveEmptyEntries); + } + + if (dto is IHasAlbumArtist hasAlbumArtists) + { + hasAlbumArtists.AlbumArtists = entity.AlbumArtists?.Split('|', StringSplitOptions.RemoveEmptyEntries); + } + + if (dto is LiveTvProgram program) + { + program.ShowId = entity.ShowId; + } + + if (entity.Images is not null) + { + dto.ImageInfos = DeserializeImages(entity.Images); + } + + // dto.Type = entity.Type; + // dto.Data = entity.Data; + // dto.MediaType = entity.MediaType; + if (dto is IHasStartDate hasStartDate) + { + hasStartDate.StartDate = entity.StartDate; + } + + // Fields that are present in the DB but are never actually used + // dto.UnratedType = entity.UnratedType; + // dto.TopParentId = entity.TopParentId; + // dto.CleanName = entity.CleanName; + // dto.UserDataKey = entity.UserDataKey; + + if (dto is Folder folder) + { + folder.DateLastMediaAdded = entity.DateLastMediaAdded; + } + + return dto; + } + + /// + /// Maps a Entity to the DTO. + /// + /// The entity. + /// The dto to map. + public BaseItemEntity Map(BaseItemDto dto) + { + var entity = new BaseItemEntity() + { + Type = dto.GetType().ToString(), + }; + entity.Id = dto.Id; + entity.ParentId = dto.ParentId; + entity.Path = GetPathToSave(dto.Path); + entity.EndDate = dto.EndDate.GetValueOrDefault(); + entity.CommunityRating = dto.CommunityRating; + entity.CustomRating = dto.CustomRating; + entity.IndexNumber = dto.IndexNumber; + entity.IsLocked = dto.IsLocked; + entity.Name = dto.Name; + entity.OfficialRating = dto.OfficialRating; + entity.Overview = dto.Overview; + entity.ParentIndexNumber = dto.ParentIndexNumber; + entity.PremiereDate = dto.PremiereDate; + entity.ProductionYear = dto.ProductionYear; + entity.SortName = dto.SortName; + entity.ForcedSortName = dto.ForcedSortName; + entity.RunTimeTicks = dto.RunTimeTicks; + entity.PreferredMetadataLanguage = dto.PreferredMetadataLanguage; + entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode; + entity.IsInMixedFolder = dto.IsInMixedFolder; + entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue; + entity.CriticRating = dto.CriticRating; + entity.PresentationUniqueKey = dto.PresentationUniqueKey; + entity.OriginalTitle = dto.OriginalTitle; + entity.Album = dto.Album; + entity.LUFS = dto.LUFS; + entity.NormalizationGain = dto.NormalizationGain; + entity.IsVirtualItem = dto.IsVirtualItem; + entity.ExternalSeriesId = dto.ExternalSeriesId; + entity.Tagline = dto.Tagline; + entity.TotalBitrate = dto.TotalBitrate; + entity.ExternalId = dto.ExternalId; + entity.Size = dto.Size; + entity.Genres = string.Join('|', dto.Genres); + entity.DateCreated = dto.DateCreated; + entity.DateModified = dto.DateModified; + entity.ChannelId = dto.ChannelId.ToString(); + entity.DateLastRefreshed = dto.DateLastRefreshed; + entity.DateLastSaved = dto.DateLastSaved; + entity.OwnerId = dto.OwnerId.ToString(); + entity.Width = dto.Width; + entity.Height = dto.Height; + 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(); + + entity.ExtraIds = string.Join('|', dto.ExtraIds); + entity.ProductionLocations = string.Join('|', dto.ProductionLocations); + entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null; + entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null; + entity.LockedFields = dto.LockedFields is not null ? string.Join('|', dto.LockedFields) : null; + + if (dto is IHasProgramAttributes hasProgramAttributes) + { + entity.IsMovie = hasProgramAttributes.IsMovie; + entity.IsSeries = hasProgramAttributes.IsSeries; + entity.EpisodeTitle = hasProgramAttributes.EpisodeTitle; + entity.IsRepeat = hasProgramAttributes.IsRepeat; + } + + if (dto is LiveTvChannel liveTvChannel) + { + entity.ExternalServiceId = liveTvChannel.ServiceName; + } + + if (dto is Trailer trailer) + { + entity.LockedFields = trailer.LockedFields is not null ? string.Join('|', trailer.LockedFields) : null; + } + + if (dto is Video video) + { + entity.PrimaryVersionId = video.PrimaryVersionId; + } + + if (dto is IHasSeries hasSeriesName) + { + entity.SeriesName = hasSeriesName.SeriesName; + entity.SeriesId = hasSeriesName.SeriesId; + entity.SeriesPresentationUniqueKey = hasSeriesName.SeriesPresentationUniqueKey; + } + + if (dto is Episode episode) + { + entity.SeasonName = episode.SeasonName; + entity.SeasonId = episode.SeasonId; + } + + if (dto is IHasArtist hasArtists) + { + entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null; + } + + if (dto is IHasAlbumArtist hasAlbumArtists) + { + entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null; + } + + if (dto is LiveTvProgram program) + { + entity.ShowId = program.ShowId; + } + + if (dto.ImageInfos is not null) + { + entity.Images = SerializeImages(dto.ImageInfos); + } + + // dto.Type = entity.Type; + // dto.Data = entity.Data; + // dto.MediaType = entity.MediaType; + if (dto is IHasStartDate hasStartDate) + { + entity.StartDate = hasStartDate.StartDate; + } + + // Fields that are present in the DB but are never actually used + // dto.UnratedType = entity.UnratedType; + // dto.TopParentId = entity.TopParentId; + // dto.CleanName = entity.CleanName; + // dto.UserDataKey = entity.UserDataKey; + + if (dto is Folder folder) + { + entity.DateLastMediaAdded = folder.DateLastMediaAdded; + entity.IsFolder = folder.IsFolder; + } + + 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."); + var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + return Map(baseItemEntity, dto); + } + + 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; + + if (offset > 0) + { + query = query.Skip(offset); + } + + if (filter.Limit.HasValue) + { + query.Take(filter.Limit.Value); + } + } + + var result = new QueryResult<(BaseItem, ItemCounts)>(); + string countText = string.Empty; + if (filter.EnableTotalRecordCount) + { + result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count(); + } + + var resultQuery = query.Select(e => new + { + item = e, + itemCount = new ItemCounts() + { + 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), + } + }); + + result.StartIndex = filter.StartIndex ?? 0; + result.Items = resultQuery.ToImmutableArray().Select(e => + { + return (DeserialiseBaseItem(e.item), e.itemCount); + }).ToImmutableArray(); + + return result; + } + + 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)) + { + return value; + } + + return value.RemoveDiacritics().ToLowerInvariant(); + } + + 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; + } + + 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('|', '\\')); + } + } + + private string? GetPathToSave(string path) + { + if (path is null) + { + return null; + } + + return appHost.ReverseVirtualPath(path); + } + + private string RestorePath(string path) + { + return appHost.ExpandVirtualPath(path); + } + + 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; + } + + 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 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/ChapterManager.cs b/Jellyfin.Server.Implementations/Item/ChapterManager.cs deleted file mode 100644 index 7b0f98fde..000000000 --- a/Jellyfin.Server.Implementations/Item/ChapterManager.cs +++ /dev/null @@ -1,99 +0,0 @@ -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; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// The Chapter manager. -/// -public class ChapterManager : IChapterManager -{ - private readonly IDbContextFactory _dbProvider; - private readonly IImageProcessor _imageProcessor; - - /// - /// Initializes a new instance of the class. - /// - /// The EFCore provider. - /// The Image Processor. - public ChapterManager(IDbContextFactory dbProvider, IImageProcessor imageProcessor) - { - _dbProvider = dbProvider; - _imageProcessor = imageProcessor; - } - - /// - public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) - { - using var context = _dbProvider.CreateDbContext(); - 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) - { - return new Chapter() - { - ChapterIndex = index, - StartPositionTicks = chapterInfo.StartPositionTicks, - ImageDateModified = chapterInfo.ImageDateModified, - ImagePath = chapterInfo.ImagePath, - ItemId = itemId, - Name = chapterInfo.Name - }; - } - - private ChapterInfo Map(Chapter chapterInfo, BaseItemDto baseItem) - { - var info = new ChapterInfo() - { - StartPositionTicks = chapterInfo.StartPositionTicks, - ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(), - ImagePath = chapterInfo.ImagePath, - Name = chapterInfo.Name, - }; - info.ImageTag = _imageProcessor.GetImageCacheTag(baseItem, info); - return info; - } -} diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs new file mode 100644 index 000000000..d215a1d7a --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -0,0 +1,123 @@ +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; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// The Chapter manager. +/// +public class ChapterRepository : IChapterRepository +{ + private readonly IDbContextFactory _dbProvider; + private readonly IImageProcessor _imageProcessor; + + /// + /// Initializes a new instance of the class. + /// + /// The EFCore provider. + /// The Image Processor. + public ChapterRepository(IDbContextFactory dbProvider, IImageProcessor imageProcessor) + { + _dbProvider = dbProvider; + _imageProcessor = imageProcessor; + } + + /// + public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) + { + return GetChapter(baseItem.Id, index); + } + + /// + public IReadOnlyList GetChapters(BaseItemDto baseItem) + { + return GetChapters(baseItem.Id); + } + + /// + public ChapterInfo? GetChapter(Guid baseItemId, int index) + { + using var context = _dbProvider.CreateDbContext(); + var chapter = context.Chapters + .Select(e => new + { + chapter = e, + baseItemPath = e.Item.Path + }) + .FirstOrDefault(e => e.chapter.ItemId.Equals(baseItemId) && e.chapter.ChapterIndex == index); + if (chapter is not null) + { + return Map(chapter.chapter, chapter.baseItemPath!); + } + + return null; + } + + /// + public IReadOnlyList GetChapters(Guid baseItemId) + { + using var context = _dbProvider.CreateDbContext(); + return context.Chapters.Where(e => e.ItemId.Equals(baseItemId)) + .Select(e => new + { + chapter = e, + baseItemPath = e.Item.Path + }) + .ToList() + .Select(e => Map(e.chapter, e.baseItemPath!)) + .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) + { + return new Chapter() + { + ChapterIndex = index, + StartPositionTicks = chapterInfo.StartPositionTicks, + ImageDateModified = chapterInfo.ImageDateModified, + ImagePath = chapterInfo.ImagePath, + ItemId = itemId, + Name = chapterInfo.Name, + Item = null! + }; + } + + private ChapterInfo Map(Chapter chapterInfo, string baseItemPath) + { + var chapterEntity = new ChapterInfo() + { + StartPositionTicks = chapterInfo.StartPositionTicks, + ImageDateModified = chapterInfo.ImageDateModified.GetValueOrDefault(), + ImagePath = chapterInfo.ImagePath, + Name = chapterInfo.Name, + }; + chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified); + return chapterEntity; + } +} diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs deleted file mode 100644 index 288b1943e..000000000 --- a/Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs +++ /dev/null @@ -1,73 +0,0 @@ -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/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs new file mode 100644 index 000000000..70c5ff1e2 --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.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 MediaAttachmentRepository(IDbContextFactory dbProvider) : IMediaAttachmentRepository +{ + /// + 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 deleted file mode 100644 index b7124283a..000000000 --- a/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs +++ /dev/null @@ -1,201 +0,0 @@ -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) : IMediaStreamManager -{ - /// - 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/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs new file mode 100644 index 000000000..f7b714c29 --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.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. +/// +/// The EFCore db factory. +/// The Application host. +/// The Localisation Provider. +public class MediaStreamRepository(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) : IMediaStreamRepository +{ + /// + 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 deleted file mode 100644 index d29d8b143..000000000 --- a/Jellyfin.Server.Implementations/Item/PeopleManager.cs +++ /dev/null @@ -1,164 +0,0 @@ -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 MediaBrowser.Controller.Persistence; -using Microsoft.EntityFrameworkCore; - -namespace Jellyfin.Server.Implementations.Item; - -/// -/// 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 = 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); - } -} diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs new file mode 100644 index 000000000..3ced6e24e --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -0,0 +1,165 @@ +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 MediaBrowser.Controller.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Manager for handling people. +/// +/// Efcore Factory. +/// +/// Initializes a new instance of the class. +/// +public class PeopleRepository(IDbContextFactory dbProvider) : IPeopleRepository +{ + private readonly IDbContextFactory _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); + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index fcc20a0d4..c1d6d58cd 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -106,7 +106,7 @@ public class JellyfinDbContext : DbContext /// /// Gets the containing the user data. /// - public DbSet BaseItems => Set(); + public DbSet BaseItems => Set(); /// /// Gets the containing the user data. diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs index c0f09670d..4aba9d07e 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -8,10 +8,10 @@ namespace Jellyfin.Server.Implementations.ModelConfiguration; /// /// Configuration for BaseItem. /// -public class BaseItemConfiguration : IEntityTypeConfiguration +public class BaseItemConfiguration : IEntityTypeConfiguration { /// - public void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { builder.HasNoKey(); builder.HasIndex(e => e.Path); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs index f34837c57..d15049a1f 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs @@ -13,7 +13,7 @@ public class BaseItemProviderConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { - builder.HasNoKey(); + builder.HasKey(e => new { e.ItemId, e.ProviderId }); builder.HasOne(e => e.Item); builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId }); } diff --git a/MediaBrowser.Controller/Chapters/ChapterManager.cs b/MediaBrowser.Controller/Chapters/ChapterManager.cs deleted file mode 100644 index a9e11f603..000000000 --- a/MediaBrowser.Controller/Chapters/ChapterManager.cs +++ /dev/null @@ -1,24 +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 - { - 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 deleted file mode 100644 index 55762c7fc..000000000 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Chapters -{ - /// - /// Interface IChapterManager. - /// - public interface IChapterManager - { - /// - /// Saves the 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/Chapters/IChapterRepository.cs b/MediaBrowser.Controller/Chapters/IChapterRepository.cs new file mode 100644 index 000000000..e22cb0f58 --- /dev/null +++ b/MediaBrowser.Controller/Chapters/IChapterRepository.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Chapters; + +/// +/// Interface IChapterManager. +/// +public interface IChapterRepository +{ + /// + /// Saves the 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); + + /// + /// Gets all chapters associated with the baseItem. + /// + /// The BaseItems id. + /// A readonly list of chapter instances. + IReadOnlyList GetChapters(Guid baseItemId); + + /// + /// Gets a single chapter of a BaseItem on a specific index. + /// + /// The BaseItems id. + /// The index of that chapter. + /// A chapter instance. + ChapterInfo? GetChapter(Guid baseItemId, int index); +} diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 0d1e2a5a0..702ce39a2 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing @@ -57,6 +58,22 @@ namespace MediaBrowser.Controller.Drawing /// BlurHash. string GetImageBlurHash(string path, ImageDimensions imageDimensions); + /// + /// Gets the image cache tag. + /// + /// The items basePath. + /// The image last modification date. + /// Guid. + string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified); + + /// + /// Gets the image cache tag. + /// + /// The item. + /// The image. + /// Guid. + string? GetImageCacheTag(BaseItemDto item, ChapterInfo image); + /// /// Gets the image cache tag. /// @@ -65,6 +82,14 @@ namespace MediaBrowser.Controller.Drawing /// Guid. string GetImageCacheTag(BaseItem item, ItemImageInfo image); + /// + /// Gets the image cache tag. + /// + /// The item. + /// The image. + /// Guid. + string GetImageCacheTag(BaseItemDto item, ItemImageInfo image); + string? GetImageCacheTag(BaseItem item, ChapterInfo chapter); string? GetImageCacheTag(User user); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index eb605f6c8..a4764dd33 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -16,6 +16,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; @@ -479,6 +480,8 @@ namespace MediaBrowser.Controller.Entities public static IItemRepository ItemRepository { get; set; } + public static IChapterRepository ChapterRepository { get; set; } + public static IFileSystem FileSystem { get; set; } public static IUserDataManager UserDataManager { get; set; } @@ -2031,7 +2034,7 @@ namespace MediaBrowser.Controller.Entities { if (imageType == ImageType.Chapter) { - var chapter = ItemRepository.GetChapter(this, imageIndex); + var chapter = ChapterRepository.GetChapter(this.Id, imageIndex); if (chapter is null) { @@ -2081,7 +2084,7 @@ namespace MediaBrowser.Controller.Entities if (image.Type == ImageType.Chapter) { - var chapters = ItemRepository.GetChapters(this); + var chapters = ChapterRepository.GetChapters(this.Id); for (var i = 0; i < chapters.Count; i++) { if (chapters[i].ImagePath == image.Path) diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 313b1459a..b27f156ef 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -52,7 +52,6 @@ public interface IItemRepository : IDisposable /// List<Guid>. IReadOnlyList GetItemIdsList(InternalItemsQuery filter); - /// /// Gets the item list. /// diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs new file mode 100644 index 000000000..1b2ca2acb --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Persistence; + +/// +/// Provides static lookup data for and for the domain. +/// +public interface IItemTypeLookup +{ + /// + /// Gets all values of the ItemFields type. + /// + public IReadOnlyList AllItemFields { get; } + + /// + /// Gets all BaseItemKinds that are considered Programs. + /// + public IReadOnlyList ProgramTypes { get; } + + /// + /// Gets all BaseItemKinds that should be excluded from parent lookup. + /// + public IReadOnlyList ProgramExcludeParentTypes { get; } + + /// + /// Gets all BaseItemKinds that are considered to be provided by services. + /// + public IReadOnlyList ServiceTypes { get; } + + /// + /// Gets all BaseItemKinds that have a StartDate. + /// + public IReadOnlyList StartDateTypes { get; } + + /// + /// Gets all BaseItemKinds that are considered Series. + /// + public IReadOnlyList SeriesTypes { get; } + + /// + /// Gets all BaseItemKinds that are not to be evaluated for Artists. + /// + public IReadOnlyList ArtistExcludeParentTypes { get; } + + /// + /// Gets all BaseItemKinds that are considered Artists. + /// + public IReadOnlyList ArtistsTypes { get; } + + /// + /// Gets mapping for all BaseItemKinds and their expected serialisaition target. + /// + public IDictionary BaseItemKindNames { get; } +} diff --git a/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs b/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs deleted file mode 100644 index 210d80afa..000000000 --- a/MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs +++ /dev/null @@ -1,29 +0,0 @@ -#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/IMediaAttachmentRepository.cs b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs new file mode 100644 index 000000000..4773f4058 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.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 IMediaAttachmentRepository +{ + /// + /// 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 deleted file mode 100644 index ec7c72935..000000000 --- a/MediaBrowser.Controller/Persistence/IMediaStreamManager.cs +++ /dev/null @@ -1,28 +0,0 @@ -#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/IMediaStreamRepository.cs b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs new file mode 100644 index 000000000..665129eaf --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs @@ -0,0 +1,31 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// +/// Provides methods for accessing MediaStreams. +/// +public interface IMediaStreamRepository +{ + /// + /// Gets the media streams. + /// + /// The query. + /// IEnumerable{MediaStream}. + IReadOnlyList 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 deleted file mode 100644 index 84e503fef..000000000 --- a/MediaBrowser.Controller/Persistence/IPeopleManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -#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); - -} diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs new file mode 100644 index 000000000..43a24703e --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs @@ -0,0 +1,33 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IPeopleRepository +{ + /// + /// 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); +} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 246ba2733..62c590944 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IEncodingManager _encodingManager; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; - private readonly IChapterManager _chapterManager; + private readonly IChapterRepository _chapterManager; private readonly ILibraryManager _libraryManager; private readonly AudioResolver _audioResolver; private readonly SubtitleResolver _subtitleResolver; @@ -54,7 +54,7 @@ namespace MediaBrowser.Providers.MediaInfo IEncodingManager encodingManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager, AudioResolver audioResolver, SubtitleResolver subtitleResolver) diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 04da8fb88..f5e9dddcf 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Providers.MediaInfo /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. /// Instance of the . /// Instance of the interface. @@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.MediaInfo IEncodingManager encodingManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager, IFileSystem fileSystem, ILoggerFactory loggerFactory, diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 5d4732234..b57f2753f 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; @@ -403,10 +404,34 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return _imageEncoder.GetImageBlurHash(xComp, yComp, path); } + /// + public string GetImageCacheTag(string baseItemPath, DateTime imageDateModified) + => (baseItemPath + imageDateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + /// public string GetImageCacheTag(BaseItem item, ItemImageInfo image) => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + /// + public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image) + => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + + /// + public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter) + { + if (chapter.ImagePath is null) + { + return null; + } + + return GetImageCacheTag(item, new ItemImageInfo + { + Path = chapter.ImagePath, + Type = ImageType.Chapter, + DateModified = chapter.ImageDateModified + }); + } + /// public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter) { -- cgit v1.2.3 From 473628ba3a9f68479e0051e76594dc47f7fa08f3 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 9 Oct 2024 21:03:57 +0200 Subject: Apply suggestions from code review Co-authored-by: Cody Robibero --- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 2 +- Jellyfin.Api/Controllers/MoviesController.cs | 2 +- Jellyfin.Data/Entities/BaseItemProvider.cs | 2 +- Jellyfin.Data/Entities/MediaStreamInfo.cs | 4 ---- Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs | 2 +- MediaBrowser.Controller/Persistence/IItemTypeLookup.cs | 2 +- MediaBrowser.Controller/Persistence/IPeopleRepository.cs | 4 ++-- 7 files changed, 7 insertions(+), 11 deletions(-) (limited to 'MediaBrowser.Controller/Persistence') diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index 14dc68a32..1f73755f5 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -12,7 +12,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Querying; -namespace Jellyfin.Server.Implementations.Item; +namespace Emby.Server.Implementations.Data; /// /// Provides static topic based lookups for the BaseItemKind. diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 11559419c..f537ffa11 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -97,7 +97,7 @@ public class MoviesController : BaseJellyfinApiController DtoOptions = dtoOptions }; - var recentlyPlayedMovies = _libraryManager.GetItemList(query)!; + var recentlyPlayedMovies = _libraryManager.GetItemList(query); var itemTypes = new List { BaseItemKind.Movie }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) diff --git a/Jellyfin.Data/Entities/BaseItemProvider.cs b/Jellyfin.Data/Entities/BaseItemProvider.cs index 1fc721d6a..9a1565728 100644 --- a/Jellyfin.Data/Entities/BaseItemProvider.cs +++ b/Jellyfin.Data/Entities/BaseItemProvider.cs @@ -6,7 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; /// -/// Represents an Key-Value relaten of an BaseItem's provider. +/// Represents a Key-Value relation of an BaseItem's provider. /// public class BaseItemProvider { diff --git a/Jellyfin.Data/Entities/MediaStreamInfo.cs b/Jellyfin.Data/Entities/MediaStreamInfo.cs index a46d3f195..1198026e7 100644 --- a/Jellyfin.Data/Entities/MediaStreamInfo.cs +++ b/Jellyfin.Data/Entities/MediaStreamInfo.cs @@ -6,10 +6,6 @@ namespace Jellyfin.Data.Entities; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public class MediaStreamInfo { - public MediaStreamInfo() - { - } - public required Guid ItemId { get; set; } public required BaseItemEntity Item { get; set; } diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index f44ead6e0..df434fdb3 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -37,7 +37,7 @@ public class MediaStreamRepository(IDbContextFactory dbProvid public IReadOnlyList GetMediaStreams(MediaStreamQuery filter) { using var context = dbProvider.CreateDbContext(); - return TranslateQuery(context.MediaStreamInfos, filter).ToList().Select(Map).ToImmutableArray(); + return TranslateQuery(context.MediaStreamInfos, filter).AsEnumerable().Select(Map).ToImmutableArray(); } private string? GetPathToSave(string? path) diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs index 1b2ca2acb..6ad8380d7 100644 --- a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -51,7 +51,7 @@ public interface IItemTypeLookup public IReadOnlyList ArtistsTypes { get; } /// - /// Gets mapping for all BaseItemKinds and their expected serialisaition target. + /// Gets mapping for all BaseItemKinds and their expected serialization target. /// public IDictionary BaseItemKindNames { get; } } diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs index 43a24703e..418289cb4 100644 --- a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs +++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs @@ -14,7 +14,7 @@ public interface IPeopleRepository /// Gets the people. /// /// The query. - /// List<PersonInfo>. + /// The list of people matching the filter. IReadOnlyList GetPeople(InternalPeopleQuery filter); /// @@ -28,6 +28,6 @@ public interface IPeopleRepository /// Gets the people names. /// /// The query. - /// List<System.String>. + /// The list of people names matching the filter. IReadOnlyList GetPeopleNames(InternalPeopleQuery filter); } -- cgit v1.2.3 From ae641b7f3af5117612b3917d93013d26191a71d8 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 10 Oct 2024 19:27:26 +0000 Subject: Applied review comments --- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 73 ++++---- .../Item/BaseItemRepository.cs | 193 ++++++++++----------- .../Persistence/IItemTypeLookup.cs | 7 +- 3 files changed, 136 insertions(+), 137 deletions(-) (limited to 'MediaBrowser.Controller/Persistence') diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index df0f4ea20..dc55211d8 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading.Channels; using Emby.Server.Implementations.Playlists; using Jellyfin.Data.Enums; @@ -82,40 +84,43 @@ public class ItemTypeLookup : IItemTypeLookup ]; /// - public IDictionary BaseItemKindNames { get; } = new Dictionary() + public IReadOnlyList MusicGenreTypes => BaseItemKindNames.Where(e => e.Key is BaseItemKind.Audio or BaseItemKind.MusicVideo or BaseItemKind.MusicAlbum or BaseItemKind.MusicArtist).Select(e => e.Value).ToImmutableArray(); + + /// + public IDictionary BaseItemKindNames { get; } = new Dictionary() { - { 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 } + { 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! } }.ToFrozenDictionary(); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index d8ce4a135..5708391a5 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -23,6 +23,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -139,63 +140,57 @@ public sealed class BaseItemRepository( /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter) { - return GetItemValues(filter, [0, 1], typeof(MusicArtist).FullName!); + return GetItemValues(filter, [ItemValueType.Artist, ItemValueType.AlbumArtist], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); } /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter) { - return GetItemValues(filter, [0], typeof(MusicArtist).FullName!); + return GetItemValues(filter, [ItemValueType.Artist], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); } /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) { - return GetItemValues(filter, [1], typeof(MusicArtist).FullName!); + return GetItemValues(filter, [ItemValueType.AlbumArtist], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); } /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter) { - return GetItemValues(filter, [3], typeof(Studio).FullName!); + return GetItemValues(filter, [ItemValueType.Studios], itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!); } /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) { - return GetItemValues(filter, [2], typeof(Genre).FullName!); + return GetItemValues(filter, [ItemValueType.Genre], itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!); } /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) { - return GetItemValues(filter, [2], typeof(MusicGenre).FullName!); + return GetItemValues(filter, [ItemValueType.Genre], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!); } /// public IReadOnlyList GetStudioNames() { - return GetItemValueNames([3], Array.Empty(), Array.Empty()); + return GetItemValueNames([ItemValueType.Studios], Array.Empty(), Array.Empty()); } /// public IReadOnlyList GetAllArtistNames() { - return GetItemValueNames([0, 1], Array.Empty(), Array.Empty()); + return GetItemValueNames([ItemValueType.Artist, ItemValueType.AlbumArtist], Array.Empty(), Array.Empty()); } /// public IReadOnlyList GetMusicGenreNames() { return GetItemValueNames( - [2], - new string[] - { - typeof(Audio).FullName!, - typeof(MusicVideo).FullName!, - typeof(MusicAlbum).FullName!, - typeof(MusicArtist).FullName! - }, + [ItemValueType.Genre], + itemTypeLookup.MusicGenreTypes, Array.Empty()); } @@ -203,15 +198,9 @@ public sealed class BaseItemRepository( public IReadOnlyList GetGenreNames() { return GetItemValueNames( - [2], + [ItemValueType.Genre], Array.Empty(), - new string[] - { - typeof(Audio).FullName!, - typeof(MusicVideo).FullName!, - typeof(MusicAlbum).FullName!, - typeof(MusicArtist).FullName! - }); + itemTypeLookup.MusicGenreTypes); } /// @@ -1084,7 +1073,7 @@ public sealed class BaseItemRepository( if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags) + .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags) .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) || (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags) @@ -1246,84 +1235,76 @@ public sealed class BaseItemRepository( tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); } - try + using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + foreach (var item in tuples) { - using var context = dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - foreach (var item in tuples) + var entity = Map(item.Item); + if (!context.BaseItems.Any(e => e.Id == entity.Id)) { - var entity = Map(item.Item); - if (!context.BaseItems.Any(e => e.Id == entity.Id)) - { - context.BaseItems.Add(entity); - } - else - { - context.BaseItems.Attach(entity).State = EntityState.Modified; - } + context.BaseItems.Add(entity); + } + else + { + context.BaseItems.Attach(entity).State = EntityState.Modified; + } - context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete(); - if (item.Item.SupportsAncestors && item.AncestorIds != null) + context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete(); + if (item.Item.SupportsAncestors && item.AncestorIds != null) + { + entity.AncestorIds = new List(); + foreach (var ancestorId in item.AncestorIds) { - entity.AncestorIds = new List(); - foreach (var ancestorId in item.AncestorIds) + entity.AncestorIds.Add(new AncestorId() { - entity.AncestorIds.Add(new AncestorId() - { - ParentItemId = ancestorId, - ItemId = entity.Id, - Item = null!, - ParentItem = null! - }); - } + ParentItemId = ancestorId, + ItemId = entity.Id, + Item = null!, + ParentItem = null! + }); } + } - var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags); - var itemValues = itemValuesToSave.Select(e => e.Value).ToArray(); - context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete(); - entity.ItemValues = new List(); - var referenceValues = context.ItemValues.Where(e => itemValues.Any(f => f == e.CleanValue)).ToArray(); + var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags); + var itemValues = itemValuesToSave.Select(e => e.Value).ToArray(); + context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete(); + entity.ItemValues = new List(); + var referenceValues = context.ItemValues.Where(e => itemValues.Any(f => f == e.CleanValue)).ToArray(); - foreach (var itemValue in itemValuesToSave) + foreach (var itemValue in itemValuesToSave) + { + var refValue = referenceValues.FirstOrDefault(f => f.CleanValue == itemValue.Value && (int)f.Type == itemValue.MagicNumber); + if (refValue is not null) { - var refValue = referenceValues.FirstOrDefault(f => f.CleanValue == itemValue.Value && (int)f.Type == itemValue.MagicNumber); - if (refValue is not null) + entity.ItemValues.Add(new ItemValueMap() { - entity.ItemValues.Add(new ItemValueMap() - { - Item = entity, - ItemId = entity.Id, - ItemValue = null!, - ItemValueId = refValue.ItemValueId - }); - } - else + Item = entity, + ItemId = entity.Id, + ItemValue = null!, + ItemValueId = refValue.ItemValueId + }); + } + else + { + entity.ItemValues.Add(new ItemValueMap() { - entity.ItemValues.Add(new ItemValueMap() + Item = entity, + ItemId = entity.Id, + ItemValue = new ItemValue() { - Item = entity, - ItemId = entity.Id, - ItemValue = new ItemValue() - { - CleanValue = GetCleanValue(itemValue.Value), - Type = (ItemValueType)itemValue.MagicNumber, - ItemValueId = Guid.NewGuid(), - Value = itemValue.Value - }, - ItemValueId = Guid.Empty - }); - } + CleanValue = GetCleanValue(itemValue.Value), + Type = (ItemValueType)itemValue.MagicNumber, + ItemValueId = Guid.NewGuid(), + Value = itemValue.Value + }, + ItemValueId = Guid.Empty + }); } } - - context.SaveChanges(); - transaction.Commit(); - } - catch (System.Exception) - { - System.Console.WriteLine(); - throw; } + + context.SaveChanges(); + transaction.Commit(); } /// @@ -1665,7 +1646,7 @@ public sealed class BaseItemRepository( return entity; } - private IReadOnlyList GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) + private IReadOnlyList GetItemValueNames(ItemValueType[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) { using var context = dbProvider.CreateDbContext(); @@ -1725,7 +1706,7 @@ public sealed class BaseItemRepository( return Map(baseItemEntity, dto); } - private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType) + private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, ItemValueType[] itemValueTypes, string returnType) { ArgumentNullException.ThrowIfNull(filter); @@ -1787,19 +1768,27 @@ public sealed class BaseItemRepository( result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count(); } + var seriesTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; + var movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; + var episodeTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; + var musicAlbumTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; + var musicArtistTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; + var audioTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; + var trailerTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; + var resultQuery = query.Select(e => new { item = e, // TODO: This is bad refactor! itemCount = new ItemCounts() { - SeriesCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Series).FullName), - EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Data.Entities.Libraries.Movie).FullName), - MovieCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Series).FullName), - AlbumCount = e.ItemValues!.Count(f => f.Item.Type == typeof(MusicAlbum).FullName), - ArtistCount = e.ItemValues!.Count(f => f.Item.Type == typeof(MusicArtist).FullName), - SongCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Audio).FullName), - TrailerCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Trailer).FullName), + SeriesCount = e.ItemValues!.Count(f => f.Item.Type == seriesTypeName), + EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == episodeTypeName), + MovieCount = e.ItemValues!.Count(f => f.Item.Type == movieTypeName), + AlbumCount = e.ItemValues!.Count(f => f.Item.Type == musicAlbumTypeName), + ArtistCount = e.ItemValues!.Count(f => f.Item.Type == musicArtistTypeName), + SongCount = e.ItemValues!.Count(f => f.Item.Type == audioTypeName), + TrailerCount = e.ItemValues!.Count(f => f.Item.Type == trailerTypeName), } }); @@ -1958,27 +1947,27 @@ public sealed class BaseItemRepository( if (IsTypeInQuery(BaseItemKind.Person, query)) { - list.Add(typeof(Person).FullName!); + list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!); } if (IsTypeInQuery(BaseItemKind.Genre, query)) { - list.Add(typeof(Genre).FullName!); + list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!); } if (IsTypeInQuery(BaseItemKind.MusicGenre, query)) { - list.Add(typeof(MusicGenre).FullName!); + list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!); } if (IsTypeInQuery(BaseItemKind.MusicArtist, query)) { - list.Add(typeof(MusicArtist).FullName!); + list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); } if (IsTypeInQuery(BaseItemKind.Studio, query)) { - list.Add(typeof(Studio).FullName!); + list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!); } return list; diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs index 6ad8380d7..343b95e9e 100644 --- a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -50,8 +50,13 @@ public interface IItemTypeLookup /// public IReadOnlyList ArtistsTypes { get; } + /// + /// Gets all serialisation target types for music related kinds. + /// + IReadOnlyList MusicGenreTypes { get; } + /// /// Gets mapping for all BaseItemKinds and their expected serialization target. /// - public IDictionary BaseItemKindNames { get; } + public IDictionary BaseItemKindNames { get; } } -- cgit v1.2.3 From 058a567e0025d2a3086de8530be613fdf2b08c8c Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 11 Oct 2024 11:46:43 +0000 Subject: Removed unused mapping tables --- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 62 ---------------------- .../Persistence/IItemTypeLookup.cs | 40 -------------- 2 files changed, 102 deletions(-) (limited to 'MediaBrowser.Controller/Persistence') diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index dc55211d8..5504012bf 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -21,68 +21,6 @@ namespace Emby.Server.Implementations.Data; /// public class ItemTypeLookup : IItemTypeLookup { - /// - public IReadOnlyList AllItemFields { get; } = Enum.GetValues(); - - /// - public IReadOnlyList ProgramTypes { get; } = - [ - BaseItemKind.Program, - BaseItemKind.TvChannel, - BaseItemKind.LiveTvProgram, - BaseItemKind.LiveTvChannel - ]; - - /// - public IReadOnlyList ProgramExcludeParentTypes { get; } = - [ - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicArtist, - BaseItemKind.PhotoAlbum - ]; - - /// - public IReadOnlyList ServiceTypes { get; } = - [ - BaseItemKind.TvChannel, - BaseItemKind.LiveTvChannel - ]; - - /// - public IReadOnlyList StartDateTypes { get; } = - [ - BaseItemKind.Program, - BaseItemKind.LiveTvProgram - ]; - - /// - public IReadOnlyList SeriesTypes { get; } = - [ - BaseItemKind.Book, - BaseItemKind.AudioBook, - BaseItemKind.Episode, - BaseItemKind.Season - ]; - - /// - public IReadOnlyList ArtistExcludeParentTypes { get; } = - [ - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.PhotoAlbum - ]; - - /// - public IReadOnlyList ArtistsTypes { get; } = - [ - BaseItemKind.Audio, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicVideo, - BaseItemKind.AudioBook - ]; - /// public IReadOnlyList MusicGenreTypes => BaseItemKindNames.Where(e => e.Key is BaseItemKind.Audio or BaseItemKind.MusicVideo or BaseItemKind.MusicAlbum or BaseItemKind.MusicArtist).Select(e => e.Value).ToImmutableArray(); diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs index 343b95e9e..d2c6ff365 100644 --- a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -10,46 +10,6 @@ namespace MediaBrowser.Controller.Persistence; /// public interface IItemTypeLookup { - /// - /// Gets all values of the ItemFields type. - /// - public IReadOnlyList AllItemFields { get; } - - /// - /// Gets all BaseItemKinds that are considered Programs. - /// - public IReadOnlyList ProgramTypes { get; } - - /// - /// Gets all BaseItemKinds that should be excluded from parent lookup. - /// - public IReadOnlyList ProgramExcludeParentTypes { get; } - - /// - /// Gets all BaseItemKinds that are considered to be provided by services. - /// - public IReadOnlyList ServiceTypes { get; } - - /// - /// Gets all BaseItemKinds that have a StartDate. - /// - public IReadOnlyList StartDateTypes { get; } - - /// - /// Gets all BaseItemKinds that are considered Series. - /// - public IReadOnlyList SeriesTypes { get; } - - /// - /// Gets all BaseItemKinds that are not to be evaluated for Artists. - /// - public IReadOnlyList ArtistExcludeParentTypes { get; } - - /// - /// Gets all BaseItemKinds that are considered Artists. - /// - public IReadOnlyList ArtistsTypes { get; } - /// /// Gets all serialisation target types for music related kinds. /// -- cgit v1.2.3 From e20ecfc670c9ef8977b0795c85e35ce165fee46e Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 11 Oct 2024 14:16:42 +0000 Subject: applied review comments --- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 10 ++++++++-- Emby.Server.Implementations/Library/MusicManager.cs | 9 +-------- Jellyfin.Server.Implementations/Item/PeopleRepository.cs | 8 +++++--- MediaBrowser.Controller/Persistence/IItemTypeLookup.cs | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) (limited to 'MediaBrowser.Controller/Persistence') diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index 5504012bf..f5db28c7a 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -22,10 +22,16 @@ namespace Emby.Server.Implementations.Data; public class ItemTypeLookup : IItemTypeLookup { /// - public IReadOnlyList MusicGenreTypes => BaseItemKindNames.Where(e => e.Key is BaseItemKind.Audio or BaseItemKind.MusicVideo or BaseItemKind.MusicAlbum or BaseItemKind.MusicArtist).Select(e => e.Value).ToImmutableArray(); + public IReadOnlyList MusicGenreTypes { get; } = [ + + typeof(Audio).FullName!, + typeof(MusicVideo).FullName!, + typeof(MusicAlbum).FullName!, + typeof(MusicArtist).FullName!, + ]; /// - public IDictionary BaseItemKindNames { get; } = new Dictionary() + public IReadOnlyDictionary BaseItemKindNames { get; } = new Dictionary() { { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! }, { BaseItemKind.Audio, typeof(Audio).FullName! }, diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 3f2909947..71c69ec50 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -27,14 +27,7 @@ namespace Emby.Server.Implementations.Library public IReadOnlyList GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) { - var list = new List - { - item - }; - - list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions)); - - return [.. list]; + return GetInstantMixFromGenres(item.Genres, user, dtoOptions); } /// diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index dee87f48f..5f5bf09af 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -16,10 +16,11 @@ namespace Jellyfin.Server.Implementations.Item; /// Manager for handling people. /// /// Efcore Factory. +/// Items lookup service. /// /// Initializes a new instance of the class. /// -public class PeopleRepository(IDbContextFactory dbProvider) : IPeopleRepository +public class PeopleRepository(IDbContextFactory dbProvider, IItemTypeLookup itemTypeLookup) : IPeopleRepository { private readonly IDbContextFactory _dbProvider = dbProvider; @@ -118,8 +119,9 @@ public class PeopleRepository(IDbContextFactory dbProvider) : { 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)) + var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]; + query = query.Where(e => e.PersonType == personType) + .Where(e => context.BaseItems.Where(d => context.UserData.Where(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id)).Any(f => f.Key == d.UserDataKey)) .Select(f => f.Name).Contains(e.Name)); } diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs index d2c6ff365..9507f79d3 100644 --- a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -18,5 +18,5 @@ public interface IItemTypeLookup /// /// Gets mapping for all BaseItemKinds and their expected serialization target. /// - public IDictionary BaseItemKindNames { get; } + public IReadOnlyDictionary BaseItemKindNames { get; } } -- cgit v1.2.3 From 6a08361f6fcbd6e24ca90705dba453a22a49fb57 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 24 Nov 2024 10:58:09 +0000 Subject: Applied review comments --- .../Item/BaseItemRepository.cs | 158 ++++++++++++--------- .../Persistence/IItemTypeLookup.cs | 2 +- 2 files changed, 92 insertions(+), 68 deletions(-) (limited to 'MediaBrowser.Controller/Persistence') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 0705c3cbd..c82c70376 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -42,20 +42,7 @@ namespace Jellyfin.Server.Implementations.Item; /// /// Handles all storage logic for BaseItems. /// -/// -/// Initializes a new instance of the class. -/// -/// The db factory. -/// The Application host. -/// The static type lookup. -/// The server Configuration manager. -/// System logger. -public sealed class BaseItemRepository( - IDbContextFactory dbProvider, - IServerApplicationHost appHost, - IItemTypeLookup itemTypeLookup, - IServerConfigurationManager serverConfigurationManager, - ILogger logger) +public sealed class BaseItemRepository : IItemRepository, IDisposable { /// @@ -63,8 +50,41 @@ public sealed class BaseItemRepository( /// so that we can de-serialize properly when we don't have strong types. /// private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); + private readonly IDbContextFactory _dbProvider; + private readonly IServerApplicationHost _appHost; + private readonly IItemTypeLookup _itemTypeLookup; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly ILogger _logger; private bool _disposed; + private static readonly IReadOnlyList _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist]; + private static readonly IReadOnlyList _getArtistValueTypes = [ItemValueType.Artist]; + private static readonly IReadOnlyList _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist]; + private static readonly IReadOnlyList _getStudiosValueTypes = [ItemValueType.Studios]; + private static readonly IReadOnlyList _getGenreValueTypes = [ItemValueType.Studios]; + + /// + /// Initializes a new instance of the class. + /// + /// The db factory. + /// The Application host. + /// The static type lookup. + /// The server Configuration manager. + /// System logger. + public BaseItemRepository( + IDbContextFactory dbProvider, + IServerApplicationHost appHost, + IItemTypeLookup itemTypeLookup, + IServerConfigurationManager serverConfigurationManager, + ILogger logger) + { + _dbProvider = dbProvider; + _appHost = appHost; + _itemTypeLookup = itemTypeLookup; + _serverConfigurationManager = serverConfigurationManager; + _logger = logger; + } + /// public void Dispose() { @@ -81,7 +101,7 @@ public sealed class BaseItemRepository( { ArgumentNullException.ThrowIfNull(id.IsEmpty() ? null : id); - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); @@ -100,11 +120,11 @@ public sealed class BaseItemRepository( /// public void UpdateInheritedValues() { - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete(); - // ItemValue Inheritence is now correctly mapped via AncestorId on demand + // ItemValue Inheritance is now correctly mapped via AncestorId on demand context.SaveChanges(); transaction.Commit(); @@ -116,64 +136,64 @@ public sealed class BaseItemRepository( ArgumentNullException.ThrowIfNull(filter); PrepareFilterQuery(filter); - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray(); } /// public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter) { - return GetItemValues(filter, [ItemValueType.Artist, ItemValueType.AlbumArtist], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); + return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter) { - return GetItemValues(filter, [ItemValueType.Artist], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); + return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) { - return GetItemValues(filter, [ItemValueType.AlbumArtist], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); + return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter) { - return GetItemValues(filter, [ItemValueType.Studios], itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!); + return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]); } /// public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) { - return GetItemValues(filter, [ItemValueType.Genre], itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!); + return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]); } /// public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) { - return GetItemValues(filter, [ItemValueType.Genre], itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!); + return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]); } /// public IReadOnlyList GetStudioNames() { - return GetItemValueNames([ItemValueType.Studios], [], []); + return GetItemValueNames(_getStudiosValueTypes, [], []); } /// public IReadOnlyList GetAllArtistNames() { - return GetItemValueNames([ItemValueType.Artist, ItemValueType.AlbumArtist], [], []); + return GetItemValueNames(_getAllArtistsValueTypes, [], []); } /// public IReadOnlyList GetMusicGenreNames() { return GetItemValueNames( - [ItemValueType.Genre], - itemTypeLookup.MusicGenreTypes, + _getGenreValueTypes, + _itemTypeLookup.MusicGenreTypes, []); } @@ -181,9 +201,9 @@ public sealed class BaseItemRepository( public IReadOnlyList GetGenreNames() { return GetItemValueNames( - [ItemValueType.Genre], + _getGenreValueTypes, [], - itemTypeLookup.MusicGenreTypes); + _itemTypeLookup.MusicGenreTypes); } /// @@ -202,12 +222,11 @@ public sealed class BaseItemRepository( PrepareFilterQuery(filter); var result = new QueryResult(); - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); IQueryable dbQuery = PrepareItemQuery(context, filter); dbQuery = TranslateQuery(dbQuery, context, filter); - // dbQuery = dbQuery.Distinct(); if (filter.EnableTotalRecordCount) { result.TotalRecordCount = dbQuery.Count(); @@ -227,11 +246,11 @@ public sealed class BaseItemRepository( ArgumentNullException.ThrowIfNull(filter); PrepareFilterQuery(filter); - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); IQueryable dbQuery = PrepareItemQuery(context, filter); dbQuery = TranslateQuery(dbQuery, context, filter); - // dbQuery = dbQuery.Distinct(); + dbQuery = ApplyGroupingFilter(dbQuery, filter); dbQuery = ApplyQueryPageing(dbQuery, filter); @@ -240,6 +259,11 @@ public sealed class BaseItemRepository( private IQueryable ApplyGroupingFilter(IQueryable dbQuery, InternalItemsQuery filter) { + // This whole block is needed to filter duplicate entries on request + // for the time beeing it cannot be used because it would destroy the ordering + // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but + // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own + // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) // { @@ -318,7 +342,7 @@ public sealed class BaseItemRepository( // Hack for right now since we currently don't support filtering out these duplicates within a query PrepareFilterQuery(filter); - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); return dbQuery.Count(); @@ -346,7 +370,7 @@ public sealed class BaseItemRepository( ArgumentNullException.ThrowIfNull(item); var images = item.ImageInfos.Select(e => Map(item.Id, e)); - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); context.BaseItemImageInfos.AddRange(images); @@ -382,9 +406,9 @@ public sealed class BaseItemRepository( tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags)); } - var localFuckingItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>(); + var localItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>(); - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); foreach (var item in tuples) { @@ -427,7 +451,7 @@ public sealed class BaseItemRepository( context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete(); foreach (var itemValue in itemValuesToSave) { - if (!localFuckingItemValueCache.TryGetValue(itemValue, out var refValue)) + if (!localItemValueCache.TryGetValue(itemValue, out var refValue)) { refValue = context.ItemValues .Where(f => f.CleanValue == GetCleanValue(itemValue.Value) && (int)f.Type == itemValue.MagicNumber) @@ -444,7 +468,7 @@ public sealed class BaseItemRepository( ItemValueId = refValue = Guid.NewGuid(), Value = itemValue.Value }); - localFuckingItemValueCache[itemValue] = refValue; + localItemValueCache[itemValue] = refValue; } context.ItemValuesMap.Add(new ItemValueMap() @@ -469,7 +493,7 @@ public sealed class BaseItemRepository( throw new ArgumentException("Guid can't be empty", nameof(id)); } - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); var item = PrepareItemQuery(context, new() { DtoOptions = new() @@ -813,9 +837,9 @@ public sealed class BaseItemRepository( return entity; } - private string[] GetItemValueNames(ItemValueType[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) + private string[] GetItemValueNames(IReadOnlyList itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) { - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); var query = context.ItemValuesMap .AsNoTracking() @@ -842,7 +866,7 @@ public sealed class BaseItemRepository( private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false) { ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity)); - if (serverConfigurationManager?.Configuration is null) + if (_serverConfigurationManager?.Configuration is null) { throw new InvalidOperationException("Server Configuration manager or configuration is null"); } @@ -850,9 +874,9 @@ public sealed class BaseItemRepository( var typeToSerialise = GetType(baseItemEntity.Type); return BaseItemRepository.DeserialiseBaseItem( baseItemEntity, - logger, - appHost, - skipDeserialization || (serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeToSerialise == typeof(Channel) || typeToSerialise == typeof(UserRootFolder)))); + _logger, + _appHost, + skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeToSerialise == typeof(Channel) || typeToSerialise == typeof(UserRootFolder)))); } /// @@ -889,7 +913,7 @@ public sealed class BaseItemRepository( return Map(baseItemEntity, dto, appHost); } - private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, ItemValueType[] itemValueTypes, string returnType) + private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList itemValueTypes, string returnType) { ArgumentNullException.ThrowIfNull(filter); @@ -898,7 +922,7 @@ public sealed class BaseItemRepository( filter.EnableTotalRecordCount = false; } - using var context = dbProvider.CreateDbContext(); + using var context = _dbProvider.CreateDbContext(); var innerQuery = new InternalItemsQuery(filter.User) { @@ -951,13 +975,13 @@ public sealed class BaseItemRepository( result.TotalRecordCount = query.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()).Count(); } - var seriesTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; - var movieTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; - var episodeTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; - var musicAlbumTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; - var musicArtistTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; - var audioTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; - var trailerTypeName = itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; + var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; + var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; + var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; + var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; + var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; + var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; + var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; var resultQuery = query.Select(e => new { @@ -1071,7 +1095,7 @@ public sealed class BaseItemRepository( return null; } - return appHost.ReverseVirtualPath(path); + return _appHost.ReverseVirtualPath(path); } private List GetItemByNameTypesInQuery(InternalItemsQuery query) @@ -1080,27 +1104,27 @@ public sealed class BaseItemRepository( if (IsTypeInQuery(BaseItemKind.Person, query)) { - list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!); + list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!); } if (IsTypeInQuery(BaseItemKind.Genre, query)) { - list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!); + list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!); } if (IsTypeInQuery(BaseItemKind.MusicGenre, query)) { - list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!); + list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!); } if (IsTypeInQuery(BaseItemKind.MusicArtist, query)) { - list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); + list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!); } if (IsTypeInQuery(BaseItemKind.Studio, query)) { - list.Add(itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!); + list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!); } return list; @@ -1193,7 +1217,7 @@ public sealed class BaseItemRepository( private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) { var orderBy = filter.OrderBy; - bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); if (hasSearch) { @@ -1390,7 +1414,7 @@ public sealed class BaseItemRepository( var excludeTypes = filter.ExcludeItemTypes; if (excludeTypes.Length == 1) { - if (itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) + if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) { baseQuery = baseQuery.Where(e => e.Type != excludeTypeName); } @@ -1400,7 +1424,7 @@ public sealed class BaseItemRepository( var excludeTypeName = new List(); foreach (var excludeType in excludeTypes) { - if (itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) + if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) { excludeTypeName.Add(baseItemKindName!); } @@ -1411,7 +1435,7 @@ public sealed class BaseItemRepository( } else if (includeTypes.Length == 1) { - if (itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) + if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) { baseQuery = baseQuery.Where(e => e.Type == includeTypeName); } @@ -1421,7 +1445,7 @@ public sealed class BaseItemRepository( var includeTypeName = new List(); foreach (var includeType in includeTypes) { - if (itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) + if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) { includeTypeName.Add(baseItemKindName!); } diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs index 9507f79d3..6699d3a4d 100644 --- a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -18,5 +18,5 @@ public interface IItemTypeLookup /// /// Gets mapping for all BaseItemKinds and their expected serialization target. /// - public IReadOnlyDictionary BaseItemKindNames { get; } + IReadOnlyDictionary BaseItemKindNames { get; } } -- cgit v1.2.3 From d716a53ec2433c6af43dfbce7f92fc9c2927592a Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 11 Jan 2025 18:13:16 +0000 Subject: Applied review comments --- .../Item/BaseItemRepository.cs | 18 +++--------------- MediaBrowser.Controller/Persistence/IItemRepository.cs | 2 +- 2 files changed, 4 insertions(+), 16 deletions(-) (limited to 'MediaBrowser.Controller/Persistence') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e087bd328..01e23f56d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -41,8 +41,8 @@ using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity; namespace Jellyfin.Server.Implementations.Item; /* - All queries in this class and all other nullable enabled EFCore repository classes will make libraral use of the null-forgiving operator "!". - This is done as the code isn't actually executed client side, but only the expressions are interpretet and the compiler cannot know that. + All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null-forgiving operator "!". + This is done as the code isn't actually executed client side, but only the expressions are interpret and the compiler cannot know that. This is your only warning/message regarding this topic. */ @@ -50,7 +50,7 @@ namespace Jellyfin.Server.Implementations.Item; /// Handles all storage logic for BaseItems. /// public sealed class BaseItemRepository - : IItemRepository, IDisposable + : IItemRepository { /// /// This holds all the types in the running assemblies @@ -62,7 +62,6 @@ public sealed class BaseItemRepository private readonly IItemTypeLookup _itemTypeLookup; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly ILogger _logger; - private bool _disposed; private static readonly IReadOnlyList _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist]; private static readonly IReadOnlyList _getArtistValueTypes = [ItemValueType.Artist]; @@ -92,17 +91,6 @@ public sealed class BaseItemRepository _logger = logger; } - /// - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - } - /// public void DeleteItem(Guid id) { diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index b27f156ef..afe2d833d 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Controller.Persistence; /// /// Provides an interface to implement an Item repository. /// -public interface IItemRepository : IDisposable +public interface IItemRepository { /// /// Deletes the item. -- cgit v1.2.3