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 --- .../Data/SqliteUserDataRepository.cs | 369 --------------------- 1 file changed, 369 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/SqliteUserDataRepository.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs deleted file mode 100644 index bfdcc08f4..000000000 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ /dev/null @@ -1,369 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Jellyfin.Data.Entities; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Data -{ - public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository - { - private readonly IUserManager _userManager; - - public SqliteUserDataRepository( - ILogger logger, - IServerConfigurationManager config, - IUserManager userManager) - : base(logger) - { - _userManager = userManager; - - DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db"); - } - - /// - /// Opens the connection to the database. - /// - public override void Initialize() - { - base.Initialize(); - - using (var connection = GetConnection()) - { - var userDatasTableExists = TableExists(connection, "UserDatas"); - var userDataTableExists = TableExists(connection, "userdata"); - - var users = userDatasTableExists ? null : _userManager.Users; - using var transaction = connection.BeginTransaction(); - connection.Execute(string.Join( - ';', - "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)", - "drop index if exists idx_userdata", - "drop index if exists idx_userdata1", - "drop index if exists idx_userdata2", - "drop index if exists userdataindex1", - "drop index if exists userdataindex", - "drop index if exists userdataindex3", - "drop index if exists userdataindex4", - "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)", - "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", - "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)", - "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)", - "create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)")); - - if (!userDataTableExists) - { - transaction.Commit(); - return; - } - - var existingColumnNames = GetColumnNames(connection, "userdata"); - - AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames); - AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames); - AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); - - if (userDatasTableExists) - { - return; - } - - ImportUserIds(connection, users); - - connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null"); - - transaction.Commit(); - } - } - - private void ImportUserIds(ManagedConnection db, IEnumerable users) - { - var userIdsWithUserData = GetAllUserIdsWithUserData(db); - - using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId")) - { - foreach (var user in users) - { - if (!userIdsWithUserData.Contains(user.Id)) - { - continue; - } - - statement.TryBind("@UserId", user.Id); - statement.TryBind("@InternalUserId", user.InternalId); - - statement.ExecuteNonQuery(); - } - } - } - - private List GetAllUserIdsWithUserData(ManagedConnection db) - { - var list = new List(); - - using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null")) - { - foreach (var row in statement.ExecuteQuery()) - { - try - { - list.Add(row.GetGuid(0)); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error while getting user"); - } - } - } - - return list; - } - - /// - public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(userData); - - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - ArgumentException.ThrowIfNullOrEmpty(key); - - PersistUserData(userId, key, userData, cancellationToken); - } - - /// - public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(userData); - - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - PersistAllUserData(userId, userData, cancellationToken); - } - - /// - /// Persists the user data. - /// - /// The user id. - /// The key. - /// The user data. - /// The cancellation token. - public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - SaveUserData(connection, internalUserId, key, userData); - transaction.Commit(); - } - } - - private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData) - { - using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) - { - statement.TryBind("@userId", internalUserId); - statement.TryBind("@key", key); - - if (userData.Rating.HasValue) - { - statement.TryBind("@rating", userData.Rating.Value); - } - else - { - statement.TryBindNull("@rating"); - } - - statement.TryBind("@played", userData.Played); - statement.TryBind("@playCount", userData.PlayCount); - statement.TryBind("@isFavorite", userData.IsFavorite); - statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks); - - if (userData.LastPlayedDate.HasValue) - { - statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue()); - } - else - { - statement.TryBindNull("@lastPlayedDate"); - } - - if (userData.AudioStreamIndex.HasValue) - { - statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value); - } - else - { - statement.TryBindNull("@AudioStreamIndex"); - } - - if (userData.SubtitleStreamIndex.HasValue) - { - statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value); - } - else - { - statement.TryBindNull("@SubtitleStreamIndex"); - } - - statement.ExecuteNonQuery(); - } - } - - /// - /// Persist all user data for the specified user. - /// - private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - foreach (var userItemData in userDataList) - { - SaveUserData(connection, internalUserId, userItemData.Key, userItemData); - } - - transaction.Commit(); - } - } - - /// - /// Gets the user data. - /// - /// The user id. - /// The key. - /// Task{UserItemData}. - /// - /// userId - /// or - /// key. - /// - public UserItemData GetUserData(long userId, string key) - { - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - ArgumentException.ThrowIfNullOrEmpty(key); - - using (var connection = GetConnection(true)) - { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) - { - statement.TryBind("@UserId", userId); - statement.TryBind("@Key", key); - - foreach (var row in statement.ExecuteQuery()) - { - return ReadRow(row); - } - } - - return null; - } - } - - public UserItemData GetUserData(long userId, List keys) - { - ArgumentNullException.ThrowIfNull(keys); - - if (keys.Count == 0) - { - return null; - } - - return GetUserData(userId, keys[0]); - } - - /// - /// Return all user-data associated with the given user. - /// - /// The internal user id. - /// The list of user item data. - public List GetAllUserData(long userId) - { - if (userId <= 0) - { - throw new ArgumentNullException(nameof(userId)); - } - - var list = new List(); - - using (var connection = GetConnection()) - { - using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId")) - { - statement.TryBind("@UserId", userId); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(ReadRow(row)); - } - } - } - - return list; - } - - /// - /// Read a row from the specified reader into the provided userData object. - /// - /// The list of result set values. - /// The user item data. - private UserItemData ReadRow(SqliteDataReader reader) - { - var userData = new UserItemData - { - Key = reader.GetString(0) - }; - - if (reader.TryGetDouble(2, out var rating)) - { - userData.Rating = rating; - } - - userData.Played = reader.GetBoolean(3); - userData.PlayCount = reader.GetInt32(4); - userData.IsFavorite = reader.GetBoolean(5); - userData.PlaybackPositionTicks = reader.GetInt64(6); - - if (reader.TryReadDateTime(7, out var lastPlayedDate)) - { - userData.LastPlayedDate = lastPlayedDate; - } - - if (reader.TryGetInt32(8, out var audioStreamIndex)) - { - userData.AudioStreamIndex = audioStreamIndex; - } - - if (reader.TryGetInt32(9, out var subtitleStreamIndex)) - { - userData.SubtitleStreamIndex = subtitleStreamIndex; - } - - return userData; - } - } -} -- cgit v1.2.3 From ee1bdf4e222125ed7382165fd7e09599ca4bd4aa Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 8 Sep 2024 16:56:14 +0000 Subject: WIP move baseitem to jellyfin.db --- .../Data/SqliteItemRepository.cs | 1656 +------------------- Jellyfin.Data/Entities/AncestorId.cs | 19 + Jellyfin.Data/Entities/AttachmentStreamInfo.cs | 21 + Jellyfin.Data/Entities/BaseItem.cs | 164 ++ Jellyfin.Data/Entities/Chapter.cs | 22 + Jellyfin.Data/Entities/ItemValue.cs | 16 + Jellyfin.Data/Entities/MediaStreamInfo.cs | 101 ++ Jellyfin.Data/Entities/People.cs | 16 + .../Item/BaseItemManager.cs | 753 +++++++++ .../Item/ChapterManager.cs | 51 + .../JellyfinDbContext.cs | 35 + .../ModelConfiguration/AncestorIdConfiguration.cs | 20 + .../ModelConfiguration/BaseItemConfiguration.cs | 42 + .../ModelConfiguration/ChapterConfiguration.cs | 20 + .../ModelConfiguration/ItemValuesConfiguration.cs | 20 + .../ModelConfiguration/PeopleConfiguration.cs | 20 + 16 files changed, 1325 insertions(+), 1651 deletions(-) create mode 100644 Jellyfin.Data/Entities/AncestorId.cs create mode 100644 Jellyfin.Data/Entities/AttachmentStreamInfo.cs create mode 100644 Jellyfin.Data/Entities/BaseItem.cs create mode 100644 Jellyfin.Data/Entities/Chapter.cs create mode 100644 Jellyfin.Data/Entities/ItemValue.cs create mode 100644 Jellyfin.Data/Entities/MediaStreamInfo.cs create mode 100644 Jellyfin.Data/Entities/People.cs create mode 100644 Jellyfin.Server.Implementations/Item/BaseItemManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/ChapterManager.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 60f5ee47a..c7a8421c6 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -63,130 +63,6 @@ namespace Emby.Server.Implementations.Data private readonly ItemFields[] _allItemFields = Enum.GetValues(); - private static readonly string[] _retrieveItemColumns = - { - "type", - "data", - "StartDate", - "EndDate", - "ChannelId", - "IsMovie", - "IsSeries", - "EpisodeTitle", - "IsRepeat", - "CommunityRating", - "CustomRating", - "IndexNumber", - "IsLocked", - "PreferredMetadataLanguage", - "PreferredMetadataCountryCode", - "Width", - "Height", - "DateLastRefreshed", - "Name", - "Path", - "PremiereDate", - "Overview", - "ParentIndexNumber", - "ProductionYear", - "OfficialRating", - "ForcedSortName", - "RunTimeTicks", - "Size", - "DateCreated", - "DateModified", - "guid", - "Genres", - "ParentId", - "Audio", - "ExternalServiceId", - "IsInMixedFolder", - "DateLastSaved", - "LockedFields", - "Studios", - "Tags", - "TrailerTypes", - "OriginalTitle", - "PrimaryVersionId", - "DateLastMediaAdded", - "Album", - "LUFS", - "NormalizationGain", - "CriticRating", - "IsVirtualItem", - "SeriesName", - "SeasonName", - "SeasonId", - "SeriesId", - "PresentationUniqueKey", - "InheritedParentalRatingValue", - "ExternalSeriesId", - "Tagline", - "ProviderIds", - "Images", - "ProductionLocations", - "ExtraIds", - "TotalBitrate", - "ExtraType", - "Artists", - "AlbumArtists", - "ExternalId", - "SeriesPresentationUniqueKey", - "ShowId", - "OwnerId" - }; - - private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid"; - - private static readonly string[] _mediaStreamSaveColumns = - { - "ItemId", - "StreamIndex", - "StreamType", - "Codec", - "Language", - "ChannelLayout", - "Profile", - "AspectRatio", - "Path", - "IsInterlaced", - "BitRate", - "Channels", - "SampleRate", - "IsDefault", - "IsForced", - "IsExternal", - "Height", - "Width", - "AverageFrameRate", - "RealFrameRate", - "Level", - "PixelFormat", - "BitDepth", - "IsAnamorphic", - "RefFrames", - "CodecTag", - "Comment", - "NalLengthSize", - "IsAvc", - "Title", - "TimeBase", - "CodecTimeBase", - "ColorPrimaries", - "ColorSpace", - "ColorTransfer", - "DvVersionMajor", - "DvVersionMinor", - "DvProfile", - "DvLevel", - "RpuPresentFlag", - "ElPresentFlag", - "BlPresentFlag", - "DvBlSignalCompatibilityId", - "IsHearingImpaired", - "Rotation" - }; - private static readonly string _mediaStreamSaveColumnsInsertQuery = $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values "; @@ -336,956 +212,14 @@ namespace Emby.Server.Implementations.Data /// protected override TempStoreMode TempStore => TempStoreMode.Memory; - /// - /// Opens the connection to the database. - /// - public override void Initialize() - { - base.Initialize(); - - const string CreateMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - - const string CreateMediaAttachmentsTableCommand - = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; - - string[] queries = - { - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", - - "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", - "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", - "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", - - "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", - - "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - - "drop index if exists idxPeopleItemId", - "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", - "create index if not exists idxPeopleName on People(Name)", - - "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - - CreateMediaStreamsTableCommand, - CreateMediaAttachmentsTableCommand, - - "pragma shrink_memory" - }; - - string[] postQueries = - { - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", - "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", - - "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", - "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", - "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", - - // covering index - "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", - - // series - "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - - // series counts - // seriesdateplayed sort order - "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", - - // live tv programs - "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", - - // covering index for getitemvalues - "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", - - // used by movie suggestions - "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", - "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", - - // latest items - "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", - "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", - - // resume - "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", - - // items by name - "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", - - // Used to update inherited tags - "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", - - "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", - "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" - }; - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - connection.Execute(string.Join(';', queries)); - - var existingColumnNames = GetColumnNames(connection, "AncestorIds"); - AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); - - AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "ItemValues"); - AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, ChaptersTableName); - AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "MediaStreams"); - AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames); - - connection.Execute(string.Join(';', postQueries)); - - transaction.Commit(); - } - } - - public void SaveImages(BaseItem item) - { - ArgumentNullException.ThrowIfNull(item); - - CheckDisposed(); - - var images = SerializeImages(item.ImageInfos); - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); - saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", images); - - saveImagesStatement.ExecuteNonQuery(); - transaction.Commit(); - } - - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// - /// or is null. - /// - public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(items); - - cancellationToken.ThrowIfCancellationRequested(); - - CheckDisposed(); - - var itemsLen = items.Count; - var tuples = new ValueTuple, BaseItem, string, List>[itemsLen]; - for (int i = 0; i < itemsLen; i++) - { - var item = items[i]; - var ancestorIds = item.SupportsAncestors ? - item.GetAncestorIds().Distinct().ToList() : - null; - - var topParent = item.GetTopParent(); - - var userdataKey = item.GetUserDataKeys().FirstOrDefault(); - var inheritedTags = item.GetInheritedTags(); - - tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); - } - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - SaveItemsInTransaction(connection, tuples); - transaction.Commit(); - } - - private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples) + private bool TypeRequiresDeserialization(Type type) { - using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) - using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) + if (_config.Configuration.SkipDeserializationForBasicTypes) { - var requiresReset = false; - foreach (var tuple in tuples) + if (type == typeof(Channel) + || type == typeof(UserRootFolder)) { - if (requiresReset) - { - saveItemStatement.Parameters.Clear(); - deleteAncestorsStatement.Parameters.Clear(); - } - - var item = tuple.Item; - var topParent = tuple.TopParent; - var userDataKey = tuple.UserDataKey; - - SaveItem(item, topParent, userDataKey, saveItemStatement); - - var inheritedTags = tuple.InheritedTags; - - if (item.SupportsAncestors) - { - UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement); - } - - UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); - - requiresReset = true; - } - } - } - - private string GetPathToSave(string path) - { - if (path is null) - { - return null; - } - - return _appHost.ReverseVirtualPath(path); - } - - private string RestorePath(string path) - { - return _appHost.ExpandVirtualPath(path); - } - - private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) - { - Type type = item.GetType(); - - saveItemStatement.TryBind("@guid", item.Id); - saveItemStatement.TryBind("@type", type.FullName); - - if (TypeRequiresDeserialization(type)) - { - saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); - } - else - { - saveItemStatement.TryBindNull("@data"); - } - - saveItemStatement.TryBind("@Path", GetPathToSave(item.Path)); - - if (item is IHasStartDate hasStartDate) - { - saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate); - } - else - { - saveItemStatement.TryBindNull("@StartDate"); - } - - if (item.EndDate.HasValue) - { - saveItemStatement.TryBind("@EndDate", item.EndDate.Value); - } - else - { - saveItemStatement.TryBindNull("@EndDate"); - } - - saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); - - if (item is IHasProgramAttributes hasProgramAttributes) - { - saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); - saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); - saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); - saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); - } - else - { - saveItemStatement.TryBindNull("@IsMovie"); - saveItemStatement.TryBindNull("@IsSeries"); - saveItemStatement.TryBindNull("@EpisodeTitle"); - saveItemStatement.TryBindNull("@IsRepeat"); - } - - saveItemStatement.TryBind("@CommunityRating", item.CommunityRating); - saveItemStatement.TryBind("@CustomRating", item.CustomRating); - saveItemStatement.TryBind("@IndexNumber", item.IndexNumber); - saveItemStatement.TryBind("@IsLocked", item.IsLocked); - saveItemStatement.TryBind("@Name", item.Name); - saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); - saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); - saveItemStatement.TryBind("@Overview", item.Overview); - saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); - saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); - saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); - - var parentId = item.ParentId; - if (parentId.IsEmpty()) - { - saveItemStatement.TryBindNull("@ParentId"); - } - else - { - saveItemStatement.TryBind("@ParentId", parentId); - } - - if (item.Genres.Length > 0) - { - saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres)); - } - else - { - saveItemStatement.TryBindNull("@Genres"); - } - - saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); - - saveItemStatement.TryBind("@SortName", item.SortName); - - saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName); - - saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); - saveItemStatement.TryBind("@Size", item.Size); - - saveItemStatement.TryBind("@DateCreated", item.DateCreated); - saveItemStatement.TryBind("@DateModified", item.DateModified); - - saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); - saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); - - if (item.Width > 0) - { - saveItemStatement.TryBind("@Width", item.Width); - } - else - { - saveItemStatement.TryBindNull("@Width"); - } - - if (item.Height > 0) - { - saveItemStatement.TryBind("@Height", item.Height); - } - else - { - saveItemStatement.TryBindNull("@Height"); - } - - if (item.DateLastRefreshed != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed); - } - else - { - saveItemStatement.TryBindNull("@DateLastRefreshed"); - } - - if (item.DateLastSaved != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved); - } - else - { - saveItemStatement.TryBindNull("@DateLastSaved"); - } - - saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); - - if (item.LockedFields.Length > 0) - { - saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields)); - } - else - { - saveItemStatement.TryBindNull("@LockedFields"); - } - - if (item.Studios.Length > 0) - { - saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios)); - } - else - { - saveItemStatement.TryBindNull("@Studios"); - } - - if (item.Audio.HasValue) - { - saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@Audio"); - } - - if (item is LiveTvChannel liveTvChannel) - { - saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName); - } - else - { - saveItemStatement.TryBindNull("@ExternalServiceId"); - } - - if (item.Tags.Length > 0) - { - saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags)); - } - else - { - saveItemStatement.TryBindNull("@Tags"); - } - - saveItemStatement.TryBind("@IsFolder", item.IsFolder); - - saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); - - if (topParent is null) - { - saveItemStatement.TryBindNull("@TopParentId"); - } - else - { - saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture)); - } - - if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) - { - saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes)); - } - else - { - saveItemStatement.TryBindNull("@TrailerTypes"); - } - - saveItemStatement.TryBind("@CriticRating", item.CriticRating); - - if (string.IsNullOrWhiteSpace(item.Name)) - { - saveItemStatement.TryBindNull("@CleanName"); - } - else - { - saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name)); - } - - saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey); - saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle); - - if (item is Video video) - { - saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId); - } - else - { - saveItemStatement.TryBindNull("@PrimaryVersionId"); - } - - if (item is Folder folder && folder.DateLastMediaAdded.HasValue) - { - saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value); - } - else - { - saveItemStatement.TryBindNull("@DateLastMediaAdded"); - } - - saveItemStatement.TryBind("@Album", item.Album); - saveItemStatement.TryBind("@LUFS", item.LUFS); - saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain); - saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); - - if (item is IHasSeries hasSeriesName) - { - saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName); - } - else - { - saveItemStatement.TryBindNull("@SeriesName"); - } - - if (string.IsNullOrWhiteSpace(userDataKey)) - { - saveItemStatement.TryBindNull("@UserDataKey"); - } - else - { - saveItemStatement.TryBind("@UserDataKey", userDataKey); - } - - if (item is Episode episode) - { - saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - - var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; - - saveItemStatement.TryBind("@SeasonId", nullableSeasonId); - } - else - { - saveItemStatement.TryBindNull("@SeasonName"); - saveItemStatement.TryBindNull("@SeasonId"); - } - - if (item is IHasSeries hasSeries) - { - var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; - - saveItemStatement.TryBind("@SeriesId", nullableSeriesId); - saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); - } - else - { - saveItemStatement.TryBindNull("@SeriesId"); - saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey"); - } - - saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); - saveItemStatement.TryBind("@Tagline", item.Tagline); - - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); - saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); - - if (item.ProductionLocations.Length > 0) - { - saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations)); - } - else - { - saveItemStatement.TryBindNull("@ProductionLocations"); - } - - if (item.ExtraIds.Length > 0) - { - saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds)); - } - else - { - saveItemStatement.TryBindNull("@ExtraIds"); - } - - saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); - if (item.ExtraType.HasValue) - { - saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@ExtraType"); - } - - string artists = null; - if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) - { - artists = string.Join('|', hasArtists.Artists); - } - - saveItemStatement.TryBind("@Artists", artists); - - string albumArtists = null; - if (item is IHasAlbumArtist hasAlbumArtists - && hasAlbumArtists.AlbumArtists.Count > 0) - { - albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists); - } - - saveItemStatement.TryBind("@AlbumArtists", albumArtists); - saveItemStatement.TryBind("@ExternalId", item.ExternalId); - - if (item is LiveTvProgram program) - { - saveItemStatement.TryBind("@ShowId", program.ShowId); - } - else - { - saveItemStatement.TryBindNull("@ShowId"); - } - - Guid ownerId = item.OwnerId; - if (ownerId.IsEmpty()) - { - saveItemStatement.TryBindNull("@OwnerId"); - } - else - { - saveItemStatement.TryBind("@OwnerId", ownerId); - } - - saveItemStatement.ExecuteNonQuery(); - } - - internal static string SerializeProviderIds(Dictionary providerIds) - { - StringBuilder str = new StringBuilder(); - foreach (var i in providerIds) - { - // Ideally we shouldn't need this IsNullOrWhiteSpace check, - // but we're seeing some cases of bad data slip through - if (string.IsNullOrWhiteSpace(i.Value)) - { - continue; - } - - str.Append(i.Key) - .Append('=') - .Append(i.Value) - .Append('|'); - } - - if (str.Length == 0) - { - return null; - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal static void DeserializeProviderIds(string value, IHasProviderIds item) - { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - foreach (var part in value.SpanSplit('|')) - { - var providerDelimiterIndex = part.IndexOf('='); - // Don't let empty values through - if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) - { - item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); - } - } - } - - internal string SerializeImages(ItemImageInfo[] images) - { - if (images.Length == 0) - { - return null; - } - - StringBuilder str = new StringBuilder(); - foreach (var i in images) - { - if (string.IsNullOrWhiteSpace(i.Path)) - { - continue; - } - - AppendItemImageInfo(str, i); - str.Append('|'); - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal ItemImageInfo[] DeserializeImages(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Array.Empty(); - } - - // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed - var valueSpan = value.AsSpan(); - var count = valueSpan.Count('|') + 1; - - var position = 0; - var result = new ItemImageInfo[count]; - foreach (var part in valueSpan.Split('|')) - { - var image = ItemImageInfoFromValueString(part); - - if (image is not null) - { - result[position++] = image; - } - } - - if (position == count) - { - return result; - } - - if (position == 0) - { - return Array.Empty(); - } - - // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. - return result[..position]; - } - - private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) - { - const char Delimiter = '*'; - - var path = image.Path ?? string.Empty; - - bldr.Append(GetPathToSave(path)) - .Append(Delimiter) - .Append(image.DateModified.Ticks) - .Append(Delimiter) - .Append(image.Type) - .Append(Delimiter) - .Append(image.Width) - .Append(Delimiter) - .Append(image.Height); - - var hash = image.BlurHash; - if (!string.IsNullOrEmpty(hash)) - { - bldr.Append(Delimiter) - // Replace delimiters with other characters. - // This can be removed when we migrate to a proper DB. - .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); - } - } - - internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value) - { - const char Delimiter = '*'; - - var nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan path = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan dateModified = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan imageType = value[..nextSegment]; - - var image = new ItemImageInfo - { - Path = RestorePath(path.ToString()) - }; - - if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) - && ticks >= DateTime.MinValue.Ticks - && ticks <= DateTime.MaxValue.Ticks) - { - image.DateModified = new DateTime(ticks, DateTimeKind.Utc); - } - else - { - return null; - } - - if (Enum.TryParse(imageType, true, out ImageType type)) - { - image.Type = type; - } - else - { - return null; - } - - // Optional parameters: width*height*blurhash - if (nextSegment + 1 < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1 || nextSegment == value.Length) - { - return image; - } - - ReadOnlySpan widthSpan = value[..nextSegment]; - - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan heightSpan = value[..nextSegment]; - - if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) - && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) - { - image.Width = width; - image.Height = height; - } - - if (nextSegment < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - var length = value.Length; - - Span blurHashSpan = stackalloc char[length]; - for (int i = 0; i < length; i++) - { - var c = value[i]; - blurHashSpan[i] = c switch - { - '/' => Delimiter, - '\\' => '|', - _ => c - }; - } - - image.BlurHash = new string(blurHashSpan); - } - } - - return image; - } - - /// - /// Internal retrieve from items or users table. - /// - /// The id. - /// BaseItem. - /// is null. - /// is . - public BaseItem RetrieveItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - CheckDisposed(); - - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) - { - statement.TryBind("@guid", id); - - foreach (var row in statement.ExecuteQuery()) - { - return GetItem(row, new InternalItemsQuery()); - } - } - - return null; - } - - private bool TypeRequiresDeserialization(Type type) - { - if (_config.Configuration.SkipDeserializationForBasicTypes) - { - if (type == typeof(Channel) - || type == typeof(UserRootFolder)) - { - return false; + return false; } } @@ -1304,586 +238,6 @@ namespace Emby.Server.Implementations.Data && type != typeof(MusicAlbum); } - private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query) - { - return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false); - } - - private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization) - { - var typeString = reader.GetString(0); - - var type = _typeMapper.GetType(typeString); - - if (type is null) - { - return null; - } - - BaseItem item = null; - - if (TypeRequiresDeserialization(type) && !skipDeserialization) - { - try - { - item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem; - } - catch (JsonException ex) - { - Logger.LogError(ex, "Error deserializing item with JSON: {Data}", reader.GetString(1)); - } - } - - if (item is null) - { - try - { - item = Activator.CreateInstance(type) as BaseItem; - } - catch - { - } - } - - if (item is null) - { - return null; - } - - var index = 2; - - if (queryHasStartDate) - { - if (item is IHasStartDate hasStartDate && reader.TryReadDateTime(index, out var startDate)) - { - hasStartDate.StartDate = startDate; - } - - index++; - } - - if (reader.TryReadDateTime(index++, out var endDate)) - { - item.EndDate = endDate; - } - - if (reader.TryGetGuid(index, out var guid)) - { - item.ChannelId = guid; - } - - index++; - - if (enableProgramAttributes) - { - if (item is IHasProgramAttributes hasProgramAttributes) - { - if (reader.TryGetBoolean(index++, out var isMovie)) - { - hasProgramAttributes.IsMovie = isMovie; - } - - if (reader.TryGetBoolean(index++, out var isSeries)) - { - hasProgramAttributes.IsSeries = isSeries; - } - - if (reader.TryGetString(index++, out var episodeTitle)) - { - hasProgramAttributes.EpisodeTitle = episodeTitle; - } - - if (reader.TryGetBoolean(index++, out var isRepeat)) - { - hasProgramAttributes.IsRepeat = isRepeat; - } - } - else - { - index += 4; - } - } - - if (reader.TryGetSingle(index++, out var communityRating)) - { - item.CommunityRating = communityRating; - } - - if (HasField(query, ItemFields.CustomRating)) - { - if (reader.TryGetString(index++, out var customRating)) - { - item.CustomRating = customRating; - } - } - - if (reader.TryGetInt32(index++, out var indexNumber)) - { - item.IndexNumber = indexNumber; - } - - if (HasField(query, ItemFields.Settings)) - { - if (reader.TryGetBoolean(index++, out var isLocked)) - { - item.IsLocked = isLocked; - } - - if (reader.TryGetString(index++, out var preferredMetadataLanguage)) - { - item.PreferredMetadataLanguage = preferredMetadataLanguage; - } - - if (reader.TryGetString(index++, out var preferredMetadataCountryCode)) - { - item.PreferredMetadataCountryCode = preferredMetadataCountryCode; - } - } - - if (HasField(query, ItemFields.Width)) - { - if (reader.TryGetInt32(index++, out var width)) - { - item.Width = width; - } - } - - if (HasField(query, ItemFields.Height)) - { - if (reader.TryGetInt32(index++, out var height)) - { - item.Height = height; - } - } - - if (HasField(query, ItemFields.DateLastRefreshed)) - { - if (reader.TryReadDateTime(index++, out var dateLastRefreshed)) - { - item.DateLastRefreshed = dateLastRefreshed; - } - } - - if (reader.TryGetString(index++, out var name)) - { - item.Name = name; - } - - if (reader.TryGetString(index++, out var restorePath)) - { - item.Path = RestorePath(restorePath); - } - - if (reader.TryReadDateTime(index++, out var premiereDate)) - { - item.PremiereDate = premiereDate; - } - - if (HasField(query, ItemFields.Overview)) - { - if (reader.TryGetString(index++, out var overview)) - { - item.Overview = overview; - } - } - - if (reader.TryGetInt32(index++, out var parentIndexNumber)) - { - item.ParentIndexNumber = parentIndexNumber; - } - - if (reader.TryGetInt32(index++, out var productionYear)) - { - item.ProductionYear = productionYear; - } - - if (reader.TryGetString(index++, out var officialRating)) - { - item.OfficialRating = officialRating; - } - - if (HasField(query, ItemFields.SortName)) - { - if (reader.TryGetString(index++, out var forcedSortName)) - { - item.ForcedSortName = forcedSortName; - } - } - - if (reader.TryGetInt64(index++, out var runTimeTicks)) - { - item.RunTimeTicks = runTimeTicks; - } - - if (reader.TryGetInt64(index++, out var size)) - { - item.Size = size; - } - - if (HasField(query, ItemFields.DateCreated)) - { - if (reader.TryReadDateTime(index++, out var dateCreated)) - { - item.DateCreated = dateCreated; - } - } - - if (reader.TryReadDateTime(index++, out var dateModified)) - { - item.DateModified = dateModified; - } - - item.Id = reader.GetGuid(index++); - - if (HasField(query, ItemFields.Genres)) - { - if (reader.TryGetString(index++, out var genres)) - { - item.Genres = genres.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (reader.TryGetGuid(index++, out var parentId)) - { - item.ParentId = parentId; - } - - if (reader.TryGetString(index++, out var audioString)) - { - if (Enum.TryParse(audioString, true, out ProgramAudio audio)) - { - item.Audio = audio; - } - } - - // TODO: Even if not needed by apps, the server needs it internally - // But get this excluded from contexts where it is not needed - if (hasServiceName) - { - if (item is LiveTvChannel liveTvChannel) - { - if (reader.TryGetString(index, out var serviceName)) - { - liveTvChannel.ServiceName = serviceName; - } - } - - index++; - } - - if (reader.TryGetBoolean(index++, out var isInMixedFolder)) - { - item.IsInMixedFolder = isInMixedFolder; - } - - if (HasField(query, ItemFields.DateLastSaved)) - { - if (reader.TryReadDateTime(index++, out var dateLastSaved)) - { - item.DateLastSaved = dateLastSaved; - } - } - - if (HasField(query, ItemFields.Settings)) - { - if (reader.TryGetString(index++, out var lockedFields)) - { - List fields = null; - foreach (var i in lockedFields.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out MetadataField parsedValue)) - { - (fields ??= new List()).Add(parsedValue); - } - } - - item.LockedFields = fields?.ToArray() ?? Array.Empty(); - } - } - - if (HasField(query, ItemFields.Studios)) - { - if (reader.TryGetString(index++, out var studios)) - { - item.Studios = studios.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (HasField(query, ItemFields.Tags)) - { - if (reader.TryGetString(index++, out var tags)) - { - item.Tags = tags.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (hasTrailerTypes) - { - if (item is Trailer trailer) - { - if (reader.TryGetString(index, out var trailerTypes)) - { - List types = null; - foreach (var i in trailerTypes.AsSpan().Split('|')) - { - if (Enum.TryParse(i, true, out TrailerType parsedValue)) - { - (types ??= new List()).Add(parsedValue); - } - } - - trailer.TrailerTypes = types?.ToArray() ?? Array.Empty(); - } - } - - index++; - } - - if (HasField(query, ItemFields.OriginalTitle)) - { - if (reader.TryGetString(index++, out var originalTitle)) - { - item.OriginalTitle = originalTitle; - } - } - - if (item is Video video) - { - if (reader.TryGetString(index, out var primaryVersionId)) - { - video.PrimaryVersionId = primaryVersionId; - } - } - - index++; - - if (HasField(query, ItemFields.DateLastMediaAdded)) - { - if (item is Folder folder && reader.TryReadDateTime(index, out var dateLastMediaAdded)) - { - folder.DateLastMediaAdded = dateLastMediaAdded; - } - - index++; - } - - if (reader.TryGetString(index++, out var album)) - { - item.Album = album; - } - - if (reader.TryGetSingle(index++, out var lUFS)) - { - item.LUFS = lUFS; - } - - if (reader.TryGetSingle(index++, out var normalizationGain)) - { - item.NormalizationGain = normalizationGain; - } - - if (reader.TryGetSingle(index++, out var criticRating)) - { - item.CriticRating = criticRating; - } - - if (reader.TryGetBoolean(index++, out var isVirtualItem)) - { - item.IsVirtualItem = isVirtualItem; - } - - if (item is IHasSeries hasSeriesName) - { - if (reader.TryGetString(index, out var seriesName)) - { - hasSeriesName.SeriesName = seriesName; - } - } - - index++; - - if (hasEpisodeAttributes) - { - if (item is Episode episode) - { - if (reader.TryGetString(index, out var seasonName)) - { - episode.SeasonName = seasonName; - } - - index++; - if (reader.TryGetGuid(index, out var seasonId)) - { - episode.SeasonId = seasonId; - } - } - else - { - index++; - } - - index++; - } - - var hasSeries = item as IHasSeries; - if (hasSeriesFields) - { - if (hasSeries is not null) - { - if (reader.TryGetGuid(index, out var seriesId)) - { - hasSeries.SeriesId = seriesId; - } - } - - index++; - } - - if (HasField(query, ItemFields.PresentationUniqueKey)) - { - if (reader.TryGetString(index++, out var presentationUniqueKey)) - { - item.PresentationUniqueKey = presentationUniqueKey; - } - } - - if (HasField(query, ItemFields.InheritedParentalRatingValue)) - { - if (reader.TryGetInt32(index++, out var parentalRating)) - { - item.InheritedParentalRatingValue = parentalRating; - } - } - - if (HasField(query, ItemFields.ExternalSeriesId)) - { - if (reader.TryGetString(index++, out var externalSeriesId)) - { - item.ExternalSeriesId = externalSeriesId; - } - } - - if (HasField(query, ItemFields.Taglines)) - { - if (reader.TryGetString(index++, out var tagLine)) - { - item.Tagline = tagLine; - } - } - - if (item.ProviderIds.Count == 0 && reader.TryGetString(index, out var providerIds)) - { - DeserializeProviderIds(providerIds, item); - } - - index++; - - if (query.DtoOptions.EnableImages) - { - if (item.ImageInfos.Length == 0 && reader.TryGetString(index, out var imageInfos)) - { - item.ImageInfos = DeserializeImages(imageInfos); - } - - index++; - } - - if (HasField(query, ItemFields.ProductionLocations)) - { - if (reader.TryGetString(index++, out var productionLocations)) - { - item.ProductionLocations = productionLocations.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - } - - if (HasField(query, ItemFields.ExtraIds)) - { - if (reader.TryGetString(index++, out var extraIds)) - { - item.ExtraIds = SplitToGuids(extraIds); - } - } - - if (reader.TryGetInt32(index++, out var totalBitrate)) - { - item.TotalBitrate = totalBitrate; - } - - if (reader.TryGetString(index++, out var extraTypeString)) - { - if (Enum.TryParse(extraTypeString, true, out ExtraType extraType)) - { - item.ExtraType = extraType; - } - } - - if (hasArtistFields) - { - if (item is IHasArtist hasArtists && reader.TryGetString(index, out var artists)) - { - hasArtists.Artists = artists.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - index++; - - if (item is IHasAlbumArtist hasAlbumArtists && reader.TryGetString(index, out var albumArtists)) - { - hasAlbumArtists.AlbumArtists = albumArtists.Split('|', StringSplitOptions.RemoveEmptyEntries); - } - - index++; - } - - if (reader.TryGetString(index++, out var externalId)) - { - item.ExternalId = externalId; - } - - if (HasField(query, ItemFields.SeriesPresentationUniqueKey)) - { - if (hasSeries is not null) - { - if (reader.TryGetString(index, out var seriesPresentationUniqueKey)) - { - hasSeries.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; - } - } - - index++; - } - - if (enableProgramAttributes) - { - if (item is LiveTvProgram program && reader.TryGetString(index, out var showId)) - { - program.ShowId = showId; - } - - index++; - } - - if (reader.TryGetGuid(index, out var ownerId)) - { - item.OwnerId = ownerId; - } - - return item; - } - - private static Guid[] SplitToGuids(string value) - { - var ids = value.Split('|'); - - var result = new Guid[ids.Length]; - - for (var i = 0; i < result.Length; i++) - { - result[i] = new Guid(ids[i]); - } - - return result; - } - /// public List GetChapters(BaseItem item) { diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Data/Entities/AncestorId.cs new file mode 100644 index 000000000..dc83b763e --- /dev/null +++ b/Jellyfin.Data/Entities/AncestorId.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CA1708 // Identifiers should differ by more than case +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class AncestorId +{ + public Guid Id { get; set; } + + public Guid ItemId { get; set; } + + public required BaseItem Item { get; set; } + + public string? AncestorIdText { get; set; } +} diff --git a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs new file mode 100644 index 000000000..d2483548b --- /dev/null +++ b/Jellyfin.Data/Entities/AttachmentStreamInfo.cs @@ -0,0 +1,21 @@ +using System; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class AttachmentStreamInfo +{ + public required Guid ItemId { get; set; } + + public required int Index { get; set; } + + public required string Codec { get; set; } + + public string? CodecTag { get; set; } + + public string? Comment { get; set; } + + public string? Filename { get; set; } + + public string? MimeType { get; set; } +} diff --git a/Jellyfin.Data/Entities/BaseItem.cs b/Jellyfin.Data/Entities/BaseItem.cs new file mode 100644 index 000000000..c0c88b2e6 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItem.cs @@ -0,0 +1,164 @@ +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 IReadOnlyList? 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 string? 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? ProviderIds { 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; } +} diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs new file mode 100644 index 000000000..6822b1902 --- /dev/null +++ b/Jellyfin.Data/Entities/Chapter.cs @@ -0,0 +1,22 @@ +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 Chapter +{ + public Guid ItemId { get; set; } + + public required int ChapterIndex { get; set; } + + public required long StartPositionTicks { get; set; } + + public string? Name { get; set; } + + public string? ImagePath { get; set; } + + public DateTime? ImageDateModified { get; set; } +} diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Data/Entities/ItemValue.cs new file mode 100644 index 000000000..a3c0908bb --- /dev/null +++ b/Jellyfin.Data/Entities/ItemValue.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; + +public class ItemValue +{ + public Guid ItemId { get; set; } + public required BaseItem Item { get; set; } + + public required int Type { get; set; } + public required string Value { get; set; } + public required string CleanValue { get; set; } +} diff --git a/Jellyfin.Data/Entities/MediaStreamInfo.cs b/Jellyfin.Data/Entities/MediaStreamInfo.cs new file mode 100644 index 000000000..3b89ca62f --- /dev/null +++ b/Jellyfin.Data/Entities/MediaStreamInfo.cs @@ -0,0 +1,101 @@ +using System; + +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public class MediaStreamInfo +{ + public Guid ItemId { get; set; } + + public required BaseItem Item { get; set; } + + public int StreamIndex { get; set; } + + public string? StreamType { get; set; } + + public string? Codec { get; set; } + + public string? Language { get; set; } + + public string? ChannelLayout { get; set; } + + public string? Profile { get; set; } + + public string? AspectRatio { get; set; } + + public string? Path { get; set; } + + public bool IsInterlaced { get; set; } + + public required int BitRate { get; set; } + + public required int Channels { get; set; } + + public required int SampleRate { get; set; } + + public bool IsDefault { get; set; } + + public bool IsForced { get; set; } + + public bool IsExternal { get; set; } + + public required int Height { get; set; } + + public required int Width { get; set; } + + public required float AverageFrameRate { get; set; } + + public required float RealFrameRate { get; set; } + + public required float Level { get; set; } + + public string? PixelFormat { get; set; } + + public required int BitDepth { get; set; } + + public required bool IsAnamorphic { get; set; } + + public required int RefFrames { get; set; } + + public required string CodecTag { get; set; } + + public required string Comment { get; set; } + + public required string NalLengthSize { get; set; } + + public required bool IsAvc { get; set; } + + public required string Title { get; set; } + + public required string TimeBase { get; set; } + + public required string CodecTimeBase { get; set; } + + public required string ColorPrimaries { get; set; } + + public required string ColorSpace { get; set; } + + public required string ColorTransfer { get; set; } + + public required int DvVersionMajor { get; set; } + + public required int DvVersionMinor { get; set; } + + public required int DvProfile { get; set; } + + public required int DvLevel { get; set; } + + public required int RpuPresentFlag { get; set; } + + public required int ElPresentFlag { get; set; } + + public required int BlPresentFlag { get; set; } + + public required int DvBlSignalCompatibilityId { get; set; } + + public required bool IsHearingImpaired { get; set; } + + public required int Rotation { get; set; } + + public string? KeyFrames { get; set; } +} diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs new file mode 100644 index 000000000..72c39699b --- /dev/null +++ b/Jellyfin.Data/Entities/People.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Jellyfin.Data.Entities; +public class People +{ + public Guid ItemId { get; set; } + + public required string Name { get; set; } + public string? Role { get; set; } + public string? PersonType { get; set; } + public int? SortOrder { get; set; } + public int? ListOrder { get; set; } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs new file mode 100644 index 000000000..4ad842e0b --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -0,0 +1,753 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using Jellyfin.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; +using Microsoft.EntityFrameworkCore; +using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; +using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Handles all storage logic for BaseItems. +/// +public class BaseItemManager +{ + private readonly IDbContextFactory _dbProvider; + private readonly IServerApplicationHost _appHost; + + /// + /// 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; + } + + /// + /// 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)); + } + + /// + /// Saves the items. + /// + /// The items. + /// The cancellation token. + /// + /// or is null. + /// + 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? GetSingle(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); + } + + 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. + /// + /// 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.ProviderIds is not null) + { + DeserializeProviderIds(entity.ProviderIds, dto); + } + + 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.ProviderIds = SerializeProviderIds(dto.ProviderIds); + + 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 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; + } +} diff --git a/Jellyfin.Server.Implementations/Item/ChapterManager.cs b/Jellyfin.Server.Implementations/Item/ChapterManager.cs new file mode 100644 index 000000000..273cc96ba --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/ChapterManager.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; +using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; + +namespace Jellyfin.Server.Implementations.Item; + +public class ChapterManager +{ + private readonly IDbContextFactory _dbProvider; + + public ChapterManager(IDbContextFactory dbProvider) + { + _dbProvider = dbProvider; + } + + public IReadOnlyList GetChapters(BaseItemDto baseItemDto) + { + using var context = _dbProvider.CreateDbContext(); + return context.Chapters.Where(e => e.ItemId.Equals(baseItemDto.Id)).Select(Map).ToList(); + } + + 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/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 8e2c21fbc..01f059db4 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -93,6 +93,41 @@ public class JellyfinDbContext : DbContext /// public DbSet UserData => Set(); + /// + /// Gets the containing the user data. + /// + public DbSet AncestorIds => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet AttachmentStreamInfos => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet BaseItems => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet Chapters => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet ItemValues => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet MediaStreamInfos => Set(); + + /// + /// Gets the containing the user data. + /// + public DbSet Peoples => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs new file mode 100644 index 000000000..b7fe909dd --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// AncestorId configuration. +/// +public class AncestorIdConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => new { e.ItemId, e.Id }); + builder.HasIndex(e => e.Id); + builder.HasIndex(e => new { e.ItemId, e.AncestorIdText }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs new file mode 100644 index 000000000..c0f09670d --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -0,0 +1,42 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// Configuration for BaseItem. +/// +public class BaseItemConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => e.Path); + builder.HasIndex(e => e.ParentId); + builder.HasIndex(e => e.PresentationUniqueKey); + builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem }); + builder.HasIndex(e => new { e.UserDataKey, e.Type }); + + // covering index + builder.HasIndex(e => new { e.TopParentId, e.Id }); + // series + builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.PresentationUniqueKey, e.SortName }); + // series counts + // seriesdateplayed sort order + builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.IsFolder, e.IsVirtualItem }); + // live tv programs + builder.HasIndex(e => new { e.Type, e.TopParentId, e.StartDate }); + // covering index for getitemvalues + builder.HasIndex(e => new { e.Type, e.TopParentId, e.Id }); + // used by movie suggestions + builder.HasIndex(e => new { e.Type, e.TopParentId, e.PresentationUniqueKey }); + // latest items + builder.HasIndex(e => new { e.Type, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated }); + builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated }); + // resume + builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs new file mode 100644 index 000000000..0e7c88931 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + + +/// +/// Chapter configuration. +/// +public class ChapterConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => new { e.ItemId, e.ChapterIndex }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs new file mode 100644 index 000000000..a7de6ec32 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// itemvalues Configuration. +/// +public class ItemValuesConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => new { e.ItemId, e.Type, e.CleanValue }); + builder.HasIndex(e => new { e.ItemId, e.Type, e.Value }); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs new file mode 100644 index 000000000..f6cd39c24 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// People configuration. +/// +public class PeopleConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasNoKey(); + builder.HasIndex(e => new { e.ItemId, e.ListOrder }); + builder.HasIndex(e => e.Name); + } +} -- cgit v1.2.3 From dafd1864104afe8d9bb8f20cf8bf40931a0417f1 Mon Sep 17 00:00:00 2001 From: elfalem Date: Mon, 23 Sep 2024 18:52:18 -0400 Subject: Ensure user's own playlists are accessible regardless of allowed tags --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 60f5ee47a..5598c81dc 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4203,6 +4203,15 @@ namespace Emby.Server.Implementations.Data 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)"); @@ -4214,6 +4223,11 @@ namespace Emby.Server.Implementations.Data { statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); } + + if (query.User != null) + { + statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%"""); + } } } -- cgit v1.2.3 From 8499be23ccfbe1b7eed6768d3ea634a6eed05217 Mon Sep 17 00:00:00 2001 From: elfalem Date: Wed, 25 Sep 2024 20:05:00 -0400 Subject: Update SqliteItemRepository.cs - incorporate review suggestion Co-authored-by: Bond-009 --- Emby.Server.Implementations/Data/SqliteItemRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 5598c81dc..3da925f93 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -4224,7 +4224,7 @@ namespace Emby.Server.Implementations.Data statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); } - if (query.User != null) + if (query.User is not null) { statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%"""); } -- cgit v1.2.3 From 0ef72683bba5e1ce2dfaf0d6aae49466c7a7d0c6 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 30 Sep 2024 04:21:24 +0200 Subject: Do not consider tags in search (#12741) --- .../Data/SqliteItemRepository.cs | 34 +++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 3da925f93..3477194cf 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Diagnostics; @@ -547,6 +545,7 @@ namespace Emby.Server.Implementations.Data } } + /// public void SaveImages(BaseItem item) { ArgumentNullException.ThrowIfNull(item); @@ -2340,9 +2339,6 @@ namespace Emby.Server.Implementations.Data if (query.SearchTerm.Length > 1) { builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); - builder.Append("+ (SELECT COUNT(1) * 1 from ItemValues where ItemId=Guid and CleanValue like @SearchTermContains)"); - builder.Append("+ (SELECT COUNT(1) * 2 from ItemValues where ItemId=Guid and CleanValue like @SearchTermStartsWith)"); - builder.Append("+ (SELECT COUNT(1) * 10 from ItemValues where ItemId=Guid and CleanValue like @SearchTermEquals)"); } builder.Append(") as SearchScore"); @@ -2373,11 +2369,6 @@ namespace Emby.Server.Implementations.Data { statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); } - - if (commandText.Contains("@SearchTermEquals", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SearchTermEquals", searchTerm); - } } private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement) @@ -2443,6 +2434,7 @@ namespace Emby.Server.Implementations.Data return string.Empty; } + /// public int GetCount(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -2490,6 +2482,7 @@ namespace Emby.Server.Implementations.Data } } + /// public List GetItemList(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -2643,6 +2636,7 @@ namespace Emby.Server.Implementations.Data items.Add(newItem); } + /// public QueryResult GetItems(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -2887,6 +2881,7 @@ namespace Emby.Server.Implementations.Data }; } + /// public List GetItemIdsList(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -4445,6 +4440,7 @@ namespace Emby.Server.Implementations.Data || query.IncludeItemTypes.Contains(BaseItemKind.Season); } + /// public void UpdateInheritedValues() { const string Statements = """ @@ -4461,6 +4457,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type transaction.Commit(); } + /// public void DeleteItem(Guid id) { if (id.IsEmpty()) @@ -4503,6 +4500,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } + /// public List GetPeopleNames(InternalPeopleQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -4541,6 +4539,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return list; } + /// public List GetPeople(InternalPeopleQuery query) { ArgumentNullException.ThrowIfNull(query); @@ -4700,46 +4699,55 @@ AND Type = @InternalPersonType)"); } } + /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) { return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); } + /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) { return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); } + /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) { return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); } + /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) { return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); } + /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) { return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); } + /// public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) { return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); } + /// public List GetStudioNames() { return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); } + /// public List GetAllArtistNames() { return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); } + /// public List GetMusicGenreNames() { return GetItemValueNames( @@ -4754,6 +4762,7 @@ AND Type = @InternalPersonType)"); Array.Empty()); } + /// public List GetGenreNames() { return GetItemValueNames( @@ -5231,6 +5240,7 @@ AND Type = @InternalPersonType)"); } } + /// public void UpdatePeople(Guid itemId, List people) { if (itemId.IsEmpty()) @@ -5332,6 +5342,7 @@ AND Type = @InternalPersonType)"); return item; } + /// public List GetMediaStreams(MediaStreamQuery query) { CheckDisposed(); @@ -5380,6 +5391,7 @@ AND Type = @InternalPersonType)"); } } + /// public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) { CheckDisposed(); @@ -5734,6 +5746,7 @@ AND Type = @InternalPersonType)"); return item; } + /// public List GetMediaAttachments(MediaAttachmentQuery query) { CheckDisposed(); @@ -5769,6 +5782,7 @@ AND Type = @InternalPersonType)"); return list; } + /// public void SaveMediaAttachments( Guid id, IReadOnlyList attachments, -- cgit v1.2.3 From 6c819fe516ba742f1dcc77d61f6eedbe987cd692 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:27:27 +0000 Subject: WIP BaseItem search refactoring --- .../Data/SqliteItemRepository.cs | 1211 +---------------- Jellyfin.Data/Entities/BaseItem.cs | 6 + Jellyfin.Data/Entities/People.cs | 1 + .../Item/BaseItemManager.cs | 1364 +++++++++++++++++++- .../Item/ChapterManager.cs | 58 +- MediaBrowser.Controller/Chapters/ChapterManager.cs | 24 + .../Chapters/IChapterManager.cs | 16 + .../Persistence/IItemRepository.cs | 22 - MediaBrowser.Providers/Chapters/ChapterManager.cs | 26 - .../MediaBrowser.Providers.csproj | 2 +- 10 files changed, 1444 insertions(+), 1286 deletions(-) create mode 100644 MediaBrowser.Controller/Chapters/ChapterManager.cs delete mode 100644 MediaBrowser.Providers/Chapters/ChapterManager.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index a2aeaf0fc..94a5eba81 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -59,119 +59,6 @@ namespace Emby.Server.Implementations.Data private readonly TypeMapper _typeMapper; private readonly JsonSerializerOptions _jsonOptions; - private readonly ItemFields[] _allItemFields = Enum.GetValues(); - - private static readonly string _mediaStreamSaveColumnsInsertQuery = - $"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values "; - - private static readonly string _mediaStreamSaveColumnsSelectQuery = - $"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId"; - - private static readonly string[] _mediaAttachmentSaveColumns = - { - "ItemId", - "AttachmentIndex", - "Codec", - "CodecTag", - "Comment", - "Filename", - "MIMEType" - }; - - private static readonly string _mediaAttachmentSaveColumnsSelectQuery = - $"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId"; - - private static readonly string _mediaAttachmentInsertPrefix = BuildMediaAttachmentInsertPrefix(); - - private static readonly BaseItemKind[] _programTypes = new[] - { - BaseItemKind.Program, - BaseItemKind.TvChannel, - BaseItemKind.LiveTvProgram, - BaseItemKind.LiveTvChannel - }; - - private static readonly BaseItemKind[] _programExcludeParentTypes = new[] - { - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicArtist, - BaseItemKind.PhotoAlbum - }; - - private static readonly BaseItemKind[] _serviceTypes = new[] - { - BaseItemKind.TvChannel, - BaseItemKind.LiveTvChannel - }; - - private static readonly BaseItemKind[] _startDateTypes = new[] - { - BaseItemKind.Program, - BaseItemKind.LiveTvProgram - }; - - private static readonly BaseItemKind[] _seriesTypes = new[] - { - BaseItemKind.Book, - BaseItemKind.AudioBook, - BaseItemKind.Episode, - BaseItemKind.Season - }; - - private static readonly BaseItemKind[] _artistExcludeParentTypes = new[] - { - BaseItemKind.Series, - BaseItemKind.Season, - BaseItemKind.PhotoAlbum - }; - - private static readonly BaseItemKind[] _artistsTypes = new[] - { - BaseItemKind.Audio, - BaseItemKind.MusicAlbum, - BaseItemKind.MusicVideo, - BaseItemKind.AudioBook - }; - - private static readonly Dictionary _baseItemKindNames = new() - { - { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, - { BaseItemKind.Audio, typeof(Audio).FullName }, - { BaseItemKind.AudioBook, typeof(AudioBook).FullName }, - { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName }, - { BaseItemKind.Book, typeof(Book).FullName }, - { BaseItemKind.BoxSet, typeof(BoxSet).FullName }, - { BaseItemKind.Channel, typeof(Channel).FullName }, - { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName }, - { BaseItemKind.Episode, typeof(Episode).FullName }, - { BaseItemKind.Folder, typeof(Folder).FullName }, - { BaseItemKind.Genre, typeof(Genre).FullName }, - { BaseItemKind.Movie, typeof(Movie).FullName }, - { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName }, - { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName }, - { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName }, - { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName }, - { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName }, - { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName }, - { BaseItemKind.Person, typeof(Person).FullName }, - { BaseItemKind.Photo, typeof(Photo).FullName }, - { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName }, - { BaseItemKind.Playlist, typeof(Playlist).FullName }, - { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName }, - { BaseItemKind.Season, typeof(Season).FullName }, - { BaseItemKind.Series, typeof(Series).FullName }, - { BaseItemKind.Studio, typeof(Studio).FullName }, - { BaseItemKind.Trailer, typeof(Trailer).FullName }, - { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName }, - { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName }, - { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName }, - { BaseItemKind.UserView, typeof(UserView).FullName }, - { BaseItemKind.Video, typeof(Video).FullName }, - { BaseItemKind.Year, typeof(Year).FullName } - }; - /// /// Initializes a new instance of the class. /// @@ -210,957 +97,15 @@ namespace Emby.Server.Implementations.Data /// protected override TempStoreMode TempStore => TempStoreMode.Memory; - /// - /// Opens the connection to the database. - /// - public override void Initialize() - { - base.Initialize(); - - const string CreateMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, Rotation INT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - - const string CreateMediaAttachmentsTableCommand - = "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))"; - - string[] queries = - { - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", - - "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", - "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", - "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", - - "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", - - "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - - "drop index if exists idxPeopleItemId", - "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", - "create index if not exists idxPeopleName on People(Name)", - - "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - - CreateMediaStreamsTableCommand, - CreateMediaAttachmentsTableCommand, - - "pragma shrink_memory" - }; - - string[] postQueries = - { - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", - "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", - - "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", - "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", - "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", - - // covering index - "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", - - // series - "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - - // series counts - // seriesdateplayed sort order - "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", - - // live tv programs - "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", - - // covering index for getitemvalues - "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", - - // used by movie suggestions - "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", - "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", - - // latest items - "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", - "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", - - // resume - "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", - - // items by name - "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", - - // Used to update inherited tags - "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", - - "CREATE INDEX IF NOT EXISTS idx_TypedBaseItemsUserDataKeyType ON TypedBaseItems(UserDataKey, Type)", - "CREATE INDEX IF NOT EXISTS idx_PeopleNameListOrder ON People(Name, ListOrder)" - }; - - using (var connection = GetConnection()) - using (var transaction = connection.BeginTransaction()) - { - connection.Execute(string.Join(';', queries)); - - var existingColumnNames = GetColumnNames(connection, "AncestorIds"); - AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "TypedBaseItems"); - - AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames); - AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "ItemValues"); - AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, ChaptersTableName); - AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - - existingColumnNames = GetColumnNames(connection, "MediaStreams"); - AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames); - AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames); - AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames); - - AddColumn(connection, "MediaStreams", "Rotation", "INT", existingColumnNames); - - connection.Execute(string.Join(';', postQueries)); - - transaction.Commit(); - } - } - - /// - public void SaveImages(BaseItem item) - { - ArgumentNullException.ThrowIfNull(item); - - CheckDisposed(); - - var images = SerializeImages(item.ImageInfos); - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id"); - saveImagesStatement.TryBind("@Id", item.Id); - saveImagesStatement.TryBind("@Images", images); - - saveImagesStatement.ExecuteNonQuery(); - transaction.Commit(); - } - - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// - /// or is null. - /// - public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(items); - - cancellationToken.ThrowIfCancellationRequested(); - - CheckDisposed(); - - var itemsLen = items.Count; - var tuples = new ValueTuple, BaseItem, string, List>[itemsLen]; - for (int i = 0; i < itemsLen; i++) - { - var item = items[i]; - var ancestorIds = item.SupportsAncestors ? - item.GetAncestorIds().Distinct().ToList() : - null; - - var topParent = item.GetTopParent(); - - var userdataKey = item.GetUserDataKeys().FirstOrDefault(); - var inheritedTags = item.GetInheritedTags(); - - tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags); - } - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - SaveItemsInTransaction(connection, tuples); - transaction.Commit(); - } - private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples) + private bool TypeRequiresDeserialization(Type type) { - using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText)) - using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) + if (_config.Configuration.SkipDeserializationForBasicTypes) { - var requiresReset = false; - foreach (var tuple in tuples) + if (type == typeof(Channel) + || type == typeof(UserRootFolder)) { - if (requiresReset) - { - saveItemStatement.Parameters.Clear(); - deleteAncestorsStatement.Parameters.Clear(); - } - - var item = tuple.Item; - var topParent = tuple.TopParent; - var userDataKey = tuple.UserDataKey; - - SaveItem(item, topParent, userDataKey, saveItemStatement); - - var inheritedTags = tuple.InheritedTags; - - if (item.SupportsAncestors) - { - UpdateAncestors(item.Id, tuple.AncestorIds, db, deleteAncestorsStatement); - } - - UpdateItemValues(item.Id, GetItemValuesToSave(item, inheritedTags), db); - - requiresReset = true; - } - } - } - - private string GetPathToSave(string path) - { - if (path is null) - { - return null; - } - - return _appHost.ReverseVirtualPath(path); - } - - private string RestorePath(string path) - { - return _appHost.ExpandVirtualPath(path); - } - - private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement) - { - Type type = item.GetType(); - - saveItemStatement.TryBind("@guid", item.Id); - saveItemStatement.TryBind("@type", type.FullName); - - if (TypeRequiresDeserialization(type)) - { - saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true); - } - else - { - saveItemStatement.TryBindNull("@data"); - } - - saveItemStatement.TryBind("@Path", GetPathToSave(item.Path)); - - if (item is IHasStartDate hasStartDate) - { - saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate); - } - else - { - saveItemStatement.TryBindNull("@StartDate"); - } - - if (item.EndDate.HasValue) - { - saveItemStatement.TryBind("@EndDate", item.EndDate.Value); - } - else - { - saveItemStatement.TryBindNull("@EndDate"); - } - - saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); - - if (item is IHasProgramAttributes hasProgramAttributes) - { - saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); - saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); - saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); - saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); - } - else - { - saveItemStatement.TryBindNull("@IsMovie"); - saveItemStatement.TryBindNull("@IsSeries"); - saveItemStatement.TryBindNull("@EpisodeTitle"); - saveItemStatement.TryBindNull("@IsRepeat"); - } - - saveItemStatement.TryBind("@CommunityRating", item.CommunityRating); - saveItemStatement.TryBind("@CustomRating", item.CustomRating); - saveItemStatement.TryBind("@IndexNumber", item.IndexNumber); - saveItemStatement.TryBind("@IsLocked", item.IsLocked); - saveItemStatement.TryBind("@Name", item.Name); - saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); - saveItemStatement.TryBind("@MediaType", item.MediaType.ToString()); - saveItemStatement.TryBind("@Overview", item.Overview); - saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); - saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); - saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); - - var parentId = item.ParentId; - if (parentId.IsEmpty()) - { - saveItemStatement.TryBindNull("@ParentId"); - } - else - { - saveItemStatement.TryBind("@ParentId", parentId); - } - - if (item.Genres.Length > 0) - { - saveItemStatement.TryBind("@Genres", string.Join('|', item.Genres)); - } - else - { - saveItemStatement.TryBindNull("@Genres"); - } - - saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); - - saveItemStatement.TryBind("@SortName", item.SortName); - - saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName); - - saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); - saveItemStatement.TryBind("@Size", item.Size); - - saveItemStatement.TryBind("@DateCreated", item.DateCreated); - saveItemStatement.TryBind("@DateModified", item.DateModified); - - saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); - saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); - - if (item.Width > 0) - { - saveItemStatement.TryBind("@Width", item.Width); - } - else - { - saveItemStatement.TryBindNull("@Width"); - } - - if (item.Height > 0) - { - saveItemStatement.TryBind("@Height", item.Height); - } - else - { - saveItemStatement.TryBindNull("@Height"); - } - - if (item.DateLastRefreshed != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed); - } - else - { - saveItemStatement.TryBindNull("@DateLastRefreshed"); - } - - if (item.DateLastSaved != default(DateTime)) - { - saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved); - } - else - { - saveItemStatement.TryBindNull("@DateLastSaved"); - } - - saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); - - if (item.LockedFields.Length > 0) - { - saveItemStatement.TryBind("@LockedFields", string.Join('|', item.LockedFields)); - } - else - { - saveItemStatement.TryBindNull("@LockedFields"); - } - - if (item.Studios.Length > 0) - { - saveItemStatement.TryBind("@Studios", string.Join('|', item.Studios)); - } - else - { - saveItemStatement.TryBindNull("@Studios"); - } - - if (item.Audio.HasValue) - { - saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@Audio"); - } - - if (item is LiveTvChannel liveTvChannel) - { - saveItemStatement.TryBind("@ExternalServiceId", liveTvChannel.ServiceName); - } - else - { - saveItemStatement.TryBindNull("@ExternalServiceId"); - } - - if (item.Tags.Length > 0) - { - saveItemStatement.TryBind("@Tags", string.Join('|', item.Tags)); - } - else - { - saveItemStatement.TryBindNull("@Tags"); - } - - saveItemStatement.TryBind("@IsFolder", item.IsFolder); - - saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); - - if (topParent is null) - { - saveItemStatement.TryBindNull("@TopParentId"); - } - else - { - saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture)); - } - - if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) - { - saveItemStatement.TryBind("@TrailerTypes", string.Join('|', trailer.TrailerTypes)); - } - else - { - saveItemStatement.TryBindNull("@TrailerTypes"); - } - - saveItemStatement.TryBind("@CriticRating", item.CriticRating); - - if (string.IsNullOrWhiteSpace(item.Name)) - { - saveItemStatement.TryBindNull("@CleanName"); - } - else - { - saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name)); - } - - saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey); - saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle); - - if (item is Video video) - { - saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId); - } - else - { - saveItemStatement.TryBindNull("@PrimaryVersionId"); - } - - if (item is Folder folder && folder.DateLastMediaAdded.HasValue) - { - saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value); - } - else - { - saveItemStatement.TryBindNull("@DateLastMediaAdded"); - } - - saveItemStatement.TryBind("@Album", item.Album); - saveItemStatement.TryBind("@LUFS", item.LUFS); - saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain); - saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); - - if (item is IHasSeries hasSeriesName) - { - saveItemStatement.TryBind("@SeriesName", hasSeriesName.SeriesName); - } - else - { - saveItemStatement.TryBindNull("@SeriesName"); - } - - if (string.IsNullOrWhiteSpace(userDataKey)) - { - saveItemStatement.TryBindNull("@UserDataKey"); - } - else - { - saveItemStatement.TryBind("@UserDataKey", userDataKey); - } - - if (item is Episode episode) - { - saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - - var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; - - saveItemStatement.TryBind("@SeasonId", nullableSeasonId); - } - else - { - saveItemStatement.TryBindNull("@SeasonName"); - saveItemStatement.TryBindNull("@SeasonId"); - } - - if (item is IHasSeries hasSeries) - { - var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; - - saveItemStatement.TryBind("@SeriesId", nullableSeriesId); - saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); - } - else - { - saveItemStatement.TryBindNull("@SeriesId"); - saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey"); - } - - saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); - saveItemStatement.TryBind("@Tagline", item.Tagline); - - saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item.ProviderIds)); - saveItemStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); - - if (item.ProductionLocations.Length > 0) - { - saveItemStatement.TryBind("@ProductionLocations", string.Join('|', item.ProductionLocations)); - } - else - { - saveItemStatement.TryBindNull("@ProductionLocations"); - } - - if (item.ExtraIds.Length > 0) - { - saveItemStatement.TryBind("@ExtraIds", string.Join('|', item.ExtraIds)); - } - else - { - saveItemStatement.TryBindNull("@ExtraIds"); - } - - saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); - if (item.ExtraType.HasValue) - { - saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString()); - } - else - { - saveItemStatement.TryBindNull("@ExtraType"); - } - - string artists = null; - if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) - { - artists = string.Join('|', hasArtists.Artists); - } - - saveItemStatement.TryBind("@Artists", artists); - - string albumArtists = null; - if (item is IHasAlbumArtist hasAlbumArtists - && hasAlbumArtists.AlbumArtists.Count > 0) - { - albumArtists = string.Join('|', hasAlbumArtists.AlbumArtists); - } - - saveItemStatement.TryBind("@AlbumArtists", albumArtists); - saveItemStatement.TryBind("@ExternalId", item.ExternalId); - - if (item is LiveTvProgram program) - { - saveItemStatement.TryBind("@ShowId", program.ShowId); - } - else - { - saveItemStatement.TryBindNull("@ShowId"); - } - - Guid ownerId = item.OwnerId; - if (ownerId.IsEmpty()) - { - saveItemStatement.TryBindNull("@OwnerId"); - } - else - { - saveItemStatement.TryBind("@OwnerId", ownerId); - } - - saveItemStatement.ExecuteNonQuery(); - } - - internal static string SerializeProviderIds(Dictionary providerIds) - { - StringBuilder str = new StringBuilder(); - foreach (var i in providerIds) - { - // Ideally we shouldn't need this IsNullOrWhiteSpace check, - // but we're seeing some cases of bad data slip through - if (string.IsNullOrWhiteSpace(i.Value)) - { - continue; - } - - str.Append(i.Key) - .Append('=') - .Append(i.Value) - .Append('|'); - } - - if (str.Length == 0) - { - return null; - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal static void DeserializeProviderIds(string value, IHasProviderIds item) - { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - foreach (var part in value.SpanSplit('|')) - { - var providerDelimiterIndex = part.IndexOf('='); - // Don't let empty values through - if (providerDelimiterIndex != -1 && part.Length != providerDelimiterIndex + 1) - { - item.SetProviderId(part[..providerDelimiterIndex].ToString(), part[(providerDelimiterIndex + 1)..].ToString()); - } - } - } - - internal string SerializeImages(ItemImageInfo[] images) - { - if (images.Length == 0) - { - return null; - } - - StringBuilder str = new StringBuilder(); - foreach (var i in images) - { - if (string.IsNullOrWhiteSpace(i.Path)) - { - continue; - } - - AppendItemImageInfo(str, i); - str.Append('|'); - } - - str.Length -= 1; // Remove last | - return str.ToString(); - } - - internal ItemImageInfo[] DeserializeImages(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Array.Empty(); - } - - // TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed - var valueSpan = value.AsSpan(); - var count = valueSpan.Count('|') + 1; - - var position = 0; - var result = new ItemImageInfo[count]; - foreach (var part in valueSpan.Split('|')) - { - var image = ItemImageInfoFromValueString(part); - - if (image is not null) - { - result[position++] = image; - } - } - - if (position == count) - { - return result; - } - - if (position == 0) - { - return Array.Empty(); - } - - // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array. - return result[..position]; - } - - private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) - { - const char Delimiter = '*'; - - var path = image.Path ?? string.Empty; - - bldr.Append(GetPathToSave(path)) - .Append(Delimiter) - .Append(image.DateModified.Ticks) - .Append(Delimiter) - .Append(image.Type) - .Append(Delimiter) - .Append(image.Width) - .Append(Delimiter) - .Append(image.Height); - - var hash = image.BlurHash; - if (!string.IsNullOrEmpty(hash)) - { - bldr.Append(Delimiter) - // Replace delimiters with other characters. - // This can be removed when we migrate to a proper DB. - .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); - } - } - - internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan value) - { - const char Delimiter = '*'; - - var nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan path = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - return null; - } - - ReadOnlySpan dateModified = value[..nextSegment]; - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan imageType = value[..nextSegment]; - - var image = new ItemImageInfo - { - Path = RestorePath(path.ToString()) - }; - - if (long.TryParse(dateModified, CultureInfo.InvariantCulture, out var ticks) - && ticks >= DateTime.MinValue.Ticks - && ticks <= DateTime.MaxValue.Ticks) - { - image.DateModified = new DateTime(ticks, DateTimeKind.Utc); - } - else - { - return null; - } - - if (Enum.TryParse(imageType, true, out ImageType type)) - { - image.Type = type; - } - else - { - return null; - } - - // Optional parameters: width*height*blurhash - if (nextSegment + 1 < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1 || nextSegment == value.Length) - { - return image; - } - - ReadOnlySpan widthSpan = value[..nextSegment]; - - value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf(Delimiter); - if (nextSegment == -1) - { - nextSegment = value.Length; - } - - ReadOnlySpan heightSpan = value[..nextSegment]; - - if (int.TryParse(widthSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var width) - && int.TryParse(heightSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var height)) - { - image.Width = width; - image.Height = height; - } - - if (nextSegment < value.Length - 1) - { - value = value[(nextSegment + 1)..]; - var length = value.Length; - - Span blurHashSpan = stackalloc char[length]; - for (int i = 0; i < length; i++) - { - var c = value[i]; - blurHashSpan[i] = c switch - { - '/' => Delimiter, - '\\' => '|', - _ => c - }; - } - - image.BlurHash = new string(blurHashSpan); - } - } - - return image; - } - - /// - /// Internal retrieve from items or users table. - /// - /// The id. - /// BaseItem. - /// is null. - /// is . - public BaseItem RetrieveItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentException("Guid can't be empty", nameof(id)); - } - - CheckDisposed(); - - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) - { - statement.TryBind("@guid", id); - - foreach (var row in statement.ExecuteQuery()) - { - return GetItem(row, new InternalItemsQuery()); - } - } - - return null; - } - - private bool TypeRequiresDeserialization(Type type) - { - if (_config.Configuration.SkipDeserializationForBasicTypes) - { - if (type == typeof(Channel) - || type == typeof(UserRootFolder)) - { - return false; + return false; } } @@ -1179,152 +124,6 @@ namespace Emby.Server.Implementations.Data && type != typeof(MusicAlbum); } - /// - public List GetChapters(BaseItem item) - { - CheckDisposed(); - - var chapters = new List(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) - { - statement.TryBind("@ItemId", item.Id); - - foreach (var row in statement.ExecuteQuery()) - { - chapters.Add(GetChapter(row, item)); - } - } - - return chapters; - } - - /// - public ChapterInfo GetChapter(BaseItem item, int index) - { - CheckDisposed(); - - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) - { - statement.TryBind("@ItemId", item.Id); - statement.TryBind("@ChapterIndex", index); - - foreach (var row in statement.ExecuteQuery()) - { - return GetChapter(row, item); - } - } - - return null; - } - - /// - /// Gets the chapter. - /// - /// The reader. - /// The item. - /// ChapterInfo. - private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item) - { - var chapter = new ChapterInfo - { - StartPositionTicks = reader.GetInt64(0) - }; - - if (reader.TryGetString(1, out var chapterName)) - { - chapter.Name = chapterName; - } - - if (reader.TryGetString(2, out var imagePath)) - { - chapter.ImagePath = imagePath; - chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter); - } - - if (reader.TryReadDateTime(3, out var imageDateModified)) - { - chapter.ImageDateModified = imageDateModified; - } - - return chapter; - } - - /// - /// Saves the chapters. - /// - /// The item id. - /// The chapters. - public void SaveChapters(Guid id, IReadOnlyList chapters) - { - CheckDisposed(); - - if (id.IsEmpty()) - { - throw new ArgumentNullException(nameof(id)); - } - - ArgumentNullException.ThrowIfNull(chapters); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // First delete chapters - using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId"); - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); - - InsertChapters(id, chapters, connection); - transaction.Commit(); - } - - private void InsertChapters(Guid idBlob, IReadOnlyList chapters, ManagedConnection db) - { - var startIndex = 0; - var limit = 100; - var chapterIndex = 0; - - const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values "; - var insertText = new StringBuilder(StartInsertText, 256); - - while (startIndex < chapters.Count) - { - var endIndex = Math.Min(chapters.Count, startIndex + limit); - - for (var i = startIndex; i < endIndex; i++) - { - insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); - } - - insertText.Length -= 1; // Remove trailing comma - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", idBlob); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var chapter = chapters[i]; - - statement.TryBind("@ChapterIndex" + index, chapterIndex); - statement.TryBind("@StartPositionTicks" + index, chapter.StartPositionTicks); - statement.TryBind("@Name" + index, chapter.Name); - statement.TryBind("@ImagePath" + index, chapter.ImagePath); - statement.TryBind("@ImageDateModified" + index, chapter.ImageDateModified); - - chapterIndex++; - } - - statement.ExecuteNonQuery(); - } - - startIndex += limit; - insertText.Length = StartInsertText.Length; - } - } - private static bool EnableJoinUserData(InternalItemsQuery query) { if (query.User is null) diff --git a/Jellyfin.Data/Entities/BaseItem.cs b/Jellyfin.Data/Entities/BaseItem.cs index c0c88b2e6..18166f7c1 100644 --- a/Jellyfin.Data/Entities/BaseItem.cs +++ b/Jellyfin.Data/Entities/BaseItem.cs @@ -161,4 +161,10 @@ public class BaseItem public int? Height { get; set; } public long? Size { get; set; } + + public ICollection? Peoples { get; set; } + + public ICollection? UserData { get; set; } + + public ICollection? ItemValues { get; set; } } diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Data/Entities/People.cs index 72c39699b..014a0f1c9 100644 --- a/Jellyfin.Data/Entities/People.cs +++ b/Jellyfin.Data/Entities/People.cs @@ -7,6 +7,7 @@ namespace Jellyfin.Data.Entities; public class People { public Guid ItemId { get; set; } + public BaseItem Item { get; set; } public required string Name { get; set; } public string? Role { get; set; } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index 4ad842e0b..85dc98e09 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -3,16 +3,24 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Linq.Expressions; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; +using System.Threading.Channels; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; @@ -22,25 +30,1316 @@ namespace Jellyfin.Server.Implementations.Item; /// /// Handles all storage logic for BaseItems. /// -public class BaseItemManager +public class BaseItemManager : IItemRepository { private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationHost _appHost; - /// + + + private readonly ItemFields[] _allItemFields = Enum.GetValues(); + + private static readonly BaseItemKind[] _programTypes = new[] + { + BaseItemKind.Program, + BaseItemKind.TvChannel, + BaseItemKind.LiveTvProgram, + BaseItemKind.LiveTvChannel + }; + + private static readonly BaseItemKind[] _programExcludeParentTypes = new[] + { + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicArtist, + BaseItemKind.PhotoAlbum + }; + + private static readonly BaseItemKind[] _serviceTypes = new[] + { + BaseItemKind.TvChannel, + BaseItemKind.LiveTvChannel + }; + + private static readonly BaseItemKind[] _startDateTypes = new[] + { + BaseItemKind.Program, + BaseItemKind.LiveTvProgram + }; + + private static readonly BaseItemKind[] _seriesTypes = new[] + { + BaseItemKind.Book, + BaseItemKind.AudioBook, + BaseItemKind.Episode, + BaseItemKind.Season + }; + + private static readonly BaseItemKind[] _artistExcludeParentTypes = new[] + { + BaseItemKind.Series, + BaseItemKind.Season, + BaseItemKind.PhotoAlbum + }; + + private static readonly BaseItemKind[] _artistsTypes = new[] + { + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.MusicVideo, + BaseItemKind.AudioBook + }; + + private static readonly Dictionary _baseItemKindNames = new() + { + { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, + { BaseItemKind.Audio, typeof(Audio).FullName }, + { BaseItemKind.AudioBook, typeof(AudioBook).FullName }, + { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName }, + { BaseItemKind.Book, typeof(Book).FullName }, + { BaseItemKind.BoxSet, typeof(BoxSet).FullName }, + { BaseItemKind.Channel, typeof(Channel).FullName }, + { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName }, + { BaseItemKind.Episode, typeof(Episode).FullName }, + { BaseItemKind.Folder, typeof(Folder).FullName }, + { BaseItemKind.Genre, typeof(Genre).FullName }, + { BaseItemKind.Movie, typeof(Movie).FullName }, + { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName }, + { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName }, + { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName }, + { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName }, + { BaseItemKind.Person, typeof(Person).FullName }, + { BaseItemKind.Photo, typeof(Photo).FullName }, + { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName }, + { BaseItemKind.Playlist, typeof(Playlist).FullName }, + { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName }, + { BaseItemKind.Season, typeof(Season).FullName }, + { BaseItemKind.Series, typeof(Series).FullName }, + { BaseItemKind.Studio, typeof(Studio).FullName }, + { BaseItemKind.Trailer, typeof(Trailer).FullName }, + { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName }, + { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName }, + { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName }, + { BaseItemKind.UserView, typeof(UserView).FullName }, + { BaseItemKind.Video, typeof(Video).FullName }, + { BaseItemKind.Year, typeof(Year).FullName } + }; + + /// /// This holds all the types in the running assemblies /// so that we can de-serialize properly when we don't have strong types. /// private static readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); - /// - /// Initializes a new instance of the class. - /// - /// The db factory. - public BaseItemManager(IDbContextFactory dbProvider, IServerApplicationHost appHost) - { - _dbProvider = dbProvider; - _appHost = appHost; + /// + /// Initializes a new instance of the class. + /// + /// The db factory. + /// The Application host. + public BaseItemManager(IDbContextFactory dbProvider, IServerApplicationHost appHost) + { + _dbProvider = dbProvider; + _appHost = appHost; + } + + public int GetCount(InternalItemsQuery query) + { + ArgumentNullException.ThrowIfNull(query); + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + + if (query.IsResumable ?? false) + { + query.IsVirtualItem = false; + } + + + + } + + private IQueryable TranslateQuery( + IQueryable baseQuery, + JellyfinDbContext context, + InternalItemsQuery query) + { + var minWidth = query.MinWidth; + var maxWidth = query.MaxWidth; + var now = DateTime.UtcNow; + + if (query.IsHD.HasValue) + { + const int Threshold = 1200; + if (query.IsHD.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (query.Is4K.HasValue) + { + const int Threshold = 3800; + if (query.Is4K.Value) + { + minWidth = Threshold; + } + else + { + maxWidth = Threshold - 1; + } + } + + if (minWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= minWidth); + } + + if (query.MinHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height >= query.MinHeight); + } + + if (maxWidth.HasValue) + { + baseQuery = baseQuery.Where(e => e.Width >= maxWidth); + } + + if (query.MaxHeight.HasValue) + { + baseQuery = baseQuery.Where(e => e.Height <= query.MaxHeight); + } + + if (query.IsLocked.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsLocked == query.IsLocked); + } + + var tags = query.Tags.ToList(); + var excludeTags = query.ExcludeTags.ToList(); + + if (query.IsMovie == true) + { + if (query.IncludeItemTypes.Length == 0 + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + { + baseQuery = baseQuery.Where(e => e.IsMovie); + } + } + else if (query.IsMovie.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsMovie == query.IsMovie); + } + + if (query.IsSeries.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsSeries == query.IsSeries); + } + + if (query.IsSports.HasValue) + { + if (query.IsSports.Value) + { + tags.Add("Sports"); + } + else + { + excludeTags.Add("Sports"); + } + } + + if (query.IsNews.HasValue) + { + if (query.IsNews.Value) + { + tags.Add("News"); + } + else + { + excludeTags.Add("News"); + } + } + + if (query.IsKids.HasValue) + { + if (query.IsKids.Value) + { + tags.Add("Kids"); + } + else + { + excludeTags.Add("Kids"); + } + } + + if (query.SimilarTo is not null && query.MinSimilarityScore > 0) + { + // TODO support similarty score via CTE + baseQuery = baseQuery.Where(e => e.Sim == query.IsSeries); + whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture)); + } + + if (!string.IsNullOrEmpty(query.SearchTerm)) + { + whereClauses.Add("SearchScore > 0"); + } + + if (query.IsFolder.HasValue) + { + baseQuery = baseQuery.Where(e => e.IsFolder == query.IsFolder); + } + + var includeTypes = query.IncludeItemTypes; + // Only specify excluded types if no included types are specified + if (query.IncludeItemTypes.Length == 0) + { + var excludeTypes = query.ExcludeItemTypes; + if (excludeTypes.Length == 1) + { + if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) + { + baseQuery = baseQuery.Where(e => e.Type != excludeTypeName); + } + } + else if (excludeTypes.Length > 1) + { + var excludeTypeName = new List(); + foreach (var excludeType in excludeTypes) + { + if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) + { + excludeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type)); + } + } + else if (includeTypes.Length == 1) + { + if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) + { + baseQuery = baseQuery.Where(e => e.Type == includeTypeName); + } + } + else if (includeTypes.Length > 1) + { + var includeTypeName = new List(); + foreach (var includeType in includeTypes) + { + if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) + { + includeTypeName.Add(baseItemKindName!); + } + } + + baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); + } + + if (query.ChannelIds.Count == 1) + { + baseQuery = baseQuery.Where(e => e.ChannelId == query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); + } + else if (query.ChannelIds.Count > 1) + { + baseQuery = baseQuery.Where(e => query.ChannelIds.Select(f => f.ToString("N", CultureInfo.InvariantCulture)).Contains(e.ChannelId)); + } + + if (!query.ParentId.IsEmpty()) + { + baseQuery = baseQuery.Where(e => e.ParentId.Equals(query.ParentId)); + } + + if (!string.IsNullOrWhiteSpace(query.Path)) + { + baseQuery = baseQuery.Where(e => e.Path == query.Path); + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + baseQuery = baseQuery.Where(e => e.PresentationUniqueKey == query.PresentationUniqueKey); + } + + if (query.MinCommunityRating.HasValue) + { + baseQuery = baseQuery.Where(e => e.CommunityRating >= query.MinCommunityRating); + } + + if (query.MinIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber >= query.MinIndexNumber); + } + + if (query.MinParentAndIndexNumber.HasValue) + { + baseQuery = baseQuery + .Where(e => (e.ParentIndexNumber == query.MinParentAndIndexNumber.Value.ParentIndexNumber && e.IndexNumber >= query.MinParentAndIndexNumber.Value.IndexNumber) || e.ParentIndexNumber > query.MinParentAndIndexNumber.Value.ParentIndexNumber); + } + + if (query.MinDateCreated.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateCreated >= query.MinDateCreated); + } + + if (query.MinDateLastSaved.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= query.MinDateLastSaved.Value); + } + + if (query.MinDateLastSavedForUser.HasValue) + { + baseQuery = baseQuery.Where(e => e.DateLastSaved != null && e.DateLastSaved >= query.MinDateLastSavedForUser.Value); + } + + if (query.IndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.IndexNumber == query.IndexNumber.Value); + } + + if (query.ParentIndexNumber.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber == query.ParentIndexNumber.Value); + } + + if (query.ParentIndexNumberNotEquals.HasValue) + { + baseQuery = baseQuery.Where(e => e.ParentIndexNumber != query.ParentIndexNumberNotEquals.Value || e.ParentIndexNumber == null); + } + + var minEndDate = query.MinEndDate; + var maxEndDate = query.MaxEndDate; + + if (query.HasAired.HasValue) + { + if (query.HasAired.Value) + { + maxEndDate = DateTime.UtcNow; + } + else + { + minEndDate = DateTime.UtcNow; + } + } + + if (minEndDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.EndDate >= minEndDate); + } + + if (maxEndDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.EndDate <= maxEndDate); + } + + if (query.MinStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate >= query.MinStartDate.Value); + } + + if (query.MaxStartDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.StartDate <= query.MaxStartDate.Value); + } + + if (query.MinPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= query.MinPremiereDate.Value); + } + + if (query.MaxPremiereDate.HasValue) + { + baseQuery = baseQuery.Where(e => e.PremiereDate <= query.MaxPremiereDate.Value); + } + + if (query.TrailerTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => query.TrailerTypes.Any(f => e.TrailerTypes!.Contains(f.ToString(), StringComparison.OrdinalIgnoreCase))); + } + + if (query.IsAiring.HasValue) + { + if (query.IsAiring.Value) + { + baseQuery = baseQuery.Where(e => e.StartDate <= now && e.EndDate >= now); + } + else + { + baseQuery = baseQuery.Where(e => e.StartDate > now && e.EndDate < now); + } + } + + if (query.PersonIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => + context.Peoples.Where(w => context.BaseItems.Where(w => query.PersonIds.Contains(w.Id)).Any(f => f.Name == w.Name)) + .Any(f => f.ItemId.Equals(e.Id))); + } + + if (!string.IsNullOrWhiteSpace(query.Person)) + { + baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.Name == query.Person)); + } + + if (!string.IsNullOrWhiteSpace(query.MinSortName)) + { + // this does not makes sense. + // baseQuery = baseQuery.Where(e => e.SortName >= query.MinSortName); + // whereClauses.Add("SortName>=@MinSortName"); + // statement?.TryBind("@MinSortName", query.MinSortName); + } + + if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) + { + baseQuery = baseQuery.Where(e => e.ExternalSeriesId == query.ExternalSeriesId); + } + + if (!string.IsNullOrWhiteSpace(query.ExternalId)) + { + baseQuery = baseQuery.Where(e => e.ExternalId == query.ExternalId); + } + + if (!string.IsNullOrWhiteSpace(query.Name)) + { + var cleanName = GetCleanValue(query.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } + + // These are the same, for now + var nameContains = query.NameContains; + if (!string.IsNullOrWhiteSpace(nameContains)) + { + baseQuery = baseQuery.Where(e => + e.CleanName == query.NameContains + || e.OriginalTitle!.Contains(query.NameContains!, StringComparison.Ordinal)); + } + + if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) + { + baseQuery = baseQuery.Where(e => e.SortName!.Contains(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] > query.NameStartsWithOrGreater[0]); + } + + if (!string.IsNullOrWhiteSpace(query.NameLessThan)) + { + // i hate this + baseQuery = baseQuery.Where(e => e.SortName![0] < query.NameLessThan[0]); + } + + if (query.ImageTypes.Length > 0) + { + baseQuery = baseQuery.Where(e => query.ImageTypes.Any(f => e.Images!.Contains(f.ToString(), StringComparison.InvariantCulture))); + } + + if (query.IsLiked.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Rating >= UserItemData.MinLikeValue); + } + + if (query.IsFavoriteOrLiked.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == query.IsFavoriteOrLiked); + } + + if (query.IsFavorite.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite == query.IsFavorite); + } + + if (query.IsPlayed.HasValue) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played == query.IsPlayed.Value); + } + + if (query.IsResumable.HasValue) + { + if (query.IsResumable.Value) + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks > 0); + } + else + { + baseQuery = baseQuery + .Where(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlaybackPositionTicks == 0); + } + } + + if (query.ArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type <= 1 && context.BaseItems.Where(w => query.ArtistIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + } + + if (query.AlbumArtistIds.Length > 0) + { + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.Type == 1 && context.BaseItems.Where(w => query.ArtistIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + } + + if (query.ContributingArtistIds.Length > 0) + { + var contributingArtists = context.BaseItems.Where(e => query.ContributingArtistIds.Contains(e.Id)); + baseQuery = baseQuery.Where(e => e.ItemValues!.Any(f => f.Type == 0 && contributingArtists.Any(w => w.CleanName == f.CleanValue))); + + clauseBuilder.Append('('); + for (var i = 0; i < query.ContributingArtistIds.Length; i++) + { + clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds") + .Append(i) + .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR "); + statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.AlbumIds.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.AlbumIds.Length; i++) + { + clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds") + .Append(i) + .Append(") OR "); + statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.ExcludeArtistIds.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.ExcludeArtistIds.Length; i++) + { + clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId") + .Append(i) + .Append(") and Type<=1)) OR "); + statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.GenreIds.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.GenreIds.Count; i++) + { + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId") + .Append(i) + .Append(") and Type=2)) OR "); + statement?.TryBind("@GenreId" + i, query.GenreIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.Genres.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.Genres.Count; i++) + { + clauseBuilder.Append("@Genre") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR "); + statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i])); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (tags.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < tags.Count; i++) + { + clauseBuilder.Append("@Tag") + .Append(i) + .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@Tag" + i, GetCleanValue(tags[i])); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (excludeTags.Count > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < excludeTags.Count; i++) + { + clauseBuilder.Append("@ExcludeTag") + .Append(i) + .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); + statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i])); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.StudioIds.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.StudioIds.Length; i++) + { + clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId") + .Append(i) + .Append(") and Type=3)) OR "); + statement?.TryBind("@StudioId" + i, query.StudioIds[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.OfficialRatings.Length > 0) + { + clauseBuilder.Append('('); + for (var i = 0; i < query.OfficialRatings.Length; i++) + { + clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or); + statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]); + } + + clauseBuilder.Length -= Or.Length; + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + clauseBuilder.Append('('); + if (query.HasParentalRating ?? false) + { + clauseBuilder.Append("InheritedParentalRatingValue not null"); + if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } + + if (query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + } + else if (query.BlockUnratedItems.Length > 0) + { + const string ParamName = "@UnratedType"; + clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in ("); + + for (int i = 0; i < query.BlockUnratedItems.Length; i++) + { + clauseBuilder.Append(ParamName).Append(i).Append(','); + statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString()); + } + + // Remove trailing comma + clauseBuilder.Length--; + clauseBuilder.Append("))"); + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(" OR ("); + } + + if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } + + if (query.MaxParentalRating.HasValue) + { + if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append(" AND "); + } + + clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + + if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(')'); + } + + if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) + { + clauseBuilder.Append(" OR InheritedParentalRatingValue not null"); + } + } + else if (query.MinParentalRating.HasValue) + { + clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); + statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); + + if (query.MaxParentalRating.HasValue) + { + clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + + clauseBuilder.Append(')'); + } + else if (query.MaxParentalRating.HasValue) + { + clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); + statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } + else if (!query.HasParentalRating ?? false) + { + clauseBuilder.Append("InheritedParentalRatingValue is null"); + } + + if (clauseBuilder.Length > 1) + { + whereClauses.Add(clauseBuilder.Append(')').ToString()); + clauseBuilder.Length = 0; + } + + if (query.HasOfficialRating.HasValue) + { + if (query.HasOfficialRating.Value) + { + whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')"); + } + else + { + whereClauses.Add("(OfficialRating is null OR OfficialRating='')"); + } + } + + if (query.HasOverview.HasValue) + { + if (query.HasOverview.Value) + { + whereClauses.Add("(Overview not null AND Overview<>'')"); + } + else + { + whereClauses.Add("(Overview is null OR Overview='')"); + } + } + + if (query.HasOwnerId.HasValue) + { + if (query.HasOwnerId.Value) + { + whereClauses.Add("OwnerId not null"); + } + else + { + whereClauses.Add("OwnerId is null"); + } + } + + if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); + } + + if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); + } + + if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); + } + + if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage)) + { + whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)"); + statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); + } + + if (query.HasSubtitles.HasValue) + { + if (query.HasSubtitles.Value) + { + whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)"); + } + else + { + whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)"); + } + } + + if (query.HasChapterImages.HasValue) + { + if (query.HasChapterImages.Value) + { + whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) not null)"); + } + else + { + whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) is null)"); + } + } + + if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value) + { + whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); + } + + if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value) + { + whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))"); + } + + if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value) + { + whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)"); + } + + if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value) + { + whereClauses.Add("Name not in (Select Name From People)"); + } + + if (query.Years.Length == 1) + { + whereClauses.Add("ProductionYear=@Years"); + statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); + } + else if (query.Years.Length > 1) + { + var val = string.Join(',', query.Years); + whereClauses.Add("ProductionYear in (" + val + ")"); + } + + var isVirtualItem = query.IsVirtualItem ?? query.IsMissing; + if (isVirtualItem.HasValue) + { + whereClauses.Add("IsVirtualItem=@IsVirtualItem"); + statement?.TryBind("@IsVirtualItem", isVirtualItem.Value); + } + + if (query.IsSpecialSeason.HasValue) + { + if (query.IsSpecialSeason.Value) + { + whereClauses.Add("IndexNumber = 0"); + } + else + { + whereClauses.Add("IndexNumber <> 0"); + } + } + + if (query.IsUnaired.HasValue) + { + if (query.IsUnaired.Value) + { + whereClauses.Add("PremiereDate >= DATETIME('now')"); + } + else + { + whereClauses.Add("PremiereDate < DATETIME('now')"); + } + } + + if (query.MediaTypes.Length == 1) + { + whereClauses.Add("MediaType=@MediaTypes"); + statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString()); + } + else if (query.MediaTypes.Length > 1) + { + var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'")); + whereClauses.Add("MediaType in (" + val + ")"); + } + + if (query.ItemIds.Length > 0) + { + var includeIds = new List(); + var index = 0; + foreach (var id in query.ItemIds) + { + includeIds.Add("Guid = @IncludeId" + index); + statement?.TryBind("@IncludeId" + index, id); + index++; + } + + whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")"); + } + + if (query.ExcludeItemIds.Length > 0) + { + var excludeIds = new List(); + var index = 0; + foreach (var id in query.ExcludeItemIds) + { + excludeIds.Add("Guid <> @ExcludeId" + index); + statement?.TryBind("@ExcludeId" + index, id); + index++; + } + + whereClauses.Add(string.Join(" AND ", excludeIds)); + } + + if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0) + { + var excludeIds = new List(); + + var index = 0; + foreach (var pair in query.ExcludeProviderIds) + { + if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var paramName = "@ExcludeProviderId" + index; + excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")"); + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); + index++; + + break; + } + + if (excludeIds.Count > 0) + { + whereClauses.Add(string.Join(" AND ", excludeIds)); + } + } + + if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0) + { + var hasProviderIds = new List(); + + var index = 0; + foreach (var pair in query.HasAnyProviderId) + { + if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // TODO this seems to be an idea for a better schema where ProviderIds are their own table + // but this is not implemented + // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); + + // TODO this is a really BAD way to do it since the pair: + // Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567 + // and maybe even NotTmdb=1234. + + // this is a placeholder for this specific pair to correlate it in the bigger query + var paramName = "@HasAnyProviderId" + index; + + // this is a search for the placeholder + hasProviderIds.Add("ProviderIds like " + paramName); + + // this replaces the placeholder with a value, here: %key=val% + statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); + index++; + + break; + } + + if (hasProviderIds.Count > 0) + { + whereClauses.Add("(" + string.Join(" OR ", hasProviderIds) + ")"); + } + } + + if (query.HasImdbId.HasValue) + { + whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb")); + } + + if (query.HasTmdbId.HasValue) + { + whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb")); + } + + if (query.HasTvdbId.HasValue) + { + whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); + } + + var queryTopParentIds = query.TopParentIds; + + if (queryTopParentIds.Length > 0) + { + var includedItemByNameTypes = GetItemByNameTypesInQuery(query); + var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; + + if (queryTopParentIds.Length == 1) + { + if (enableItemsByName && includedItemByNameTypes.Count == 1) + { + whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); + statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); + whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); + } + else + { + whereClauses.Add("(TopParentId=@TopParentId)"); + } + + statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); + } + else if (queryTopParentIds.Length > 1) + { + var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + + if (enableItemsByName && includedItemByNameTypes.Count == 1) + { + whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); + statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); + } + else if (enableItemsByName && includedItemByNameTypes.Count > 1) + { + var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); + whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); + } + else + { + whereClauses.Add("TopParentId in (" + val + ")"); + } + } + } + + if (query.AncestorIds.Length == 1) + { + whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); + statement?.TryBind("@AncestorId", query.AncestorIds[0]); + } + + if (query.AncestorIds.Length > 1) + { + var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); + whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); + } + + if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) + { + var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; + whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); + statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); + } + + if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) + { + whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); + statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); + } + + if (query.ExcludeInheritedTags.Length > 0) + { + var paramName = "@ExcludeInheritedTags"; + if (statement is null) + { + int index = 0; + string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++)); + whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)"); + } + else + { + for (int index = 0; index < query.ExcludeInheritedTags.Length; index++) + { + statement.TryBind(paramName + index, GetCleanValue(query.ExcludeInheritedTags[index])); + } + } + } + + if (query.IncludeInheritedTags.Length > 0) + { + var paramName = "@IncludeInheritedTags"; + if (statement is null) + { + int index = 0; + string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++)); + // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. + // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. + if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) + { + whereClauses.Add($""" + ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null + OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null) + """); + } + + // A playlist should be accessible to its owner regardless of allowed tags. + else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) + { + whereClauses.Add($""" + ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null + OR data like @PlaylistOwnerUserId) + """); + } + else + { + whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)"); + } + } + else + { + for (int index = 0; index < query.IncludeInheritedTags.Length; index++) + { + statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); + } + + if (query.User is not null) + { + statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%"""); + } + } + } + + if (query.SeriesStatuses.Length > 0) + { + var statuses = new List(); + + foreach (var seriesStatus in query.SeriesStatuses) + { + statuses.Add("data like '%" + seriesStatus + "%'"); + } + + whereClauses.Add("(" + string.Join(" OR ", statuses) + ")"); + } + + if (query.BoxSetLibraryFolders.Length > 0) + { + var folderIdQueries = new List(); + + foreach (var folderId in query.BoxSetLibraryFolders) + { + folderIdQueries.Add("data like '%" + folderId.ToString("N", CultureInfo.InvariantCulture) + "%'"); + } + + whereClauses.Add("(" + string.Join(" OR ", folderIdQueries) + ")"); + } + + if (query.VideoTypes.Length > 0) + { + var videoTypes = new List(); + + foreach (var videoType in query.VideoTypes) + { + videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'"); + } + + whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")"); + } + + if (query.Is3D.HasValue) + { + if (query.Is3D.Value) + { + whereClauses.Add("data like '%Video3DFormat%'"); + } + else + { + whereClauses.Add("data not like '%Video3DFormat%'"); + } + } + + if (query.IsPlaceHolder.HasValue) + { + if (query.IsPlaceHolder.Value) + { + whereClauses.Add("data like '%\"IsPlaceHolder\":true%'"); + } + else + { + whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')"); + } + } + + if (query.HasSpecialFeature.HasValue) + { + if (query.HasSpecialFeature.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + + if (query.HasTrailer.HasValue) + { + if (query.HasTrailer.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + + if (query.HasThemeSong.HasValue) + { + if (query.HasThemeSong.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } + + if (query.HasThemeVideo.HasValue) + { + if (query.HasThemeVideo.Value) + { + whereClauses.Add("ExtraIds not null"); + } + else + { + whereClauses.Add("ExtraIds is null"); + } + } } /// @@ -58,14 +1357,26 @@ public class BaseItemManager .FirstOrDefault(t => t is not null)); } - /// - /// Saves the items. - /// - /// The items. - /// The cancellation token. - /// - /// or is null. - /// + /// + public void SaveImages(BaseItem item) + { + ArgumentNullException.ThrowIfNull(item); + + var images = SerializeImages(item.ImageInfos); + using var db = _dbProvider.CreateDbContext(); + + db.BaseItems + .Where(e => e.Id.Equals(item.Id)) + .ExecuteUpdate(e => e.SetProperty(f => f.Images, images)); + } + + /// + public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) + { + UpdateOrInsertItems(items, cancellationToken); + } + + /// public void UpdateOrInsertItems(IReadOnlyList items, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(items); @@ -124,7 +1435,8 @@ public class BaseItemManager context.SaveChanges(true); } - public BaseItemDto? GetSingle(Guid id) + /// + public BaseItemDto? RetrieveItem(Guid id) { if (id.IsEmpty()) { @@ -141,13 +1453,6 @@ public class BaseItemManager return DeserialiseBaseItem(item); } - private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) - { - var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); - var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type.");; - return Map(baseItemEntity, dto); - } - /// /// Maps a Entity to the DTO. /// @@ -462,6 +1767,13 @@ public class BaseItemManager return entity; } + private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) + { + var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); + var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); ; + return Map(baseItemEntity, dto); + } + private string GetCleanValue(string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/Jellyfin.Server.Implementations/Item/ChapterManager.cs b/Jellyfin.Server.Implementations/Item/ChapterManager.cs index 273cc96ba..7b0f98fde 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterManager.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterManager.cs @@ -1,26 +1,74 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; -using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; namespace Jellyfin.Server.Implementations.Item; -public class ChapterManager +/// +/// The Chapter manager. +/// +public class ChapterManager : IChapterManager { private readonly IDbContextFactory _dbProvider; + private readonly IImageProcessor _imageProcessor; - public ChapterManager(IDbContextFactory dbProvider) + /// + /// Initializes a new instance of the class. + /// + /// The EFCore provider. + /// The Image Processor. + public ChapterManager(IDbContextFactory dbProvider, IImageProcessor imageProcessor) { _dbProvider = dbProvider; + _imageProcessor = imageProcessor; } - public IReadOnlyList GetChapters(BaseItemDto baseItemDto) + /// + public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) { using var context = _dbProvider.CreateDbContext(); - return context.Chapters.Where(e => e.ItemId.Equals(baseItemDto.Id)).Select(Map).ToList(); + var chapter = context.Chapters.FirstOrDefault(e => e.ItemId.Equals(baseItem.Id) && e.ChapterIndex == index); + if (chapter is not null) + { + return Map(chapter, baseItem); + } + + return null; + } + + /// + public IReadOnlyList GetChapters(BaseItemDto baseItem) + { + using var context = _dbProvider.CreateDbContext(); + return context.Chapters.Where(e => e.ItemId.Equals(baseItem.Id)) + .ToList() + .Select(e => Map(e, baseItem)) + .ToImmutableArray(); + } + + /// + public void SaveChapters(Guid itemId, IReadOnlyList chapters) + { + using var context = _dbProvider.CreateDbContext(); + using (var transaction = context.Database.BeginTransaction()) + { + context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + for (var i = 0; i < chapters.Count; i++) + { + var chapter = chapters[i]; + context.Chapters.Add(Map(chapter, i, itemId)); + } + + context.SaveChanges(); + transaction.Commit(); + } } private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId) diff --git a/MediaBrowser.Controller/Chapters/ChapterManager.cs b/MediaBrowser.Controller/Chapters/ChapterManager.cs new file mode 100644 index 000000000..a9e11f603 --- /dev/null +++ b/MediaBrowser.Controller/Chapters/ChapterManager.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Chapters +{ + public class ChapterManager : IChapterManager + { + public ChapterManager(IDbContextFactory dbProvider) + { + _itemRepo = itemRepo; + } + + /// + public void SaveChapters(Guid itemId, IReadOnlyList chapters) + { + _itemRepo.SaveChapters(itemId, chapters); + } + } +} diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index c049bb97e..55762c7fc 100644 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Chapters @@ -15,5 +16,20 @@ namespace MediaBrowser.Controller.Chapters /// The item. /// The set of chapters. void SaveChapters(Guid itemId, IReadOnlyList chapters); + + /// + /// Gets all chapters associated with the baseItem. + /// + /// The baseitem. + /// A readonly list of chapter instances. + IReadOnlyList GetChapters(BaseItemDto baseItem); + + /// + /// Gets a single chapter of a BaseItem on a specific index. + /// + /// The baseitem. + /// The index of that chapter. + /// A chapter instance. + ChapterInfo? GetChapter(BaseItemDto baseItem, int index); } } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 2c52b2b45..21b9ee4b7 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -39,28 +39,6 @@ namespace MediaBrowser.Controller.Persistence /// BaseItem. BaseItem RetrieveItem(Guid id); - /// - /// Gets chapters for an item. - /// - /// The item. - /// The list of chapter info. - List GetChapters(BaseItem item); - - /// - /// Gets a single chapter for an item. - /// - /// The item. - /// The chapter index. - /// The chapter info at the specified index. - ChapterInfo GetChapter(BaseItem item, int index); - - /// - /// Saves the chapters. - /// - /// The item id. - /// The list of chapters to save. - void SaveChapters(Guid id, IReadOnlyList chapters); - /// /// Gets the media streams. /// diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs deleted file mode 100644 index 3cbfe7d4d..000000000 --- a/MediaBrowser.Providers/Chapters/ChapterManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Providers.Chapters -{ - public class ChapterManager : IChapterManager - { - private readonly IItemRepository _itemRepo; - - public ChapterManager(IItemRepository itemRepo) - { - _itemRepo = itemRepo; - } - - /// - public void SaveChapters(Guid itemId, IReadOnlyList chapters) - { - _itemRepo.SaveChapters(itemId, chapters); - } - } -} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9a65852f0..f2df731c0 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -23,7 +23,7 @@ - + -- cgit v1.2.3 From ea81db67f412dee6203e3f18798e449dce7c06f9 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:27:47 +0000 Subject: Added Sorting and Grouping --- .../Data/SqliteItemRepository.cs | 1816 +------------------- Jellyfin.Data/Enums/ItemSortBy.cs | 10 - .../Item/BaseItemManager.cs | 306 +++- 3 files changed, 301 insertions(+), 1831 deletions(-) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 94a5eba81..26255e6aa 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -529,159 +529,6 @@ namespace Emby.Server.Implementations.Data return string.Empty; } - /// - public int GetCount(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) - { - query.Limit = query.Limit.Value + 4; - } - - var columns = new List { "count(distinct PresentationUniqueKey)" }; - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 256) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - if (whereClauses.Count != 0) - { - commandTextBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses); - } - - var commandText = commandTextBuilder.ToString(); - - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - return statement.SelectScalarInt(); - } - } - - /// - public List GetItemList(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) - { - query.Limit = query.Limit.Value + 4; - } - - var columns = _retrieveItemColumns.ToList(); - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 1024) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - - if (whereClauses.Count != 0) - { - commandTextBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses); - } - - commandTextBuilder.Append(GetGroupBy(query)) - .Append(GetOrderByText(query)); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandTextBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - commandTextBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var commandText = commandTextBuilder.ToString(); - var items = new List(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasServiceName = HasServiceName(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization); - if (item is not null) - { - items.Add(item); - } - } - } - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.EnableGroupByMetadataKey) - { - var limit = query.Limit ?? int.MaxValue; - limit -= 4; - var newList = new List(); - - foreach (var item in items) - { - AddItem(newList, item); - - if (newList.Count >= limit) - { - break; - } - } - - items = newList; - } - - return items; - } private string FixUnicodeChars(string buffer) { @@ -703,204 +550,6 @@ namespace Emby.Server.Implementations.Data return buffer.Replace('\u00B4', '\''); // acute accent } - private void AddItem(List items, BaseItem newItem) - { - for (var i = 0; i < items.Count; i++) - { - var item = items[i]; - - foreach (var providerId in newItem.ProviderIds) - { - if (string.Equals(providerId.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.Ordinal)) - { - continue; - } - - if (string.Equals(item.GetProviderId(providerId.Key), providerId.Value, StringComparison.Ordinal)) - { - if (newItem.SourceType == SourceType.Library) - { - items[i] = newItem; - } - - return; - } - } - } - - items.Add(newItem); - } - - /// - public QueryResult GetItems(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) - { - var returnList = GetItemList(query); - return new QueryResult( - query.StartIndex, - returnList.Count, - returnList); - } - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) - { - query.Limit = query.Limit.Value + 4; - } - - var columns = _retrieveItemColumns.ToList(); - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 512) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - - var whereText = whereClauses.Count == 0 ? - string.Empty : - string.Join(" AND ", whereClauses); - - if (!string.IsNullOrEmpty(whereText)) - { - commandTextBuilder.Append(" where ") - .Append(whereText); - } - - commandTextBuilder.Append(GetGroupBy(query)) - .Append(GetOrderByText(query)); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandTextBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - commandTextBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - var itemQuery = string.Empty; - var totalRecordCountQuery = string.Empty; - if (!isReturningZeroItems) - { - itemQuery = commandTextBuilder.ToString(); - } - - if (query.EnableTotalRecordCount) - { - commandTextBuilder.Clear(); - - commandTextBuilder.Append(" select "); - - List columnsToSelect; - if (EnableGroupByPresentationUniqueKey(query)) - { - columnsToSelect = new List { "count (distinct PresentationUniqueKey)" }; - } - else if (query.GroupBySeriesPresentationUniqueKey) - { - columnsToSelect = new List { "count (distinct SeriesPresentationUniqueKey)" }; - } - else - { - columnsToSelect = new List { "count (guid)" }; - } - - SetFinalColumnsToSelect(query, columnsToSelect); - - commandTextBuilder.AppendJoin(',', columnsToSelect) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - if (!string.IsNullOrEmpty(whereText)) - { - commandTextBuilder.Append(" where ") - .Append(whereText); - } - - totalRecordCountQuery = commandTextBuilder.ToString(); - } - - var list = new List(); - var result = new QueryResult(); - using var connection = GetConnection(true); - using var transaction = connection.BeginTransaction(); - if (!isReturningZeroItems) - { - using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery")) - using (var statement = PrepareStatement(connection, itemQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasServiceName = HasServiceName(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false); - if (item is not null) - { - list.Add(item); - } - } - } - } - - if (query.EnableTotalRecordCount) - { - using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount")) - using (var statement = PrepareStatement(connection, totalRecordCountQuery)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - result.TotalRecordCount = statement.SelectScalarInt(); - } - } - - transaction.Commit(); - - result.StartIndex = query.StartIndex ?? 0; - result.Items = list; - return result; - } - private string GetOrderByText(InternalItemsQuery query) { var orderBy = query.OrderBy; @@ -1066,1433 +715,19 @@ namespace Emby.Server.Implementations.Data return IsAlphaNumeric(value); } -#nullable enable - private List GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement) + private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) { - if (query.IsResumable ?? false) + if (query.ExcludeItemTypes.Contains(type)) { - query.IsVirtualItem = false; + return false; } - var minWidth = query.MinWidth; - var maxWidth = query.MaxWidth; + return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); + } - if (query.IsHD.HasValue) - { - const int Threshold = 1200; - if (query.IsHD.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - if (query.Is4K.HasValue) - { - const int Threshold = 3800; - if (query.Is4K.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } - - var whereClauses = new List(); - - if (minWidth.HasValue) - { - whereClauses.Add("Width>=@MinWidth"); - statement?.TryBind("@MinWidth", minWidth); - } - - if (query.MinHeight.HasValue) - { - whereClauses.Add("Height>=@MinHeight"); - statement?.TryBind("@MinHeight", query.MinHeight); - } - - if (maxWidth.HasValue) - { - whereClauses.Add("Width<=@MaxWidth"); - statement?.TryBind("@MaxWidth", maxWidth); - } - - if (query.MaxHeight.HasValue) - { - whereClauses.Add("Height<=@MaxHeight"); - statement?.TryBind("@MaxHeight", query.MaxHeight); - } - - if (query.IsLocked.HasValue) - { - whereClauses.Add("IsLocked=@IsLocked"); - statement?.TryBind("@IsLocked", query.IsLocked); - } - - var tags = query.Tags.ToList(); - var excludeTags = query.ExcludeTags.ToList(); - - if (query.IsMovie == true) - { - if (query.IncludeItemTypes.Length == 0 - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.Trailer)) - { - whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); - } - else - { - whereClauses.Add("IsMovie=@IsMovie"); - } - - statement?.TryBind("@IsMovie", true); - } - else if (query.IsMovie.HasValue) - { - whereClauses.Add("IsMovie=@IsMovie"); - statement?.TryBind("@IsMovie", query.IsMovie); - } - - if (query.IsSeries.HasValue) - { - whereClauses.Add("IsSeries=@IsSeries"); - statement?.TryBind("@IsSeries", query.IsSeries); - } - - if (query.IsSports.HasValue) - { - if (query.IsSports.Value) - { - tags.Add("Sports"); - } - else - { - excludeTags.Add("Sports"); - } - } - - if (query.IsNews.HasValue) - { - if (query.IsNews.Value) - { - tags.Add("News"); - } - else - { - excludeTags.Add("News"); - } - } - - if (query.IsKids.HasValue) - { - if (query.IsKids.Value) - { - tags.Add("Kids"); - } - else - { - excludeTags.Add("Kids"); - } - } - - if (query.SimilarTo is not null && query.MinSimilarityScore > 0) - { - whereClauses.Add("SimilarityScore > " + (query.MinSimilarityScore - 1).ToString(CultureInfo.InvariantCulture)); - } - - if (!string.IsNullOrEmpty(query.SearchTerm)) - { - whereClauses.Add("SearchScore > 0"); - } - - if (query.IsFolder.HasValue) - { - whereClauses.Add("IsFolder=@IsFolder"); - statement?.TryBind("@IsFolder", query.IsFolder); - } - - var includeTypes = query.IncludeItemTypes; - // Only specify excluded types if no included types are specified - if (query.IncludeItemTypes.Length == 0) - { - var excludeTypes = query.ExcludeItemTypes; - if (excludeTypes.Length == 1) - { - if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName)) - { - whereClauses.Add("type<>@type"); - statement?.TryBind("@type", excludeTypeName); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeTypes[0]); - } - } - else if (excludeTypes.Length > 1) - { - var whereBuilder = new StringBuilder("type not in ("); - foreach (var excludeType in excludeTypes) - { - if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName)) - { - whereBuilder - .Append('\'') - .Append(baseItemKindName) - .Append("',"); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeType); - } - } - - // Remove trailing comma. - whereBuilder.Length--; - whereBuilder.Append(')'); - whereClauses.Add(whereBuilder.ToString()); - } - } - else if (includeTypes.Length == 1) - { - if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) - { - whereClauses.Add("type=@type"); - statement?.TryBind("@type", includeTypeName); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeTypes[0]); - } - } - else if (includeTypes.Length > 1) - { - var whereBuilder = new StringBuilder("type in ("); - foreach (var includeType in includeTypes) - { - if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) - { - whereBuilder - .Append('\'') - .Append(baseItemKindName) - .Append("',"); - } - else - { - Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeType); - } - } - - // Remove trailing comma. - whereBuilder.Length--; - whereBuilder.Append(')'); - whereClauses.Add(whereBuilder.ToString()); - } - - if (query.ChannelIds.Count == 1) - { - whereClauses.Add("ChannelId=@ChannelId"); - statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); - } - else if (query.ChannelIds.Count > 1) - { - var inClause = string.Join(',', query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - whereClauses.Add($"ChannelId in ({inClause})"); - } - - if (!query.ParentId.IsEmpty()) - { - whereClauses.Add("ParentId=@ParentId"); - statement?.TryBind("@ParentId", query.ParentId); - } - - if (!string.IsNullOrWhiteSpace(query.Path)) - { - whereClauses.Add("Path=@Path"); - statement?.TryBind("@Path", GetPathToSave(query.Path)); - } - - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) - { - whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey"); - statement?.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey); - } - - if (query.MinCommunityRating.HasValue) - { - whereClauses.Add("CommunityRating>=@MinCommunityRating"); - statement?.TryBind("@MinCommunityRating", query.MinCommunityRating.Value); - } - - if (query.MinIndexNumber.HasValue) - { - whereClauses.Add("IndexNumber>=@MinIndexNumber"); - statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value); - } - - if (query.MinParentAndIndexNumber.HasValue) - { - whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)"); - statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber); - statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber); - } - - if (query.MinDateCreated.HasValue) - { - whereClauses.Add("DateCreated>=@MinDateCreated"); - statement?.TryBind("@MinDateCreated", query.MinDateCreated.Value); - } - - if (query.MinDateLastSaved.HasValue) - { - whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)"); - statement?.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value); - } - - if (query.MinDateLastSavedForUser.HasValue) - { - whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)"); - statement?.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value); - } - - if (query.IndexNumber.HasValue) - { - whereClauses.Add("IndexNumber=@IndexNumber"); - statement?.TryBind("@IndexNumber", query.IndexNumber.Value); - } - - if (query.ParentIndexNumber.HasValue) - { - whereClauses.Add("ParentIndexNumber=@ParentIndexNumber"); - statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value); - } - - if (query.ParentIndexNumberNotEquals.HasValue) - { - whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)"); - statement?.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value); - } - - var minEndDate = query.MinEndDate; - var maxEndDate = query.MaxEndDate; - - if (query.HasAired.HasValue) - { - if (query.HasAired.Value) - { - maxEndDate = DateTime.UtcNow; - } - else - { - minEndDate = DateTime.UtcNow; - } - } - - if (minEndDate.HasValue) - { - whereClauses.Add("EndDate>=@MinEndDate"); - statement?.TryBind("@MinEndDate", minEndDate.Value); - } - - if (maxEndDate.HasValue) - { - whereClauses.Add("EndDate<=@MaxEndDate"); - statement?.TryBind("@MaxEndDate", maxEndDate.Value); - } - - if (query.MinStartDate.HasValue) - { - whereClauses.Add("StartDate>=@MinStartDate"); - statement?.TryBind("@MinStartDate", query.MinStartDate.Value); - } - - if (query.MaxStartDate.HasValue) - { - whereClauses.Add("StartDate<=@MaxStartDate"); - statement?.TryBind("@MaxStartDate", query.MaxStartDate.Value); - } - - if (query.MinPremiereDate.HasValue) - { - whereClauses.Add("PremiereDate>=@MinPremiereDate"); - statement?.TryBind("@MinPremiereDate", query.MinPremiereDate.Value); - } - - if (query.MaxPremiereDate.HasValue) - { - whereClauses.Add("PremiereDate<=@MaxPremiereDate"); - statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value); - } - - StringBuilder clauseBuilder = new StringBuilder(); - const string Or = " OR "; - - var trailerTypes = query.TrailerTypes; - int trailerTypesLen = trailerTypes.Length; - if (trailerTypesLen > 0) - { - clauseBuilder.Append('('); - - for (int i = 0; i < trailerTypesLen; i++) - { - var paramName = "@TrailerTypes" + i; - clauseBuilder.Append("TrailerTypes like ") - .Append(paramName) - .Append(Or); - statement?.TryBind(paramName, "%" + trailerTypes[i] + "%"); - } - - clauseBuilder.Length -= Or.Length; - clauseBuilder.Append(')'); - - whereClauses.Add(clauseBuilder.ToString()); - - clauseBuilder.Length = 0; - } - - if (query.IsAiring.HasValue) - { - if (query.IsAiring.Value) - { - whereClauses.Add("StartDate<=@MaxStartDate"); - statement?.TryBind("@MaxStartDate", DateTime.UtcNow); - - whereClauses.Add("EndDate>=@MinEndDate"); - statement?.TryBind("@MinEndDate", DateTime.UtcNow); - } - else - { - whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)"); - statement?.TryBind("@IsAiringDate", DateTime.UtcNow); - } - } - - int personIdsLen = query.PersonIds.Length; - if (personIdsLen > 0) - { - // TODO: Should this query with CleanName ? - - clauseBuilder.Append('('); - - Span idBytes = stackalloc byte[16]; - for (int i = 0; i < personIdsLen; i++) - { - string paramName = "@PersonId" + i; - clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=") - .Append(paramName) - .Append("))) OR "); - - statement?.TryBind(paramName, query.PersonIds[i]); - } - - clauseBuilder.Length -= Or.Length; - clauseBuilder.Append(')'); - - whereClauses.Add(clauseBuilder.ToString()); - - clauseBuilder.Length = 0; - } - - if (!string.IsNullOrWhiteSpace(query.Person)) - { - whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)"); - statement?.TryBind("@PersonName", query.Person); - } - - if (!string.IsNullOrWhiteSpace(query.MinSortName)) - { - whereClauses.Add("SortName>=@MinSortName"); - statement?.TryBind("@MinSortName", query.MinSortName); - } - - if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) - { - whereClauses.Add("ExternalSeriesId=@ExternalSeriesId"); - statement?.TryBind("@ExternalSeriesId", query.ExternalSeriesId); - } - - if (!string.IsNullOrWhiteSpace(query.ExternalId)) - { - whereClauses.Add("ExternalId=@ExternalId"); - statement?.TryBind("@ExternalId", query.ExternalId); - } - - if (!string.IsNullOrWhiteSpace(query.Name)) - { - whereClauses.Add("CleanName=@Name"); - statement?.TryBind("@Name", GetCleanValue(query.Name)); - } - - // These are the same, for now - var nameContains = query.NameContains; - if (!string.IsNullOrWhiteSpace(nameContains)) - { - whereClauses.Add("(CleanName like @NameContains or OriginalTitle like @NameContains)"); - if (statement is not null) - { - nameContains = FixUnicodeChars(nameContains); - statement.TryBind("@NameContains", "%" + GetCleanValue(nameContains) + "%"); - } - } - - if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) - { - whereClauses.Add("SortName like @NameStartsWith"); - statement?.TryBind("@NameStartsWith", query.NameStartsWith + "%"); - } - - if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) - { - whereClauses.Add("SortName >= @NameStartsWithOrGreater"); - // lowercase this because SortName is stored as lowercase - statement?.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant()); - } - - if (!string.IsNullOrWhiteSpace(query.NameLessThan)) - { - whereClauses.Add("SortName < @NameLessThan"); - // lowercase this because SortName is stored as lowercase - statement?.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant()); - } - - if (query.ImageTypes.Length > 0) - { - foreach (var requiredImage in query.ImageTypes) - { - whereClauses.Add("Images like '%" + requiredImage + "%'"); - } - } - - if (query.IsLiked.HasValue) - { - if (query.IsLiked.Value) - { - whereClauses.Add("rating>=@UserRating"); - statement?.TryBind("@UserRating", UserItemData.MinLikeValue); - } - else - { - whereClauses.Add("(rating is null or rating<@UserRating)"); - statement?.TryBind("@UserRating", UserItemData.MinLikeValue); - } - } - - if (query.IsFavoriteOrLiked.HasValue) - { - if (query.IsFavoriteOrLiked.Value) - { - whereClauses.Add("IsFavorite=@IsFavoriteOrLiked"); - } - else - { - whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)"); - } - - statement?.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value); - } - - if (query.IsFavorite.HasValue) - { - if (query.IsFavorite.Value) - { - whereClauses.Add("IsFavorite=@IsFavorite"); - } - else - { - whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)"); - } - - statement?.TryBind("@IsFavorite", query.IsFavorite.Value); - } - - if (EnableJoinUserData(query)) - { - if (query.IsPlayed.HasValue) - { - // We should probably figure this out for all folders, but for right now, this is the only place where we need it - if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.Series) - { - if (query.IsPlayed.Value) - { - whereClauses.Add("PresentationUniqueKey not in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)"); - } - else - { - whereClauses.Add("PresentationUniqueKey in (select S.SeriesPresentationUniqueKey from TypedBaseitems S left join UserDatas UD on S.UserDataKey=UD.Key And UD.UserId=@UserId where Coalesce(UD.Played, 0)=0 and S.IsFolder=0 and S.IsVirtualItem=0 and S.SeriesPresentationUniqueKey not null)"); - } - } - else - { - if (query.IsPlayed.Value) - { - whereClauses.Add("(played=@IsPlayed)"); - } - else - { - whereClauses.Add("(played is null or played=@IsPlayed)"); - } - - statement?.TryBind("@IsPlayed", query.IsPlayed.Value); - } - } - } - - if (query.IsResumable.HasValue) - { - if (query.IsResumable.Value) - { - whereClauses.Add("playbackPositionTicks > 0"); - } - else - { - whereClauses.Add("(playbackPositionTicks is null or playbackPositionTicks = 0)"); - } - } - - if (query.ArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.ArtistIds.Length; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") and Type<=1)) OR "); - statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.AlbumArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.AlbumArtistIds.Length; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") and Type=1)) OR "); - statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.ContributingArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.ContributingArtistIds.Length; i++) - { - clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds") - .Append(i) - .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR "); - statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.AlbumIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.AlbumIds.Length; i++) - { - clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds") - .Append(i) - .Append(") OR "); - statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.ExcludeArtistIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.ExcludeArtistIds.Length; i++) - { - clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId") - .Append(i) - .Append(") and Type<=1)) OR "); - statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.GenreIds.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.GenreIds.Count; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId") - .Append(i) - .Append(") and Type=2)) OR "); - statement?.TryBind("@GenreId" + i, query.GenreIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.Genres.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.Genres.Count; i++) - { - clauseBuilder.Append("@Genre") - .Append(i) - .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR "); - statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i])); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (tags.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < tags.Count; i++) - { - clauseBuilder.Append("@Tag") - .Append(i) - .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); - statement?.TryBind("@Tag" + i, GetCleanValue(tags[i])); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (excludeTags.Count > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < excludeTags.Count; i++) - { - clauseBuilder.Append("@ExcludeTag") - .Append(i) - .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR "); - statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i])); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.StudioIds.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.StudioIds.Length; i++) - { - clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId") - .Append(i) - .Append(") and Type=3)) OR "); - statement?.TryBind("@StudioId" + i, query.StudioIds[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.OfficialRatings.Length > 0) - { - clauseBuilder.Append('('); - for (var i = 0; i < query.OfficialRatings.Length; i++) - { - clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or); - statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]); - } - - clauseBuilder.Length -= Or.Length; - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - clauseBuilder.Append('('); - if (query.HasParentalRating ?? false) - { - clauseBuilder.Append("InheritedParentalRatingValue not null"); - if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating"); - statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); - } - - if (query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - } - else if (query.BlockUnratedItems.Length > 0) - { - const string ParamName = "@UnratedType"; - clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in ("); - - for (int i = 0; i < query.BlockUnratedItems.Length; i++) - { - clauseBuilder.Append(ParamName).Append(i).Append(','); - statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString()); - } - - // Remove trailing comma - clauseBuilder.Length--; - clauseBuilder.Append("))"); - - if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(" OR ("); - } - - if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating"); - statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); - } - - if (query.MaxParentalRating.HasValue) - { - if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append(" AND "); - } - - clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - - if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(')'); - } - - if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)) - { - clauseBuilder.Append(" OR InheritedParentalRatingValue not null"); - } - } - else if (query.MinParentalRating.HasValue) - { - clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating"); - statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value); - - if (query.MaxParentalRating.HasValue) - { - clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - - clauseBuilder.Append(')'); - } - else if (query.MaxParentalRating.HasValue) - { - clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating"); - statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); - } - else if (!query.HasParentalRating ?? false) - { - clauseBuilder.Append("InheritedParentalRatingValue is null"); - } - - if (clauseBuilder.Length > 1) - { - whereClauses.Add(clauseBuilder.Append(')').ToString()); - clauseBuilder.Length = 0; - } - - if (query.HasOfficialRating.HasValue) - { - if (query.HasOfficialRating.Value) - { - whereClauses.Add("(OfficialRating not null AND OfficialRating<>'')"); - } - else - { - whereClauses.Add("(OfficialRating is null OR OfficialRating='')"); - } - } - - if (query.HasOverview.HasValue) - { - if (query.HasOverview.Value) - { - whereClauses.Add("(Overview not null AND Overview<>'')"); - } - else - { - whereClauses.Add("(Overview is null OR Overview='')"); - } - } - - if (query.HasOwnerId.HasValue) - { - if (query.HasOwnerId.Value) - { - whereClauses.Add("OwnerId not null"); - } - else - { - whereClauses.Add("OwnerId is null"); - } - } - - if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage); - } - - if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage); - } - - if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage); - } - - if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage)) - { - whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)"); - statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage); - } - - if (query.HasSubtitles.HasValue) - { - if (query.HasSubtitles.Value) - { - whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)"); - } - else - { - whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)"); - } - } - - if (query.HasChapterImages.HasValue) - { - if (query.HasChapterImages.Value) - { - whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) not null)"); - } - else - { - whereClauses.Add("((select imagepath from Chapters2 where Chapters2.ItemId=A.Guid and imagepath not null limit 1) is null)"); - } - } - - if (query.HasDeadParentId.HasValue && query.HasDeadParentId.Value) - { - whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); - } - - if (query.IsDeadArtist.HasValue && query.IsDeadArtist.Value) - { - whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type in (0,1))"); - } - - if (query.IsDeadStudio.HasValue && query.IsDeadStudio.Value) - { - whereClauses.Add("CleanName not in (Select CleanValue From ItemValues where Type = 3)"); - } - - if (query.IsDeadPerson.HasValue && query.IsDeadPerson.Value) - { - whereClauses.Add("Name not in (Select Name From People)"); - } - - if (query.Years.Length == 1) - { - whereClauses.Add("ProductionYear=@Years"); - statement?.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); - } - else if (query.Years.Length > 1) - { - var val = string.Join(',', query.Years); - whereClauses.Add("ProductionYear in (" + val + ")"); - } - - var isVirtualItem = query.IsVirtualItem ?? query.IsMissing; - if (isVirtualItem.HasValue) - { - whereClauses.Add("IsVirtualItem=@IsVirtualItem"); - statement?.TryBind("@IsVirtualItem", isVirtualItem.Value); - } - - if (query.IsSpecialSeason.HasValue) - { - if (query.IsSpecialSeason.Value) - { - whereClauses.Add("IndexNumber = 0"); - } - else - { - whereClauses.Add("IndexNumber <> 0"); - } - } - - if (query.IsUnaired.HasValue) - { - if (query.IsUnaired.Value) - { - whereClauses.Add("PremiereDate >= DATETIME('now')"); - } - else - { - whereClauses.Add("PremiereDate < DATETIME('now')"); - } - } - - if (query.MediaTypes.Length == 1) - { - whereClauses.Add("MediaType=@MediaTypes"); - statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString()); - } - else if (query.MediaTypes.Length > 1) - { - var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'")); - whereClauses.Add("MediaType in (" + val + ")"); - } - - if (query.ItemIds.Length > 0) - { - var includeIds = new List(); - var index = 0; - foreach (var id in query.ItemIds) - { - includeIds.Add("Guid = @IncludeId" + index); - statement?.TryBind("@IncludeId" + index, id); - index++; - } - - whereClauses.Add("(" + string.Join(" OR ", includeIds) + ")"); - } - - if (query.ExcludeItemIds.Length > 0) - { - var excludeIds = new List(); - var index = 0; - foreach (var id in query.ExcludeItemIds) - { - excludeIds.Add("Guid <> @ExcludeId" + index); - statement?.TryBind("@ExcludeId" + index, id); - index++; - } - - whereClauses.Add(string.Join(" AND ", excludeIds)); - } - - if (query.ExcludeProviderIds is not null && query.ExcludeProviderIds.Count > 0) - { - var excludeIds = new List(); - - var index = 0; - foreach (var pair in query.ExcludeProviderIds) - { - if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var paramName = "@ExcludeProviderId" + index; - excludeIds.Add("(ProviderIds is null or ProviderIds not like " + paramName + ")"); - statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - index++; - - break; - } - - if (excludeIds.Count > 0) - { - whereClauses.Add(string.Join(" AND ", excludeIds)); - } - } - - if (query.HasAnyProviderId is not null && query.HasAnyProviderId.Count > 0) - { - var hasProviderIds = new List(); - - var index = 0; - foreach (var pair in query.HasAnyProviderId) - { - if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - // TODO this seems to be an idea for a better schema where ProviderIds are their own table - // but this is not implemented - // hasProviderIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); - - // TODO this is a really BAD way to do it since the pair: - // Tmdb, 1234 matches Tmdb=1234 but also Tmdb=1234567 - // and maybe even NotTmdb=1234. - - // this is a placeholder for this specific pair to correlate it in the bigger query - var paramName = "@HasAnyProviderId" + index; - - // this is a search for the placeholder - hasProviderIds.Add("ProviderIds like " + paramName); - - // this replaces the placeholder with a value, here: %key=val% - statement?.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); - index++; - - break; - } - - if (hasProviderIds.Count > 0) - { - whereClauses.Add("(" + string.Join(" OR ", hasProviderIds) + ")"); - } - } - - if (query.HasImdbId.HasValue) - { - whereClauses.Add(GetProviderIdClause(query.HasImdbId.Value, "imdb")); - } - - if (query.HasTmdbId.HasValue) - { - whereClauses.Add(GetProviderIdClause(query.HasTmdbId.Value, "tmdb")); - } - - if (query.HasTvdbId.HasValue) - { - whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb")); - } - - var queryTopParentIds = query.TopParentIds; - - if (queryTopParentIds.Length > 0) - { - var includedItemByNameTypes = GetItemByNameTypesInQuery(query); - var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0; - - if (queryTopParentIds.Length == 1) - { - if (enableItemsByName && includedItemByNameTypes.Count == 1) - { - whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)"); - statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))"); - } - else - { - whereClauses.Add("(TopParentId=@TopParentId)"); - } - - statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); - } - else if (queryTopParentIds.Length > 1) - { - var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - - if (enableItemsByName && includedItemByNameTypes.Count == 1) - { - whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))"); - statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]); - } - else if (enableItemsByName && includedItemByNameTypes.Count > 1) - { - var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'")); - whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))"); - } - else - { - whereClauses.Add("TopParentId in (" + val + ")"); - } - } - } - - if (query.AncestorIds.Length == 1) - { - whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); - statement?.TryBind("@AncestorId", query.AncestorIds[0]); - } - - if (query.AncestorIds.Length > 1) - { - var inClause = string.Join(',', query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); - } - - if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) - { - var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; - whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); - statement?.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); - } - - if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) - { - whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); - statement?.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); - } - - if (query.ExcludeInheritedTags.Length > 0) - { - var paramName = "@ExcludeInheritedTags"; - if (statement is null) - { - int index = 0; - string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(_ => paramName + index++)); - whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)"); - } - else - { - for (int index = 0; index < query.ExcludeInheritedTags.Length; index++) - { - statement.TryBind(paramName + index, GetCleanValue(query.ExcludeInheritedTags[index])); - } - } - } - - if (query.IncludeInheritedTags.Length > 0) - { - var paramName = "@IncludeInheritedTags"; - if (statement is null) - { - int index = 0; - string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++)); - // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. - // In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. - if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) - { - whereClauses.Add($""" - ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null - OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null) - """); - } - - // A playlist should be accessible to its owner regardless of allowed tags. - else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) - { - whereClauses.Add($""" - ((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null - OR data like @PlaylistOwnerUserId) - """); - } - else - { - whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)"); - } - } - else - { - for (int index = 0; index < query.IncludeInheritedTags.Length; index++) - { - statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index])); - } - - if (query.User is not null) - { - statement.TryBind("@PlaylistOwnerUserId", $"""%"OwnerUserId":"{query.User.Id.ToString("N")}"%"""); - } - } - } - - if (query.SeriesStatuses.Length > 0) - { - var statuses = new List(); - - foreach (var seriesStatus in query.SeriesStatuses) - { - statuses.Add("data like '%" + seriesStatus + "%'"); - } - - whereClauses.Add("(" + string.Join(" OR ", statuses) + ")"); - } - - if (query.BoxSetLibraryFolders.Length > 0) - { - var folderIdQueries = new List(); - - foreach (var folderId in query.BoxSetLibraryFolders) - { - folderIdQueries.Add("data like '%" + folderId.ToString("N", CultureInfo.InvariantCulture) + "%'"); - } - - whereClauses.Add("(" + string.Join(" OR ", folderIdQueries) + ")"); - } - - if (query.VideoTypes.Length > 0) - { - var videoTypes = new List(); - - foreach (var videoType in query.VideoTypes) - { - videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'"); - } - - whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")"); - } - - if (query.Is3D.HasValue) - { - if (query.Is3D.Value) - { - whereClauses.Add("data like '%Video3DFormat%'"); - } - else - { - whereClauses.Add("data not like '%Video3DFormat%'"); - } - } - - if (query.IsPlaceHolder.HasValue) - { - if (query.IsPlaceHolder.Value) - { - whereClauses.Add("data like '%\"IsPlaceHolder\":true%'"); - } - else - { - whereClauses.Add("(data is null or data not like '%\"IsPlaceHolder\":true%')"); - } - } - - if (query.HasSpecialFeature.HasValue) - { - if (query.HasSpecialFeature.Value) - { - whereClauses.Add("ExtraIds not null"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - if (query.HasTrailer.HasValue) - { - if (query.HasTrailer.Value) - { - whereClauses.Add("ExtraIds not null"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - if (query.HasThemeSong.HasValue) - { - if (query.HasThemeSong.Value) - { - whereClauses.Add("ExtraIds not null"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - if (query.HasThemeVideo.HasValue) - { - if (query.HasThemeVideo.Value) - { - whereClauses.Add("ExtraIds not null"); - } - else - { - whereClauses.Add("ExtraIds is null"); - } - } - - return whereClauses; - } - - /// - /// Formats a where clause for the specified provider. - /// - /// Whether or not to include items with this provider's ids. - /// Provider name. - /// Formatted SQL clause. - private string GetProviderIdClause(bool includeResults, string provider) - { - return string.Format( - CultureInfo.InvariantCulture, - "ProviderIds {0} like '%{1}=%'", - includeResults ? string.Empty : "not", - provider); - } - -#nullable disable - private List GetItemByNameTypesInQuery(InternalItemsQuery query) - { - var list = new List(); - - if (IsTypeInQuery(BaseItemKind.Person, query)) - { - list.Add(typeof(Person).FullName); - } - - if (IsTypeInQuery(BaseItemKind.Genre, query)) - { - list.Add(typeof(Genre).FullName); - } - - if (IsTypeInQuery(BaseItemKind.MusicGenre, query)) - { - list.Add(typeof(MusicGenre).FullName); - } - - if (IsTypeInQuery(BaseItemKind.MusicArtist, query)) - { - list.Add(typeof(MusicArtist).FullName); - } - - if (IsTypeInQuery(BaseItemKind.Studio, query)) - { - list.Add(typeof(Studio).FullName); - } - - return list; - } - - private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) - { - if (query.ExcludeItemTypes.Contains(type)) - { - return false; - } - - return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); - } - - private string GetCleanValue(string value) - { - if (string.IsNullOrWhiteSpace(value)) + private string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) { return value; } @@ -2500,41 +735,6 @@ namespace Emby.Server.Implementations.Data return value.RemoveDiacritics().ToLowerInvariant(); } - private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) - { - if (!query.GroupByPresentationUniqueKey) - { - return false; - } - - if (query.GroupBySeriesPresentationUniqueKey) - { - return false; - } - - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) - { - return false; - } - - if (query.User is null) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Contains(BaseItemKind.Episode) - || query.IncludeItemTypes.Contains(BaseItemKind.Video) - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) - || query.IncludeItemTypes.Contains(BaseItemKind.Series) - || query.IncludeItemTypes.Contains(BaseItemKind.Season); - } - /// public void UpdateInheritedValues() { diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs index 17bf1166d..ef7650294 100644 --- a/Jellyfin.Data/Enums/ItemSortBy.cs +++ b/Jellyfin.Data/Enums/ItemSortBy.cs @@ -154,14 +154,4 @@ public enum ItemSortBy /// The index number. /// IndexNumber = 29, - - /// - /// The similarity score. - /// - SimilarityScore = 30, - - /// - /// The search score. - /// - SearchScore = 31, } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index 8f3c9636e..f2d6b6261 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Linq.Expressions; @@ -36,8 +37,6 @@ public class BaseItemManager : IItemRepository private readonly IDbContextFactory _dbProvider; private readonly IServerApplicationHost _appHost; - - private readonly ItemFields[] _allItemFields = Enum.GetValues(); private static readonly BaseItemKind[] _programTypes = new[] @@ -146,22 +145,284 @@ public class BaseItemManager : IItemRepository _appHost = appHost; } - public int GetCount(InternalItemsQuery query) + private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) + { + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + query = query.Skip(offset); + } + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + } + + return query; + } + + private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + { +#pragma warning disable CS8603 // Possible null reference return. + return sortBy switch + { + ItemSortBy.AirTime => e => e.SortName, // TODO + ItemSortBy.Runtime => e => e.RunTimeTicks, + ItemSortBy.Random => e => EF.Functions.Random(), + ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate, + ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount, + ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite, + ItemSortBy.IsFolder => e => e.IsFolder, + ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, + ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue), + ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue), + ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), + ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => e => e.SeriesName, + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => e => e.Album, + ItemSortBy.DateCreated => e => e.DateCreated, + ItemSortBy.PremiereDate => e => e.PremiereDate, + ItemSortBy.StartDate => e => e.StartDate, + ItemSortBy.Name => e => e.Name, + ItemSortBy.CommunityRating => e => e.CommunityRating, + ItemSortBy.ProductionYear => e => e.ProductionYear, + ItemSortBy.CriticRating => e => e.CriticRating, + ItemSortBy.VideoBitRate => e => e.TotalBitrate, + ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, + ItemSortBy.IndexNumber => e => e.IndexNumber, + _ => e => e.SortName + }; +#pragma warning restore CS8603 // Possible null reference return. + + } + + private IQueryable MapOrderByField(IQueryable dbQuery, ItemSortBy sortBy, InternalItemsQuery query) + { + return sortBy switch + { + ItemSortBy.AirTime => dbQuery.OrderBy(e => e.SortName), // TODO + ItemSortBy.Runtime => dbQuery.OrderBy(e => e.RunTimeTicks), + ItemSortBy.Random => dbQuery.OrderBy(e => EF.Functions.Random()), + ItemSortBy.DatePlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate), + ItemSortBy.PlayCount => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount), + ItemSortBy.IsFavoriteOrLiked => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite), + ItemSortBy.IsFolder => dbQuery.OrderBy(e => e.IsFolder), + ItemSortBy.IsPlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), + ItemSortBy.IsUnplayed => dbQuery.OrderBy(e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), + ItemSortBy.DateLastContentAdded => dbQuery.OrderBy(e => e.DateLastMediaAdded), + ItemSortBy.Artist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue)), + ItemSortBy.AlbumArtist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue)), + ItemSortBy.Studio => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue)), + ItemSortBy.OfficialRating => dbQuery.OrderBy(e => e.InheritedParentalRatingValue), + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => dbQuery.OrderBy(e => e.SeriesName), + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => dbQuery.OrderBy(e => e.Album), + ItemSortBy.DateCreated => dbQuery.OrderBy(e => e.DateCreated), + ItemSortBy.PremiereDate => dbQuery.OrderBy(e => e.PremiereDate), + ItemSortBy.StartDate => dbQuery.OrderBy(e => e.StartDate), + ItemSortBy.Name => dbQuery.OrderBy(e => e.Name), + ItemSortBy.CommunityRating => dbQuery.OrderBy(e => e.CommunityRating), + ItemSortBy.ProductionYear => dbQuery.OrderBy(e => e.ProductionYear), + ItemSortBy.CriticRating => dbQuery.OrderBy(e => e.CriticRating), + ItemSortBy.VideoBitRate => dbQuery.OrderBy(e => e.TotalBitrate), + ItemSortBy.ParentIndexNumber => dbQuery.OrderBy(e => e.ParentIndexNumber), + ItemSortBy.IndexNumber => dbQuery.OrderBy(e => e.IndexNumber), + _ => dbQuery.OrderBy(e => e.SortName) + }; + } + + private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) + { + var orderBy = filter.OrderBy; + bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + + if (hasSearch) + { + List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); + if (hasSearch) + { + prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); + } + + orderBy = filter.OrderBy = [.. prepend, .. orderBy]; + } + else if (orderBy.Count == 0) + { + return query; + } + + foreach (var item in orderBy) + { + var expression = MapOrderByField(item.OrderBy, filter); + if (item.SortOrder == SortOrder.Ascending) + { + query = query.OrderBy(expression); + } + else + { + query = query.OrderByDescending(expression); + } + } + + return query; + } + + public IReadOnlyList GetItemIdsList(InternalItemsQuery filter) + { + ArgumentNullException.ThrowIfNull(filter); + PrepareFilterQuery(filter); + + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, filter) + .DistinctBy(e => e.Id); + + var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); + if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).SelectMany(e => e); + } + + if (enableGroupByPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).SelectMany(e => e); + } + + if (filter.GroupBySeriesPresentationUniqueKey) + { + dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).SelectMany(e => e); + } + + dbQuery = ApplyOrder(dbQuery, filter); + + return Pageinate(dbQuery, filter).Select(e => e.Id).ToImmutableArray(); + } + + private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + { + if (!query.GroupByPresentationUniqueKey) + { + return false; + } + + if (query.GroupBySeriesPresentationUniqueKey) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + return false; + } + + if (query.User is null) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + return query.IncludeItemTypes.Contains(BaseItemKind.Episode) + || query.IncludeItemTypes.Contains(BaseItemKind.Video) + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) + || query.IncludeItemTypes.Contains(BaseItemKind.Series) + || query.IncludeItemTypes.Contains(BaseItemKind.Season); + } + + /// + public QueryResult GetItems(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { - query.Limit = query.Limit.Value + 4; + var returnList = GetItemList(query); + return new QueryResult( + query.StartIndex, + returnList.Count, + returnList); } - if (query.IsResumable ?? false) + PrepareFilterQuery(query); + var result = new QueryResult(); + + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, query) + .DistinctBy(e => e.Id); + if (query.EnableTotalRecordCount) { - query.IsVirtualItem = false; + result.TotalRecordCount = dbQuery.Count(); + } + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (query.Limit.HasValue) + { + dbQuery = dbQuery.Take(query.Limit.Value); + } + } + + result.Items = dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + result.StartIndex = query.StartIndex ?? 0; + return result; + } + + /// + public IReadOnlyList GetItemList(InternalItemsQuery query) + { + ArgumentNullException.ThrowIfNull(query); + PrepareFilterQuery(query); + + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, query) + .DistinctBy(e => e.Id); + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (offset > 0) + { + dbQuery = dbQuery.Skip(offset); + } + + if (query.Limit.HasValue) + { + dbQuery = dbQuery.Take(query.Limit.Value); + } } + return dbQuery.ToList().Select(DeserialiseBaseItem).ToImmutableArray(); + } + + /// + public int GetCount(InternalItemsQuery query) + { + ArgumentNullException.ThrowIfNull(query); + // Hack for right now since we currently don't support filtering out these duplicates within a query + PrepareFilterQuery(query); + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.BaseItems, context, query); + return dbQuery.Count(); } private IQueryable TranslateQuery( @@ -1049,6 +1310,8 @@ public class BaseItemManager : IItemRepository .Where(e => e.ExtraIds == null); } } + + return baseQuery; } /// @@ -1212,9 +1475,9 @@ public class BaseItemManager : IItemRepository dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : Guid.Parse(entity.OwnerId); dto.Width = entity.Width.GetValueOrDefault(); dto.Height = entity.Height.GetValueOrDefault(); - if (entity.ProviderIds is not null) + if (entity.Provider is not null) { - DeserializeProviderIds(entity.ProviderIds, dto); + dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue); } if (entity.ExtraType is not null) @@ -1386,7 +1649,12 @@ public class BaseItemManager : IItemRepository entity.OwnerId = dto.OwnerId.ToString(); entity.Width = dto.Width; entity.Height = dto.Height; - entity.ProviderIds = SerializeProviderIds(dto.ProviderIds); + entity.Provider = dto.ProviderIds.Select(e => new Data.Entities.BaseItemProvider() + { + Item = entity, + ProviderId = e.Key, + ProviderValue = e.Value + }).ToList(); entity.Audio = dto.Audio?.ToString(); entity.ExtraType = dto.ExtraType?.ToString(); @@ -1479,10 +1747,23 @@ public class BaseItemManager : IItemRepository private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) { var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type."); - var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); ; + var dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type."); return Map(baseItemEntity, dto); } + private static void PrepareFilterQuery(InternalItemsQuery query) + { + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + + if (query.IsResumable ?? false) + { + query.IsVirtualItem = false; + } + } + private string GetCleanValue(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -1813,5 +2094,4 @@ public class BaseItemManager : IItemRepository return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); } - } -- cgit v1.2.3 From 6acd146d17691d1fd58e8a110425cf1d7e2cdc44 Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:11:31 +0000 Subject: WIP migration sqlite item repository to efcore --- .../Data/SqliteItemRepository.cs | 1935 +------------------- Jellyfin.Data/Entities/PeopleKind.cs | 133 ++ .../Item/BaseItemManager.cs | 434 +++-- .../Item/MediaStreamManager.cs | 201 ++ .../Item/PeopleManager.cs | 164 ++ 5 files changed, 861 insertions(+), 2006 deletions(-) create mode 100644 Jellyfin.Data/Entities/PeopleKind.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaStreamManager.cs create mode 100644 Jellyfin.Server.Implementations/Item/PeopleManager.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 26255e6aa..a650f9555 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -146,1901 +146,111 @@ namespace Emby.Server.Implementations.Data || query.IsLiked.HasValue; } - private bool HasField(InternalItemsQuery query, ItemFields name) - { - switch (name) - { - case ItemFields.Tags: - return query.DtoOptions.ContainsField(name) || HasProgramAttributes(query); - case ItemFields.CustomRating: - case ItemFields.ProductionLocations: - case ItemFields.Settings: - case ItemFields.OriginalTitle: - case ItemFields.Taglines: - case ItemFields.SortName: - case ItemFields.Studios: - case ItemFields.ExtraIds: - case ItemFields.DateCreated: - case ItemFields.Overview: - case ItemFields.Genres: - case ItemFields.DateLastMediaAdded: - case ItemFields.PresentationUniqueKey: - case ItemFields.InheritedParentalRatingValue: - case ItemFields.ExternalSeriesId: - case ItemFields.SeriesPresentationUniqueKey: - case ItemFields.DateLastRefreshed: - case ItemFields.DateLastSaved: - return query.DtoOptions.ContainsField(name); - case ItemFields.ServiceName: - return HasServiceName(query); - default: - return true; - } - } - - private bool HasProgramAttributes(InternalItemsQuery query) - { - if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _programTypes.Contains(x)); - } - - private bool HasServiceName(InternalItemsQuery query) - { - if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x)); - } - - private bool HasStartDate(InternalItemsQuery query) - { - if (query.ParentType is not null && _programExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _startDateTypes.Contains(x)); - } - - private bool HasEpisodeAttributes(InternalItemsQuery query) - { - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Contains(BaseItemKind.Episode); - } - - private bool HasTrailerTypes(InternalItemsQuery query) - { - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Contains(BaseItemKind.Trailer); - } - - private bool HasArtistFields(InternalItemsQuery query) - { - if (query.ParentType is not null && _artistExcludeParentTypes.Contains(query.ParentType.Value)) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x)); - } - - private bool HasSeriesFields(InternalItemsQuery query) - { - if (query.ParentType == BaseItemKind.PhotoAlbum) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0) - { - return true; - } - - return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x)); - } - - private void SetFinalColumnsToSelect(InternalItemsQuery query, List columns) - { - foreach (var field in _allItemFields) - { - if (!HasField(query, field)) - { - switch (field) - { - case ItemFields.Settings: - columns.Remove("IsLocked"); - columns.Remove("PreferredMetadataCountryCode"); - columns.Remove("PreferredMetadataLanguage"); - columns.Remove("LockedFields"); - break; - case ItemFields.ServiceName: - columns.Remove("ExternalServiceId"); - break; - case ItemFields.SortName: - columns.Remove("ForcedSortName"); - break; - case ItemFields.Taglines: - columns.Remove("Tagline"); - break; - case ItemFields.Tags: - columns.Remove("Tags"); - break; - case ItemFields.IsHD: - // do nothing - break; - default: - columns.Remove(field.ToString()); - break; - } - } - } - - if (!HasProgramAttributes(query)) - { - columns.Remove("IsMovie"); - columns.Remove("IsSeries"); - columns.Remove("EpisodeTitle"); - columns.Remove("IsRepeat"); - columns.Remove("ShowId"); - } - - if (!HasEpisodeAttributes(query)) - { - columns.Remove("SeasonName"); - columns.Remove("SeasonId"); - } - - if (!HasStartDate(query)) - { - columns.Remove("StartDate"); - } - - if (!HasTrailerTypes(query)) - { - columns.Remove("TrailerTypes"); - } - - if (!HasArtistFields(query)) - { - columns.Remove("AlbumArtists"); - columns.Remove("Artists"); - } - - if (!HasSeriesFields(query)) - { - columns.Remove("SeriesId"); - } - - if (!HasEpisodeAttributes(query)) - { - columns.Remove("SeasonName"); - columns.Remove("SeasonId"); - } - - if (!query.DtoOptions.EnableImages) - { - columns.Remove("Images"); - } - - if (EnableJoinUserData(query)) - { - columns.Add("UserDatas.UserId"); - columns.Add("UserDatas.lastPlayedDate"); - columns.Add("UserDatas.playbackPositionTicks"); - columns.Add("UserDatas.playcount"); - columns.Add("UserDatas.isFavorite"); - columns.Add("UserDatas.played"); - columns.Add("UserDatas.rating"); - } - - if (query.SimilarTo is not null) - { - var item = query.SimilarTo; - - var builder = new StringBuilder(); - builder.Append('('); - - if (item.InheritedParentalRatingValue == 0) - { - builder.Append("((InheritedParentalRatingValue=0) * 10)"); - } - else - { - builder.Append( - @"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0 - THEN 0 - ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue)) - END)"); - } - - if (item.ProductionYear.HasValue) - { - builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 10 Else 0 End )"); - builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )"); - } - - // genres, tags, studios, person, year? - builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))"); - builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))"); - - if (item is MusicArtist) - { - // Match albums where the artist is AlbumArtist against other albums. - // It is assumed that similar albums => similar artists. - builder.Append( - @"+ (WITH artistValues AS ( - SELECT DISTINCT albumValues.CleanValue - FROM ItemValues albumValues - INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId - INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId - ), similarArtist AS ( - SELECT albumValues.ItemId - FROM ItemValues albumValues - INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId - INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid - ) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))"); - } - - builder.Append(") as SimilarityScore"); - - columns.Add(builder.ToString()); - - query.ExcludeItemIds = [.. query.ExcludeItemIds, item.Id, .. item.ExtraIds]; - query.ExcludeProviderIds = item.ProviderIds; - } - - if (!string.IsNullOrEmpty(query.SearchTerm)) - { - var builder = new StringBuilder(); - builder.Append('('); - - builder.Append("((CleanName like @SearchTermStartsWith or (OriginalTitle not null and OriginalTitle like @SearchTermStartsWith)) * 10)"); - builder.Append("+ ((CleanName = @SearchTermStartsWith COLLATE NOCASE or (OriginalTitle not null and OriginalTitle = @SearchTermStartsWith COLLATE NOCASE)) * 10)"); - - if (query.SearchTerm.Length > 1) - { - builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)"); - } - - builder.Append(") as SearchScore"); - - columns.Add(builder.ToString()); - } - } - - private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement) - { - var searchTerm = query.SearchTerm; - - if (string.IsNullOrEmpty(searchTerm)) - { - return; - } - - searchTerm = FixUnicodeChars(searchTerm); - searchTerm = GetCleanValue(searchTerm); - - var commandText = statement.CommandText; - if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SearchTermStartsWith", searchTerm + "%"); - } - - if (commandText.Contains("@SearchTermContains", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); - } - } - - private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement) - { - var item = query.SimilarTo; - - if (item is null) - { - return; - } - - var commandText = statement.CommandText; - - if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@ItemOfficialRating", item.OfficialRating); - } - - if (commandText.Contains("@ItemProductionYear", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0); - } - - if (commandText.Contains("@SimilarItemId", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@SimilarItemId", item.Id); - } - - if (commandText.Contains("@InheritedParentalRatingValue", StringComparison.OrdinalIgnoreCase)) - { - statement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); - } - } - - private string GetJoinUserDataText(InternalItemsQuery query) - { - if (!EnableJoinUserData(query)) - { - return string.Empty; - } - - return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)"; - } - - private string GetGroupBy(InternalItemsQuery query) - { - var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query); - if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey) - { - return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey"; - } - - if (enableGroupByPresentationUniqueKey) - { - return " Group by PresentationUniqueKey"; - } - - if (query.GroupBySeriesPresentationUniqueKey) - { - return " Group by SeriesPresentationUniqueKey"; - } - - return string.Empty; - } - - - private string FixUnicodeChars(string buffer) - { - buffer = buffer.Replace('\u2013', '-'); // en dash - buffer = buffer.Replace('\u2014', '-'); // em dash - buffer = buffer.Replace('\u2015', '-'); // horizontal bar - buffer = buffer.Replace('\u2017', '_'); // double low line - buffer = buffer.Replace('\u2018', '\''); // left single quotation mark - buffer = buffer.Replace('\u2019', '\''); // right single quotation mark - buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark - buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark - buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark - buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark - buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark - buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis - buffer = buffer.Replace('\u2032', '\''); // prime - buffer = buffer.Replace('\u2033', '\"'); // double prime - buffer = buffer.Replace('\u0060', '\''); // grave accent - return buffer.Replace('\u00B4', '\''); // acute accent - } - - private string GetOrderByText(InternalItemsQuery query) - { - var orderBy = query.OrderBy; - bool hasSimilar = query.SimilarTo is not null; - bool hasSearch = !string.IsNullOrEmpty(query.SearchTerm); - - if (hasSimilar || hasSearch) - { - List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); - if (hasSearch) - { - prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending)); - prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); - } - - if (hasSimilar) - { - prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending)); - prepend.Add((ItemSortBy.Random, SortOrder.Ascending)); - } - - orderBy = query.OrderBy = [.. prepend, .. orderBy]; - } - else if (orderBy.Count == 0) - { - return string.Empty; - } - - return " ORDER BY " + string.Join(',', orderBy.Select(i => - { - var sortBy = MapOrderByField(i.OrderBy, query); - var sortOrder = i.SortOrder == SortOrder.Ascending ? "ASC" : "DESC"; - return sortBy + " " + sortOrder; - })); - } - - private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) - { - return sortBy switch - { - ItemSortBy.AirTime => "SortName", // TODO - ItemSortBy.Runtime => "RuntimeTicks", - ItemSortBy.Random => "RANDOM()", - ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)", - ItemSortBy.DatePlayed => "LastPlayedDate", - ItemSortBy.PlayCount => "PlayCount", - ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )", - ItemSortBy.IsFolder => "IsFolder", - ItemSortBy.IsPlayed => "played", - ItemSortBy.IsUnplayed => "played", - ItemSortBy.DateLastContentAdded => "DateLastMediaAdded", - ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)", - ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)", - ItemSortBy.OfficialRating => "InheritedParentalRatingValue", - ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)", - ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => "SeriesName", - ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => "Album", - ItemSortBy.DateCreated => "DateCreated", - ItemSortBy.PremiereDate => "PremiereDate", - ItemSortBy.StartDate => "StartDate", - ItemSortBy.Name => "Name", - ItemSortBy.CommunityRating => "CommunityRating", - ItemSortBy.ProductionYear => "ProductionYear", - ItemSortBy.CriticRating => "CriticRating", - ItemSortBy.VideoBitRate => "VideoBitRate", - ItemSortBy.ParentIndexNumber => "ParentIndexNumber", - ItemSortBy.IndexNumber => "IndexNumber", - ItemSortBy.SimilarityScore => "SimilarityScore", - ItemSortBy.SearchScore => "SearchScore", - _ => "SortName" - }; - } - - /// - public List GetItemIdsList(InternalItemsQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - var columns = new List { "guid" }; - SetFinalColumnsToSelect(query, columns); - var commandTextBuilder = new StringBuilder("select ", 256) - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)); - - var whereClauses = GetWhereClauses(query, null); - if (whereClauses.Count != 0) - { - commandTextBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses); - } - - commandTextBuilder.Append(GetGroupBy(query)) - .Append(GetOrderByText(query)); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandTextBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - commandTextBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var commandText = commandTextBuilder.ToString(); - var list = new List(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row.GetGuid(0)); - } - } - - return list; - } - - private bool IsAlphaNumeric(string str) - { - if (string.IsNullOrWhiteSpace(str)) - { - return false; - } - - for (int i = 0; i < str.Length; i++) - { - if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) - { - return false; - } - } - - return true; - } - - private bool IsValidPersonType(string value) - { - return IsAlphaNumeric(value); - } - - private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query) - { - if (query.ExcludeItemTypes.Contains(type)) - { - return false; - } - - return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); - } - - private string GetCleanValue(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return value; - } - - return value.RemoveDiacritics().ToLowerInvariant(); - } - - /// - public void UpdateInheritedValues() - { - const string Statements = """ -delete from ItemValues where type = 6; -insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4; -insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue -FROM AncestorIds -LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId) -where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4; -"""; - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - connection.Execute(Statements); - transaction.Commit(); - } - - /// - public void DeleteItem(Guid id) - { - if (id.IsEmpty()) - { - throw new ArgumentNullException(nameof(id)); - } - - CheckDisposed(); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // Delete people - ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id); - - // Delete chapters - ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id); - - // Delete media streams - ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id); - - // Delete ancestors - ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id); - - // Delete item values - ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id); - - // Delete the item - ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id); - - transaction.Commit(); - } - - private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value) - { - using (var statement = PrepareStatement(db, query)) - { - statement.TryBind("@Id", value); - - statement.ExecuteNonQuery(); - } - } - - /// - public List GetPeopleNames(InternalPeopleQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - var commandText = new StringBuilder("select Distinct p.Name from People p"); - - var whereClauses = GetPeopleWhereClauses(query, null); - - if (whereClauses.Count != 0) - { - commandText.Append(" where ").AppendJoin(" AND ", whereClauses); - } - - commandText.Append(" order by ListOrder"); - - if (query.Limit > 0) - { - commandText.Append(" LIMIT ").Append(query.Limit); - } - - var list = new List(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText.ToString())) - { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row.GetString(0)); - } - } - - return list; - } - - /// - public List GetPeople(InternalPeopleQuery query) - { - ArgumentNullException.ThrowIfNull(query); - - CheckDisposed(); - - StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p"); - - var whereClauses = GetPeopleWhereClauses(query, null); - - if (whereClauses.Count != 0) - { - commandText.Append(" where ").AppendJoin(" AND ", whereClauses); - } - - commandText.Append(" order by ListOrder"); - - if (query.Limit > 0) - { - commandText.Append(" LIMIT ").Append(query.Limit); - } - - var list = new List(); - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText.ToString())) - { - // Run this again to bind the params - GetPeopleWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetPerson(row)); - } - } - - return list; - } - - private List GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement) - { - var whereClauses = new List(); - - if (query.User is not null && query.IsFavorite.HasValue) - { - whereClauses.Add(@"p.Name IN ( -SELECT Name FROM TypedBaseItems WHERE UserDataKey IN ( -SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId) -AND Type = @InternalPersonType)"); - statement?.TryBind("@IsFavorite", query.IsFavorite.Value); - statement?.TryBind("@InternalPersonType", typeof(Person).FullName); - statement?.TryBind("@UserId", query.User.InternalId); - } - - if (!query.ItemId.IsEmpty()) - { - whereClauses.Add("ItemId=@ItemId"); - statement?.TryBind("@ItemId", query.ItemId); - } - - if (!query.AppearsInItemId.IsEmpty()) - { - whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)"); - statement?.TryBind("@AppearsInItemId", query.AppearsInItemId); - } - - var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); - - if (queryPersonTypes.Count == 1) - { - whereClauses.Add("PersonType=@PersonType"); - statement?.TryBind("@PersonType", queryPersonTypes[0]); - } - else if (queryPersonTypes.Count > 1) - { - var val = string.Join(',', queryPersonTypes.Select(i => "'" + i + "'")); - - whereClauses.Add("PersonType in (" + val + ")"); - } - - var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList(); - - if (queryExcludePersonTypes.Count == 1) - { - whereClauses.Add("PersonType<>@PersonType"); - statement?.TryBind("@PersonType", queryExcludePersonTypes[0]); - } - else if (queryExcludePersonTypes.Count > 1) - { - var val = string.Join(',', queryExcludePersonTypes.Select(i => "'" + i + "'")); - - whereClauses.Add("PersonType not in (" + val + ")"); - } - - if (query.MaxListOrder.HasValue) - { - whereClauses.Add("ListOrder<=@MaxListOrder"); - statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value); - } - - if (!string.IsNullOrWhiteSpace(query.NameContains)) - { - whereClauses.Add("p.Name like @NameContains"); - statement?.TryBind("@NameContains", "%" + query.NameContains + "%"); - } - - return whereClauses; - } - - private void UpdateAncestors(Guid itemId, List ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement) - { - if (itemId.IsEmpty()) - { - throw new ArgumentNullException(nameof(itemId)); - } - - ArgumentNullException.ThrowIfNull(ancestorIds); - - CheckDisposed(); - - // First delete - deleteAncestorsStatement.TryBind("@ItemId", itemId); - deleteAncestorsStatement.ExecuteNonQuery(); - - if (ancestorIds.Count == 0) - { - return; - } - - var insertText = new StringBuilder("insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values "); - - for (var i = 0; i < ancestorIds.Count; i++) - { - insertText.AppendFormat( - CultureInfo.InvariantCulture, - "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),", - i.ToString(CultureInfo.InvariantCulture)); - } - - // Remove trailing comma - insertText.Length--; - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", itemId); - - for (var i = 0; i < ancestorIds.Count; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var ancestorId = ancestorIds[i]; - - statement.TryBind("@AncestorId" + index, ancestorId); - statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.ExecuteNonQuery(); - } - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); - } - - /// - public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) - { - return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); - } - - /// - public List GetStudioNames() - { - return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); - } - - /// - public List GetAllArtistNames() - { - return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); - } - - /// - public List GetMusicGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - new string[] - { - typeof(Audio).FullName, - typeof(MusicVideo).FullName, - typeof(MusicAlbum).FullName, - typeof(MusicArtist).FullName - }, - Array.Empty()); - } - - /// - public List GetGenreNames() - { - return GetItemValueNames( - new[] { 2 }, - Array.Empty(), - new string[] - { - typeof(Audio).FullName, - typeof(MusicVideo).FullName, - typeof(MusicAlbum).FullName, - typeof(MusicArtist).FullName - }); - } - - private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) - { - CheckDisposed(); - - var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); - if (itemValueTypes.Length == 1) - { - stringBuilder.Append('=') - .Append(itemValueTypes[0]); - } - else - { - stringBuilder.Append(" in (") - .AppendJoin(',', itemValueTypes) - .Append(')'); - } - - if (withItemTypes.Count > 0) - { - stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (") - .AppendJoinInSingleQuotes(',', withItemTypes) - .Append("))"); - } - - if (excludeItemTypes.Count > 0) - { - stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (") - .AppendJoinInSingleQuotes(',', excludeItemTypes) - .Append("))"); - } - - stringBuilder.Append(" Group By CleanValue"); - var commandText = stringBuilder.ToString(); - - var list = new List(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var statement = PrepareStatement(connection, commandText)) - { - foreach (var row in statement.ExecuteQuery()) - { - if (row.TryGetString(0, out var result)) - { - list.Add(result); - } - } - } - - return list; - } - - private QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) - { - ArgumentNullException.ThrowIfNull(query); - - if (!query.Limit.HasValue) - { - query.EnableTotalRecordCount = false; - } - - CheckDisposed(); - - var typeClause = itemValueTypes.Length == 1 ? - ("Type=" + itemValueTypes[0]) : - ("Type in (" + string.Join(',', itemValueTypes) + ")"); - - InternalItemsQuery typeSubQuery = null; - - string itemCountColumns = null; - - var stringBuilder = new StringBuilder(1024); - var typesToCount = query.IncludeItemTypes; - - if (typesToCount.Length > 0) - { - stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B"); - - typeSubQuery = new InternalItemsQuery(query.User) - { - ExcludeItemTypes = query.ExcludeItemTypes, - IncludeItemTypes = query.IncludeItemTypes, - MediaTypes = query.MediaTypes, - AncestorIds = query.AncestorIds, - ExcludeItemIds = query.ExcludeItemIds, - ItemIds = query.ItemIds, - TopParentIds = query.TopParentIds, - ParentId = query.ParentId, - IsPlayed = query.IsPlayed - }; - var whereClauses = GetWhereClauses(typeSubQuery, null); - - stringBuilder.Append(" where ") - .AppendJoin(" AND ", whereClauses) - .Append(" AND ") - .Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ") - .Append(typeClause) - .Append(")) as itemTypes"); - - itemCountColumns = stringBuilder.ToString(); - stringBuilder.Clear(); - } - - List columns = _retrieveItemColumns.ToList(); - // Unfortunately we need to add it to columns to ensure the order of the columns in the select - if (!string.IsNullOrEmpty(itemCountColumns)) - { - columns.Add(itemCountColumns); - } - - // do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo - var innerQuery = new InternalItemsQuery(query.User) - { - ExcludeItemTypes = query.ExcludeItemTypes, - IncludeItemTypes = query.IncludeItemTypes, - MediaTypes = query.MediaTypes, - AncestorIds = query.AncestorIds, - ItemIds = query.ItemIds, - TopParentIds = query.TopParentIds, - ParentId = query.ParentId, - IsAiring = query.IsAiring, - IsMovie = query.IsMovie, - IsSports = query.IsSports, - IsKids = query.IsKids, - IsNews = query.IsNews, - IsSeries = query.IsSeries - }; - - SetFinalColumnsToSelect(query, columns); - - var innerWhereClauses = GetWhereClauses(innerQuery, null); - - stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ") - .Append(typeClause) - .Append(" AND ItemId in (select guid from TypedBaseItems"); - if (innerWhereClauses.Count > 0) - { - stringBuilder.Append(" where ") - .AppendJoin(" AND ", innerWhereClauses); - } - - stringBuilder.Append("))"); - - var outerQuery = new InternalItemsQuery(query.User) - { - IsPlayed = query.IsPlayed, - IsFavorite = query.IsFavorite, - IsFavoriteOrLiked = query.IsFavoriteOrLiked, - IsLiked = query.IsLiked, - IsLocked = query.IsLocked, - NameLessThan = query.NameLessThan, - NameStartsWith = query.NameStartsWith, - NameStartsWithOrGreater = query.NameStartsWithOrGreater, - Tags = query.Tags, - OfficialRatings = query.OfficialRatings, - StudioIds = query.StudioIds, - GenreIds = query.GenreIds, - Genres = query.Genres, - Years = query.Years, - NameContains = query.NameContains, - SearchTerm = query.SearchTerm, - SimilarTo = query.SimilarTo, - ExcludeItemIds = query.ExcludeItemIds - }; - - var outerWhereClauses = GetWhereClauses(outerQuery, null); - if (outerWhereClauses.Count != 0) - { - stringBuilder.Append(" AND ") - .AppendJoin(" AND ", outerWhereClauses); - } - - var whereText = stringBuilder.ToString(); - stringBuilder.Clear(); - - stringBuilder.Append("select ") - .AppendJoin(',', columns) - .Append(FromText) - .Append(GetJoinUserDataText(query)) - .Append(whereText) - .Append(" group by PresentationUniqueKey"); - - if (query.OrderBy.Count != 0 - || query.SimilarTo is not null - || !string.IsNullOrEmpty(query.SearchTerm)) - { - stringBuilder.Append(GetOrderByText(query)); - } - else - { - stringBuilder.Append(" order by SortName"); - } - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - stringBuilder.Append(" LIMIT ") - .Append(query.Limit ?? int.MaxValue); - } - - if (offset > 0) - { - stringBuilder.Append(" OFFSET ") - .Append(offset); - } - } - - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - string commandText = string.Empty; - - if (!isReturningZeroItems) - { - commandText = stringBuilder.ToString(); - } - - string countText = string.Empty; - if (query.EnableTotalRecordCount) - { - stringBuilder.Clear(); - var columnsToSelect = new List { "count (distinct PresentationUniqueKey)" }; - SetFinalColumnsToSelect(query, columnsToSelect); - stringBuilder.Append("select ") - .AppendJoin(',', columnsToSelect) - .Append(FromText) - .Append(GetJoinUserDataText(query)) - .Append(whereText); - - countText = stringBuilder.ToString(); - } - - var list = new List<(BaseItem, ItemCounts)>(); - var result = new QueryResult<(BaseItem, ItemCounts)>(); - using (new QueryTimeLogger(Logger, commandText)) - using (var connection = GetConnection(true)) - using (var transaction = connection.BeginTransaction()) - { - if (!isReturningZeroItems) - { - using (var statement = PrepareStatement(connection, commandText)) - { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasServiceName = HasServiceName(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); - - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false); - if (item is not null) - { - var countStartColumn = columns.Count - 1; - - list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); - } - } - } - } - - if (query.EnableTotalRecordCount) - { - using (var statement = PrepareStatement(connection, countText)) - { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - if (typeSubQuery is not null) - { - GetWhereClauses(typeSubQuery, null); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); - - result.TotalRecordCount = statement.SelectScalarInt(); - } - } - - transaction.Commit(); - } - - if (result.TotalRecordCount == 0) - { - result.TotalRecordCount = list.Count; - } - - result.StartIndex = query.StartIndex ?? 0; - result.Items = list; - - return result; - } - - private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount) - { - var counts = new ItemCounts(); - - if (typesToCount.Length == 0) - { - return counts; - } - - if (!reader.TryGetString(countStartColumn, out var typeString)) - { - return counts; - } - - foreach (var typeName in typeString.AsSpan().Split('|')) - { - if (typeName.Equals(typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.SeriesCount++; - } - else if (typeName.Equals(typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.EpisodeCount++; - } - else if (typeName.Equals(typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.MovieCount++; - } - else if (typeName.Equals(typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.AlbumCount++; - } - else if (typeName.Equals(typeof(MusicArtist).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.ArtistCount++; - } - else if (typeName.Equals(typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.SongCount++; - } - else if (typeName.Equals(typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) - { - counts.TrailerCount++; - } - - counts.ItemCount++; - } - - return counts; - } - - private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItem item, List inheritedTags) - { - var list = new List<(int, string)>(); - - if (item is IHasArtist hasArtist) - { - list.AddRange(hasArtist.Artists.Select(i => (0, i))); - } - - if (item is IHasAlbumArtist hasAlbumArtist) - { - list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); - } - - list.AddRange(item.Genres.Select(i => (2, i))); - list.AddRange(item.Studios.Select(i => (3, i))); - list.AddRange(item.Tags.Select(i => (4, i))); - - // keywords was 5 - - list.AddRange(inheritedTags.Select(i => (6, i))); - - // Remove all invalid values. - list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2)); - - return list; - } - - private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db) - { - if (itemId.IsEmpty()) - { - throw new ArgumentNullException(nameof(itemId)); - } - - ArgumentNullException.ThrowIfNull(values); - - CheckDisposed(); - - // First delete - using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id"); - command.TryBind("@Id", itemId); - command.ExecuteNonQuery(); - - InsertItemValues(itemId, values, db); - } - - private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db) - { - const int Limit = 100; - var startIndex = 0; - - const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values "; - var insertText = new StringBuilder(StartInsertText); - while (startIndex < values.Count) - { - var endIndex = Math.Min(values.Count, startIndex + Limit); - - for (var i = startIndex; i < endIndex; i++) - { - insertText.AppendFormat( - CultureInfo.InvariantCulture, - "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0}),", - i); - } - - // Remove trailing comma - insertText.Length--; - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", id); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var currentValueInfo = values[i]; - - var itemValue = currentValueInfo.Value; - - statement.TryBind("@Type" + index, currentValueInfo.MagicNumber); - statement.TryBind("@Value" + index, itemValue); - statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue)); - } - - statement.ExecuteNonQuery(); - } - - startIndex += Limit; - insertText.Length = StartInsertText.Length; - } - } - - /// - public void UpdatePeople(Guid itemId, List people) - { - if (itemId.IsEmpty()) - { - throw new ArgumentNullException(nameof(itemId)); - } - - CheckDisposed(); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // Delete all existing people first - using var command = connection.CreateCommand(); - command.CommandText = "delete from People where ItemId=@ItemId"; - command.TryBind("@ItemId", itemId); - command.ExecuteNonQuery(); - - if (people is not null) - { - InsertPeople(itemId, people, connection); - } - - transaction.Commit(); - } - - private void InsertPeople(Guid id, List people, ManagedConnection db) - { - const int Limit = 100; - var startIndex = 0; - var listIndex = 0; - - const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values "; - var insertText = new StringBuilder(StartInsertText); - while (startIndex < people.Count) - { - var endIndex = Math.Min(people.Count, startIndex + Limit); - for (var i = startIndex; i < endIndex; i++) - { - insertText.AppendFormat( - CultureInfo.InvariantCulture, - "(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", - i.ToString(CultureInfo.InvariantCulture)); - } - - // Remove trailing comma - insertText.Length--; - - using (var statement = PrepareStatement(db, insertText.ToString())) - { - statement.TryBind("@ItemId", id); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var person = people[i]; - - statement.TryBind("@Name" + index, person.Name); - statement.TryBind("@Role" + index, person.Role); - statement.TryBind("@PersonType" + index, person.Type.ToString()); - statement.TryBind("@SortOrder" + index, person.SortOrder); - statement.TryBind("@ListOrder" + index, listIndex); - - listIndex++; - } - - statement.ExecuteNonQuery(); - } - - startIndex += Limit; - insertText.Length = StartInsertText.Length; - } - } - - private PersonInfo GetPerson(SqliteDataReader reader) - { - var item = new PersonInfo - { - ItemId = reader.GetGuid(0), - Name = reader.GetString(1) - }; - - if (reader.TryGetString(2, out var role)) - { - item.Role = role; - } - - if (reader.TryGetString(3, out var type) - && Enum.TryParse(type, true, out PersonKind personKind)) - { - item.Type = personKind; - } - - if (reader.TryGetInt32(4, out var sortOrder)) + private string GetJoinUserDataText(InternalItemsQuery query) + { + if (!EnableJoinUserData(query)) { - item.SortOrder = sortOrder; + return string.Empty; } - return item; + return " left join UserDatas on UserDataKey=UserDatas.Key And (UserId=@UserId)"; } /// - public List GetMediaStreams(MediaStreamQuery query) + public List GetStudioNames() { - CheckDisposed(); - - ArgumentNullException.ThrowIfNull(query); - - var cmdText = _mediaStreamSaveColumnsSelectQuery; - - if (query.Type.HasValue) - { - cmdText += " AND StreamType=@StreamType"; - } - - if (query.Index.HasValue) - { - cmdText += " AND StreamIndex=@StreamIndex"; - } - - cmdText += " order by StreamIndex ASC"; - - using (var connection = GetConnection(true)) - { - var list = new List(); - - using (var statement = PrepareStatement(connection, cmdText)) - { - statement.TryBind("@ItemId", query.ItemId); - - if (query.Type.HasValue) - { - statement.TryBind("@StreamType", query.Type.Value.ToString()); - } - - if (query.Index.HasValue) - { - statement.TryBind("@StreamIndex", query.Index.Value); - } - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(GetMediaStream(row)); - } - } - - return list; - } + return GetItemValueNames(new[] { 3 }, Array.Empty(), Array.Empty()); } /// - public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) + public List GetAllArtistNames() { - CheckDisposed(); - - if (id.IsEmpty()) - { - throw new ArgumentNullException(nameof(id)); - } - - ArgumentNullException.ThrowIfNull(streams); - - cancellationToken.ThrowIfCancellationRequested(); - - using var connection = GetConnection(); - using var transaction = connection.BeginTransaction(); - // Delete existing mediastreams - using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId"); - command.TryBind("@ItemId", id); - command.ExecuteNonQuery(); - - InsertMediaStreams(id, streams, connection); - - transaction.Commit(); + return GetItemValueNames(new[] { 0, 1 }, Array.Empty(), Array.Empty()); } - private void InsertMediaStreams(Guid id, IReadOnlyList streams, ManagedConnection db) + /// + public List GetMusicGenreNames() { - const int Limit = 10; - var startIndex = 0; - - var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery); - while (startIndex < streams.Count) - { - var endIndex = Math.Min(streams.Count, startIndex + Limit); - - for (var i = startIndex; i < endIndex; i++) + return GetItemValueNames( + new[] { 2 }, + new string[] { - if (i != startIndex) - { - insertText.Append(','); - } - - var index = i.ToString(CultureInfo.InvariantCulture); - insertText.Append("(@ItemId, "); - - foreach (var column in _mediaStreamSaveColumns.Skip(1)) - { - insertText.Append('@').Append(column).Append(index).Append(','); - } - - insertText.Length -= 1; // Remove the last comma - - insertText.Append(')'); - } + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }, + Array.Empty()); + } - using (var statement = PrepareStatement(db, insertText.ToString())) + /// + public List GetGenreNames() + { + return GetItemValueNames( + new[] { 2 }, + Array.Empty(), + new string[] { - statement.TryBind("@ItemId", id); - - for (var i = startIndex; i < endIndex; i++) - { - var index = i.ToString(CultureInfo.InvariantCulture); - - var stream = streams[i]; - - statement.TryBind("@StreamIndex" + index, stream.Index); - statement.TryBind("@StreamType" + index, stream.Type.ToString()); - statement.TryBind("@Codec" + index, stream.Codec); - statement.TryBind("@Language" + index, stream.Language); - statement.TryBind("@ChannelLayout" + index, stream.ChannelLayout); - statement.TryBind("@Profile" + index, stream.Profile); - statement.TryBind("@AspectRatio" + index, stream.AspectRatio); - statement.TryBind("@Path" + index, GetPathToSave(stream.Path)); - - statement.TryBind("@IsInterlaced" + index, stream.IsInterlaced); - statement.TryBind("@BitRate" + index, stream.BitRate); - statement.TryBind("@Channels" + index, stream.Channels); - statement.TryBind("@SampleRate" + index, stream.SampleRate); - - statement.TryBind("@IsDefault" + index, stream.IsDefault); - statement.TryBind("@IsForced" + index, stream.IsForced); - statement.TryBind("@IsExternal" + index, stream.IsExternal); - - // Yes these are backwards due to a mistake - statement.TryBind("@Width" + index, stream.Height); - statement.TryBind("@Height" + index, stream.Width); - - statement.TryBind("@AverageFrameRate" + index, stream.AverageFrameRate); - statement.TryBind("@RealFrameRate" + index, stream.RealFrameRate); - statement.TryBind("@Level" + index, stream.Level); - - statement.TryBind("@PixelFormat" + index, stream.PixelFormat); - statement.TryBind("@BitDepth" + index, stream.BitDepth); - statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic); - statement.TryBind("@IsExternal" + index, stream.IsExternal); - statement.TryBind("@RefFrames" + index, stream.RefFrames); - - statement.TryBind("@CodecTag" + index, stream.CodecTag); - statement.TryBind("@Comment" + index, stream.Comment); - statement.TryBind("@NalLengthSize" + index, stream.NalLengthSize); - statement.TryBind("@IsAvc" + index, stream.IsAVC); - statement.TryBind("@Title" + index, stream.Title); - - statement.TryBind("@TimeBase" + index, stream.TimeBase); - statement.TryBind("@CodecTimeBase" + index, stream.CodecTimeBase); - - statement.TryBind("@ColorPrimaries" + index, stream.ColorPrimaries); - statement.TryBind("@ColorSpace" + index, stream.ColorSpace); - statement.TryBind("@ColorTransfer" + index, stream.ColorTransfer); - - statement.TryBind("@DvVersionMajor" + index, stream.DvVersionMajor); - statement.TryBind("@DvVersionMinor" + index, stream.DvVersionMinor); - statement.TryBind("@DvProfile" + index, stream.DvProfile); - statement.TryBind("@DvLevel" + index, stream.DvLevel); - statement.TryBind("@RpuPresentFlag" + index, stream.RpuPresentFlag); - statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag); - statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag); - statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId); - - statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired); - - statement.TryBind("@Rotation" + index, stream.Rotation); - } - - statement.ExecuteNonQuery(); - } - - startIndex += Limit; - insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length; - } + typeof(Audio).FullName, + typeof(MusicVideo).FullName, + typeof(MusicAlbum).FullName, + typeof(MusicArtist).FullName + }); } - /// - /// Gets the media stream. - /// - /// The reader. - /// MediaStream. - private MediaStream GetMediaStream(SqliteDataReader reader) + private List GetItemValueNames(int[] itemValueTypes, IReadOnlyList withItemTypes, IReadOnlyList excludeItemTypes) { - var item = new MediaStream - { - Index = reader.GetInt32(1), - Type = Enum.Parse(reader.GetString(2), true) - }; - - if (reader.TryGetString(3, out var codec)) - { - item.Codec = codec; - } - - if (reader.TryGetString(4, out var language)) - { - item.Language = language; - } - - if (reader.TryGetString(5, out var channelLayout)) - { - item.ChannelLayout = channelLayout; - } - - if (reader.TryGetString(6, out var profile)) - { - item.Profile = profile; - } - - if (reader.TryGetString(7, out var aspectRatio)) - { - item.AspectRatio = aspectRatio; - } - - if (reader.TryGetString(8, out var path)) - { - item.Path = RestorePath(path); - } - - item.IsInterlaced = reader.GetBoolean(9); - - if (reader.TryGetInt32(10, out var bitrate)) - { - item.BitRate = bitrate; - } - - if (reader.TryGetInt32(11, out var channels)) - { - item.Channels = channels; - } - - if (reader.TryGetInt32(12, out var sampleRate)) - { - item.SampleRate = sampleRate; - } - - item.IsDefault = reader.GetBoolean(13); - item.IsForced = reader.GetBoolean(14); - item.IsExternal = reader.GetBoolean(15); - - if (reader.TryGetInt32(16, out var width)) - { - item.Width = width; - } - - if (reader.TryGetInt32(17, out var height)) - { - item.Height = height; - } - - if (reader.TryGetSingle(18, out var averageFrameRate)) - { - item.AverageFrameRate = averageFrameRate; - } - - if (reader.TryGetSingle(19, out var realFrameRate)) - { - item.RealFrameRate = realFrameRate; - } - - if (reader.TryGetSingle(20, out var level)) - { - item.Level = level; - } - - if (reader.TryGetString(21, out var pixelFormat)) - { - item.PixelFormat = pixelFormat; - } - - if (reader.TryGetInt32(22, out var bitDepth)) - { - item.BitDepth = bitDepth; - } - - if (reader.TryGetBoolean(23, out var isAnamorphic)) - { - item.IsAnamorphic = isAnamorphic; - } - - if (reader.TryGetInt32(24, out var refFrames)) - { - item.RefFrames = refFrames; - } - - if (reader.TryGetString(25, out var codecTag)) - { - item.CodecTag = codecTag; - } - - if (reader.TryGetString(26, out var comment)) - { - item.Comment = comment; - } - - if (reader.TryGetString(27, out var nalLengthSize)) - { - item.NalLengthSize = nalLengthSize; - } - - if (reader.TryGetBoolean(28, out var isAVC)) - { - item.IsAVC = isAVC; - } - - if (reader.TryGetString(29, out var title)) - { - item.Title = title; - } - - if (reader.TryGetString(30, out var timeBase)) - { - item.TimeBase = timeBase; - } - - if (reader.TryGetString(31, out var codecTimeBase)) - { - item.CodecTimeBase = codecTimeBase; - } - - if (reader.TryGetString(32, out var colorPrimaries)) - { - item.ColorPrimaries = colorPrimaries; - } - - if (reader.TryGetString(33, out var colorSpace)) - { - item.ColorSpace = colorSpace; - } - - if (reader.TryGetString(34, out var colorTransfer)) - { - item.ColorTransfer = colorTransfer; - } - - if (reader.TryGetInt32(35, out var dvVersionMajor)) - { - item.DvVersionMajor = dvVersionMajor; - } - - if (reader.TryGetInt32(36, out var dvVersionMinor)) - { - item.DvVersionMinor = dvVersionMinor; - } - - if (reader.TryGetInt32(37, out var dvProfile)) - { - item.DvProfile = dvProfile; - } - - if (reader.TryGetInt32(38, out var dvLevel)) - { - item.DvLevel = dvLevel; - } + CheckDisposed(); - if (reader.TryGetInt32(39, out var rpuPresentFlag)) + var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128); + if (itemValueTypes.Length == 1) { - item.RpuPresentFlag = rpuPresentFlag; + stringBuilder.Append('=') + .Append(itemValueTypes[0]); } - - if (reader.TryGetInt32(40, out var elPresentFlag)) + else { - item.ElPresentFlag = elPresentFlag; + stringBuilder.Append(" in (") + .AppendJoin(',', itemValueTypes) + .Append(')'); } - if (reader.TryGetInt32(41, out var blPresentFlag)) + if (withItemTypes.Count > 0) { - item.BlPresentFlag = blPresentFlag; + stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', withItemTypes) + .Append("))"); } - if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId)) + if (excludeItemTypes.Count > 0) { - item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId; + stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (") + .AppendJoinInSingleQuotes(',', excludeItemTypes) + .Append("))"); } - item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result; - - if (reader.TryGetInt32(44, out var rotation)) - { - item.Rotation = rotation; - } + stringBuilder.Append(" Group By CleanValue"); + var commandText = stringBuilder.ToString(); - if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) + var list = new List(); + using (new QueryTimeLogger(Logger, commandText)) + using (var connection = GetConnection(true)) + using (var statement = PrepareStatement(connection, commandText)) { - item.LocalizedDefault = _localization.GetLocalizedString("Default"); - item.LocalizedExternal = _localization.GetLocalizedString("External"); - - if (item.Type is MediaStreamType.Subtitle) + foreach (var row in statement.ExecuteQuery()) { - item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); - item.LocalizedForced = _localization.GetLocalizedString("Forced"); - item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); + if (row.TryGetString(0, out var result)) + { + list.Add(result); + } } } - return item; + return list; } + + /// public List GetMediaAttachments(MediaAttachmentQuery query) { @@ -2205,21 +415,6 @@ AND Type = @InternalPersonType)"); return item; } - private static string BuildMediaAttachmentInsertPrefix() - { - var queryPrefixText = new StringBuilder(); - queryPrefixText.Append("insert into mediaattachments ("); - foreach (var column in _mediaAttachmentSaveColumns) - { - queryPrefixText.Append(column) - .Append(','); - } - - queryPrefixText.Length -= 1; - queryPrefixText.Append(") values "); - return queryPrefixText.ToString(); - } - #nullable enable private readonly struct QueryTimeLogger : IDisposable diff --git a/Jellyfin.Data/Entities/PeopleKind.cs b/Jellyfin.Data/Entities/PeopleKind.cs new file mode 100644 index 000000000..967f7c11f --- /dev/null +++ b/Jellyfin.Data/Entities/PeopleKind.cs @@ -0,0 +1,133 @@ +namespace Jellyfin.Data.Entities; + +/// +/// The person kind. +/// +public enum PeopleKind +{ + /// + /// An unknown person kind. + /// + Unknown, + + /// + /// A person whose profession is acting on the stage, in films, or on television. + /// + Actor, + + /// + /// A person who supervises the actors and other staff in a film, play, or similar production. + /// + Director, + + /// + /// A person who writes music, especially as a professional occupation. + /// + Composer, + + /// + /// A writer of a book, article, or document. Can also be used as a generic term for music writer if there is a lack of specificity. + /// + Writer, + + /// + /// A well-known actor or other performer who appears in a work in which they do not have a regular role. + /// + GuestStar, + + /// + /// A person responsible for the financial and managerial aspects of the making of a film or broadcast or for staging a play, opera, etc. + /// + Producer, + + /// + /// A person who directs the performance of an orchestra or choir. + /// + Conductor, + + /// + /// A person who writes the words to a song or musical. + /// + Lyricist, + + /// + /// A person who adapts a musical composition for performance. + /// + Arranger, + + /// + /// An audio engineer who performed a general engineering role. + /// + Engineer, + + /// + /// An engineer responsible for using a mixing console to mix a recorded track into a single piece of music suitable for release. + /// + Mixer, + + /// + /// A person who remixed a recording by taking one or more other tracks, substantially altering them and mixing them together with other material. + /// + Remixer, + + /// + /// A person who created the material. + /// + Creator, + + /// + /// A person who was the artist. + /// + Artist, + + /// + /// A person who was the album artist. + /// + AlbumArtist, + + /// + /// A person who was the author. + /// + Author, + + /// + /// A person who was the illustrator. + /// + Illustrator, + + /// + /// A person responsible for drawing the art. + /// + Penciller, + + /// + /// A person responsible for inking the pencil art. + /// + Inker, + + /// + /// A person responsible for applying color to drawings. + /// + Colorist, + + /// + /// A person responsible for drawing text and speech bubbles. + /// + Letterer, + + /// + /// A person responsible for drawing the cover art. + /// + CoverArtist, + + /// + /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter. + /// An editor may also prepare a resource for production, publication, or distribution. + /// + Editor, + + /// + /// A person who renders a text from one language into another. + /// + Translator +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs index f2d6b6261..022f26cd7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemManager.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemManager.cs @@ -19,10 +19,12 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; using BaseItemEntity = Jellyfin.Data.Entities.BaseItem; @@ -145,8 +147,72 @@ public class BaseItemManager : IItemRepository _appHost = appHost; } - private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) + private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, int[] itemValueTypes, string returnType) { + ArgumentNullException.ThrowIfNull(filter); + + if (!filter.Limit.HasValue) + { + filter.EnableTotalRecordCount = false; + } + + using var context = _dbProvider.CreateDbContext(); + + var innerQuery = new InternalItemsQuery(filter.User) + { + ExcludeItemTypes = filter.ExcludeItemTypes, + IncludeItemTypes = filter.IncludeItemTypes, + MediaTypes = filter.MediaTypes, + AncestorIds = filter.AncestorIds, + ItemIds = filter.ItemIds, + TopParentIds = filter.TopParentIds, + ParentId = filter.ParentId, + IsAiring = filter.IsAiring, + IsMovie = filter.IsMovie, + IsSports = filter.IsSports, + IsKids = filter.IsKids, + IsNews = filter.IsNews, + IsSeries = filter.IsSeries + }; + var query = TranslateQuery(context.BaseItems, context, innerQuery); + + query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Contains(f.Type))); + + var outerQuery = new InternalItemsQuery(filter.User) + { + IsPlayed = filter.IsPlayed, + IsFavorite = filter.IsFavorite, + IsFavoriteOrLiked = filter.IsFavoriteOrLiked, + IsLiked = filter.IsLiked, + IsLocked = filter.IsLocked, + NameLessThan = filter.NameLessThan, + NameStartsWith = filter.NameStartsWith, + NameStartsWithOrGreater = filter.NameStartsWithOrGreater, + Tags = filter.Tags, + OfficialRatings = filter.OfficialRatings, + StudioIds = filter.StudioIds, + GenreIds = filter.GenreIds, + Genres = filter.Genres, + Years = filter.Years, + NameContains = filter.NameContains, + SearchTerm = filter.SearchTerm, + SimilarTo = filter.SimilarTo, + ExcludeItemIds = filter.ExcludeItemIds + }; + query = TranslateQuery(query, context, outerQuery) + .OrderBy(e => e.PresentationUniqueKey); + + if (filter.OrderBy.Count != 0 + || filter.SimilarTo is not null + || !string.IsNullOrEmpty(filter.SearchTerm)) + { + query = ApplyOrder(query, filter); + } + else + { + query = query.OrderBy(e => e.SortName); + } + if (filter.Limit.HasValue || filter.StartIndex.HasValue) { var offset = filter.StartIndex ?? 0; @@ -158,131 +224,96 @@ public class BaseItemManager : IItemRepository if (filter.Limit.HasValue) { - query = query.Take(filter.Limit.Value); + query.Take(filter.Limit.Value); } } - return query; - } - - private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) - { -#pragma warning disable CS8603 // Possible null reference return. - return sortBy switch - { - ItemSortBy.AirTime => e => e.SortName, // TODO - ItemSortBy.Runtime => e => e.RunTimeTicks, - ItemSortBy.Random => e => EF.Functions.Random(), - ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate, - ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount, - ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite, - ItemSortBy.IsFolder => e => e.IsFolder, - ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, - ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, - ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, - ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue), - ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue), - ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), - ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => e => e.SeriesName, - // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => e => e.Album, - ItemSortBy.DateCreated => e => e.DateCreated, - ItemSortBy.PremiereDate => e => e.PremiereDate, - ItemSortBy.StartDate => e => e.StartDate, - ItemSortBy.Name => e => e.Name, - ItemSortBy.CommunityRating => e => e.CommunityRating, - ItemSortBy.ProductionYear => e => e.ProductionYear, - ItemSortBy.CriticRating => e => e.CriticRating, - ItemSortBy.VideoBitRate => e => e.TotalBitrate, - ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, - ItemSortBy.IndexNumber => e => e.IndexNumber, - _ => e => e.SortName - }; -#pragma warning restore CS8603 // Possible null reference return. - - } - - private IQueryable MapOrderByField(IQueryable dbQuery, ItemSortBy sortBy, InternalItemsQuery query) - { - return sortBy switch + var result = new QueryResult<(BaseItem, ItemCounts)>(); + string countText = string.Empty; + if (filter.EnableTotalRecordCount) { - ItemSortBy.AirTime => dbQuery.OrderBy(e => e.SortName), // TODO - ItemSortBy.Runtime => dbQuery.OrderBy(e => e.RunTimeTicks), - ItemSortBy.Random => dbQuery.OrderBy(e => EF.Functions.Random()), - ItemSortBy.DatePlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate), - ItemSortBy.PlayCount => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount), - ItemSortBy.IsFavoriteOrLiked => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite), - ItemSortBy.IsFolder => dbQuery.OrderBy(e => e.IsFolder), - ItemSortBy.IsPlayed => dbQuery.OrderBy(e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), - ItemSortBy.IsUnplayed => dbQuery.OrderBy(e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played), - ItemSortBy.DateLastContentAdded => dbQuery.OrderBy(e => e.DateLastMediaAdded), - ItemSortBy.Artist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue)), - ItemSortBy.AlbumArtist => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue)), - ItemSortBy.Studio => dbQuery.OrderBy(e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue)), - ItemSortBy.OfficialRating => dbQuery.OrderBy(e => e.InheritedParentalRatingValue), - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => dbQuery.OrderBy(e => e.SeriesName), - // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => dbQuery.OrderBy(e => e.Album), - ItemSortBy.DateCreated => dbQuery.OrderBy(e => e.DateCreated), - ItemSortBy.PremiereDate => dbQuery.OrderBy(e => e.PremiereDate), - ItemSortBy.StartDate => dbQuery.OrderBy(e => e.StartDate), - ItemSortBy.Name => dbQuery.OrderBy(e => e.Name), - ItemSortBy.CommunityRating => dbQuery.OrderBy(e => e.CommunityRating), - ItemSortBy.ProductionYear => dbQuery.OrderBy(e => e.ProductionYear), - ItemSortBy.CriticRating => dbQuery.OrderBy(e => e.CriticRating), - ItemSortBy.VideoBitRate => dbQuery.OrderBy(e => e.TotalBitrate), - ItemSortBy.ParentIndexNumber => dbQuery.OrderBy(e => e.ParentIndexNumber), - ItemSortBy.IndexNumber => dbQuery.OrderBy(e => e.IndexNumber), - _ => dbQuery.OrderBy(e => e.SortName) - }; - } - - private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) - { - var orderBy = filter.OrderBy; - bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + result.TotalRecordCount = query.DistinctBy(e => e.PresentationUniqueKey).Count(); + } - if (hasSearch) + var resultQuery = query.Select(e => new { - List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); - if (hasSearch) + item = e, + itemCount = new ItemCounts() { - prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); + SeriesCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Series), + EpisodeCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Episode), + MovieCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Movie), + AlbumCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), + ArtistCount = e.ItemValues!.Count(e => e.Type == 0 || e.Type == 1), + SongCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.MusicAlbum), + TrailerCount = e.ItemValues!.Count(e => e.Type == (int)BaseItemKind.Trailer), } + }); - orderBy = filter.OrderBy = [.. prepend, .. orderBy]; - } - else if (orderBy.Count == 0) + result.StartIndex = filter.StartIndex ?? 0; + result.Items = resultQuery.ToImmutableArray().Select(e => { - return query; - } + return (DeserialiseBaseItem(e.item), e.itemCount); + }).ToImmutableArray(); - foreach (var item in orderBy) - { - var expression = MapOrderByField(item.OrderBy, filter); - if (item.SortOrder == SortOrder.Ascending) - { - query = query.OrderBy(expression); - } - else - { - query = query.OrderByDescending(expression); - } - } + return result; + } - return query; + /// + public void DeleteItem(Guid id) + { + ArgumentNullException.ThrowIfNull(id.IsEmpty() ? null : id); + + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + context.Peoples.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.Chapters.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.AncestorIds.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.ItemValues.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.BaseItems.Where(e => e.Id.Equals(id)).ExecuteDelete(); + context.SaveChanges(); + transaction.Commit(); + } + + /// + public void UpdateInheritedValues() + { + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.ItemValues.Where(e => e.Type == 6).ExecuteDelete(); + context.ItemValues.AddRange(context.ItemValues.Where(e => e.Type == 4).Select(e => new Data.Entities.ItemValue() + { + CleanValue = e.CleanValue, + ItemId = e.ItemId, + Type = 6, + Value = e.Value, + Item = null! + })); + + context.ItemValues.AddRange( + context.AncestorIds.Where(e => e.AncestorIdText != null).Join(context.ItemValues.Where(e => e.Value != null && e.Type == 4), e => e.Id, e => e.ItemId, (e, f) => new Data.Entities.ItemValue() + { + CleanValue = f.CleanValue, + ItemId = e.ItemId, + Item = null!, + Type = 6, + Value = f.Value + })); + context.SaveChanges(); + + transaction.Commit(); } + /// public IReadOnlyList GetItemIdsList(InternalItemsQuery filter) { ArgumentNullException.ThrowIfNull(filter); PrepareFilterQuery(filter); using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) + var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter) .DistinctBy(e => e.Id); var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); @@ -306,56 +337,57 @@ public class BaseItemManager : IItemRepository return Pageinate(dbQuery, filter).Select(e => e.Id).ToImmutableArray(); } - private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query) { - if (!query.GroupByPresentationUniqueKey) - { - return false; - } + return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName); + } - if (query.GroupBySeriesPresentationUniqueKey) - { - return false; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName); + } - if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) - { - return false; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName); + } - if (query.User is null) - { - return false; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName); + } - if (query.IncludeItemTypes.Length == 0) - { - return true; - } + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName); + } - return query.IncludeItemTypes.Contains(BaseItemKind.Episode) - || query.IncludeItemTypes.Contains(BaseItemKind.Video) - || query.IncludeItemTypes.Contains(BaseItemKind.Movie) - || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) - || query.IncludeItemTypes.Contains(BaseItemKind.Series) - || query.IncludeItemTypes.Contains(BaseItemKind.Season); + /// + public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query) + { + return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName); } /// - public QueryResult GetItems(InternalItemsQuery query) + public QueryResult GetItems(InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(query); if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { var returnList = GetItemList(query); - return new QueryResult( + return new QueryResult( query.StartIndex, returnList.Count, returnList); } PrepareFilterQuery(query); - var result = new QueryResult(); + var result = new QueryResult(); using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.BaseItems, context, query) @@ -2094,4 +2126,134 @@ public class BaseItemManager : IItemRepository return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); } + + private IQueryable Pageinate(IQueryable query, InternalItemsQuery filter) + { + if (filter.Limit.HasValue || filter.StartIndex.HasValue) + { + var offset = filter.StartIndex ?? 0; + + if (offset > 0) + { + query = query.Skip(offset); + } + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + } + + return query; + } + + private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + { +#pragma warning disable CS8603 // Possible null reference return. + return sortBy switch + { + ItemSortBy.AirTime => e => e.SortName, // TODO + ItemSortBy.Runtime => e => e.RunTimeTicks, + ItemSortBy.Random => e => EF.Functions.Random(), + ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.LastPlayedDate, + ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.PlayCount, + ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.IsFavorite, + ItemSortBy.IsFolder => e => e.IsFolder, + ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id) && f.Key == e.UserDataKey)!.Played, + ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, + ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue), + ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == 1).Select(f => f.CleanValue), + ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == 3).Select(f => f.CleanValue), + ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => e => e.SeriesName, + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => e => e.Album, + ItemSortBy.DateCreated => e => e.DateCreated, + ItemSortBy.PremiereDate => e => e.PremiereDate, + ItemSortBy.StartDate => e => e.StartDate, + ItemSortBy.Name => e => e.Name, + ItemSortBy.CommunityRating => e => e.CommunityRating, + ItemSortBy.ProductionYear => e => e.ProductionYear, + ItemSortBy.CriticRating => e => e.CriticRating, + ItemSortBy.VideoBitRate => e => e.TotalBitrate, + ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, + ItemSortBy.IndexNumber => e => e.IndexNumber, + _ => e => e.SortName + }; +#pragma warning restore CS8603 // Possible null reference return. + + } + + private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + { + if (!query.GroupByPresentationUniqueKey) + { + return false; + } + + if (query.GroupBySeriesPresentationUniqueKey) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + return false; + } + + if (query.User is null) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + return query.IncludeItemTypes.Contains(BaseItemKind.Episode) + || query.IncludeItemTypes.Contains(BaseItemKind.Video) + || query.IncludeItemTypes.Contains(BaseItemKind.Movie) + || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo) + || query.IncludeItemTypes.Contains(BaseItemKind.Series) + || query.IncludeItemTypes.Contains(BaseItemKind.Season); + } + + private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) + { + var orderBy = filter.OrderBy; + bool hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); + + if (hasSearch) + { + List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4); + if (hasSearch) + { + prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); + } + + orderBy = filter.OrderBy = [.. prepend, .. orderBy]; + } + else if (orderBy.Count == 0) + { + return query; + } + + foreach (var item in orderBy) + { + var expression = MapOrderByField(item.OrderBy, filter); + if (item.SortOrder == SortOrder.Ascending) + { + query = query.OrderBy(expression); + } + else + { + query = query.OrderByDescending(expression); + } + } + + return query; + } } diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs b/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs new file mode 100644 index 000000000..e609cdc1e --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/MediaStreamManager.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Initializes a new instance of the class. +/// +/// +/// +/// +public class MediaStreamManager(IDbContextFactory dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization) +{ + /// + public void SaveMediaStreams(Guid id, IReadOnlyList streams, CancellationToken cancellationToken) + { + using var context = dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.MediaStreamInfos.Where(e => e.ItemId.Equals(id)).ExecuteDelete(); + context.MediaStreamInfos.AddRange(streams.Select(f => Map(f, id))); + context.SaveChanges(); + + transaction.Commit(); + } + + /// + public IReadOnlyList GetMediaStreams(MediaStreamQuery filter) + { + using var context = dbProvider.CreateDbContext(); + return TranslateQuery(context.MediaStreamInfos, filter).ToList().Select(Map).ToImmutableArray(); + } + + private string? GetPathToSave(string? path) + { + if (path is null) + { + return null; + } + + return serverApplicationHost.ReverseVirtualPath(path); + } + + private string? RestorePath(string? path) + { + if (path is null) + { + return null; + } + + return serverApplicationHost.ExpandVirtualPath(path); + } + + private IQueryable TranslateQuery(IQueryable query, MediaStreamQuery filter) + { + query = query.Where(e => e.ItemId.Equals(filter.ItemId)); + if (filter.Index.HasValue) + { + query = query.Where(e => e.StreamIndex == filter.Index); + } + + if (filter.Type.HasValue) + { + query = query.Where(e => e.StreamType == filter.Type.ToString()); + } + + return query; + } + + private MediaStream Map(MediaStreamInfo entity) + { + var dto = new MediaStream(); + dto.Index = entity.StreamIndex; + if (entity.StreamType != null) + { + dto.Type = Enum.Parse(entity.StreamType); + } + + dto.IsAVC = entity.IsAvc; + dto.Codec = entity.Codec; + dto.Language = entity.Language; + dto.ChannelLayout = entity.ChannelLayout; + dto.Profile = entity.Profile; + dto.AspectRatio = entity.AspectRatio; + dto.Path = RestorePath(entity.Path); + dto.IsInterlaced = entity.IsInterlaced; + dto.BitRate = entity.BitRate; + dto.Channels = entity.Channels; + dto.SampleRate = entity.SampleRate; + dto.IsDefault = entity.IsDefault; + dto.IsForced = entity.IsForced; + dto.IsExternal = entity.IsExternal; + dto.Height = entity.Height; + dto.Width = entity.Width; + dto.AverageFrameRate = entity.AverageFrameRate; + dto.RealFrameRate = entity.RealFrameRate; + dto.Level = entity.Level; + dto.PixelFormat = entity.PixelFormat; + dto.BitDepth = entity.BitDepth; + dto.IsAnamorphic = entity.IsAnamorphic; + dto.RefFrames = entity.RefFrames; + dto.CodecTag = entity.CodecTag; + dto.Comment = entity.Comment; + dto.NalLengthSize = entity.NalLengthSize; + dto.Title = entity.Title; + dto.TimeBase = entity.TimeBase; + dto.CodecTimeBase = entity.CodecTimeBase; + dto.ColorPrimaries = entity.ColorPrimaries; + dto.ColorSpace = entity.ColorSpace; + dto.ColorTransfer = entity.ColorTransfer; + dto.DvVersionMajor = entity.DvVersionMajor; + dto.DvVersionMinor = entity.DvVersionMinor; + dto.DvProfile = entity.DvProfile; + dto.DvLevel = entity.DvLevel; + dto.RpuPresentFlag = entity.RpuPresentFlag; + dto.ElPresentFlag = entity.ElPresentFlag; + dto.BlPresentFlag = entity.BlPresentFlag; + dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId; + dto.IsHearingImpaired = entity.IsHearingImpaired; + dto.Rotation = entity.Rotation; + + if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) + { + dto.LocalizedDefault = localization.GetLocalizedString("Default"); + dto.LocalizedExternal = localization.GetLocalizedString("External"); + + if (dto.Type is MediaStreamType.Subtitle) + { + dto.LocalizedUndefined = localization.GetLocalizedString("Undefined"); + dto.LocalizedForced = localization.GetLocalizedString("Forced"); + dto.LocalizedHearingImpaired = localization.GetLocalizedString("HearingImpaired"); + } + } + + return dto; + } + + private MediaStreamInfo Map(MediaStream dto, Guid itemId) + { + var entity = new MediaStreamInfo + { + Item = null!, + ItemId = itemId, + StreamIndex = dto.Index, + StreamType = dto.Type.ToString(), + IsAvc = dto.IsAVC.GetValueOrDefault(), + + Codec = dto.Codec, + Language = dto.Language, + ChannelLayout = dto.ChannelLayout, + Profile = dto.Profile, + AspectRatio = dto.AspectRatio, + Path = GetPathToSave(dto.Path), + IsInterlaced = dto.IsInterlaced, + BitRate = dto.BitRate.GetValueOrDefault(0), + Channels = dto.Channels.GetValueOrDefault(0), + SampleRate = dto.SampleRate.GetValueOrDefault(0), + IsDefault = dto.IsDefault, + IsForced = dto.IsForced, + IsExternal = dto.IsExternal, + Height = dto.Height.GetValueOrDefault(0), + Width = dto.Width.GetValueOrDefault(0), + AverageFrameRate = dto.AverageFrameRate.GetValueOrDefault(0), + RealFrameRate = dto.RealFrameRate.GetValueOrDefault(0), + Level = (float)dto.Level.GetValueOrDefault(), + PixelFormat = dto.PixelFormat, + BitDepth = dto.BitDepth.GetValueOrDefault(0), + IsAnamorphic = dto.IsAnamorphic.GetValueOrDefault(0), + RefFrames = dto.RefFrames.GetValueOrDefault(0), + CodecTag = dto.CodecTag, + Comment = dto.Comment, + NalLengthSize = dto.NalLengthSize, + Title = dto.Title, + TimeBase = dto.TimeBase, + CodecTimeBase = dto.CodecTimeBase, + ColorPrimaries = dto.ColorPrimaries, + ColorSpace = dto.ColorSpace, + ColorTransfer = dto.ColorTransfer, + DvVersionMajor = dto.DvVersionMajor.GetValueOrDefault(0), + DvVersionMinor = dto.DvVersionMinor.GetValueOrDefault(0), + DvProfile = dto.DvProfile.GetValueOrDefault(0), + DvLevel = dto.DvLevel.GetValueOrDefault(0), + RpuPresentFlag = dto.RpuPresentFlag.GetValueOrDefault(0), + ElPresentFlag = dto.ElPresentFlag.GetValueOrDefault(0), + BlPresentFlag = dto.BlPresentFlag.GetValueOrDefault(0), + DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId.GetValueOrDefault(0), + IsHearingImpaired = dto.IsHearingImpaired, + Rotation = dto.Rotation.GetValueOrDefault(0) + }; + return entity; + } +} diff --git a/Jellyfin.Server.Implementations/Item/PeopleManager.cs b/Jellyfin.Server.Implementations/Item/PeopleManager.cs new file mode 100644 index 000000000..0f1760cbd --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/PeopleManager.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +public class PeopleManager +{ + private readonly IDbContextFactory _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The EFCore Context factory. + public PeopleManager(IDbContextFactory dbProvider) + { + _dbProvider = dbProvider; + } + + public IReadOnlyList GetPeople(InternalPeopleQuery filter) + { + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); + + dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.Limit > 0) + { + dbQuery = dbQuery.Take(filter.Limit); + } + + return dbQuery.ToList().Select(Map).ToImmutableArray(); + } + + public IReadOnlyList GetPeopleNames(InternalPeopleQuery filter) + { + using var context = _dbProvider.CreateDbContext(); + var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); + + dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.Limit > 0) + { + dbQuery = dbQuery.Take(filter.Limit); + } + + return dbQuery.Select(e => e.Name).ToImmutableArray(); + } + + /// + public void UpdatePeople(Guid itemId, IReadOnlyList people) + { + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + + context.Peoples.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + context.Peoples.AddRange(people.Select(Map)); + context.SaveChanges(); + transaction.Commit(); + } + + private PersonInfo Map(People people) + { + var personInfo = new PersonInfo() + { + ItemId = people.ItemId, + Name = people.Name, + Role = people.Role, + SortOrder = people.SortOrder, + }; + if (Enum.TryParse(people.PersonType, out var kind)) + { + personInfo.Type = kind; + } + + return personInfo; + } + + private People Map(PersonInfo people) + { + var personInfo = new People() + { + ItemId = people.ItemId, + Name = people.Name, + Role = people.Role, + SortOrder = people.SortOrder, + PersonType = people.Type.ToString() + }; + + return personInfo; + } + + private IQueryable TranslateQuery(IQueryable query, JellyfinDbContext context, InternalPeopleQuery filter) + { + if (filter.User is not null && filter.IsFavorite.HasValue) + { + query = query.Where(e => e.PersonType == typeof(Person).FullName) + .Where(e => context.BaseItems.Where(d => context.UserData.Where(e => e.IsFavorite == filter.IsFavorite && e.UserId.Equals(filter.User.Id)).Any(f => f.Key == d.UserDataKey)) + .Select(f => f.Name).Contains(e.Name)); + } + + if (!filter.ItemId.IsEmpty()) + { + query = query.Where(e => e.ItemId.Equals(filter.ItemId)); + } + + if (!filter.AppearsInItemId.IsEmpty()) + { + query = query.Where(e => context.Peoples.Where(f => f.ItemId.Equals(filter.AppearsInItemId)).Select(e => e.Name).Contains(e.Name)); + } + + var queryPersonTypes = filter.PersonTypes.Where(IsValidPersonType).ToList(); + if (queryPersonTypes.Count > 0) + { + query = query.Where(e => queryPersonTypes.Contains(e.PersonType)); + } + + var queryExcludePersonTypes = filter.ExcludePersonTypes.Where(IsValidPersonType).ToList(); + + if (queryExcludePersonTypes.Count > 0) + { + query = query.Where(e => !queryPersonTypes.Contains(e.PersonType)); + } + + if (filter.MaxListOrder.HasValue) + { + query = query.Where(e => e.ListOrder <= filter.MaxListOrder.Value); + } + + if (!string.IsNullOrWhiteSpace(filter.NameContains)) + { + query = query.Where(e => e.Name.Contains(filter.NameContains)); + } + + return query; + } + + private bool IsAlphaNumeric(string str) + { + if (string.IsNullOrWhiteSpace(str)) + { + return false; + } + + for (int i = 0; i < str.Length; i++) + { + if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) + { + return false; + } + } + + return true; + } + + private bool IsValidPersonType(string value) + { + return IsAlphaNumeric(value); + } +} -- cgit v1.2.3 From 15bf43e3adc69fc0ec5413e81a20b1f0d5dccd5c Mon Sep 17 00:00:00 2001 From: JPVenson <6794763+JPVenson@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:53:26 +0000 Subject: Removed BaseSqliteRepository --- .../Data/BaseSqliteRepository.cs | 269 --------- .../Data/ManagedConnection.cs | 62 --- .../Data/SqliteItemRepository.cs | 461 ---------------- .../Data/SynchronousMode.cs | 30 - Emby.Server.Implementations/Data/TempStoreMode.cs | 23 - Jellyfin.Data/Entities/AttachmentStreamInfo.cs | 2 + .../Item/BaseItemManager.cs | 604 ++++++++++++--------- .../Item/MediaAttachmentManager.cs | 73 +++ .../Item/MediaStreamManager.cs | 2 +- .../Item/PeopleManager.cs | 22 +- .../Persistence/IItemRepository.cs | 200 +++---- .../Persistence/IMediaAttachmentManager.cs | 29 + .../Persistence/IMediaStreamManager.cs | 28 + .../Persistence/IPeopleManager.cs | 34 ++ 14 files changed, 591 insertions(+), 1248 deletions(-) delete mode 100644 Emby.Server.Implementations/Data/BaseSqliteRepository.cs delete mode 100644 Emby.Server.Implementations/Data/ManagedConnection.cs delete mode 100644 Emby.Server.Implementations/Data/SqliteItemRepository.cs delete mode 100644 Emby.Server.Implementations/Data/SynchronousMode.cs delete mode 100644 Emby.Server.Implementations/Data/TempStoreMode.cs create mode 100644 Jellyfin.Server.Implementations/Item/MediaAttachmentManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaAttachmentManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IMediaStreamManager.cs create mode 100644 MediaBrowser.Controller/Persistence/IPeopleManager.cs (limited to 'Emby.Server.Implementations/Data') 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 'Emby.Server.Implementations/Data') 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 'Emby.Server.Implementations/Data') 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 eb601e944cd392a8007b540ab5627977a37368c6 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 9 Oct 2024 23:01:54 +0000 Subject: Expanded BaseItem aggregate types --- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 41 +- Jellyfin.Data/Entities/BaseItemEntity.cs | 22 +- Jellyfin.Data/Entities/BaseItemExtraType.cs | 18 + Jellyfin.Data/Entities/BaseItemImageInfo.cs | 57 + Jellyfin.Data/Entities/BaseItemMetadataField.cs | 26 + Jellyfin.Data/Entities/BaseItemTrailerType.cs | 25 + Jellyfin.Data/Entities/EnumLikeTable.cs | 14 + Jellyfin.Data/Entities/ImageInfoImageType.cs | 76 + Jellyfin.Data/Entities/ProgramAudioEntity.cs | 37 + .../Item/BaseItemRepository.cs | 316 ++-- .../JellyfinDbContext.cs | 15 + ...241009225800_ExpandedBaseItemFields.Designer.cs | 1540 ++++++++++++++++++++ .../20241009225800_ExpandedBaseItemFields.cs | 169 +++ .../Migrations/JellyfinDbModelSnapshot.cs | 123 +- .../ModelConfiguration/BaseItemConfiguration.cs | 3 + .../BaseItemMetadataFieldConfiguration.cs | 22 + .../BaseItemTrailerTypeConfiguration.cs | 22 + .../Migrations/Routines/MigrateLibraryDb.cs | 323 +++- .../Data/SqliteItemRepositoryTests.cs | 66 - 19 files changed, 2487 insertions(+), 428 deletions(-) create mode 100644 Jellyfin.Data/Entities/BaseItemExtraType.cs create mode 100644 Jellyfin.Data/Entities/BaseItemImageInfo.cs create mode 100644 Jellyfin.Data/Entities/BaseItemMetadataField.cs create mode 100644 Jellyfin.Data/Entities/BaseItemTrailerType.cs create mode 100644 Jellyfin.Data/Entities/EnumLikeTable.cs create mode 100644 Jellyfin.Data/Entities/ImageInfoImageType.cs create mode 100644 Jellyfin.Data/Entities/ProgramAudioEntity.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index 1f73755f5..b66e7f5d9 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Channels; using Emby.Server.Implementations.Playlists; using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -14,19 +15,13 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Data; -/// -/// 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, @@ -35,9 +30,7 @@ public class ItemTypeLookup : IItemTypeLookup BaseItemKind.LiveTvChannel ]; - /// - /// Gets all BaseItemKinds that should be excluded from parent lookup. - /// + /// public IReadOnlyList ProgramExcludeParentTypes { get; } = [ BaseItemKind.Series, @@ -47,27 +40,21 @@ public class ItemTypeLookup : IItemTypeLookup 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, @@ -76,9 +63,7 @@ public class ItemTypeLookup : IItemTypeLookup BaseItemKind.Season ]; - /// - /// Gets all BaseItemKinds that are not to be evaluated for Artists. - /// + /// public IReadOnlyList ArtistExcludeParentTypes { get; } = [ BaseItemKind.Series, @@ -86,9 +71,7 @@ public class ItemTypeLookup : IItemTypeLookup BaseItemKind.PhotoAlbum ]; - /// - /// Gets all BaseItemKinds that are considered Artists. - /// + /// public IReadOnlyList ArtistsTypes { get; } = [ BaseItemKind.Audio, @@ -97,9 +80,7 @@ public class ItemTypeLookup : IItemTypeLookup BaseItemKind.AudioBook ]; - /// - /// Gets mapping for all BaseItemKinds and their expected serialisaition target. - /// + /// public IDictionary BaseItemKindNames { get; } = new Dictionary() { { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName }, diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs index dbe5a5372..cd1991891 100644 --- a/Jellyfin.Data/Entities/BaseItemEntity.cs +++ b/Jellyfin.Data/Entities/BaseItemEntity.cs @@ -10,9 +10,7 @@ namespace Jellyfin.Data.Entities; public class BaseItemEntity { - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - - public Guid Id { get; set; } + public required Guid Id { get; set; } public required string Type { get; set; } @@ -78,12 +76,8 @@ public class BaseItemEntity 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; } @@ -94,8 +88,6 @@ public class BaseItemEntity public string? UnratedType { get; set; } - public string? TrailerTypes { get; set; } - public float? CriticRating { get; set; } public string? CleanName { get; set; } @@ -126,15 +118,13 @@ public class BaseItemEntity 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 BaseItemExtraType? ExtraType { get; set; } public string? Artists { get; set; } @@ -154,6 +144,8 @@ public class BaseItemEntity public long? Size { get; set; } + public ProgramAudioEntity? Audio { get; set; } + public Guid? ParentId { get; set; } public Guid? TopParentId { get; set; } @@ -176,6 +168,12 @@ public class BaseItemEntity public ICollection? AncestorIds { get; set; } + public ICollection? LockedFields { get; set; } + + public ICollection? TrailerTypes { get; set; } + + public ICollection? Images { get; set; } + // those are references to __LOCAL__ ids not DB ids ... TODO: Bring the whole folder structure into the DB // public ICollection? SeriesEpisodes { get; set; } // public BaseItemEntity? Series { get; set; } diff --git a/Jellyfin.Data/Entities/BaseItemExtraType.cs b/Jellyfin.Data/Entities/BaseItemExtraType.cs new file mode 100644 index 000000000..341697436 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItemExtraType.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Data.Entities; + +#pragma warning disable CS1591 +public enum BaseItemExtraType +{ + Unknown = 0, + Clip = 1, + Trailer = 2, + BehindTheScenes = 3, + DeletedScene = 4, + Interview = 5, + Scene = 6, + Sample = 7, + ThemeSong = 8, + ThemeVideo = 9, + Featurette = 10, + Short = 11 +} diff --git a/Jellyfin.Data/Entities/BaseItemImageInfo.cs b/Jellyfin.Data/Entities/BaseItemImageInfo.cs new file mode 100644 index 000000000..6390cac58 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItemImageInfo.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities; +#pragma warning disable CA2227 + +/// +/// Enum TrailerTypes. +/// +public class BaseItemImageInfo +{ + /// + /// Gets or Sets. + /// + public required Guid Id { get; set; } + + /// + /// Gets or Sets the path to the original image. + /// + public required string Path { get; set; } + + /// + /// Gets or Sets the time the image was last modified. + /// + public DateTime DateModified { get; set; } + + /// + /// Gets or Sets the imagetype. + /// + public ImageInfoImageType ImageType { get; set; } + + /// + /// Gets or Sets the width of the original image. + /// + public int Width { get; set; } + + /// + /// Gets or Sets the height of the original image. + /// + public int Height { get; set; } + +#pragma warning disable CA1819 + /// + /// Gets or Sets the blurhash. + /// + public byte[]? Blurhash { get; set; } + + /// + /// Gets or Sets the reference id to the BaseItem. + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or Sets the referenced Item. + /// + public required BaseItemEntity Item { get; set; } +} diff --git a/Jellyfin.Data/Entities/BaseItemMetadataField.cs b/Jellyfin.Data/Entities/BaseItemMetadataField.cs new file mode 100644 index 000000000..2f8e910f2 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItemMetadataField.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities; +#pragma warning disable CA2227 + +/// +/// Enum MetadataFields. +/// +public class BaseItemMetadataField +{ + /// + /// Gets or Sets Numerical ID of this enumeratable. + /// + public required int Id { get; set; } + + /// + /// Gets or Sets all referenced . + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or Sets all referenced . + /// + public required BaseItemEntity Item { get; set; } +} diff --git a/Jellyfin.Data/Entities/BaseItemTrailerType.cs b/Jellyfin.Data/Entities/BaseItemTrailerType.cs new file mode 100644 index 000000000..7dee20c87 --- /dev/null +++ b/Jellyfin.Data/Entities/BaseItemTrailerType.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities; +#pragma warning disable CA2227 +/// +/// Enum TrailerTypes. +/// +public class BaseItemTrailerType +{ + /// + /// Gets or Sets Numerical ID of this enumeratable. + /// + public required int Id { get; set; } + + /// + /// Gets or Sets all referenced . + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or Sets all referenced . + /// + public required BaseItemEntity Item { get; set; } +} diff --git a/Jellyfin.Data/Entities/EnumLikeTable.cs b/Jellyfin.Data/Entities/EnumLikeTable.cs new file mode 100644 index 000000000..11e1d0aa9 --- /dev/null +++ b/Jellyfin.Data/Entities/EnumLikeTable.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities; + +/// +/// Defines an Entity that is modeled after an Enum. +/// +public abstract class EnumLikeTable +{ + /// + /// Gets or Sets Numerical ID of this enumeratable. + /// + public required int Id { get; set; } +} diff --git a/Jellyfin.Data/Entities/ImageInfoImageType.cs b/Jellyfin.Data/Entities/ImageInfoImageType.cs new file mode 100644 index 000000000..f78178dd2 --- /dev/null +++ b/Jellyfin.Data/Entities/ImageInfoImageType.cs @@ -0,0 +1,76 @@ +namespace Jellyfin.Data.Entities; + +/// +/// Enum ImageType. +/// +public enum ImageInfoImageType +{ + /// + /// The primary. + /// + Primary = 0, + + /// + /// The art. + /// + Art = 1, + + /// + /// The backdrop. + /// + Backdrop = 2, + + /// + /// The banner. + /// + Banner = 3, + + /// + /// The logo. + /// + Logo = 4, + + /// + /// The thumb. + /// + Thumb = 5, + + /// + /// The disc. + /// + Disc = 6, + + /// + /// The box. + /// + Box = 7, + + /// + /// The screenshot. + /// + /// + /// This enum value is obsolete. + /// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete]. + /// + Screenshot = 8, + + /// + /// The menu. + /// + Menu = 9, + + /// + /// The chapter image. + /// + Chapter = 10, + + /// + /// The box rear. + /// + BoxRear = 11, + + /// + /// The user profile image. + /// + Profile = 12 +} diff --git a/Jellyfin.Data/Entities/ProgramAudioEntity.cs b/Jellyfin.Data/Entities/ProgramAudioEntity.cs new file mode 100644 index 000000000..fafccb13c --- /dev/null +++ b/Jellyfin.Data/Entities/ProgramAudioEntity.cs @@ -0,0 +1,37 @@ +namespace Jellyfin.Data.Entities; + +/// +/// Lists types of Audio. +/// +public enum ProgramAudioEntity +{ + /// + /// Mono. + /// + Mono, + + /// + /// Sterio. + /// + Stereo, + + /// + /// Dolby. + /// + Dolby, + + /// + /// DolbyDigital. + /// + DolbyDigital, + + /// + /// Thx. + /// + Thx, + + /// + /// Atmos. + /// + Atmos +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 480d83eb1..6ddab9e3d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading; +using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller; @@ -69,6 +70,8 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr context.AncestorIds.Where(e => e.ItemId == id).ExecuteDelete(); context.ItemValues.Where(e => e.ItemId == id).ExecuteDelete(); context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); + context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); context.SaveChanges(); transaction.Commit(); } @@ -229,7 +232,12 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr var result = new QueryResult(); using var context = dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.BaseItems, context, filter) + IQueryable dbQuery = context.BaseItems + .Include(e => e.ExtraType) + .Include(e => e.TrailerTypes) + .Include(e => e.Images) + .Include(e => e.LockedFields); + dbQuery = TranslateQuery(dbQuery, context, filter) .DistinctBy(e => e.Id); if (filter.EnableTotalRecordCount) { @@ -585,8 +593,8 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (filter.TrailerTypes.Length > 0) { - var trailerTypes = filter.TrailerTypes.Select(e => e.ToString()).ToArray(); - baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Contains(f))); + var trailerTypes = filter.TrailerTypes.Select(e => (int)e).ToArray(); + baseQuery = baseQuery.Where(e => trailerTypes.Any(f => e.TrailerTypes!.Any(w => w.Id == f))); } if (filter.IsAiring.HasValue) @@ -666,8 +674,8 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (filter.ImageTypes.Length > 0) { - var imgTypes = filter.ImageTypes.Select(e => e.ToString()).ToArray(); - baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Contains(f))); + var imgTypes = filter.ImageTypes.Select(e => (ImageInfoImageType)e).ToArray(); + baseQuery = baseQuery.Where(e => imgTypes.Any(f => e.Images!.Any(w => w.ImageType == f))); } if (filter.IsLiked.HasValue) @@ -1206,12 +1214,12 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr { ArgumentNullException.ThrowIfNull(item); - var images = SerializeImages(item.ImageInfos); + var images = item.ImageInfos.Select(e => Map(item.Id, e)); using var db = dbProvider.CreateDbContext(); - - db.BaseItems - .Where(e => e.Id == item.Id) - .ExecuteUpdate(e => e.SetProperty(f => f.Images, images)); + using var transaction = db.Database.BeginTransaction(); + db.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); + db.BaseItemImageInfos.AddRange(images); + transaction.Commit(); } /// @@ -1260,29 +1268,32 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr 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) { - context.AncestorIds.Add(new Data.Entities.AncestorId() + entity.AncestorIds.Add(new AncestorId() { Item = entity, AncestorIdText = ancestorId.ToString(), Id = ancestorId, - ItemId = Guid.Empty + ItemId = entity.Id }); } } var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); context.ItemValues.Where(e => e.ItemId == entity.Id).ExecuteDelete(); + entity.ItemValues = new List(); + foreach (var itemValue in itemValues) { - context.ItemValues.Add(new() + entity.ItemValues.Add(new() { Item = entity, Type = itemValue.MagicNumber, Value = itemValue.Value, CleanValue = GetCleanValue(itemValue.Value), - ItemId = Guid.Empty + ItemId = entity.Id }); } } @@ -1366,26 +1377,17 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (entity.ExtraType is not null) { - dto.ExtraType = Enum.Parse(entity.ExtraType); + dto.ExtraType = (ExtraType)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(); + dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? []; } if (entity.Audio is not null) { - dto.Audio = Enum.Parse(entity.Audio); + dto.Audio = (ProgramAudio)entity.Audio; } dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? null : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray(); @@ -1408,16 +1410,7 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr 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(); + trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? []; } if (dto is Video video) @@ -1455,7 +1448,7 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (entity.Images is not null) { - dto.ImageInfos = DeserializeImages(entity.Images); + dto.ImageInfos = entity.Images.Select(Map).ToArray(); } // dto.Type = entity.Type; @@ -1490,8 +1483,8 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr var entity = new BaseItemEntity() { Type = dto.GetType().ToString(), + Id = dto.Id }; - entity.Id = dto.Id; entity.ParentId = dto.ParentId; entity.Path = GetPathToSave(dto.Path); entity.EndDate = dto.EndDate.GetValueOrDefault(); @@ -1533,21 +1526,35 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr entity.OwnerId = dto.OwnerId.ToString(); entity.Width = dto.Width; entity.Height = dto.Height; - entity.Provider = dto.ProviderIds.Select(e => new Data.Entities.BaseItemProvider() + entity.Provider = dto.ProviderIds.Select(e => new BaseItemProvider() { Item = entity, ProviderId = e.Key, ProviderValue = e.Value }).ToList(); - entity.Audio = dto.Audio?.ToString(); - entity.ExtraType = dto.ExtraType?.ToString(); + if (dto.Audio.HasValue) + { + entity.Audio = (ProgramAudioEntity)dto.Audio; + } + + if (dto.ExtraType.HasValue) + { + entity.ExtraType = (BaseItemExtraType)dto.ExtraType; + } entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null; entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null; 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; + entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields + .Select(e => new BaseItemMetadataField() + { + Id = (int)e, + Item = entity, + ItemId = entity.Id + }) + .ToArray() : null; if (dto is IHasProgramAttributes hasProgramAttributes) { @@ -1562,11 +1569,6 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr 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; @@ -1602,7 +1604,17 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (dto.ImageInfos is not null) { - entity.Images = SerializeImages(dto.ImageInfos); + entity.Images = dto.ImageInfos.Select(f => Map(dto.Id, f)).ToArray(); + } + + if (dto is Trailer trailer) + { + entity.TrailerTypes = trailer.TrailerTypes?.Select(e => new BaseItemTrailerType() + { + Id = (int)e, + Item = entity, + ItemId = entity.Id + }).ToArray() ?? []; } // dto.Type = entity.Type; @@ -1863,90 +1875,33 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr } } - 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) + private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e) { - 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]; + return new BaseItemImageInfo() + { + ItemId = baseItemId, + Id = Guid.NewGuid(), + Path = e.Path, + Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null, + DateModified = e.DateModified, + Height = e.Height, + Width = e.Width, + ImageType = (ImageInfoImageType)e.Type, + Item = null! + }; } - private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image) + private static ItemImageInfo Map(BaseItemImageInfo e) { - 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('|', '\\')); - } + return new ItemImageInfo() + { + Path = e.Path, + BlurHash = e.Blurhash != null ? Encoding.UTF8.GetString(e.Blurhash) : null, + DateModified = e.DateModified, + Height = e.Height, + Width = e.Width, + Type = (ImageType)e.ImageType + }; } private string? GetPathToSave(string path) @@ -1964,111 +1919,6 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr 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(); diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index a9eda1b64..406230a70 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -131,6 +131,21 @@ public class JellyfinDbContext(DbContextOptions options, ILog /// public DbSet BaseItemProviders => Set(); + /// + /// Gets the . + /// + public DbSet BaseItemImageInfos => Set(); + + /// + /// Gets the . + /// + public DbSet BaseItemMetadataFields => Set(); + + /// + /// Gets the . + /// + public DbSet BaseItemTrailerTypes => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); diff --git a/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs new file mode 100644 index 000000000..7f69e8448 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.Designer.cs @@ -0,0 +1,1540 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20241009225800_ExpandedBaseItemFields")] + partial class ExpandedBaseItemFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AncestorIdText") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Id"); + + b.HasIndex("Id"); + + b.HasIndex("ItemId", "AncestorIdText"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("UserDataKey") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("UserDataKey", "Type"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Type", "Value"); + + b.HasIndex("ItemId", "Type", "CleanValue"); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("TEXT"); + + b.Property("TimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Role", "ListOrder"); + + b.HasIndex("Name"); + + b.HasIndex("ItemId", "ListOrder"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Key", "UserId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("UserId"); + + b.HasIndex("Key", "UserId", "IsFavorite"); + + b.HasIndex("Key", "UserId", "LastPlayedDate"); + + b.HasIndex("Key", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("Key", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("AncestorIds") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("UserData") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("AncestorIds"); + + b.Navigation("Chapters"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs new file mode 100644 index 000000000..f1238db82 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241009225800_ExpandedBaseItemFields.cs @@ -0,0 +1,169 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class ExpandedBaseItemFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Images", + table: "BaseItems"); + + migrationBuilder.DropColumn( + name: "LockedFields", + table: "BaseItems"); + + migrationBuilder.DropColumn( + name: "TrailerTypes", + table: "BaseItems"); + + migrationBuilder.AlterColumn( + name: "ExtraType", + table: "BaseItems", + type: "INTEGER", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Audio", + table: "BaseItems", + type: "INTEGER", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "BaseItemImageInfos", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Path = table.Column(type: "TEXT", nullable: false), + DateModified = table.Column(type: "TEXT", nullable: false), + ImageType = table.Column(type: "INTEGER", nullable: false), + Width = table.Column(type: "INTEGER", nullable: false), + Height = table.Column(type: "INTEGER", nullable: false), + Blurhash = table.Column(type: "BLOB", nullable: true), + ItemId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItemImageInfos", x => x.Id); + table.ForeignKey( + name: "FK_BaseItemImageInfos_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BaseItemMetadataFields", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false), + ItemId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItemMetadataFields", x => new { x.Id, x.ItemId }); + table.ForeignKey( + name: "FK_BaseItemMetadataFields_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BaseItemTrailerTypes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false), + ItemId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItemTrailerTypes", x => new { x.Id, x.ItemId }); + table.ForeignKey( + name: "FK_BaseItemTrailerTypes_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItemImageInfos_ItemId", + table: "BaseItemImageInfos", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_BaseItemMetadataFields_ItemId", + table: "BaseItemMetadataFields", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_BaseItemTrailerTypes_ItemId", + table: "BaseItemTrailerTypes", + column: "ItemId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BaseItemImageInfos"); + + migrationBuilder.DropTable( + name: "BaseItemMetadataFields"); + + migrationBuilder.DropTable( + name: "BaseItemTrailerTypes"); + + migrationBuilder.AlterColumn( + name: "ExtraType", + table: "BaseItems", + type: "TEXT", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Audio", + table: "BaseItems", + type: "TEXT", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Images", + table: "BaseItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "LockedFields", + table: "BaseItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "TrailerTypes", + table: "BaseItems", + type: "TEXT", + nullable: true); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index dd280489b..1a3a5910f 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -154,8 +154,8 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Artists") .HasColumnType("TEXT"); - b.Property("Audio") - .HasColumnType("TEXT"); + b.Property("Audio") + .HasColumnType("INTEGER"); b.Property("ChannelId") .HasColumnType("TEXT"); @@ -208,8 +208,8 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("ExtraIds") .HasColumnType("TEXT"); - b.Property("ExtraType") - .HasColumnType("TEXT"); + b.Property("ExtraType") + .HasColumnType("INTEGER"); b.Property("ForcedSortName") .HasColumnType("TEXT"); @@ -220,9 +220,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Height") .HasColumnType("INTEGER"); - b.Property("Images") - .HasColumnType("TEXT"); - b.Property("IndexNumber") .HasColumnType("INTEGER"); @@ -253,9 +250,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("LUFS") .HasColumnType("REAL"); - b.Property("LockedFields") - .HasColumnType("TEXT"); - b.Property("MediaType") .HasColumnType("TEXT"); @@ -352,9 +346,6 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("TotalBitrate") .HasColumnType("INTEGER"); - b.Property("TrailerTypes") - .HasColumnType("TEXT"); - b.Property("Type") .IsRequired() .HasColumnType("TEXT"); @@ -401,6 +392,56 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("BaseItems"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => { b.Property("ItemId") @@ -420,6 +461,21 @@ namespace Jellyfin.Server.Implementations.Migrations b.ToTable("BaseItemProviders"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => { b.Property("ItemId") @@ -1268,6 +1324,28 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Item"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => { b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") @@ -1279,6 +1357,17 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Item"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => { b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") @@ -1406,14 +1495,20 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("Chapters"); + b.Navigation("Images"); + b.Navigation("ItemValues"); + b.Navigation("LockedFields"); + b.Navigation("MediaStreams"); b.Navigation("Peoples"); b.Navigation("Provider"); + b.Navigation("TrailerTypes"); + b.Navigation("UserData"); }); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs index 6c36a1591..ab5403271 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -27,6 +27,9 @@ public class BaseItemConfiguration : IEntityTypeConfiguration builder.HasMany(e => e.Chapters); builder.HasMany(e => e.Provider); builder.HasMany(e => e.AncestorIds); + builder.HasMany(e => e.LockedFields); + builder.HasMany(e => e.TrailerTypes); + builder.HasMany(e => e.Images); builder.HasIndex(e => e.Path); builder.HasIndex(e => e.ParentId); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs new file mode 100644 index 000000000..137f4a883 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SQLitePCL; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// Provides configuration for the BaseItemMetadataField entity. +/// +public class BaseItemMetadataFieldConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => new { e.Id, e.ItemId }); + builder.HasOne(e => e.Item); + } +} diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs new file mode 100644 index 000000000..f03d99c29 --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using SQLitePCL; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// Provides configuration for the BaseItemMetadataField entity. +/// +public class BaseItemTrailerTypeConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => new { e.Id, e.ItemId }); + builder.HasOne(e => e.Item); + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index c4a15c64e..8ce423298 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -2,13 +2,17 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.IO; using System.Linq; +using System.Text; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Libraries; +using Jellyfin.Extensions; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using Microsoft.Data.Sqlite; @@ -503,293 +507,308 @@ public class MigrateLibraryDb : IMigrationRoutine private BaseItemEntity GetItem(SqliteDataReader reader) { - var item = new BaseItemEntity() + var entity = new BaseItemEntity() { - Type = reader.GetString(0) + Type = reader.GetString(0), + Id = Guid.NewGuid() }; var index = 1; if (reader.TryGetString(index++, out var data)) { - item.Data = data; + entity.Data = data; } if (reader.TryReadDateTime(index++, out var startDate)) { - item.StartDate = startDate; + entity.StartDate = startDate; } if (reader.TryReadDateTime(index++, out var endDate)) { - item.EndDate = endDate; + entity.EndDate = endDate; } if (reader.TryGetGuid(index++, out var guid)) { - item.ChannelId = guid.ToString("N"); + entity.ChannelId = guid.ToString("N"); } if (reader.TryGetBoolean(index++, out var isMovie)) { - item.IsMovie = isMovie; + entity.IsMovie = isMovie; } if (reader.TryGetBoolean(index++, out var isSeries)) { - item.IsSeries = isSeries; + entity.IsSeries = isSeries; } if (reader.TryGetString(index++, out var episodeTitle)) { - item.EpisodeTitle = episodeTitle; + entity.EpisodeTitle = episodeTitle; } if (reader.TryGetBoolean(index++, out var isRepeat)) { - item.IsRepeat = isRepeat; + entity.IsRepeat = isRepeat; } if (reader.TryGetSingle(index++, out var communityRating)) { - item.CommunityRating = communityRating; + entity.CommunityRating = communityRating; } if (reader.TryGetString(index++, out var customRating)) { - item.CustomRating = customRating; + entity.CustomRating = customRating; } if (reader.TryGetInt32(index++, out var indexNumber)) { - item.IndexNumber = indexNumber; + entity.IndexNumber = indexNumber; } if (reader.TryGetBoolean(index++, out var isLocked)) { - item.IsLocked = isLocked; + entity.IsLocked = isLocked; } if (reader.TryGetString(index++, out var preferredMetadataLanguage)) { - item.PreferredMetadataLanguage = preferredMetadataLanguage; + entity.PreferredMetadataLanguage = preferredMetadataLanguage; } if (reader.TryGetString(index++, out var preferredMetadataCountryCode)) { - item.PreferredMetadataCountryCode = preferredMetadataCountryCode; + entity.PreferredMetadataCountryCode = preferredMetadataCountryCode; } if (reader.TryGetInt32(index++, out var width)) { - item.Width = width; + entity.Width = width; } if (reader.TryGetInt32(index++, out var height)) { - item.Height = height; + entity.Height = height; } if (reader.TryReadDateTime(index++, out var dateLastRefreshed)) { - item.DateLastRefreshed = dateLastRefreshed; + entity.DateLastRefreshed = dateLastRefreshed; } if (reader.TryGetString(index++, out var name)) { - item.Name = name; + entity.Name = name; } if (reader.TryGetString(index++, out var restorePath)) { - item.Path = restorePath; + entity.Path = restorePath; } if (reader.TryReadDateTime(index++, out var premiereDate)) { - item.PremiereDate = premiereDate; + entity.PremiereDate = premiereDate; } if (reader.TryGetString(index++, out var overview)) { - item.Overview = overview; + entity.Overview = overview; } if (reader.TryGetInt32(index++, out var parentIndexNumber)) { - item.ParentIndexNumber = parentIndexNumber; + entity.ParentIndexNumber = parentIndexNumber; } if (reader.TryGetInt32(index++, out var productionYear)) { - item.ProductionYear = productionYear; + entity.ProductionYear = productionYear; } if (reader.TryGetString(index++, out var officialRating)) { - item.OfficialRating = officialRating; + entity.OfficialRating = officialRating; } if (reader.TryGetString(index++, out var forcedSortName)) { - item.ForcedSortName = forcedSortName; + entity.ForcedSortName = forcedSortName; } if (reader.TryGetInt64(index++, out var runTimeTicks)) { - item.RunTimeTicks = runTimeTicks; + entity.RunTimeTicks = runTimeTicks; } if (reader.TryGetInt64(index++, out var size)) { - item.Size = size; + entity.Size = size; } if (reader.TryReadDateTime(index++, out var dateCreated)) { - item.DateCreated = dateCreated; + entity.DateCreated = dateCreated; } if (reader.TryReadDateTime(index++, out var dateModified)) { - item.DateModified = dateModified; + entity.DateModified = dateModified; } - item.Id = reader.GetGuid(index++); + entity.Id = reader.GetGuid(index++); if (reader.TryGetString(index++, out var genres)) { - item.Genres = genres; + entity.Genres = genres; } if (reader.TryGetGuid(index++, out var parentId)) { - item.ParentId = parentId; + entity.ParentId = parentId; } - if (reader.TryGetString(index++, out var audioString)) + if (reader.TryGetString(index++, out var audioString) && Enum.TryParse(audioString, out var audioType)) { - item.Audio = audioString; + entity.Audio = audioType; } if (reader.TryGetString(index++, out var serviceName)) { - item.ExternalServiceId = serviceName; + entity.ExternalServiceId = serviceName; } if (reader.TryGetBoolean(index++, out var isInMixedFolder)) { - item.IsInMixedFolder = isInMixedFolder; + entity.IsInMixedFolder = isInMixedFolder; } if (reader.TryReadDateTime(index++, out var dateLastSaved)) { - item.DateLastSaved = dateLastSaved; + entity.DateLastSaved = dateLastSaved; } if (reader.TryGetString(index++, out var lockedFields)) { - item.LockedFields = lockedFields; + entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse) + .Select(e => new BaseItemMetadataField() + { + Id = (int)e, + Item = entity, + ItemId = entity.Id + }) + .ToArray(); } if (reader.TryGetString(index++, out var studios)) { - item.Studios = studios; + entity.Studios = studios; } if (reader.TryGetString(index++, out var tags)) { - item.Tags = tags; + entity.Tags = tags; } if (reader.TryGetString(index++, out var trailerTypes)) { - item.TrailerTypes = trailerTypes; + entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse) + .Select(e => new BaseItemTrailerType() + { + Id = (int)e, + Item = entity, + ItemId = entity.Id + }) + .ToArray(); } if (reader.TryGetString(index++, out var originalTitle)) { - item.OriginalTitle = originalTitle; + entity.OriginalTitle = originalTitle; } if (reader.TryGetString(index++, out var primaryVersionId)) { - item.PrimaryVersionId = primaryVersionId; + entity.PrimaryVersionId = primaryVersionId; } if (reader.TryReadDateTime(index++, out var dateLastMediaAdded)) { - item.DateLastMediaAdded = dateLastMediaAdded; + entity.DateLastMediaAdded = dateLastMediaAdded; } if (reader.TryGetString(index++, out var album)) { - item.Album = album; + entity.Album = album; } if (reader.TryGetSingle(index++, out var lUFS)) { - item.LUFS = lUFS; + entity.LUFS = lUFS; } if (reader.TryGetSingle(index++, out var normalizationGain)) { - item.NormalizationGain = normalizationGain; + entity.NormalizationGain = normalizationGain; } if (reader.TryGetSingle(index++, out var criticRating)) { - item.CriticRating = criticRating; + entity.CriticRating = criticRating; } if (reader.TryGetBoolean(index++, out var isVirtualItem)) { - item.IsVirtualItem = isVirtualItem; + entity.IsVirtualItem = isVirtualItem; } if (reader.TryGetString(index++, out var seriesName)) { - item.SeriesName = seriesName; + entity.SeriesName = seriesName; } if (reader.TryGetString(index++, out var seasonName)) { - item.SeasonName = seasonName; + entity.SeasonName = seasonName; } if (reader.TryGetGuid(index++, out var seasonId)) { - item.SeasonId = seasonId; + entity.SeasonId = seasonId; } if (reader.TryGetGuid(index++, out var seriesId)) { - item.SeriesId = seriesId; + entity.SeriesId = seriesId; } if (reader.TryGetString(index++, out var presentationUniqueKey)) { - item.PresentationUniqueKey = presentationUniqueKey; + entity.PresentationUniqueKey = presentationUniqueKey; } if (reader.TryGetInt32(index++, out var parentalRating)) { - item.InheritedParentalRatingValue = parentalRating; + entity.InheritedParentalRatingValue = parentalRating; } if (reader.TryGetString(index++, out var externalSeriesId)) { - item.ExternalSeriesId = externalSeriesId; + entity.ExternalSeriesId = externalSeriesId; } if (reader.TryGetString(index++, out var tagLine)) { - item.Tagline = tagLine; + entity.Tagline = tagLine; } if (reader.TryGetString(index++, out var providerIds)) { - item.Provider = providerIds.Split('|').Select(e => e.Split("=")) + entity.Provider = providerIds.Split('|').Select(e => e.Split("=")) .Select(e => new BaseItemProvider() { Item = null!, @@ -800,59 +819,217 @@ public class MigrateLibraryDb : IMigrationRoutine if (reader.TryGetString(index++, out var imageInfos)) { - item.Images = imageInfos; + entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray(); } if (reader.TryGetString(index++, out var productionLocations)) { - item.ProductionLocations = productionLocations; + entity.ProductionLocations = productionLocations; } if (reader.TryGetString(index++, out var extraIds)) { - item.ExtraIds = extraIds; + entity.ExtraIds = extraIds; } if (reader.TryGetInt32(index++, out var totalBitrate)) { - item.TotalBitrate = totalBitrate; + entity.TotalBitrate = totalBitrate; } - if (reader.TryGetString(index++, out var extraTypeString)) + if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse(extraTypeString, out var extraType)) { - item.ExtraType = extraTypeString; + entity.ExtraType = extraType; } if (reader.TryGetString(index++, out var artists)) { - item.Artists = artists; + entity.Artists = artists; } if (reader.TryGetString(index++, out var albumArtists)) { - item.AlbumArtists = albumArtists; + entity.AlbumArtists = albumArtists; } if (reader.TryGetString(index++, out var externalId)) { - item.ExternalId = externalId; + entity.ExternalId = externalId; } if (reader.TryGetString(index++, out var seriesPresentationUniqueKey)) { - item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; + entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey; } if (reader.TryGetString(index++, out var showId)) { - item.ShowId = showId; + entity.ShowId = showId; } if (reader.TryGetGuid(index++, out var ownerId)) { - item.OwnerId = ownerId.ToString("N"); + entity.OwnerId = ownerId.ToString("N"); } - return item; + return entity; + } + + private static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e) + { + return new BaseItemImageInfo() + { + ItemId = baseItemId, + Id = Guid.NewGuid(), + Path = e.Path, + Blurhash = e.BlurHash != null ? Encoding.UTF8.GetBytes(e.BlurHash) : null, + DateModified = e.DateModified, + Height = e.Height, + Width = e.Width, + ImageType = (ImageInfoImageType)e.Type, + Item = null! + }; + } + + 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]; + } + + 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 = 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; } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs index 1cf9e864d..105f5d7af 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SqliteItemRepositoryTests.cs @@ -99,31 +99,6 @@ namespace Jellyfin.Server.Implementations.Tests.Data return data; } - [Theory] - [MemberData(nameof(ItemImageInfoFromValueString_Valid_TestData))] - public void ItemImageInfoFromValueString_Valid_Success(string value, ItemImageInfo expected) - { - var result = _sqliteItemRepository.ItemImageInfoFromValueString(value)!; - Assert.Equal(expected.Path, result.Path); - Assert.Equal(expected.Type, result.Type); - Assert.Equal(expected.DateModified, result.DateModified); - Assert.Equal(expected.Width, result.Width); - Assert.Equal(expected.Height, result.Height); - Assert.Equal(expected.BlurHash, result.BlurHash); - } - - [Theory] - [InlineData("")] - [InlineData("*")] - [InlineData("https://image.tmdb.org/t/p/original/zhB5CHEgqqh4wnEqDNJLfWXJlcL.jpg*0")] - [InlineData("/mnt/series/Family Guy/Season 1/Family Guy - S01E01-thumb.jpg*6374520964785129080*WjQbtJtSO8nhNZ%L_Io#R/oaS DeserializeImages_Valid_TestData() { var data = new TheoryData(); @@ -204,47 +179,6 @@ namespace Jellyfin.Server.Implementations.Tests.Data return data; } - [Theory] - [MemberData(nameof(DeserializeImages_Valid_TestData))] - public void DeserializeImages_Valid_Success(string value, ItemImageInfo[] expected) - { - var result = _sqliteItemRepository.DeserializeImages(value); - Assert.Equal(expected.Length, result.Length); - for (int i = 0; i < expected.Length; i++) - { - Assert.Equal(expected[i].Path, result[i].Path); - Assert.Equal(expected[i].Type, result[i].Type); - Assert.Equal(expected[i].DateModified, result[i].DateModified); - Assert.Equal(expected[i].Width, result[i].Width); - Assert.Equal(expected[i].Height, result[i].Height); - Assert.Equal(expected[i].BlurHash, result[i].BlurHash); - } - } - - [Theory] - [MemberData(nameof(DeserializeImages_ValidAndInvalid_TestData))] - public void DeserializeImages_ValidAndInvalid_Success(string value, ItemImageInfo[] expected) - { - var result = _sqliteItemRepository.DeserializeImages(value); - Assert.Equal(expected.Length, result.Length); - for (int i = 0; i < expected.Length; i++) - { - Assert.Equal(expected[i].Path, result[i].Path); - Assert.Equal(expected[i].Type, result[i].Type); - Assert.Equal(expected[i].DateModified, result[i].DateModified); - Assert.Equal(expected[i].Width, result[i].Width); - Assert.Equal(expected[i].Height, result[i].Height); - Assert.Equal(expected[i].BlurHash, result[i].BlurHash); - } - } - - [Theory] - [MemberData(nameof(DeserializeImages_Valid_TestData))] - public void SerializeImages_Valid_Success(string expected, ItemImageInfo[] value) - { - Assert.Equal(expected, _sqliteItemRepository.SerializeImages(value)); - } - private sealed class ProviderIdsExtensionsTestsObject : IHasProviderIds { public Dictionary ProviderIds { get; set; } = new Dictionary(); -- cgit v1.2.3 From ee0dad6f432e5bfdda074e3f006f4c4d3c418d08 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 10 Oct 2024 14:32:49 +0000 Subject: Refactored ItemValue structure --- .../Data/CleanDatabaseScheduledTask.cs | 17 +- Jellyfin.Data/Entities/AncestorId.cs | 14 +- Jellyfin.Data/Entities/BaseItemEntity.cs | 2 +- Jellyfin.Data/Entities/ItemValue.cs | 18 +- Jellyfin.Data/Entities/ItemValueMap.cs | 30 + .../Item/BaseItemRepository.cs | 153 +- .../JellyfinDbContext.cs | 5 + ...142722_FixedItemValueReferenceStyle.Designer.cs | 1582 ++++++++++++++++++++ .../20241010142722_FixedItemValueReferenceStyle.cs | 133 ++ .../Migrations/JellyfinDbModelSnapshot.cs | 60 +- .../ModelConfiguration/AncestorIdConfiguration.cs | 2 + .../ModelConfiguration/ItemValuesConfiguration.cs | 4 +- .../ItemValuesMapConfiguration.cs | 20 + .../Migrations/Routines/MigrateLibraryDb.cs | 30 +- 14 files changed, 1967 insertions(+), 103 deletions(-) create mode 100644 Jellyfin.Data/Entities/ItemValueMap.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs create mode 100644 Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 4516b89dc..932bd2b05 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -1,10 +1,13 @@ #pragma warning disable CS1591 using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Server.Implementations; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Data @@ -13,11 +16,16 @@ namespace Emby.Server.Implementations.Data { private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; + private readonly IDbContextFactory _dbProvider; - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger logger) + public CleanDatabaseScheduledTask( + ILibraryManager libraryManager, + ILogger logger, + IDbContextFactory dbProvider) { _libraryManager = libraryManager; _logger = logger; + _dbProvider = dbProvider; } public Task Run(IProgress progress, CancellationToken cancellationToken) @@ -34,7 +42,7 @@ namespace Emby.Server.Implementations.Data }); var numComplete = 0; - var numItems = itemIds.Count; + var numItems = itemIds.Count + 1; _logger.LogDebug("Cleaning {0} items with dead parent links", numItems); @@ -60,6 +68,11 @@ namespace Emby.Server.Implementations.Data progress.Report(percent * 100); } + using var context = _dbProvider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); + transaction.Commit(); + progress.Report(100); } } diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Data/Entities/AncestorId.cs index 941a8eb2e..ef0fe0ba7 100644 --- a/Jellyfin.Data/Entities/AncestorId.cs +++ b/Jellyfin.Data/Entities/AncestorId.cs @@ -8,12 +8,22 @@ namespace Jellyfin.Data.Entities; public class AncestorId { /// - /// Gets or Sets the AncestorId that may or may not be an database managed Item or an materialised local item. + /// Gets or Sets the AncestorId. /// public required Guid ParentItemId { get; set; } /// - /// Gets or Sets the related that may or may not be an database managed Item or an materialised local item. + /// Gets or Sets the related BaseItem. /// public required Guid ItemId { get; set; } + + /// + /// Gets or Sets the ParentItem. + /// + public required BaseItemEntity ParentItem { get; set; } + + /// + /// Gets or Sets the Child item. + /// + public required BaseItemEntity Item { get; set; } } diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs index cd1991891..7670c1893 100644 --- a/Jellyfin.Data/Entities/BaseItemEntity.cs +++ b/Jellyfin.Data/Entities/BaseItemEntity.cs @@ -158,7 +158,7 @@ public class BaseItemEntity public ICollection? UserData { get; set; } - public ICollection? ItemValues { get; set; } + public ICollection? ItemValues { get; set; } public ICollection? MediaStreams { get; set; } diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Data/Entities/ItemValue.cs index bfa53cd46..7b1048c10 100644 --- a/Jellyfin.Data/Entities/ItemValue.cs +++ b/Jellyfin.Data/Entities/ItemValue.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; @@ -11,14 +9,9 @@ namespace Jellyfin.Data.Entities; public class ItemValue { /// - /// Gets or Sets the reference ItemId. + /// Gets or Sets the ItemValueId. /// - public required Guid ItemId { get; set; } - - /// - /// Gets or Sets the referenced BaseItem. - /// - public required BaseItemEntity Item { get; set; } + public required Guid ItemValueId { get; set; } /// /// Gets or Sets the Type. @@ -34,4 +27,11 @@ public class ItemValue /// Gets or Sets the sanatised Value. /// public required string CleanValue { get; set; } + + /// + /// Gets or Sets all associated BaseItems. + /// +#pragma warning disable CA2227 // Collection properties should be read only + public ICollection? BaseItemsMap { get; set; } +#pragma warning restore CA2227 // Collection properties should be read only } diff --git a/Jellyfin.Data/Entities/ItemValueMap.cs b/Jellyfin.Data/Entities/ItemValueMap.cs new file mode 100644 index 000000000..94db6a011 --- /dev/null +++ b/Jellyfin.Data/Entities/ItemValueMap.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities; + +/// +/// Mapping table for the ItemValue BaseItem relation. +/// +public class ItemValueMap +{ + /// + /// Gets or Sets the ItemId. + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or Sets the ItemValueId. + /// + public required Guid ItemValueId { get; set; } + + /// + /// Gets or Sets the referenced . + /// + public required BaseItemEntity Item { get; set; } + + /// + /// Gets or Sets the referenced . + /// + public required ItemValue ItemValue { get; set; } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 86c682027..d7de7e9bd 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -69,10 +69,11 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.AncestorIds.Where(e => e.ItemId == id).ExecuteDelete(); - context.ItemValues.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); + context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); context.SaveChanges(); transaction.Commit(); } @@ -83,25 +84,8 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr using var context = dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); - context.ItemValues.Where(e => e.Type == ItemValueType.InheritedTags).ExecuteDelete(); - context.ItemValues.AddRange(context.ItemValues.Where(e => e.Type == ItemValueType.Tags).Select(e => new ItemValue() - { - CleanValue = e.CleanValue, - ItemId = e.ItemId, - Type = ItemValueType.InheritedTags, - Value = e.Value, - Item = null! - })); - - context.ItemValues.AddRange( - context.AncestorIds.Join(context.ItemValues.Where(e => e.Value != null && e.Type == ItemValueType.Tags), e => e.ParentItemId, e => e.ItemId, (e, f) => new ItemValue() - { - CleanValue = f.CleanValue, - ItemId = e.ItemId, - Item = null!, - Type = ItemValueType.InheritedTags, - Value = f.Value - })); + context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete(); + // ItemValue Inheritence is now correctly mapped via AncestorId on demand context.SaveChanges(); transaction.Commit(); @@ -717,24 +701,22 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr } } - 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 <= ItemValueType.Artist && artistQuery.Any(w => w.CleanName == f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type <= ItemValueType.Artist && filter.ArtistIds.Contains(f.ItemId))); } if (filter.AlbumArtistIds.Length > 0) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Artist && artistQuery.Any(w => w.CleanName == f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId))); } 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))); + baseQuery = baseQuery + .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId))); } if (filter.AlbumIds.Length > 0) @@ -744,42 +726,41 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr 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 <= ItemValueType.Artist && artistQuery.Any(w => w.CleanName == f.CleanValue))); + .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ExcludeArtistIds.Contains(f.ItemId))); } if (filter.GenreIds.Count > 0) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Genre && context.BaseItems.Where(w => filter.GenreIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && filter.GenreIds.Contains(f.ItemId))); } if (filter.Genres.Count > 0) { var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray(); baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Genre && cleanGenres.Contains(f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && cleanGenres.Contains(f.ItemValue.CleanValue))); } if (tags.Count > 0) { var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray(); baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Tags && cleanValues.Contains(f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue))); } if (excludeTags.Count > 0) { var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray(); baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => f.Type == ItemValueType.Tags && cleanValues.Contains(f.CleanValue))); + .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue))); } if (filter.StudioIds.Length > 0) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Studios && context.BaseItems.Where(w => filter.StudioIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue))); + .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId))); } if (filter.OfficialRatings.Length > 0) @@ -936,13 +917,13 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value) { baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => (f.Type == ItemValueType.Artist || f.Type == ItemValueType.AlbumArtist) && f.CleanValue == e.CleanName)); + .Where(e => e.ItemValues!.Count(f => (f.ItemValue.Type == ItemValueType.Artist || f.ItemValue.Type == ItemValueType.AlbumArtist)) == 1); } if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value) { baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => f.Type == ItemValueType.Studios && f.CleanValue == e.CleanName)); + .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1); } if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) @@ -1081,8 +1062,8 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (filter.ExcludeInheritedTags.Length > 0) { baseQuery = baseQuery - .Where(e => !e.ItemValues!.Where(e => e.Type == ItemValueType.InheritedTags) - .Any(f => filter.ExcludeInheritedTags.Contains(f.CleanValue))); + .Where(e => !e.ItemValues!.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags) + .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))); } if (filter.IncludeInheritedTags.Length > 0) @@ -1092,26 +1073,25 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(e => e.Type == ItemValueType.InheritedTags) - .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)) + .Where(e => e.ItemValues!.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags) + .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) || - (e.ParentId.HasValue && context.ItemValues.Where(w => w.ItemId == e.ParentId.Value)!.Where(e => e.Type == ItemValueType.InheritedTags) - .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)))); + (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags) + .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.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 == ItemValueType.InheritedTags) - .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); - // d ^^ this is stupid it hate this. + .Where(e => e.AncestorIds!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.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 == ItemValueType.InheritedTags) - .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue))); + .Where(e => e.AncestorIds!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue)))); } } @@ -1277,25 +1257,48 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr entity.AncestorIds.Add(new AncestorId() { ParentItemId = ancestorId, - ItemId = entity.Id + ItemId = entity.Id, + Item = null!, + ParentItem = null! }); } } - var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags); - context.ItemValues.Where(e => e.ItemId == entity.Id).ExecuteDelete(); - entity.ItemValues = new List(); + 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 itemValues) + foreach (var itemValue in itemValuesToSave) { - entity.ItemValues.Add(new() + var refValue = referenceValues.FirstOrDefault(f => f.CleanValue == itemValue.Value && (int)f.Type == itemValue.MagicNumber); + if (refValue is not null) { - Item = entity, - Type = (ItemValueType)itemValue.MagicNumber, - Value = itemValue.Value, - CleanValue = GetCleanValue(itemValue.Value), - ItemId = entity.Id - }); + entity.ItemValues.Add(new ItemValueMap() + { + Item = entity, + ItemId = entity.Id, + ItemValue = null!, + ItemValueId = refValue.ItemValueId + }); + } + else + { + entity.ItemValues.Add(new ItemValueMap() + { + 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 + }); + } } } @@ -1652,21 +1655,21 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr { using var context = dbProvider.CreateDbContext(); - var query = context.ItemValues + var query = context.ItemValuesMap .AsNoTracking() - .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.Type)); + .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.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 == e.ItemId))); + query = query.Where(e => withItemTypes.Contains(e.Item.Type)); } if (excludeItemTypes.Count > 0) { - query = query.Where(e => !context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId == e.ItemId))); + query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type)); } // query = query.DistinctBy(e => e.CleanValue); - return query.Select(e => e.CleanValue).ToImmutableArray(); + return query.Select(e => e.ItemValue.CleanValue).ToImmutableArray(); } private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity) @@ -1705,7 +1708,7 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr }; var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery); - query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.Type))); + query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); if (filter.OrderBy.Count != 0 || !string.IsNullOrEmpty(filter.SearchTerm)) @@ -1745,13 +1748,13 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr // TODO: This is bad refactor! itemCount = new ItemCounts() { - SeriesCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Series).FullName)), - EpisodeCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Episode).FullName)), - MovieCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Data.Entities.Libraries.Movie).FullName)), - AlbumCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(MusicAlbum).FullName)), - ArtistCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(MusicArtist).FullName)), - SongCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Audio).FullName)), - TrailerCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Trailer).FullName)), + 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), } }); @@ -1981,9 +1984,9 @@ public sealed class BaseItemRepository(IDbContextFactory dbPr ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.Played, ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == 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 == ItemValueType.AlbumArtist).Select(f => f.CleanValue), - ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == ItemValueType.Studios).Select(f => f.CleanValue), + ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue), + ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue), + ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.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, diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 406230a70..284897c99 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs @@ -116,6 +116,11 @@ public class JellyfinDbContext(DbContextOptions options, ILog /// public DbSet ItemValues => Set(); + /// + /// Gets the . + /// + public DbSet ItemValuesMap => Set(); + /// /// Gets the containing the user data. /// diff --git a/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs new file mode 100644 index 000000000..00a943f79 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs @@ -0,0 +1,1582 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20241010142722_FixedItemValueReferenceStyle")] + partial class FixedItemValueReferenceStyle + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("UserDataKey") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("UserDataKey", "Type"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Role", "ListOrder"); + + b.HasIndex("Name"); + + b.HasIndex("ItemId", "ListOrder"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("BaseItemEntityId") + .HasColumnType("TEXT"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Key", "UserId"); + + b.HasIndex("BaseItemEntityId"); + + b.HasIndex("UserId"); + + b.HasIndex("Key", "UserId", "IsFavorite"); + + b.HasIndex("Key", "UserId", "LastPlayedDate"); + + b.HasIndex("Key", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("Key", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("AncestorIds") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany() + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) + .WithMany("UserData") + .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("AncestorIds"); + + b.Navigation("Chapters"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs new file mode 100644 index 000000000..9b1985254 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs @@ -0,0 +1,133 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class FixedItemValueReferenceStyle : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ItemValues_BaseItems_ItemId", + table: "ItemValues"); + + migrationBuilder.DropPrimaryKey( + name: "PK_ItemValues", + table: "ItemValues"); + + migrationBuilder.DropIndex( + name: "IX_ItemValues_ItemId_Type_CleanValue", + table: "ItemValues"); + + migrationBuilder.RenameColumn( + name: "ItemId", + table: "ItemValues", + newName: "ItemValueId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ItemValues", + table: "ItemValues", + column: "ItemValueId"); + + migrationBuilder.CreateTable( + name: "ItemValuesMap", + columns: table => new + { + ItemId = table.Column(type: "TEXT", nullable: false), + ItemValueId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemValuesMap", x => new { x.ItemValueId, x.ItemId }); + table.ForeignKey( + name: "FK_ItemValuesMap_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ItemValuesMap_ItemValues_ItemValueId", + column: x => x.ItemValueId, + principalTable: "ItemValues", + principalColumn: "ItemValueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ItemValues_Type_CleanValue", + table: "ItemValues", + columns: new[] { "Type", "CleanValue" }); + + migrationBuilder.CreateIndex( + name: "IX_ItemValuesMap_ItemId", + table: "ItemValuesMap", + column: "ItemId"); + + migrationBuilder.AddForeignKey( + name: "FK_AncestorIds_BaseItems_ItemId", + table: "AncestorIds", + column: "ItemId", + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_AncestorIds_BaseItems_ParentItemId", + table: "AncestorIds", + column: "ParentItemId", + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AncestorIds_BaseItems_ItemId", + table: "AncestorIds"); + + migrationBuilder.DropForeignKey( + name: "FK_AncestorIds_BaseItems_ParentItemId", + table: "AncestorIds"); + + migrationBuilder.DropTable( + name: "ItemValuesMap"); + + migrationBuilder.DropPrimaryKey( + name: "PK_ItemValues", + table: "ItemValues"); + + migrationBuilder.DropIndex( + name: "IX_ItemValues_Type_CleanValue", + table: "ItemValues"); + + migrationBuilder.RenameColumn( + name: "ItemValueId", + table: "ItemValues", + newName: "ItemId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ItemValues", + table: "ItemValues", + columns: new[] { "ItemId", "Type", "Value" }); + + migrationBuilder.CreateIndex( + name: "IX_ItemValues_ItemId_Type_CleanValue", + table: "ItemValues", + columns: new[] { "ItemId", "Type", "CleanValue" }); + + migrationBuilder.AddForeignKey( + name: "FK_ItemValues_BaseItems_ItemId", + table: "ItemValues", + column: "ItemId", + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs index 49abeef5c..20d7cf3dd 100644 --- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -683,26 +683,43 @@ namespace Jellyfin.Server.Implementations.Migrations modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => { - b.Property("ItemId") + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() .HasColumnType("TEXT"); b.Property("Type") .HasColumnType("INTEGER"); b.Property("Value") - .HasColumnType("TEXT"); - - b.Property("CleanValue") .IsRequired() .HasColumnType("TEXT"); - b.HasKey("ItemId", "Type", "Value"); + b.HasKey("ItemValueId"); - b.HasIndex("ItemId", "Type", "CleanValue"); + b.HasIndex("Type", "CleanValue"); b.ToTable("ItemValues"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => { b.Property("Id") @@ -1307,6 +1324,22 @@ namespace Jellyfin.Server.Implementations.Migrations b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null) .WithMany("AncestorIds") .HasForeignKey("BaseItemEntityId"); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany() + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); }); modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => @@ -1410,7 +1443,7 @@ namespace Jellyfin.Server.Implementations.Migrations .IsRequired(); }); - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => { b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") .WithMany("ItemValues") @@ -1418,7 +1451,15 @@ namespace Jellyfin.Server.Implementations.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.Navigation("Item"); + + b.Navigation("ItemValue"); }); modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => @@ -1513,6 +1554,11 @@ namespace Jellyfin.Server.Implementations.Migrations b.Navigation("HomeSections"); }); + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => { b.Navigation("AccessSchedules"); diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs index 0e90b8d82..fe5cf30ac 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs @@ -15,5 +15,7 @@ public class AncestorIdConfiguration : IEntityTypeConfiguration { builder.HasKey(e => new { e.ItemId, e.ParentItemId }); builder.HasIndex(e => e.ParentItemId); + builder.HasOne(e => e.ParentItem); + builder.HasOne(e => e.Item); } } diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs index c39854f5a..7dfa2032e 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs @@ -13,7 +13,7 @@ public class ItemValuesConfiguration : IEntityTypeConfiguration /// public void Configure(EntityTypeBuilder builder) { - builder.HasKey(e => new { e.ItemId, e.Type, e.Value }); - builder.HasIndex(e => new { e.ItemId, e.Type, e.CleanValue }); + builder.HasKey(e => e.ItemValueId); + builder.HasIndex(e => new { e.Type, e.CleanValue }); } } diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs new file mode 100644 index 000000000..9c22b114c --- /dev/null +++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs @@ -0,0 +1,20 @@ +using System; +using Jellyfin.Data.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Server.Implementations.ModelConfiguration; + +/// +/// itemvalues Configuration. +/// +public class ItemValuesMapConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => new { e.ItemValueId, e.ItemId }); + builder.HasOne(e => e.Item); + builder.HasOne(e => e.ItemValue); + } +} diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 85d537380..294c4e8a6 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -113,12 +113,31 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.SaveChanges(); - var itemValueQuery = "select ItemId, Type, Value, CleanValue FROM ItemValues"; + // do not migrate inherited types as they are now properly mapped in search and lookup. + var itemValueQuery = "select ItemId, Type, Value, CleanValue FROM ItemValues WHERE Type <> 6"; dbContext.ItemValues.ExecuteDelete(); foreach (SqliteDataReader dto in connection.Query(itemValueQuery)) { - dbContext.ItemValues.Add(GetItemValue(dto)); + var itemId = dto.GetGuid(0); + var entity = GetItemValue(dto); + var existingItemValue = dbContext.ItemValues.FirstOrDefault(f => f.Type == entity.Type && f.Value == entity.Value); + if (existingItemValue is null) + { + dbContext.ItemValues.Add(entity); + } + else + { + entity = existingItemValue; + } + + dbContext.ItemValuesMap.Add(new ItemValueMap() + { + Item = null!, + ItemValue = null!, + ItemId = itemId, + ItemValueId = entity.ItemValueId + }); } dbContext.SaveChanges(); @@ -185,7 +204,9 @@ public class MigrateLibraryDb : IMigrationRoutine return new AncestorId() { ItemId = reader.GetGuid(0), - ParentItemId = reader.GetGuid(1) + ParentItemId = reader.GetGuid(1), + Item = null!, + ParentItem = null! }; } @@ -226,11 +247,10 @@ public class MigrateLibraryDb : IMigrationRoutine { return new ItemValue { - ItemId = reader.GetGuid(0), + ItemValueId = Guid.NewGuid(), Type = (ItemValueType)reader.GetInt32(1), Value = reader.GetString(2), CleanValue = reader.GetString(3), - Item = null! }; } -- cgit v1.2.3 From 441b99518988212a84070dffe1ef51d6faa4e0af Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 10 Oct 2024 15:27:13 +0000 Subject: Applied Review Suggestions --- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 3 ++- Jellyfin.Data/Entities/BaseItemMetadataField.cs | 2 -- Jellyfin.Data/Entities/BaseItemTrailerType.cs | 3 +-- Jellyfin.Data/Entities/EnumLikeTable.cs | 14 -------------- Jellyfin.Data/Entities/MediaStreamTypeEntity.cs | 12 ++++++------ Jellyfin.Data/Entities/ProgramAudioEntity.cs | 12 ++++++------ 6 files changed, 15 insertions(+), 31 deletions(-) delete mode 100644 Jellyfin.Data/Entities/EnumLikeTable.cs (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index b66e7f5d9..df0f4ea20 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Threading.Channels; using Emby.Server.Implementations.Playlists; @@ -116,5 +117,5 @@ public class ItemTypeLookup : IItemTypeLookup { BaseItemKind.UserView, typeof(UserView).FullName }, { BaseItemKind.Video, typeof(Video).FullName }, { BaseItemKind.Year, typeof(Year).FullName } - }.AsReadOnly(); + }.ToFrozenDictionary(); } diff --git a/Jellyfin.Data/Entities/BaseItemMetadataField.cs b/Jellyfin.Data/Entities/BaseItemMetadataField.cs index 2f8e910f2..c9d44c046 100644 --- a/Jellyfin.Data/Entities/BaseItemMetadataField.cs +++ b/Jellyfin.Data/Entities/BaseItemMetadataField.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; namespace Jellyfin.Data.Entities; -#pragma warning disable CA2227 /// /// Enum MetadataFields. diff --git a/Jellyfin.Data/Entities/BaseItemTrailerType.cs b/Jellyfin.Data/Entities/BaseItemTrailerType.cs index 7dee20c87..fb31fc8a4 100644 --- a/Jellyfin.Data/Entities/BaseItemTrailerType.cs +++ b/Jellyfin.Data/Entities/BaseItemTrailerType.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; namespace Jellyfin.Data.Entities; -#pragma warning disable CA2227 + /// /// Enum TrailerTypes. /// diff --git a/Jellyfin.Data/Entities/EnumLikeTable.cs b/Jellyfin.Data/Entities/EnumLikeTable.cs deleted file mode 100644 index 11e1d0aa9..000000000 --- a/Jellyfin.Data/Entities/EnumLikeTable.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace Jellyfin.Data.Entities; - -/// -/// Defines an Entity that is modeled after an Enum. -/// -public abstract class EnumLikeTable -{ - /// - /// Gets or Sets Numerical ID of this enumeratable. - /// - public required int Id { get; set; } -} diff --git a/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs b/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs index d1f6f1b18..f57672a2c 100644 --- a/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs +++ b/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs @@ -8,30 +8,30 @@ public enum MediaStreamTypeEntity /// /// The audio. /// - Audio, + Audio = 0, /// /// The video. /// - Video, + Video = 1, /// /// The subtitle. /// - Subtitle, + Subtitle = 2, /// /// The embedded image. /// - EmbeddedImage, + EmbeddedImage = 3, /// /// The data. /// - Data, + Data = 4, /// /// The lyric. /// - Lyric + Lyric = 5 } diff --git a/Jellyfin.Data/Entities/ProgramAudioEntity.cs b/Jellyfin.Data/Entities/ProgramAudioEntity.cs index fafccb13c..5b225a002 100644 --- a/Jellyfin.Data/Entities/ProgramAudioEntity.cs +++ b/Jellyfin.Data/Entities/ProgramAudioEntity.cs @@ -8,30 +8,30 @@ public enum ProgramAudioEntity /// /// Mono. /// - Mono, + Mono = 0, /// /// Sterio. /// - Stereo, + Stereo = 1, /// /// Dolby. /// - Dolby, + Dolby = 2, /// /// DolbyDigital. /// - DolbyDigital, + DolbyDigital = 3, /// /// Thx. /// - Thx, + Thx = 4, /// /// Atmos. /// - Atmos + Atmos = 5 } -- 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 'Emby.Server.Implementations/Data') 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 'Emby.Server.Implementations/Data') 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 'Emby.Server.Implementations/Data') 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 447ff1d23cb818a1f478d6540ec98eb9d180543f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 22 Oct 2024 10:52:34 +0000 Subject: Made Clean task async --- .../Data/CleanDatabaseScheduledTask.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 932bd2b05..6ea7d9197 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -30,11 +30,10 @@ namespace Emby.Server.Implementations.Data public Task Run(IProgress progress, CancellationToken cancellationToken) { - CleanDeadItems(cancellationToken, progress); - return Task.CompletedTask; + return CleanDeadItems(cancellationToken, progress); } - private void CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) { var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { @@ -68,10 +67,10 @@ namespace Emby.Server.Implementations.Data progress.Report(percent * 100); } - using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); - transaction.Commit(); + using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); progress.Report(100); } -- cgit v1.2.3 From 07ed9a3ea4db0d594c35e449da6c1383b821f723 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 28 Oct 2024 09:22:32 +0000 Subject: Updated TryGetGuid for migration --- Emby.Server.Implementations/Data/SqliteExtensions.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 25ef57d27..0efef4ded 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -127,8 +127,16 @@ namespace Emby.Server.Implementations.Data return false; } - result = reader.GetGuid(index); - return true; + try + { + result = reader.GetGuid(index); + return true; + } + catch + { + result = Guid.Empty; + return false; + } } public static bool TryGetString(this SqliteDataReader reader, int index, out string result) -- cgit v1.2.3 From b39553611d0d6702ef657f76573cefa2ee437745 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 17 Nov 2024 11:03:43 +0000 Subject: Applied coding style --- .../Data/CleanDatabaseScheduledTask.cs | 4 ++-- Emby.Server.Implementations/Data/ItemTypeLookup.cs | 6 ------ Emby.Server.Implementations/Library/LibraryManager.cs | 7 ++++--- Emby.Server.Implementations/Library/MediaSourceManager.cs | 2 +- Jellyfin.Api/Controllers/InstantMixController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 4 ++-- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 12 ++++++------ Jellyfin.Server.Implementations/Item/ChapterRepository.cs | 2 +- .../Item/MediaAttachmentRepository.cs | 2 +- .../Item/MediaStreamRepository.cs | 2 +- Jellyfin.Server.Implementations/Item/PeopleRepository.cs | 4 ++-- .../MediaSegments/MediaSegmentManager.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 10 +++++----- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 4 ++-- MediaBrowser.Controller/Library/ILibraryManager.cs | 2 +- MediaBrowser.Providers/Music/ArtistMetadataService.cs | 2 +- src/Jellyfin.LiveTv/Guide/GuideManager.cs | 2 +- 18 files changed, 33 insertions(+), 38 deletions(-) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 6ea7d9197..aceff8b53 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -28,9 +28,9 @@ namespace Emby.Server.Implementations.Data _dbProvider = dbProvider; } - public Task Run(IProgress progress, CancellationToken cancellationToken) + public async Task Run(IProgress progress, CancellationToken cancellationToken) { - return CleanDeadItems(cancellationToken, progress); + await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); } private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) diff --git a/Emby.Server.Implementations/Data/ItemTypeLookup.cs b/Emby.Server.Implementations/Data/ItemTypeLookup.cs index f5db28c7a..82c0a8b6c 100644 --- a/Emby.Server.Implementations/Data/ItemTypeLookup.cs +++ b/Emby.Server.Implementations/Data/ItemTypeLookup.cs @@ -1,12 +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; -using Jellyfin.Server.Implementations; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -14,7 +10,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Data; @@ -23,7 +18,6 @@ public class ItemTypeLookup : IItemTypeLookup { /// public IReadOnlyList MusicGenreTypes { get; } = [ - typeof(Audio).FullName!, typeof(MusicVideo).FullName!, typeof(MusicAlbum).FullName!, diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 7e059be23..7b37011cb 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1810,11 +1810,11 @@ namespace Emby.Server.Implementations.Library /// public void CreateItem(BaseItem item, BaseItem? parent) { - CreateItems(new[] { item }, parent, CancellationToken.None); + CreateOrUpdateItems(new[] { item }, parent, CancellationToken.None); } /// - public void CreateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken) + public void CreateOrUpdateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken) { _itemRepository.SaveItems(items, cancellationToken); @@ -2971,10 +2971,11 @@ namespace Emby.Server.Implementations.Library { if (createEntity) { - CreateItems([personEntity], null, CancellationToken.None); + CreateOrUpdateItems([personEntity], null, CancellationToken.None); } await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false); + CreateOrUpdateItems([personEntity], null, CancellationToken.None); } } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 2fb571a10..d0f5e60f7 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Library list.Add(source); } - return SortMediaSources(list).ToImmutableArray(); + return SortMediaSources(list).ToArray(); } /// > diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index e89e7ce26..87a856d38 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -397,7 +397,7 @@ public class InstantMixController : BaseJellyfinApiController if (limit.HasValue && limit < items.Count) { - items = items.Take(limit.Value).ToImmutableArray(); + items = items.Take(limit.Value).ToArray(); } var result = new QueryResult( diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 907724e04..e709e43e2 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -113,11 +113,11 @@ public class YearsController : BaseJellyfinApiController if (userId.IsNullOrEmpty()) { - items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToImmutableArray(); + items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToArray(); } else { - items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToImmutableArray(); + items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray(); } } else diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 0183685be..8670b06cc 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -117,7 +117,7 @@ public sealed class BaseItemRepository( PrepareFilterQuery(filter); using var context = dbProvider.CreateDbContext(); - return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToImmutableArray(); + return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray(); } /// @@ -216,7 +216,7 @@ public sealed class BaseItemRepository( dbQuery = ApplyGroupingFilter(dbQuery, filter); dbQuery = ApplyQueryPageing(dbQuery, filter); - result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToImmutableArray(); + result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); result.StartIndex = filter.StartIndex ?? 0; return result; } @@ -235,7 +235,7 @@ public sealed class BaseItemRepository( dbQuery = ApplyGroupingFilter(dbQuery, filter); dbQuery = ApplyQueryPageing(dbQuery, filter); - return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToImmutableArray(); + return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); } private IQueryable ApplyGroupingFilter(IQueryable dbQuery, InternalItemsQuery filter) @@ -831,7 +831,7 @@ public sealed class BaseItemRepository( } // query = query.DistinctBy(e => e.CleanValue); - return query.Select(e => e.ItemValue.CleanValue).ToImmutableArray(); + return query.Select(e => e.ItemValue.CleanValue).ToArray(); } private static bool TypeRequiresDeserialization(Type type) @@ -976,10 +976,10 @@ public sealed class BaseItemRepository( }); result.StartIndex = filter.StartIndex ?? 0; - result.Items = resultQuery.ToImmutableArray().Where(e => e is not null).Select(e => + result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e => { return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount); - }).ToImmutableArray(); + }).ToArray(); return result; } diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index dc55484c9..16e8c205d 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -73,7 +73,7 @@ public class ChapterRepository : IChapterRepository }) .ToList() .Select(e => Map(e.chapter, e.baseItemPath!)) - .ToImmutableArray(); + .ToArray(); } /// diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs index c6488f321..155798209 100644 --- a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs @@ -40,7 +40,7 @@ public class MediaAttachmentRepository(IDbContextFactory dbPr query = query.Where(e => e.Index == filter.Index); } - return query.AsEnumerable().Select(Map).ToImmutableArray(); + return query.AsEnumerable().Select(Map).ToArray(); } private MediaAttachment Map(AttachmentStreamInfo attachment) diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index 0617dd81e..d6bfc1a8f 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -51,7 +51,7 @@ public class MediaStreamRepository : IMediaStreamRepository public IReadOnlyList GetMediaStreams(MediaStreamQuery filter) { using var context = _dbProvider.CreateDbContext(); - return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToImmutableArray(); + return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray(); } private string? GetPathToSave(string? path) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 417212ba4..d1823514a 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -36,7 +36,7 @@ public class PeopleRepository(IDbContextFactory dbProvider, I dbQuery = dbQuery.Take(filter.Limit); } - return dbQuery.AsEnumerable().Select(Map).ToImmutableArray(); + return dbQuery.AsEnumerable().Select(Map).ToArray(); } /// @@ -51,7 +51,7 @@ public class PeopleRepository(IDbContextFactory dbProvider, I dbQuery = dbQuery.Take(filter.Limit); } - return dbQuery.Select(e => e.Name).ToImmutableArray(); + return dbQuery.Select(e => e.Name).ToArray(); } /// diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 151b616f7..d0f41c6fa 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -154,7 +154,7 @@ public class MediaSegmentManager : IMediaSegmentManager return query .OrderBy(e => e.StartTicks) .AsNoTracking() - .ToImmutableArray() + .ToArray() .Select(Map); } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d92407a3f..a6bc35a9f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1092,7 +1092,7 @@ namespace MediaBrowser.Controller.Entities return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => i, new MediaSourceWidthComparator()) - .ToImmutableArray(); + .ToArray(); } protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8fff7dbc4..a13f04614 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.Entities if (newItems.Count > 0) { - LibraryManager.CreateItems(newItems, this, cancellationToken); + LibraryManager.CreateOrUpdateItems(newItems, this, cancellationToken); } } else @@ -1306,7 +1306,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, includeLinkedChildren, result, false, query); - return result.Values.ToImmutableArray(); + return result.Values.ToArray(); } protected virtual IEnumerable GetEligibleChildrenForRecursiveChildren(User user) @@ -1379,7 +1379,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, true, result, true, query); - return result.Values.ToImmutableArray(); + return result.Values.ToArray(); } /// @@ -1407,7 +1407,7 @@ namespace MediaBrowser.Controller.Entities AddChildrenToList(result, includeLinkedChildren, true, filter); - return result.Values.ToImmutableArray(); + return result.Values.ToArray(); } /// @@ -1563,7 +1563,7 @@ namespace MediaBrowser.Controller.Entities return LinkedChildren .Select(i => new Tuple(i, GetLinkedChild(i))) .Where(i => i.Item2 is not null) - .ToImmutableArray(); + .ToArray(); } protected override async Task RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList fileSystemChildren, CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index cb17e3faf..d0c9f049a 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -131,13 +131,13 @@ namespace MediaBrowser.Controller.Entities.Movies public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { var children = base.GetChildren(user, includeLinkedChildren, query); - return Sort(children, user).ToImmutableArray(); + return Sort(children, user).ToArray(); } public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query) { var children = base.GetRecursiveChildren(user, query); - return Sort(children, user).ToImmutableArray(); + return Sort(children, user).ToArray(); } public BoxSetInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 47b1cb16e..8fcd5f605 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.Library /// Items to create. /// Parent of new items. /// CancellationToken to use for operation. - void CreateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken); + void CreateOrUpdateItems(IReadOnlyList items, BaseItem? parent, CancellationToken cancellationToken); /// /// Updates the item. diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs index 8af6de925..c47f9a500 100644 --- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs +++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Providers.Music Recursive = true, IsFolder = false }) - : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder).ToImmutableArray(); + : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder); } } } diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index f657422a0..ff31b7123 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -265,7 +265,7 @@ public class GuideManager : IGuideManager if (newPrograms.Count > 0) { - _libraryManager.CreateItems(newPrograms, null, cancellationToken); + _libraryManager.CreateOrUpdateItems(newPrograms, null, cancellationToken); await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false); } -- cgit v1.2.3 From b33810534b85f96702035a54a4c661cc4d31d928 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 15 Jan 2025 20:12:41 +0000 Subject: Applied review comments --- .../Data/CleanDatabaseScheduledTask.cs | 14 ++++++++++---- .../Library/UserDataManager.cs | 12 +++++++++--- .../Item/BaseItemRepository.cs | 19 ++++++++++--------- .../Item/ChapterRepository.cs | 2 +- .../Migrations/Routines/MigrateLibraryDb.cs | 15 ++++++++------- src/Jellyfin.Drawing/ImageProcessor.cs | 10 +++++----- .../Controllers/LibraryStructureControllerTests.cs | 2 +- 7 files changed, 44 insertions(+), 30 deletions(-) (limited to 'Emby.Server.Implementations/Data') diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index aceff8b53..7ea863d76 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -67,10 +67,16 @@ namespace Emby.Server.Implementations.Data progress.Report(percent * 100); } - using var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + } progress.Report(100); } diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 9b3a0c1f9..cc45f2fcb 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -146,8 +146,8 @@ namespace Emby.Server.Implementations.Library { ItemId = itemId, CustomDataKey = dto.Key, - Item = null!, - User = null!, + Item = null, + User = null, AudioStreamIndex = dto.AudioStreamIndex, IsFavorite = dto.IsFavorite, LastPlayedDate = dto.LastPlayedDate, @@ -181,7 +181,13 @@ namespace Emby.Server.Implementations.Library private UserItemData? GetUserData(User user, Guid itemId, List keys) { var cacheKey = GetCacheKey(user.InternalId, itemId); - var data = GetUserDataInternal(user.Id, itemId, keys); + + if (_userData.TryGetValue(cacheKey, out var data)) + { + return data; + } + + data = GetUserDataInternal(user.Id, itemId, keys); if (data is null) { diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 01e23f56d..1eca0713d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -125,7 +125,7 @@ public sealed class BaseItemRepository transaction.Commit(); } - /// + /// public IReadOnlyList GetItemIdsList(InternalItemsQuery filter) { ArgumentNullException.ThrowIfNull(filter); @@ -201,7 +201,7 @@ public sealed class BaseItemRepository _itemTypeLookup.MusicGenreTypes); } - /// + /// public QueryResult GetItems(InternalItemsQuery filter) { ArgumentNullException.ThrowIfNull(filter); @@ -235,7 +235,7 @@ public sealed class BaseItemRepository return result; } - /// + /// public IReadOnlyList GetItemList(InternalItemsQuery filter) { ArgumentNullException.ThrowIfNull(filter); @@ -354,12 +354,14 @@ public sealed class BaseItemRepository { ArgumentException.ThrowIfNullOrEmpty(typeName); + // TODO: this isn't great. Refactor later to be both globally handled by a dedicated service not just an static variable and be loaded eagar. + // currently this is done so that plugins may introduce their own type of baseitems as we dont know when we are first called, before or after plugins are loaded return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(k)) .FirstOrDefault(t => t is not null)); } - /// + /// public void SaveImages(BaseItemDto item) { ArgumentNullException.ThrowIfNull(item); @@ -373,13 +375,13 @@ public sealed class BaseItemRepository transaction.Commit(); } - /// + /// public void SaveItems(IReadOnlyList items, CancellationToken cancellationToken) { UpdateOrInsertItems(items, cancellationToken); } - /// + /// public void UpdateOrInsertItems(IReadOnlyList items, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(items); @@ -479,7 +481,7 @@ public sealed class BaseItemRepository transaction.Commit(); } - /// + /// public BaseItemDto? RetrieveItem(Guid id) { if (id.IsEmpty()) @@ -890,8 +892,7 @@ public sealed class BaseItemRepository { try { - using var dataAsStream = new MemoryStream(Encoding.UTF8.GetBytes(baseItemEntity.Data!)); - dto = JsonSerializer.Deserialize(dataAsStream, type, JsonDefaults.Options) as BaseItemDto; + dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto; } catch (JsonException ex) { diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index 16e8c205d..fc6f04d56 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -71,7 +71,7 @@ public class ChapterRepository : IChapterRepository chapter = e, baseItemPath = e.Item.Path }) - .ToList() + .AsEnumerable() .Select(e => Map(e.chapter, e.baseItemPath!)) .ToArray(); } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 8b2664ecd..d0360a56d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1,3 +1,5 @@ +#pragma warning disable RS0030 // Do not use banned APIs + using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -21,7 +23,6 @@ using Microsoft.Extensions.Logging; using Chapter = Jellyfin.Data.Entities.Chapter; namespace Jellyfin.Server.Migrations.Routines; -#pragma warning disable RS0030 // Do not use banned APIs /// /// The migration routine for migrating the userdata database to EF Core. @@ -80,7 +81,7 @@ public class MigrateLibraryDb : IMigrationRoutine stopwatch.Restart(); _logger.LogInformation("Start moving TypedBaseItem."); - var typedBaseItemsQuery = """ + const string typedBaseItemsQuery = """ SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie, IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage, PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber, @@ -111,7 +112,7 @@ public class MigrateLibraryDb : IMigrationRoutine _logger.LogInformation("Start moving ItemValues."); // do not migrate inherited types as they are now properly mapped in search and lookup. - var itemValueQuery = + const string itemValueQuery = """ SELECT ItemId, Type, Value, CleanValue FROM ItemValues WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId) @@ -187,7 +188,7 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.SaveChanges(); _logger.LogInformation("Start moving MediaStreamInfos."); - var mediaStreamQuery = """ + const string mediaStreamQuery = """ SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path, IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width, AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag, @@ -211,7 +212,7 @@ public class MigrateLibraryDb : IMigrationRoutine stopwatch.Restart(); _logger.LogInformation("Start moving People."); - var personsQuery = """ + const string personsQuery = """ SELECT ItemId, Name, Role, PersonType, SortOrder FROM People WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId) """; @@ -268,7 +269,7 @@ public class MigrateLibraryDb : IMigrationRoutine stopwatch.Restart(); _logger.LogInformation("Start moving Chapters."); - var chapterQuery = """ + const string chapterQuery = """ SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2 WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId) """; @@ -287,7 +288,7 @@ public class MigrateLibraryDb : IMigrationRoutine stopwatch.Restart(); _logger.LogInformation("Start moving AncestorIds."); - var ancestorIdsQuery = """ + const string ancestorIdsQuery = """ SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId) diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 7ba9ff172..0bd3b8920 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net.Mime; +using System.Reflection.Metadata.Ecma335; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -410,11 +411,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable /// public string GetImageCacheTag(BaseItem item, ItemImageInfo image) - => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + => GetImageCacheTag(item.Path, image.DateModified); /// public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image) - => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + => GetImageCacheTag(item.Path, image.DateModified); /// public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter) @@ -424,7 +425,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return null; } - return (item.Path + chapter.ImageDateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); + return GetImageCacheTag(item.Path, chapter.ImageDateModified); } /// @@ -451,8 +452,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable return null; } - return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5() - .ToString("N", CultureInfo.InvariantCulture); + return GetImageCacheTag(user.ProfileImage.Path, user.ProfileImage.LastModified); } private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs index 0376f57cc..e7166d424 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs @@ -45,7 +45,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture