Bläddra i källkod

Merge pull request #12798 from JPVenson/feature/EFUserData

Refactor library.db into jellyfin.db and EFCore
Joshua M. Boniface 4 månader sedan
förälder
incheckning
93b8eade61
100 ändrade filer med 19559 tillägg och 7012 borttagningar
  1. 3 0
      .editorconfig
  2. 13 8
      Emby.Server.Implementations/ApplicationHost.cs
  3. 0 269
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  4. 24 6
      Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
  5. 64 0
      Emby.Server.Implementations/Data/ItemTypeLookup.cs
  6. 0 62
      Emby.Server.Implementations/Data/ManagedConnection.cs
  7. 10 2
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  8. 0 5971
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  9. 0 369
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  10. 0 30
      Emby.Server.Implementations/Data/SynchronousMode.cs
  11. 0 23
      Emby.Server.Implementations/Data/TempStoreMode.cs
  12. 8 4
      Emby.Server.Implementations/Dto/DtoService.cs
  13. 6 0
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  14. 33 29
      Emby.Server.Implementations/Library/LibraryManager.cs
  15. 22 20
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  16. 10 16
      Emby.Server.Implementations/Library/MusicManager.cs
  17. 1 1
      Emby.Server.Implementations/Library/SearchEngine.cs
  18. 109 30
      Emby.Server.Implementations/Library/UserDataManager.cs
  19. 2 2
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  20. 2 0
      Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
  21. 7 2
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  22. 7 2
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  23. 7 9
      Jellyfin.Api/Controllers/InstantMixController.cs
  24. 4 4
      Jellyfin.Api/Controllers/ItemsController.cs
  25. 1 3
      Jellyfin.Api/Controllers/LibraryController.cs
  26. 3 0
      Jellyfin.Api/Controllers/LibraryStructureController.cs
  27. 1 2
      Jellyfin.Api/Controllers/MoviesController.cs
  28. 5 5
      Jellyfin.Api/Controllers/PlaystateController.cs
  29. 17 11
      Jellyfin.Api/Controllers/UserLibraryController.cs
  30. 4 3
      Jellyfin.Api/Controllers/YearsController.cs
  31. 1 1
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  32. 29 0
      Jellyfin.Data/Entities/AncestorId.cs
  33. 49 0
      Jellyfin.Data/Entities/AttachmentStreamInfo.cs
  34. 186 0
      Jellyfin.Data/Entities/BaseItemEntity.cs
  35. 18 0
      Jellyfin.Data/Entities/BaseItemExtraType.cs
  36. 59 0
      Jellyfin.Data/Entities/BaseItemImageInfo.cs
  37. 24 0
      Jellyfin.Data/Entities/BaseItemMetadataField.cs
  38. 32 0
      Jellyfin.Data/Entities/BaseItemProvider.cs
  39. 24 0
      Jellyfin.Data/Entities/BaseItemTrailerType.cs
  40. 44 0
      Jellyfin.Data/Entities/Chapter.cs
  41. 76 0
      Jellyfin.Data/Entities/ImageInfoImageType.cs
  42. 37 0
      Jellyfin.Data/Entities/ItemValue.cs
  43. 30 0
      Jellyfin.Data/Entities/ItemValueMap.cs
  44. 38 0
      Jellyfin.Data/Entities/ItemValueType.cs
  45. 103 0
      Jellyfin.Data/Entities/MediaStreamInfo.cs
  46. 37 0
      Jellyfin.Data/Entities/MediaStreamTypeEntity.cs
  47. 32 0
      Jellyfin.Data/Entities/People.cs
  48. 44 0
      Jellyfin.Data/Entities/PeopleBaseItemMap.cs
  49. 37 0
      Jellyfin.Data/Entities/ProgramAudioEntity.cs
  50. 92 0
      Jellyfin.Data/Entities/UserData.cs
  51. 0 10
      Jellyfin.Data/Enums/ItemSortBy.cs
  52. 1 1
      Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
  53. 2176 0
      Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
  54. 123 0
      Jellyfin.Server.Implementations/Item/ChapterRepository.cs
  55. 73 0
      Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
  56. 213 0
      Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
  57. 186 0
      Jellyfin.Server.Implementations/Item/PeopleRepository.cs
  58. 86 10
      Jellyfin.Server.Implementations/JellyfinDbContext.cs
  59. 1607 0
      Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs
  60. 639 0
      Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs
  61. 1610 0
      Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs
  62. 28 0
      Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs
  63. 1610 0
      Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs
  64. 54 0
      Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs
  65. 1603 0
      Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs
  66. 49 0
      Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs
  67. 1600 0
      Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs
  68. 702 0
      Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs
  69. 1594 0
      Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs
  70. 144 0
      Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs
  71. 1595 0
      Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs
  72. 37 0
      Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs
  73. 2 1
      Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs
  74. 862 82
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  75. 21 0
      Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
  76. 17 0
      Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs
  77. 59 0
      Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs
  78. 22 0
      Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs
  79. 20 0
      Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs
  80. 22 0
      Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs
  81. 19 0
      Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs
  82. 19 0
      Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
  83. 20 0
      Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
  84. 22 0
      Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
  85. 22 0
      Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
  86. 20 0
      Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs
  87. 23 0
      Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs
  88. 1 1
      Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
  89. 2 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  90. 1201 0
      Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
  91. 8 0
      Jellyfin.Server/Program.cs
  92. 11 0
      MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs
  93. 0 19
      MediaBrowser.Controller/Chapters/IChapterManager.cs
  94. 49 0
      MediaBrowser.Controller/Chapters/IChapterRepository.cs
  95. 25 0
      MediaBrowser.Controller/Drawing/IImageProcessor.cs
  96. 1 1
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  97. 1 0
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  98. 2 1
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  99. 2 1
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  100. 1 0
      MediaBrowser.Controller/Entities/AudioBook.cs

+ 3 - 0
.editorconfig

@@ -527,3 +527,6 @@ dotnet_diagnostic.CA2234.severity = suggestion
 
 # disable warning xUnit1028: Test methods must have a supported return type.
 dotnet_diagnostic.xUnit1028.severity = none
+
+# CA1826: Do not use Enumerable methods on indexable collections
+dotnet_diagnostic.CA1826.severity = suggestion

+ 13 - 8
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;
@@ -268,6 +268,11 @@ namespace Emby.Server.Implementations
 
         public string ExpandVirtualPath(string path)
         {
+            if (path is null)
+            {
+                return null;
+            }
+
             var appPaths = ApplicationPaths;
 
             return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
@@ -492,10 +497,14 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
 
-            serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+            serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
+            serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
+            serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
+            serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
+            serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
+            serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
 
             serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
             serviceCollection.AddSingleton<EncodingHelper>();
@@ -540,8 +549,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
-
             serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
             serviceCollection.AddSingleton<IAuthService, AuthService>();
@@ -579,9 +586,6 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
-            ((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
-
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
 
@@ -635,6 +639,7 @@ namespace Emby.Server.Implementations
             BaseItem.ProviderManager = Resolve<IProviderManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
             BaseItem.ItemRepository = Resolve<IItemRepository>();
+            BaseItem.ChapterRepository = Resolve<IChapterRepository>();
             BaseItem.FileSystem = Resolve<IFileSystem>();
             BaseItem.UserDataManager = Resolve<IUserDataManager>();
             BaseItem.ChannelManager = Resolve<IChannelManager>();

+ 0 - 269
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -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;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
-        /// </summary>
-        /// <param name="logger">The logger.</param>
-        protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
-        {
-            Logger = logger;
-        }
-
-        /// <summary>
-        /// Gets or sets the path to the DB file.
-        /// </summary>
-        protected string DbFilePath { get; set; }
-
-        /// <summary>
-        /// Gets the logger.
-        /// </summary>
-        /// <value>The logger.</value>
-        protected ILogger<BaseSqliteRepository> Logger { get; }
-
-        /// <summary>
-        /// Gets the cache size.
-        /// </summary>
-        /// <value>The cache size or null.</value>
-        protected virtual int? CacheSize => null;
-
-        /// <summary>
-        /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
-        /// </summary>
-        protected virtual string LockingMode => "NORMAL";
-
-        /// <summary>
-        /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
-        /// </summary>
-        /// <value>The journal mode.</value>
-        protected virtual string JournalMode => "WAL";
-
-        /// <summary>
-        /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
-        /// The default (-1) is overridden to prevent unconstrained WAL size, as reported by users.
-        /// </summary>
-        /// <value>The journal size limit.</value>
-        protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
-
-        /// <summary>
-        /// Gets the page size.
-        /// </summary>
-        /// <value>The page size or null.</value>
-        protected virtual int? PageSize => null;
-
-        /// <summary>
-        /// Gets the temp store mode.
-        /// </summary>
-        /// <value>The temp store mode.</value>
-        /// <see cref="TempStoreMode"/>
-        protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
-
-        /// <summary>
-        /// Gets the synchronous mode.
-        /// </summary>
-        /// <value>The synchronous mode or null.</value>
-        /// <see cref="SynchronousMode"/>
-        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<string> GetColumnNames(ManagedConnection connection, string table)
-        {
-            var columnNames = new List<string>();
-
-            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<string> 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);
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        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;
-        }
-    }
-}

+ 24 - 6
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,20 +16,24 @@ namespace Emby.Server.Implementations.Data
     {
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+        private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 
-        public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
+        public CleanDatabaseScheduledTask(
+            ILibraryManager libraryManager,
+            ILogger<CleanDatabaseScheduledTask> logger,
+            IDbContextFactory<JellyfinDbContext> dbProvider)
         {
             _libraryManager = libraryManager;
             _logger = logger;
+            _dbProvider = dbProvider;
         }
 
-        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
         {
-            CleanDeadItems(cancellationToken, progress);
-            return Task.CompletedTask;
+            await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
         }
 
-        private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+        private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
         {
             var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
             {
@@ -34,7 +41,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 +67,17 @@ namespace Emby.Server.Implementations.Data
                 progress.Report(percent * 100);
             }
 
+            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);
         }
     }

+ 64 - 0
Emby.Server.Implementations/Data/ItemTypeLookup.cs

@@ -0,0 +1,64 @@
+using System.Collections.Frozen;
+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;
+
+namespace Emby.Server.Implementations.Data;
+
+/// <inheritdoc />
+public class ItemTypeLookup : IItemTypeLookup
+{
+    /// <inheritdoc />
+    public IReadOnlyList<string> MusicGenreTypes { get; } = [
+         typeof(Audio).FullName!,
+         typeof(MusicVideo).FullName!,
+         typeof(MusicAlbum).FullName!,
+         typeof(MusicArtist).FullName!,
+    ];
+
+    /// <inheritdoc />
+    public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
+    {
+        { 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();
+}

+ 0 - 62
Emby.Server.Implementations/Data/ManagedConnection.cs

@@ -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<SqliteDataReader> 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;
-    }
-}

+ 10 - 2
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)

+ 0 - 5971
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -1,5971 +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
-{
-    /// <summary>
-    /// Class SQLiteItemRepository.
-    /// </summary>
-    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;
-
-        private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
-
-        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 ";
-
-        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<BaseItemKind, string> _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 }
-        };
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
-        /// </summary>
-        /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
-        /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
-        /// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
-        /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
-        /// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
-        /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
-        /// <exception cref="ArgumentNullException">config is null.</exception>
-        public SqliteItemRepository(
-            IServerConfigurationManager config,
-            IServerApplicationHost appHost,
-            ILogger<SqliteItemRepository> 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();
-        }
-
-        /// <inheritdoc />
-        protected override int? CacheSize { get; }
-
-        /// <inheritdoc />
-        protected override TempStoreMode TempStore => TempStoreMode.Memory;
-
-        /// <summary>
-        /// Opens the connection to the database.
-        /// </summary>
-        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();
-            }
-        }
-
-        /// <inheritdoc />
-        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();
-        }
-
-        /// <summary>
-        /// Saves the items.
-        /// </summary>
-        /// <param name="items">The items.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <exception cref="ArgumentNullException">
-        /// <paramref name="items"/> or <paramref name="cancellationToken"/> is <c>null</c>.
-        /// </exception>
-        public void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken)
-        {
-            ArgumentNullException.ThrowIfNull(items);
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            CheckDisposed();
-
-            var itemsLen = items.Count;
-            var tuples = new ValueTuple<BaseItem, List<Guid>, BaseItem, string, List<string>>[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<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
-        {
-            using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
-            using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
-            {
-                var requiresReset = false;
-                foreach (var tuple in tuples)
-                {
-                    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<string, string> 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<ItemImageInfo>();
-            }
-
-            // 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<ItemImageInfo>();
-            }
-
-            // 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<char> value)
-        {
-            const char Delimiter = '*';
-
-            var nextSegment = value.IndexOf(Delimiter);
-            if (nextSegment == -1)
-            {
-                return null;
-            }
-
-            ReadOnlySpan<char> path = value[..nextSegment];
-            value = value[(nextSegment + 1)..];
-            nextSegment = value.IndexOf(Delimiter);
-            if (nextSegment == -1)
-            {
-                return null;
-            }
-
-            ReadOnlySpan<char> dateModified = value[..nextSegment];
-            value = value[(nextSegment + 1)..];
-            nextSegment = value.IndexOf(Delimiter);
-            if (nextSegment == -1)
-            {
-                nextSegment = value.Length;
-            }
-
-            ReadOnlySpan<char> 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<char> widthSpan = value[..nextSegment];
-
-                value = value[(nextSegment + 1)..];
-                nextSegment = value.IndexOf(Delimiter);
-                if (nextSegment == -1)
-                {
-                    nextSegment = value.Length;
-                }
-
-                ReadOnlySpan<char> 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<char> 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;
-        }
-
-        /// <summary>
-        /// Internal retrieve from items or users table.
-        /// </summary>
-        /// <param name="id">The id.</param>
-        /// <returns>BaseItem.</returns>
-        /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
-        /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
-        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 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 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<MetadataField> fields = null;
-                    foreach (var i in lockedFields.AsSpan().Split('|'))
-                    {
-                        if (Enum.TryParse(i, true, out MetadataField parsedValue))
-                        {
-                            (fields ??= new List<MetadataField>()).Add(parsedValue);
-                        }
-                    }
-
-                    item.LockedFields = fields?.ToArray() ?? Array.Empty<MetadataField>();
-                }
-            }
-
-            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<TrailerType> types = null;
-                        foreach (var i in trailerTypes.AsSpan().Split('|'))
-                        {
-                            if (Enum.TryParse(i, true, out TrailerType parsedValue))
-                            {
-                                (types ??= new List<TrailerType>()).Add(parsedValue);
-                            }
-                        }
-
-                        trailer.TrailerTypes = types?.ToArray() ?? Array.Empty<TrailerType>();
-                    }
-                }
-
-                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;
-        }
-
-        /// <inheritdoc />
-        public List<ChapterInfo> GetChapters(BaseItem item)
-        {
-            CheckDisposed();
-
-            var chapters = new List<ChapterInfo>();
-            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;
-        }
-
-        /// <inheritdoc />
-        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;
-        }
-
-        /// <summary>
-        /// Gets the chapter.
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <param name="item">The item.</param>
-        /// <returns>ChapterInfo.</returns>
-        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;
-        }
-
-        /// <summary>
-        /// Saves the chapters.
-        /// </summary>
-        /// <param name="id">The item id.</param>
-        /// <param name="chapters">The chapters.</param>
-        public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> 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<ChapterInfo> 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)
-            {
-                return false;
-            }
-
-            var sortingFields = new HashSet<ItemSortBy>(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 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<string> 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;
-        }
-
-        /// <inheritdoc />
-        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<string> { "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();
-            }
-        }
-
-        /// <inheritdoc />
-        public List<BaseItem> 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<BaseItem>();
-            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<BaseItem>();
-
-                foreach (var item in items)
-                {
-                    AddItem(newList, item);
-
-                    if (newList.Count >= limit)
-                    {
-                        break;
-                    }
-                }
-
-                items = newList;
-            }
-
-            return items;
-        }
-
-        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 void AddItem(List<BaseItem> 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);
-        }
-
-        /// <inheritdoc />
-        public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
-        {
-            ArgumentNullException.ThrowIfNull(query);
-
-            CheckDisposed();
-
-            if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0))
-            {
-                var returnList = GetItemList(query);
-                return new QueryResult<BaseItem>(
-                    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<string> columnsToSelect;
-                if (EnableGroupByPresentationUniqueKey(query))
-                {
-                    columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
-                }
-                else if (query.GroupBySeriesPresentationUniqueKey)
-                {
-                    columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
-                }
-                else
-                {
-                    columnsToSelect = new List<string> { "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<BaseItem>();
-            var result = new QueryResult<BaseItem>();
-            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;
-            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"
-            };
-        }
-
-        /// <inheritdoc />
-        public List<Guid> GetItemIdsList(InternalItemsQuery query)
-        {
-            ArgumentNullException.ThrowIfNull(query);
-
-            CheckDisposed();
-
-            var columns = new List<string> { "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<Guid>();
-            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);
-        }
-
-#nullable enable
-        private List<string> GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement)
-        {
-            if (query.IsResumable ?? false)
-            {
-                query.IsVirtualItem = false;
-            }
-
-            var minWidth = query.MinWidth;
-            var maxWidth = query.MaxWidth;
-
-            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<string>();
-
-            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<byte> 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<string>();
-                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<string>();
-                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<string>();
-
-                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<string>();
-
-                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<string>();
-
-                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<string>();
-
-                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<string>();
-
-                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;
-        }
-
-        /// <summary>
-        /// Formats a where clause for the specified provider.
-        /// </summary>
-        /// <param name="includeResults">Whether or not to include items with this provider's ids.</param>
-        /// <param name="provider">Provider name.</param>
-        /// <returns>Formatted SQL clause.</returns>
-        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<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
-        {
-            var list = new List<string>();
-
-            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))
-            {
-                return value;
-            }
-
-            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);
-        }
-
-        /// <inheritdoc />
-        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();
-        }
-
-        /// <inheritdoc />
-        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();
-            }
-        }
-
-        /// <inheritdoc />
-        public List<string> 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<string>();
-            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;
-        }
-
-        /// <inheritdoc />
-        public List<PersonInfo> 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<PersonInfo>();
-            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<string> GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement)
-        {
-            var whereClauses = new List<string>();
-
-            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<Guid> 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();
-            }
-        }
-
-        /// <inheritdoc />
-        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
-        {
-            return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
-        }
-
-        /// <inheritdoc />
-        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
-        {
-            return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
-        }
-
-        /// <inheritdoc />
-        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
-        {
-            return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
-        }
-
-        /// <inheritdoc />
-        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
-        {
-            return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
-        }
-
-        /// <inheritdoc />
-        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
-        {
-            return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
-        }
-
-        /// <inheritdoc />
-        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
-        {
-            return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
-        }
-
-        /// <inheritdoc />
-        public List<string> GetStudioNames()
-        {
-            return GetItemValueNames(new[] { 3 }, Array.Empty<string>(), Array.Empty<string>());
-        }
-
-        /// <inheritdoc />
-        public List<string> GetAllArtistNames()
-        {
-            return GetItemValueNames(new[] { 0, 1 }, Array.Empty<string>(), Array.Empty<string>());
-        }
-
-        /// <inheritdoc />
-        public List<string> GetMusicGenreNames()
-        {
-            return GetItemValueNames(
-                new[] { 2 },
-                new string[]
-                {
-                    typeof(Audio).FullName,
-                    typeof(MusicVideo).FullName,
-                    typeof(MusicAlbum).FullName,
-                    typeof(MusicArtist).FullName
-                },
-                Array.Empty<string>());
-        }
-
-        /// <inheritdoc />
-        public List<string> GetGenreNames()
-        {
-            return GetItemValueNames(
-                new[] { 2 },
-                Array.Empty<string>(),
-                new string[]
-                {
-                    typeof(Audio).FullName,
-                    typeof(MusicVideo).FullName,
-                    typeof(MusicAlbum).FullName,
-                    typeof(MusicArtist).FullName
-                });
-        }
-
-        private List<string> GetItemValueNames(int[] itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> 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<string>();
-            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<string> 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<string> { "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<string> 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;
-            }
-        }
-
-        /// <inheritdoc />
-        public void UpdatePeople(Guid itemId, List<PersonInfo> 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<PersonInfo> 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))
-            {
-                item.SortOrder = sortOrder;
-            }
-
-            return item;
-        }
-
-        /// <inheritdoc />
-        public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
-        {
-            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<MediaStream>();
-
-                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;
-            }
-        }
-
-        /// <inheritdoc />
-        public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken)
-        {
-            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();
-        }
-
-        private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
-        {
-            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++)
-                {
-                    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(')');
-                }
-
-                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 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;
-            }
-        }
-
-        /// <summary>
-        /// Gets the media stream.
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <returns>MediaStream.</returns>
-        private MediaStream GetMediaStream(SqliteDataReader reader)
-        {
-            var item = new MediaStream
-            {
-                Index = reader.GetInt32(1),
-                Type = Enum.Parse<MediaStreamType>(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;
-            }
-
-            if (reader.TryGetInt32(39, out var rpuPresentFlag))
-            {
-                item.RpuPresentFlag = rpuPresentFlag;
-            }
-
-            if (reader.TryGetInt32(40, out var elPresentFlag))
-            {
-                item.ElPresentFlag = elPresentFlag;
-            }
-
-            if (reader.TryGetInt32(41, out var blPresentFlag))
-            {
-                item.BlPresentFlag = blPresentFlag;
-            }
-
-            if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
-            {
-                item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
-            }
-
-            item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
-
-            if (reader.TryGetInt32(44, out var rotation))
-            {
-                item.Rotation = rotation;
-            }
-
-            if (item.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
-            {
-                item.LocalizedDefault = _localization.GetLocalizedString("Default");
-                item.LocalizedExternal = _localization.GetLocalizedString("External");
-
-                if (item.Type is MediaStreamType.Subtitle)
-                {
-                    item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
-                    item.LocalizedForced = _localization.GetLocalizedString("Forced");
-                    item.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
-                }
-            }
-
-            return item;
-        }
-
-        /// <inheritdoc />
-        public List<MediaAttachment> 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<MediaAttachment>();
-            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;
-        }
-
-        /// <inheritdoc />
-        public void SaveMediaAttachments(
-            Guid id,
-            IReadOnlyList<MediaAttachment> 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<MediaAttachment> 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;
-            }
-        }
-
-        /// <summary>
-        /// Gets the attachment.
-        /// </summary>
-        /// <param name="reader">The reader.</param>
-        /// <returns>MediaAttachment.</returns>
-        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;
-        }
-
-        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
-        {
-            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);
-                }
-            }
-        }
-    }
-}

+ 0 - 369
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -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<SqliteUserDataRepository> logger,
-            IServerConfigurationManager config,
-            IUserManager userManager)
-            : base(logger)
-        {
-            _userManager = userManager;
-
-            DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
-        }
-
-        /// <summary>
-        /// Opens the connection to the database.
-        /// </summary>
-        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<User> 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<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
-        {
-            var list = new List<Guid>();
-
-            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;
-        }
-
-        /// <inheritdoc />
-        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);
-        }
-
-        /// <inheritdoc />
-        public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
-        {
-            ArgumentNullException.ThrowIfNull(userData);
-
-            if (userId <= 0)
-            {
-                throw new ArgumentNullException(nameof(userId));
-            }
-
-            PersistAllUserData(userId, userData, cancellationToken);
-        }
-
-        /// <summary>
-        /// Persists the user data.
-        /// </summary>
-        /// <param name="internalUserId">The user id.</param>
-        /// <param name="key">The key.</param>
-        /// <param name="userData">The user data.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        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();
-            }
-        }
-
-        /// <summary>
-        /// Persist all user data for the specified user.
-        /// </summary>
-        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();
-            }
-        }
-
-        /// <summary>
-        /// Gets the user data.
-        /// </summary>
-        /// <param name="userId">The user id.</param>
-        /// <param name="key">The key.</param>
-        /// <returns>Task{UserItemData}.</returns>
-        /// <exception cref="ArgumentNullException">
-        /// userId
-        /// or
-        /// key.
-        /// </exception>
-        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<string> keys)
-        {
-            ArgumentNullException.ThrowIfNull(keys);
-
-            if (keys.Count == 0)
-            {
-                return null;
-            }
-
-            return GetUserData(userId, keys[0]);
-        }
-
-        /// <summary>
-        /// Return all user-data associated with the given user.
-        /// </summary>
-        /// <param name="userId">The internal user id.</param>
-        /// <returns>The list of user item data.</returns>
-        public List<UserItemData> GetAllUserData(long userId)
-        {
-            if (userId <= 0)
-            {
-                throw new ArgumentNullException(nameof(userId));
-            }
-
-            var list = new List<UserItemData>();
-
-            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;
-        }
-
-        /// <summary>
-        /// Read a row from the specified reader into the provided userData object.
-        /// </summary>
-        /// <param name="reader">The list of result set values.</param>
-        /// <returns>The user item data.</returns>
-        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;
-        }
-    }
-}

+ 0 - 30
Emby.Server.Implementations/Data/SynchronousMode.cs

@@ -1,30 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-/// <summary>
-/// The disk synchronization mode, controls how aggressively SQLite will write data
-/// all the way out to physical storage.
-/// </summary>
-public enum SynchronousMode
-{
-    /// <summary>
-    /// SQLite continues without syncing as soon as it has handed data off to the operating system.
-    /// </summary>
-    Off = 0,
-
-    /// <summary>
-    /// SQLite database engine will still sync at the most critical moments.
-    /// </summary>
-    Normal = 1,
-
-    /// <summary>
-    /// 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.
-    /// </summary>
-    Full = 2,
-
-    /// <summary>
-    /// 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.
-    /// </summary>
-    Extra = 3
-}

+ 0 - 23
Emby.Server.Implementations/Data/TempStoreMode.cs

@@ -1,23 +0,0 @@
-namespace Emby.Server.Implementations.Data;
-
-/// <summary>
-/// Storage mode used by temporary database files.
-/// </summary>
-public enum TempStoreMode
-{
-    /// <summary>
-    /// The compile-time C preprocessor macro SQLITE_TEMP_STORE
-    /// is used to determine where temporary tables and indices are stored.
-    /// </summary>
-    Default = 0,
-
-    /// <summary>
-    /// Temporary tables and indices are stored in a file.
-    /// </summary>
-    File = 1,
-
-    /// <summary>
-    /// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
-    /// </summary>
-    Memory = 2
-}

+ 8 - 4
Emby.Server.Implementations/Dto/DtoService.cs

@@ -10,6 +10,7 @@ using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Common;
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -51,6 +52,7 @@ namespace Emby.Server.Implementations.Dto
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
 
         private readonly ITrickplayManager _trickplayManager;
+        private readonly IChapterRepository _chapterRepository;
 
         public DtoService(
             ILogger<DtoService> logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
             IApplicationHost appHost,
             IMediaSourceManager mediaSourceManager,
             Lazy<ILiveTvManager> livetvManagerFactory,
-            ITrickplayManager trickplayManager)
+            ITrickplayManager trickplayManager,
+            IChapterRepository chapterRepository)
         {
             _logger = logger;
             _libraryManager = libraryManager;
@@ -76,6 +79,7 @@ namespace Emby.Server.Implementations.Dto
             _mediaSourceManager = mediaSourceManager;
             _livetvManagerFactory = livetvManagerFactory;
             _trickplayManager = trickplayManager;
+            _chapterRepository = chapterRepository;
         }
 
         private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -165,7 +169,7 @@ namespace Emby.Server.Implementations.Dto
             return dto;
         }
 
-        private static IList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
+        private static IReadOnlyList<BaseItem> GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
         {
             return byName.GetTaggedItems(
                 new InternalItemsQuery(user)
@@ -327,7 +331,7 @@ namespace Emby.Server.Implementations.Dto
             return dto;
         }
 
-        private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
+        private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
         {
             if (item is MusicArtist)
             {
@@ -1060,7 +1064,7 @@ namespace Emby.Server.Implementations.Dto
 
                 if (options.ContainsField(ItemFields.Chapters))
                 {
-                    dto.Chapters = _itemRepo.GetChapters(item);
+                    dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList();
                 }
 
                 if (options.ContainsField(ItemFields.Trickplay))

+ 6 - 0
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -144,9 +144,15 @@ namespace Emby.Server.Implementations.EntryPoints
                     .Select(i =>
                     {
                         var dto = _userDataManager.GetUserDataDto(i, user);
+                        if (dto is null)
+                        {
+                            return null!;
+                        }
+
                         dto.ItemId = i.Id;
                         return dto;
                     })
+                    .Where(e => e is not null)
                     .ToArray()
             };
         }

+ 33 - 29
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -76,6 +76,7 @@ namespace Emby.Server.Implementations.Library
         private readonly IItemRepository _itemRepository;
         private readonly IImageProcessor _imageProcessor;
         private readonly NamingOptions _namingOptions;
+        private readonly IPeopleRepository _peopleRepository;
         private readonly ExtraResolver _extraResolver;
 
         /// <summary>
@@ -112,6 +113,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="directoryService">The directory service.</param>
+        /// <param name="peopleRepository">The People Repository.</param>
         public LibraryManager(
             IServerApplicationHost appHost,
             ILoggerFactory loggerFactory,
@@ -127,7 +129,8 @@ namespace Emby.Server.Implementations.Library
             IItemRepository itemRepository,
             IImageProcessor imageProcessor,
             NamingOptions namingOptions,
-            IDirectoryService directoryService)
+            IDirectoryService directoryService,
+            IPeopleRepository peopleRepository)
         {
             _appHost = appHost;
             _logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -144,7 +147,7 @@ namespace Emby.Server.Implementations.Library
             _imageProcessor = imageProcessor;
             _cache = new ConcurrentDictionary<Guid, BaseItem>();
             _namingOptions = namingOptions;
-
+            _peopleRepository = peopleRepository;
             _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
 
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@@ -1274,7 +1277,7 @@ namespace Emby.Server.Implementations.Library
             return ItemIsVisible(item, user) ? item : null;
         }
 
-        public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
+        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
         {
             if (query.Recursive && !query.ParentId.IsEmpty())
             {
@@ -1300,7 +1303,7 @@ namespace Emby.Server.Implementations.Library
             return itemList;
         }
 
-        public List<BaseItem> GetItemList(InternalItemsQuery query)
+        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
         {
             return GetItemList(query, true);
         }
@@ -1324,7 +1327,7 @@ namespace Emby.Server.Implementations.Library
             return _itemRepository.GetCount(query);
         }
 
-        public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
+        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
         {
             SetTopParentIdsOrAncestors(query, parents);
 
@@ -1357,7 +1360,7 @@ namespace Emby.Server.Implementations.Library
                 _itemRepository.GetItemList(query));
         }
 
-        public List<Guid> GetItemIds(InternalItemsQuery query)
+        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
         {
             if (query.User is not null)
             {
@@ -1807,11 +1810,11 @@ namespace Emby.Server.Implementations.Library
         /// <inheritdoc />
         public void CreateItem(BaseItem item, BaseItem? parent)
         {
-            CreateItems(new[] { item }, parent, CancellationToken.None);
+            CreateOrUpdateItems(new[] { item }, parent, CancellationToken.None);
         }
 
         /// <inheritdoc />
-        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
+        public void CreateOrUpdateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
         {
             _itemRepository.SaveItems(items, cancellationToken);
 
@@ -1955,13 +1958,13 @@ namespace Emby.Server.Implementations.Library
         /// <inheritdoc />
         public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
+            _itemRepository.SaveItems(items, cancellationToken);
+
             foreach (var item in items)
             {
                 await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
             }
 
-            _itemRepository.SaveItems(items, cancellationToken);
-
             if (ItemUpdated is not null)
             {
                 foreach (var item in items)
@@ -2736,12 +2739,12 @@ namespace Emby.Server.Implementations.Library
             return path;
         }
 
-        public List<PersonInfo> GetPeople(InternalPeopleQuery query)
+        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
         {
-            return _itemRepository.GetPeople(query);
+            return _peopleRepository.GetPeople(query);
         }
 
-        public List<PersonInfo> GetPeople(BaseItem item)
+        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
         {
             if (item.SupportsPeople)
             {
@@ -2756,12 +2759,12 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            return new List<PersonInfo>();
+            return [];
         }
 
-        public List<Person> GetPeopleItems(InternalPeopleQuery query)
+        public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
         {
-            return _itemRepository.GetPeopleNames(query)
+            return _peopleRepository.GetPeopleNames(query)
             .Select(i =>
             {
                 try
@@ -2779,9 +2782,9 @@ namespace Emby.Server.Implementations.Library
             .ToList()!; // null values are filtered out
         }
 
-        public List<string> GetPeopleNames(InternalPeopleQuery query)
+        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
         {
-            return _itemRepository.GetPeopleNames(query);
+            return _peopleRepository.GetPeopleNames(query);
         }
 
         public void UpdatePeople(BaseItem item, List<PersonInfo> people)
@@ -2790,16 +2793,17 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <inheritdoc />
-        public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
+        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken)
         {
             if (!item.SupportsPeople)
             {
                 return;
             }
 
-            _itemRepository.UpdatePeople(item.Id, people);
             if (people is not null)
             {
+                people = people.Where(e => e is not null).ToArray();
+                _peopleRepository.UpdatePeople(item.Id, people);
                 await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
             }
         }
@@ -2914,14 +2918,13 @@ namespace Emby.Server.Implementations.Library
 
         private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
         {
-            List<BaseItem>? personsToSave = null;
-
             foreach (var person in people)
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
                 var itemUpdateType = ItemUpdateType.MetadataDownload;
                 var saveEntity = false;
+                var createEntity = false;
                 var personEntity = GetPerson(person.Name);
 
                 if (personEntity is null)
@@ -2938,6 +2941,7 @@ namespace Emby.Server.Implementations.Library
 
                     personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
                     saveEntity = true;
+                    createEntity = true;
                 }
 
                 foreach (var id in person.ProviderIds)
@@ -2965,15 +2969,15 @@ namespace Emby.Server.Implementations.Library
 
                 if (saveEntity)
                 {
-                    (personsToSave ??= new()).Add(personEntity);
+                    if (createEntity)
+                    {
+                        CreateOrUpdateItems([personEntity], null, CancellationToken.None);
+                    }
+
                     await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
+                    CreateOrUpdateItems([personEntity], null, CancellationToken.None);
                 }
             }
-
-            if (personsToSave is not null)
-            {
-                CreateItems(personsToSave, null, CancellationToken.None);
-            }
         }
 
         private void StartScanInBackground()
@@ -3027,7 +3031,7 @@ namespace Emby.Server.Implementations.Library
             {
                 var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 
-                libraryOptions.PathInfos = [..libraryOptions.PathInfos, pathInfo];
+                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 
                 SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 

+ 22 - 20
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -5,6 +5,7 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -51,7 +52,8 @@ namespace Emby.Server.Implementations.Library
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
         private readonly IDirectoryService _directoryService;
-
+        private readonly IMediaStreamRepository _mediaStreamRepository;
+        private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
         private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -69,7 +71,9 @@ namespace Emby.Server.Implementations.Library
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
             IMediaEncoder mediaEncoder,
-            IDirectoryService directoryService)
+            IDirectoryService directoryService,
+            IMediaStreamRepository mediaStreamRepository,
+            IMediaAttachmentRepository mediaAttachmentRepository)
         {
             _appHost = appHost;
             _itemRepo = itemRepo;
@@ -82,6 +86,8 @@ namespace Emby.Server.Implementations.Library
             _localizationManager = localizationManager;
             _appPaths = applicationPaths;
             _directoryService = directoryService;
+            _mediaStreamRepository = mediaStreamRepository;
+            _mediaAttachmentRepository = mediaAttachmentRepository;
         }
 
         public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -89,9 +95,9 @@ namespace Emby.Server.Implementations.Library
             _providers = providers.ToArray();
         }
 
-        public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
+        public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
         {
-            var list = _itemRepo.GetMediaStreams(query);
+            var list = _mediaStreamRepository.GetMediaStreams(query);
 
             foreach (var stream in list)
             {
@@ -121,7 +127,7 @@ namespace Emby.Server.Implementations.Library
             return false;
         }
 
-        public List<MediaStream> GetMediaStreams(Guid itemId)
+        public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
         {
             var list = GetMediaStreams(new MediaStreamQuery
             {
@@ -131,7 +137,7 @@ namespace Emby.Server.Implementations.Library
             return GetMediaStreamsForItem(list);
         }
 
-        private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
+        private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
         {
             foreach (var stream in streams)
             {
@@ -145,13 +151,13 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <inheritdoc />
-        public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
+        public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
         {
-            return _itemRepo.GetMediaAttachments(query);
+            return _mediaAttachmentRepository.GetMediaAttachments(query);
         }
 
         /// <inheritdoc />
-        public List<MediaAttachment> GetMediaAttachments(Guid itemId)
+        public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
         {
             return GetMediaAttachments(new MediaAttachmentQuery
             {
@@ -159,7 +165,7 @@ namespace Emby.Server.Implementations.Library
             });
         }
 
-        public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
+        public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
         {
             var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
 
@@ -212,7 +218,7 @@ namespace Emby.Server.Implementations.Library
                 list.Add(source);
             }
 
-            return SortMediaSources(list);
+            return SortMediaSources(list).ToArray();
         }
 
         /// <inheritdoc />>
@@ -332,7 +338,7 @@ namespace Emby.Server.Implementations.Library
             return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
         }
 
-        public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
+        public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
         {
             ArgumentNullException.ThrowIfNull(item);
 
@@ -453,7 +459,7 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
+        private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
         {
             return sources.OrderBy(i =>
             {
@@ -470,8 +476,7 @@ namespace Emby.Server.Implementations.Library
 
                 return stream?.Width ?? 0;
             })
-            .Where(i => i.Type != MediaSourceType.Placeholder)
-            .ToList();
+            .Where(i => i.Type != MediaSourceType.Placeholder);
         }
 
         public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -806,7 +811,7 @@ namespace Emby.Server.Implementations.Library
             return result.Item1;
         }
 
-        public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
+        public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
         {
             var stream = new MediaSourceInfo
             {
@@ -829,10 +834,7 @@ namespace Emby.Server.Implementations.Library
             await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
                 .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
 
-            return new List<MediaSourceInfo>
-            {
-                stream
-            };
+            return [stream];
         }
 
         public async Task CloseLiveStream(string id)

+ 10 - 16
Emby.Server.Implementations/Library/MusicManager.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
@@ -24,30 +25,23 @@ namespace Emby.Server.Implementations.Library
             _libraryManager = libraryManager;
         }
 
-        public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
         {
-            var list = new List<BaseItem>
-            {
-                item
-            };
-
-            list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
-
-            return list;
+            return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
         }
 
         /// <inheritdoc />
-        public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
         {
             return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
         }
 
-        public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
         {
             return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
         }
 
-        public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
         {
             var genres = item
                .GetRecursiveChildren(user, new InternalItemsQuery(user)
@@ -63,12 +57,12 @@ namespace Emby.Server.Implementations.Library
             return GetInstantMixFromGenres(genres, user, dtoOptions);
         }
 
-        public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
         {
             return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
         }
 
-        public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
         {
             var genreIds = genres.DistinctNames().Select(i =>
             {
@@ -85,7 +79,7 @@ namespace Emby.Server.Implementations.Library
             return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
         }
 
-        public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
         {
             return _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
@@ -97,7 +91,7 @@ namespace Emby.Server.Implementations.Library
             });
         }
 
-        public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
+        public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
         {
             if (item is MusicGenre)
             {

+ 1 - 1
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -171,7 +171,7 @@ namespace Emby.Server.Implementations.Library
                 }
             };
 
-            List<BaseItem> mediaItems;
+            IReadOnlyList<BaseItem> mediaItems;
 
             if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
             {

+ 109 - 30
Emby.Server.Implementations/Library/UserDataManager.cs

@@ -1,17 +1,21 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.Globalization;
+using System.Linq;
 using System.Threading;
 using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
+using Jellyfin.Server.Implementations;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using Microsoft.EntityFrameworkCore;
 using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
 using Book = MediaBrowser.Controller.Entities.Book;
 
@@ -26,22 +30,18 @@ namespace Emby.Server.Implementations.Library
             new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
 
         private readonly IServerConfigurationManager _config;
-        private readonly IUserManager _userManager;
-        private readonly IUserDataRepository _repository;
+        private readonly IDbContextFactory<JellyfinDbContext> _repository;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UserDataManager"/> class.
         /// </summary>
         /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
-        /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
-        /// <param name="repository">Instance of the <see cref="IUserDataRepository"/> interface.</param>
+        /// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
         public UserDataManager(
             IServerConfigurationManager config,
-            IUserManager userManager,
-            IUserDataRepository repository)
+            IDbContextFactory<JellyfinDbContext> repository)
         {
             _config = config;
-            _userManager = userManager;
             _repository = repository;
         }
 
@@ -59,13 +59,27 @@ namespace Emby.Server.Implementations.Library
 
             var keys = item.GetUserDataKeys();
 
-            var userId = user.InternalId;
+            using var dbContext = _repository.CreateDbContext();
+            using var transaction = dbContext.Database.BeginTransaction();
 
             foreach (var key in keys)
             {
-                _repository.SaveUserData(userId, key, userData, cancellationToken);
+                userData.Key = key;
+                var userDataEntry = Map(userData, user.Id, item.Id);
+                if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
+                {
+                    dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
+                }
+                else
+                {
+                    dbContext.UserData.Add(userDataEntry);
+                }
             }
 
+            dbContext.SaveChanges();
+            transaction.Commit();
+
+            var userId = user.InternalId;
             var cacheKey = GetCacheKey(userId, item.Id);
             _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
 
@@ -86,7 +100,7 @@ namespace Emby.Server.Implementations.Library
             ArgumentNullException.ThrowIfNull(item);
             ArgumentNullException.ThrowIfNull(userDataDto);
 
-            var userData = GetUserData(user, item);
+            var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null.");
 
             if (userDataDto.PlaybackPositionTicks.HasValue)
             {
@@ -126,33 +140,91 @@ namespace Emby.Server.Implementations.Library
             SaveUserData(user, item, userData, reason, CancellationToken.None);
         }
 
-        private UserItemData GetUserData(User user, Guid itemId, List<string> keys)
+        private UserData Map(UserItemData dto, Guid userId, Guid itemId)
         {
-            var userId = user.InternalId;
-
-            var cacheKey = GetCacheKey(userId, itemId);
+            return new UserData()
+            {
+                ItemId = itemId,
+                CustomDataKey = dto.Key,
+                Item = null,
+                User = null,
+                AudioStreamIndex = dto.AudioStreamIndex,
+                IsFavorite = dto.IsFavorite,
+                LastPlayedDate = dto.LastPlayedDate,
+                Likes = dto.Likes,
+                PlaybackPositionTicks = dto.PlaybackPositionTicks,
+                PlayCount = dto.PlayCount,
+                Played = dto.Played,
+                Rating = dto.Rating,
+                UserId = userId,
+                SubtitleStreamIndex = dto.SubtitleStreamIndex,
+            };
+        }
 
-            return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
+        private UserItemData Map(UserData dto)
+        {
+            return new UserItemData()
+            {
+                Key = dto.CustomDataKey!,
+                AudioStreamIndex = dto.AudioStreamIndex,
+                IsFavorite = dto.IsFavorite,
+                LastPlayedDate = dto.LastPlayedDate,
+                Likes = dto.Likes,
+                PlaybackPositionTicks = dto.PlaybackPositionTicks,
+                PlayCount = dto.PlayCount,
+                Played = dto.Played,
+                Rating = dto.Rating,
+                SubtitleStreamIndex = dto.SubtitleStreamIndex,
+            };
         }
 
-        private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
+        private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
         {
-            var userData = _repository.GetUserData(internalUserId, keys);
+            var cacheKey = GetCacheKey(user.InternalId, itemId);
 
-            if (userData is not null)
+            if (_userData.TryGetValue(cacheKey, out var data))
             {
-                return userData;
+                return data;
             }
 
-            if (keys.Count > 0)
+            data = GetUserDataInternal(user.Id, itemId, keys);
+
+            if (data is null)
             {
-                return new UserItemData
+                return new UserItemData()
                 {
-                    Key = keys[0]
+                    Key = keys[0],
                 };
             }
 
-            throw new UnreachableException();
+            return _userData.GetOrAdd(cacheKey, data);
+        }
+
+        private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
+        {
+            if (keys.Count == 0)
+            {
+                return null;
+            }
+
+            using var context = _repository.CreateDbContext();
+            var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
+
+            if (userData.Length > 0)
+            {
+                var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
+                if (directDataReference is not null)
+                {
+                    return Map(directDataReference);
+                }
+
+                return Map(userData.First());
+            }
+
+            return new UserItemData
+            {
+                Key = keys.Last()!
+            };
         }
 
         /// <summary>
@@ -165,20 +237,25 @@ namespace Emby.Server.Implementations.Library
         }
 
         /// <inheritdoc />
-        public UserItemData GetUserData(User user, BaseItem item)
+        public UserItemData? GetUserData(User user, BaseItem item)
         {
             return GetUserData(user, item.Id, item.GetUserDataKeys());
         }
 
         /// <inheritdoc />
-        public UserItemDataDto GetUserDataDto(BaseItem item, User user)
+        public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
             => GetUserDataDto(item, null, user, new DtoOptions());
 
         /// <inheritdoc />
-        public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
+        public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
         {
             var userData = GetUserData(user, item);
-            var dto = GetUserItemDataDto(userData);
+            if (userData is null)
+            {
+                return null;
+            }
+
+            var dto = GetUserItemDataDto(userData, item.Id);
 
             item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
             return dto;
@@ -188,9 +265,10 @@ namespace Emby.Server.Implementations.Library
         /// Converts a UserItemData to a DTOUserItemData.
         /// </summary>
         /// <param name="data">The data.</param>
+        /// <param name="itemId">The reference key to an Item.</param>
         /// <returns>DtoUserItemData.</returns>
         /// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
-        private UserItemDataDto GetUserItemDataDto(UserItemData data)
+        private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
         {
             ArgumentNullException.ThrowIfNull(data);
 
@@ -203,6 +281,7 @@ namespace Emby.Server.Implementations.Library
                 Rating = data.Rating,
                 Played = data.Played,
                 LastPlayedDate = data.LastPlayedDate,
+                ItemId = itemId,
                 Key = data.Key
             };
         }

+ 2 - 2
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.MediaEncoder
         private readonly IFileSystem _fileSystem;
         private readonly ILogger<EncodingManager> _logger;
         private readonly IMediaEncoder _encoder;
-        private readonly IChapterManager _chapterManager;
+        private readonly IChapterRepository _chapterManager;
         private readonly ILibraryManager _libraryManager;
 
         /// <summary>
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.MediaEncoder
             ILogger<EncodingManager> logger,
             IFileSystem fileSystem,
             IMediaEncoder encoder,
-            IChapterManager chapterManager,
+            IChapterRepository chapterManager,
             ILibraryManager libraryManager)
         {
             _logger = logger;

+ 2 - 0
Emby.Server.Implementations/Playlists/PlaylistsFolder.cs

@@ -5,12 +5,14 @@ using System.Linq;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using MediaBrowser.Common;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Model.Querying;
 
 namespace Emby.Server.Implementations.Playlists
 {
+    [RequiresSourceSerialisation]
     public class PlaylistsFolder : BasePluginFolder
     {
         public PlaylistsFolder()

+ 7 - 2
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -32,6 +33,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         private readonly IEncodingManager _encodingManager;
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
+        private readonly IChapterRepository _chapterRepository;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
@@ -43,6 +45,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
         /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+        /// <param name="chapterRepository">Instance of the <see cref="IChapterRepository"/> interface.</param>
         public ChapterImagesTask(
             ILogger<ChapterImagesTask> logger,
             ILibraryManager libraryManager,
@@ -50,7 +53,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             IApplicationPaths appPaths,
             IEncodingManager encodingManager,
             IFileSystem fileSystem,
-            ILocalizationManager localization)
+            ILocalizationManager localization,
+            IChapterRepository chapterRepository)
         {
             _logger = logger;
             _libraryManager = libraryManager;
@@ -59,6 +63,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             _encodingManager = encodingManager;
             _fileSystem = fileSystem;
             _localization = localization;
+            _chapterRepository = chapterRepository;
         }
 
         /// <inheritdoc />
@@ -141,7 +146,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
                 try
                 {
-                    var chapters = _itemRepo.GetChapters(video);
+                    var chapters = _chapterRepository.GetChapters(video.Id);
 
                     var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false);
 

+ 7 - 2
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.TV
                 .ToList();
 
             // Avoid implicitly captured closure
-            var episodes = GetNextUpEpisodes(request, user, items, options);
+            var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options);
 
             return GetResult(episodes, request);
         }
@@ -262,7 +262,7 @@ namespace Emby.Server.Implementations.TV
                 {
                     var userData = _userDataManager.GetUserData(user, nextEpisode);
 
-                    if (userData.PlaybackPositionTicks > 0)
+                    if (userData?.PlaybackPositionTicks > 0)
                     {
                         return null;
                     }
@@ -275,6 +275,11 @@ namespace Emby.Server.Implementations.TV
             {
                 var userData = _userDataManager.GetUserData(user, lastWatchedEpisode);
 
+                if (userData is null)
+                {
+                    return (DateTime.MinValue, GetEpisode);
+                }
+
                 var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
 
                 return (lastWatchedDate, GetEpisode);

+ 7 - 9
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.ComponentModel.DataAnnotations;
+using System.Linq;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
@@ -389,23 +391,19 @@ public class InstantMixController : BaseJellyfinApiController
         return GetResult(items, user, limit, dtoOptions);
     }
 
-    private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
+    private QueryResult<BaseItemDto> GetResult(IReadOnlyList<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
     {
-        var list = items;
+        var totalCount = items.Count;
 
-        var totalCount = list.Count;
-
-        if (limit.HasValue && limit < list.Count)
+        if (limit.HasValue && limit < items.Count)
         {
-            list = list.GetRange(0, limit.Value);
+            items = items.Take(limit.Value).ToArray();
         }
 
-        var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
-
         var result = new QueryResult<BaseItemDto>(
             0,
             totalCount,
-            returnList);
+            _dtoService.GetBaseItemDtos(items, dtoOptions, user));
 
         return result;
     }

+ 4 - 4
Jellyfin.Api/Controllers/ItemsController.cs

@@ -967,7 +967,7 @@ public class ItemsController : BaseJellyfinApiController
     [HttpGet("UserItems/{itemId}/UserData")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public ActionResult<UserItemDataDto> GetItemUserData(
+    public ActionResult<UserItemDataDto?> GetItemUserData(
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
     {
@@ -1005,7 +1005,7 @@ public class ItemsController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Obsolete("Kept for backwards compatibility")]
     [ApiExplorerSettings(IgnoreApi = true)]
-    public ActionResult<UserItemDataDto> GetItemUserDataLegacy(
+    public ActionResult<UserItemDataDto?> GetItemUserDataLegacy(
         [FromRoute, Required] Guid userId,
         [FromRoute, Required] Guid itemId)
         => GetItemUserData(userId, itemId);
@@ -1022,7 +1022,7 @@ public class ItemsController : BaseJellyfinApiController
     [HttpPost("UserItems/{itemId}/UserData")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public ActionResult<UserItemDataDto> UpdateItemUserData(
+    public ActionResult<UserItemDataDto?> UpdateItemUserData(
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId,
         [FromBody, Required] UpdateUserItemDataDto userDataDto)
@@ -1064,7 +1064,7 @@ public class ItemsController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Obsolete("Kept for backwards compatibility")]
     [ApiExplorerSettings(IgnoreApi = true)]
-    public ActionResult<UserItemDataDto> UpdateItemUserDataLegacy(
+    public ActionResult<UserItemDataDto?> UpdateItemUserDataLegacy(
         [FromRoute, Required] Guid userId,
         [FromRoute, Required] Guid itemId,
         [FromBody, Required] UpdateUserItemDataDto userDataDto)

+ 1 - 3
Jellyfin.Api/Controllers/LibraryController.cs

@@ -780,11 +780,9 @@ public class LibraryController : BaseJellyfinApiController
             Genres = item.Genres,
             Limit = limit,
             IncludeItemTypes = includeItemTypes.ToArray(),
-            SimilarTo = item,
             DtoOptions = dtoOptions,
             EnableTotalRecordCount = !isMovie ?? true,
             EnableGroupByMetadataKey = isMovie ?? false,
-            MinSimilarityScore = 2 // A remnant from album/artist scoring
         };
 
         // ExcludeArtistIds
@@ -793,7 +791,7 @@ public class LibraryController : BaseJellyfinApiController
             query.ExcludeArtistIds = excludeArtistIds;
         }
 
-        List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
+        var itemsResult = _libraryManager.GetItemList(query);
 
         var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
 

+ 3 - 0
Jellyfin.Api/Controllers/LibraryStructureController.cs

@@ -99,6 +99,7 @@ public class LibraryStructureController : BaseJellyfinApiController
     /// <param name="name">The name of the folder.</param>
     /// <param name="refreshLibrary">Whether to refresh the library.</param>
     /// <response code="204">Folder removed.</response>
+    /// <response code="404">Folder not found.</response>
     /// <returns>A <see cref="NoContentResult"/>.</returns>
     [HttpDelete]
     [ProducesResponseType(StatusCodes.Status204NoContent)]
@@ -106,7 +107,9 @@ public class LibraryStructureController : BaseJellyfinApiController
         [FromQuery] string name,
         [FromQuery] bool refreshLibrary = false)
     {
+        // TODO: refactor! this relies on an FileNotFound exception to return NotFound when attempting to remove a library that does not exist.
         await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
+
         return NoContent();
     }
 

+ 1 - 2
Jellyfin.Api/Controllers/MoviesController.cs

@@ -120,7 +120,7 @@ public class MoviesController : BaseJellyfinApiController
             DtoOptions = dtoOptions
         });
 
-        var mostRecentMovies = recentlyPlayedMovies.GetRange(0, Math.Min(recentlyPlayedMovies.Count, 6));
+        var mostRecentMovies = recentlyPlayedMovies.Take(Math.Min(recentlyPlayedMovies.Count, 6)).ToList();
         // Get recently played directors
         var recentDirectors = GetDirectors(mostRecentMovies)
             .ToList();
@@ -276,7 +276,6 @@ public class MoviesController : BaseJellyfinApiController
                 Limit = itemLimit,
                 IncludeItemTypes = itemTypes.ToArray(),
                 IsMovie = true,
-                SimilarTo = item,
                 EnableGroupByMetadataKey = true,
                 DtoOptions = dtoOptions
             });

+ 5 - 5
Jellyfin.Api/Controllers/PlaystateController.cs

@@ -72,7 +72,7 @@ public class PlaystateController : BaseJellyfinApiController
     [HttpPost("UserPlayedItems/{itemId}")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public async Task<ActionResult<UserItemDataDto>> MarkPlayedItem(
+    public async Task<ActionResult<UserItemDataDto?>> MarkPlayedItem(
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId,
         [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@@ -121,7 +121,7 @@ public class PlaystateController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Obsolete("Kept for backwards compatibility")]
     [ApiExplorerSettings(IgnoreApi = true)]
-    public Task<ActionResult<UserItemDataDto>> MarkPlayedItemLegacy(
+    public Task<ActionResult<UserItemDataDto?>> MarkPlayedItemLegacy(
         [FromRoute, Required] Guid userId,
         [FromRoute, Required] Guid itemId,
         [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
@@ -138,7 +138,7 @@ public class PlaystateController : BaseJellyfinApiController
     [HttpDelete("UserPlayedItems/{itemId}")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public async Task<ActionResult<UserItemDataDto>> MarkUnplayedItem(
+    public async Task<ActionResult<UserItemDataDto?>> MarkUnplayedItem(
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
     {
@@ -185,7 +185,7 @@ public class PlaystateController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     [Obsolete("Kept for backwards compatibility")]
     [ApiExplorerSettings(IgnoreApi = true)]
-    public Task<ActionResult<UserItemDataDto>> MarkUnplayedItemLegacy(
+    public Task<ActionResult<UserItemDataDto?>> MarkUnplayedItemLegacy(
         [FromRoute, Required] Guid userId,
         [FromRoute, Required] Guid itemId)
         => MarkUnplayedItem(userId, itemId);
@@ -502,7 +502,7 @@ public class PlaystateController : BaseJellyfinApiController
     /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
     /// <param name="datePlayed">The date played.</param>
     /// <returns>Task.</returns>
-    private UserItemDataDto UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
+    private UserItemDataDto? UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
     {
         if (wasPlayed)
         {

+ 17 - 11
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -305,7 +305,7 @@ public class UserLibraryController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
     [HttpDelete("UserItems/{itemId}/Rating")]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<UserItemDataDto> DeleteUserItemRating(
+    public ActionResult<UserItemDataDto?> DeleteUserItemRating(
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId)
     {
@@ -338,7 +338,7 @@ public class UserLibraryController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status200OK)]
     [Obsolete("Kept for backwards compatibility")]
     [ApiExplorerSettings(IgnoreApi = true)]
-    public ActionResult<UserItemDataDto> DeleteUserItemRatingLegacy(
+    public ActionResult<UserItemDataDto?> DeleteUserItemRatingLegacy(
         [FromRoute, Required] Guid userId,
         [FromRoute, Required] Guid itemId)
         => DeleteUserItemRating(userId, itemId);
@@ -353,7 +353,7 @@ public class UserLibraryController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
     [HttpPost("UserItems/{itemId}/Rating")]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<UserItemDataDto> UpdateUserItemRating(
+    public ActionResult<UserItemDataDto?> UpdateUserItemRating(
         [FromQuery] Guid? userId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] bool? likes)
@@ -388,7 +388,7 @@ public class UserLibraryController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status200OK)]
     [Obsolete("Kept for backwards compatibility")]
     [ApiExplorerSettings(IgnoreApi = true)]
-    public ActionResult<UserItemDataDto> UpdateUserItemRatingLegacy(
+    public ActionResult<UserItemDataDto?> UpdateUserItemRatingLegacy(
         [FromRoute, Required] Guid userId,
         [FromRoute, Required] Guid itemId,
         [FromQuery] bool? likes)
@@ -662,12 +662,15 @@ public class UserLibraryController : BaseJellyfinApiController
         // Get the user data for this item
         var data = _userDataRepository.GetUserData(user, item);
 
-        // Set favorite status
-        data.IsFavorite = isFavorite;
+        if (data is not null)
+        {
+            // Set favorite status
+            data.IsFavorite = isFavorite;
 
-        _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+        }
 
-        return _userDataRepository.GetUserDataDto(item, user);
+        return _userDataRepository.GetUserDataDto(item, user)!;
     }
 
     /// <summary>
@@ -676,14 +679,17 @@ public class UserLibraryController : BaseJellyfinApiController
     /// <param name="user">The user.</param>
     /// <param name="item">The item.</param>
     /// <param name="likes">if set to <c>true</c> [likes].</param>
-    private UserItemDataDto UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
+    private UserItemDataDto? UpdateUserItemRatingInternal(User user, BaseItem item, bool? likes)
     {
         // Get the user data for this item
         var data = _userDataRepository.GetUserData(user, item);
 
-        data.Likes = likes;
+        if (data is not null)
+        {
+            data.Likes = likes;
 
-        _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+            _userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
+        }
 
         return _userDataRepository.GetUserDataDto(item, user);
     }

+ 4 - 3
Jellyfin.Api/Controllers/YearsController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using Jellyfin.Api.Extensions;
@@ -105,18 +106,18 @@ public class YearsController : BaseJellyfinApiController
 
         bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
 
-        IList<BaseItem> items;
+        IReadOnlyList<BaseItem> items;
         if (parentItem.IsFolder)
         {
             var folder = (Folder)parentItem;
 
             if (userId.IsNullOrEmpty())
             {
-                items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
+                items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToArray();
             }
             else
             {
-                items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
+                items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray();
             }
         }
         else

+ 1 - 1
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -132,7 +132,7 @@ public static class StreamingHelpers
 
                 mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
                     ? mediaSources[0]
-                    : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
+                    : mediaSources.FirstOrDefault(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
 
                 if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
                 {

+ 29 - 0
Jellyfin.Data/Entities/AncestorId.cs

@@ -0,0 +1,29 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Represents the relational informations for an <see cref="BaseItemEntity"/>.
+/// </summary>
+public class AncestorId
+{
+    /// <summary>
+    /// Gets or Sets the AncestorId.
+    /// </summary>
+    public required Guid ParentItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the related BaseItem.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the ParentItem.
+    /// </summary>
+    public required BaseItemEntity ParentItem { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Child item.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+}

+ 49 - 0
Jellyfin.Data/Entities/AttachmentStreamInfo.cs

@@ -0,0 +1,49 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Provides informations about an Attachment to an <see cref="BaseItemEntity"/>.
+/// </summary>
+public class AttachmentStreamInfo
+{
+    /// <summary>
+    /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets The index within the source file.
+    /// </summary>
+    public required int Index { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the codec of the attachment.
+    /// </summary>
+    public required string Codec { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the codec tag of the attachment.
+    /// </summary>
+    public string? CodecTag { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the comment of the attachment.
+    /// </summary>
+    public string? Comment { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the filename of the attachment.
+    /// </summary>
+    public string? Filename { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the attachments mimetype.
+    /// </summary>
+    public string? MimeType { get; set; }
+}

+ 186 - 0
Jellyfin.Data/Entities/BaseItemEntity.cs

@@ -0,0 +1,186 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities;
+
+public class BaseItemEntity
+{
+    public required Guid Id { get; set; }
+
+    public required string Type { get; set; }
+
+    public string? Data { 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? Studios { 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 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? SeasonName { get; set; }
+
+    public string? ExternalSeriesId { get; set; }
+
+    public string? Tagline { get; set; }
+
+    public string? ProductionLocations { get; set; }
+
+    public string? ExtraIds { get; set; }
+
+    public int? TotalBitrate { get; set; }
+
+    public BaseItemExtraType? 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 ProgramAudioEntity? Audio { get; set; }
+
+    public Guid? ParentId { get; set; }
+
+    public Guid? TopParentId { get; set; }
+
+    public Guid? SeasonId { get; set; }
+
+    public Guid? SeriesId { get; set; }
+
+    public ICollection<PeopleBaseItemMap>? Peoples { get; set; }
+
+    public ICollection<UserData>? UserData { get; set; }
+
+    public ICollection<ItemValueMap>? ItemValues { get; set; }
+
+    public ICollection<MediaStreamInfo>? MediaStreams { get; set; }
+
+    public ICollection<Chapter>? Chapters { get; set; }
+
+    public ICollection<BaseItemProvider>? Provider { get; set; }
+
+    public ICollection<AncestorId>? ParentAncestors { get; set; }
+
+    public ICollection<AncestorId>? Children { get; set; }
+
+    public ICollection<BaseItemMetadataField>? LockedFields { get; set; }
+
+    public ICollection<BaseItemTrailerType>? TrailerTypes { get; set; }
+
+    public ICollection<BaseItemImageInfo>? Images { get; set; }
+
+    // those are references to __LOCAL__ ids not DB ids ... TODO: Bring the whole folder structure into the DB
+    // public ICollection<BaseItemEntity>? SeriesEpisodes { get; set; }
+    // public BaseItemEntity? Series { get; set; }
+    // public BaseItemEntity? Season { get; set; }
+    // public BaseItemEntity? Parent { get; set; }
+    // public ICollection<BaseItemEntity>? DirectChildren { get; set; }
+    // public BaseItemEntity? TopParent { get; set; }
+    // public ICollection<BaseItemEntity>? AllChildren { get; set; }
+    // public ICollection<BaseItemEntity>? SeasonEpisodes { get; set; }
+}

+ 18 - 0
Jellyfin.Data/Entities/BaseItemExtraType.cs

@@ -0,0 +1,18 @@
+#pragma warning disable CS1591
+namespace Jellyfin.Data.Entities;
+
+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
+}

+ 59 - 0
Jellyfin.Data/Entities/BaseItemImageInfo.cs

@@ -0,0 +1,59 @@
+#pragma warning disable CA2227
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemImageInfo
+{
+    /// <summary>
+    /// Gets or Sets.
+    /// </summary>
+    public required Guid Id { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the path to the original image.
+    /// </summary>
+    public required string Path { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the time the image was last modified.
+    /// </summary>
+    public DateTime DateModified { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the imagetype.
+    /// </summary>
+    public ImageInfoImageType ImageType { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the width of the original image.
+    /// </summary>
+    public int Width { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the height of the original image.
+    /// </summary>
+    public int Height { get; set; }
+
+#pragma warning disable CA1819 // Properties should not return arrays
+    /// <summary>
+    /// Gets or Sets the blurhash.
+    /// </summary>
+    public byte[]? Blurhash { get; set; }
+#pragma warning restore CA1819
+
+    /// <summary>
+    /// Gets or Sets the reference id to the BaseItem.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the referenced Item.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+}

+ 24 - 0
Jellyfin.Data/Entities/BaseItemMetadataField.cs

@@ -0,0 +1,24 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum MetadataFields.
+/// </summary>
+public class BaseItemMetadataField
+{
+    /// <summary>
+    /// Gets or Sets Numerical ID of this enumeratable.
+    /// </summary>
+    public required int Id { get; set; }
+
+    /// <summary>
+    /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+}

+ 32 - 0
Jellyfin.Data/Entities/BaseItemProvider.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Represents a Key-Value relation of an BaseItem's provider.
+/// </summary>
+public class BaseItemProvider
+{
+    /// <summary>
+    /// Gets or Sets the reference ItemId.
+    /// </summary>
+    public Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the reference BaseItem.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the ProvidersId.
+    /// </summary>
+    public required string ProviderId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Providers Value.
+    /// </summary>
+    public required string ProviderValue { get; set; }
+}

+ 24 - 0
Jellyfin.Data/Entities/BaseItemTrailerType.cs

@@ -0,0 +1,24 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemTrailerType
+{
+    /// <summary>
+    /// Gets or Sets Numerical ID of this enumeratable.
+    /// </summary>
+    public required int Id { get; set; }
+
+    /// <summary>
+    /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+}

+ 44 - 0
Jellyfin.Data/Entities/Chapter.cs

@@ -0,0 +1,44 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// The Chapter entity.
+/// </summary>
+public class Chapter
+{
+    /// <summary>
+    /// Gets or Sets the <see cref="BaseItemEntity"/> reference id.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the chapters index in Item.
+    /// </summary>
+    public required int ChapterIndex { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the position within the source file.
+    /// </summary>
+    public required long StartPositionTicks { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the common name.
+    /// </summary>
+    public string? Name { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the image path.
+    /// </summary>
+    public string? ImagePath { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the time the image was last modified.
+    /// </summary>
+    public DateTime? ImageDateModified { get; set; }
+}

+ 76 - 0
Jellyfin.Data/Entities/ImageInfoImageType.cs

@@ -0,0 +1,76 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum ImageType.
+/// </summary>
+public enum ImageInfoImageType
+{
+    /// <summary>
+    /// The primary.
+    /// </summary>
+    Primary = 0,
+
+    /// <summary>
+    /// The art.
+    /// </summary>
+    Art = 1,
+
+    /// <summary>
+    /// The backdrop.
+    /// </summary>
+    Backdrop = 2,
+
+    /// <summary>
+    /// The banner.
+    /// </summary>
+    Banner = 3,
+
+    /// <summary>
+    /// The logo.
+    /// </summary>
+    Logo = 4,
+
+    /// <summary>
+    /// The thumb.
+    /// </summary>
+    Thumb = 5,
+
+    /// <summary>
+    /// The disc.
+    /// </summary>
+    Disc = 6,
+
+    /// <summary>
+    /// The box.
+    /// </summary>
+    Box = 7,
+
+    /// <summary>
+    /// The screenshot.
+    /// </summary>
+    /// <remarks>
+    /// This enum value is obsolete.
+    /// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete].
+    /// </remarks>
+    Screenshot = 8,
+
+    /// <summary>
+    /// The menu.
+    /// </summary>
+    Menu = 9,
+
+    /// <summary>
+    /// The chapter image.
+    /// </summary>
+    Chapter = 10,
+
+    /// <summary>
+    /// The box rear.
+    /// </summary>
+    BoxRear = 11,
+
+    /// <summary>
+    /// The user profile image.
+    /// </summary>
+    Profile = 12
+}

+ 37 - 0
Jellyfin.Data/Entities/ItemValue.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Represents an ItemValue for a BaseItem.
+/// </summary>
+public class ItemValue
+{
+    /// <summary>
+    /// Gets or Sets the ItemValueId.
+    /// </summary>
+    public required Guid ItemValueId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Type.
+    /// </summary>
+    public required ItemValueType Type { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Value.
+    /// </summary>
+    public required string Value { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the sanatised Value.
+    /// </summary>
+    public required string CleanValue { get; set; }
+
+    /// <summary>
+    /// Gets or Sets all associated BaseItems.
+    /// </summary>
+#pragma warning disable CA2227 // Collection properties should be read only
+    public ICollection<ItemValueMap>? BaseItemsMap { get; set; }
+#pragma warning restore CA2227 // Collection properties should be read only
+}

+ 30 - 0
Jellyfin.Data/Entities/ItemValueMap.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Mapping table for the ItemValue BaseItem relation.
+/// </summary>
+public class ItemValueMap
+{
+    /// <summary>
+    /// Gets or Sets the ItemId.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the ItemValueId.
+    /// </summary>
+    public required Guid ItemValueId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the referenced <see cref="BaseItemEntity"/>.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the referenced <see cref="ItemValue"/>.
+    /// </summary>
+    public required ItemValue ItemValue { get; set; }
+}

+ 38 - 0
Jellyfin.Data/Entities/ItemValueType.cs

@@ -0,0 +1,38 @@
+#pragma warning disable CA1027 // Mark enums with FlagsAttribute
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Provides the Value types for an <see cref="ItemValue"/>.
+/// </summary>
+public enum ItemValueType
+{
+    /// <summary>
+    /// Artists.
+    /// </summary>
+    Artist = 0,
+
+    /// <summary>
+    /// Album.
+    /// </summary>
+    AlbumArtist = 1,
+
+    /// <summary>
+    /// Genre.
+    /// </summary>
+    Genre = 2,
+
+    /// <summary>
+    /// Studios.
+    /// </summary>
+    Studios = 3,
+
+    /// <summary>
+    /// Tags.
+    /// </summary>
+    Tags = 4,
+
+    /// <summary>
+    /// InheritedTags.
+    /// </summary>
+    InheritedTags = 6,
+}

+ 103 - 0
Jellyfin.Data/Entities/MediaStreamInfo.cs

@@ -0,0 +1,103 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Jellyfin.Data.Entities;
+
+public class MediaStreamInfo
+{
+    public required Guid ItemId { get; set; }
+
+    public required BaseItemEntity Item { get; set; }
+
+    public int StreamIndex { get; set; }
+
+    public required MediaStreamTypeEntity 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 int? BitRate { get; set; }
+
+    public int? Channels { get; set; }
+
+    public int? SampleRate { get; set; }
+
+    public bool IsDefault { get; set; }
+
+    public bool IsForced { get; set; }
+
+    public bool IsExternal { get; set; }
+
+    public int? Height { get; set; }
+
+    public int? Width { get; set; }
+
+    public float? AverageFrameRate { get; set; }
+
+    public float? RealFrameRate { get; set; }
+
+    public float? Level { get; set; }
+
+    public string? PixelFormat { get; set; }
+
+    public int? BitDepth { get; set; }
+
+    public bool? IsAnamorphic { get; set; }
+
+    public int? RefFrames { get; set; }
+
+    public string? CodecTag { get; set; }
+
+    public string? Comment { get; set; }
+
+    public string? NalLengthSize { get; set; }
+
+    public bool? IsAvc { get; set; }
+
+    public string? Title { get; set; }
+
+    public string? TimeBase { get; set; }
+
+    public string? CodecTimeBase { get; set; }
+
+    public string? ColorPrimaries { get; set; }
+
+    public string? ColorSpace { get; set; }
+
+    public string? ColorTransfer { get; set; }
+
+    public int? DvVersionMajor { get; set; }
+
+    public int? DvVersionMinor { get; set; }
+
+    public int? DvProfile { get; set; }
+
+    public int? DvLevel { get; set; }
+
+    public int? RpuPresentFlag { get; set; }
+
+    public int? ElPresentFlag { get; set; }
+
+    public int? BlPresentFlag { get; set; }
+
+    public int? DvBlSignalCompatibilityId { get; set; }
+
+    public bool? IsHearingImpaired { get; set; }
+
+    public int? Rotation { get; set; }
+
+    public string? KeyFrames { get; set; }
+}

+ 37 - 0
Jellyfin.Data/Entities/MediaStreamTypeEntity.cs

@@ -0,0 +1,37 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Enum MediaStreamType.
+/// </summary>
+public enum MediaStreamTypeEntity
+{
+    /// <summary>
+    /// The audio.
+    /// </summary>
+    Audio = 0,
+
+    /// <summary>
+    /// The video.
+    /// </summary>
+    Video = 1,
+
+    /// <summary>
+    /// The subtitle.
+    /// </summary>
+    Subtitle = 2,
+
+    /// <summary>
+    /// The embedded image.
+    /// </summary>
+    EmbeddedImage = 3,
+
+    /// <summary>
+    /// The data.
+    /// </summary>
+    Data = 4,
+
+    /// <summary>
+    /// The lyric.
+    /// </summary>
+    Lyric = 5
+}

+ 32 - 0
Jellyfin.Data/Entities/People.cs

@@ -0,0 +1,32 @@
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// People entity.
+/// </summary>
+public class People
+{
+    /// <summary>
+    /// Gets or Sets the PeopleId.
+    /// </summary>
+    public required Guid Id { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Persons Name.
+    /// </summary>
+    public required string Name { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Type.
+    /// </summary>
+    public string? PersonType { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the mapping of People to BaseItems.
+    /// </summary>
+    public ICollection<PeopleBaseItemMap>? BaseItems { get; set; }
+}

+ 44 - 0
Jellyfin.Data/Entities/PeopleBaseItemMap.cs

@@ -0,0 +1,44 @@
+using System;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Mapping table for People to BaseItems.
+/// </summary>
+public class PeopleBaseItemMap
+{
+    /// <summary>
+    /// Gets or Sets the SortOrder.
+    /// </summary>
+    public int? SortOrder { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the ListOrder.
+    /// </summary>
+    public int? ListOrder { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the Role name the assosiated actor played in the <see cref="BaseItemEntity"/>.
+    /// </summary>
+    public string? Role { get; set; }
+
+    /// <summary>
+    /// Gets or Sets The ItemId.
+    /// </summary>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets Reference Item.
+    /// </summary>
+    public required BaseItemEntity Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets The PeopleId.
+    /// </summary>
+    public required Guid PeopleId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets Reference People.
+    /// </summary>
+    public required People People { get; set; }
+}

+ 37 - 0
Jellyfin.Data/Entities/ProgramAudioEntity.cs

@@ -0,0 +1,37 @@
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Lists types of Audio.
+/// </summary>
+public enum ProgramAudioEntity
+{
+    /// <summary>
+    /// Mono.
+    /// </summary>
+    Mono = 0,
+
+    /// <summary>
+    /// Sterio.
+    /// </summary>
+    Stereo = 1,
+
+    /// <summary>
+    /// Dolby.
+    /// </summary>
+    Dolby = 2,
+
+    /// <summary>
+    /// DolbyDigital.
+    /// </summary>
+    DolbyDigital = 3,
+
+    /// <summary>
+    /// Thx.
+    /// </summary>
+    Thx = 4,
+
+    /// <summary>
+    /// Atmos.
+    /// </summary>
+    Atmos = 5
+}

+ 92 - 0
Jellyfin.Data/Entities/UserData.cs

@@ -0,0 +1,92 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// Provides <see cref="BaseItemEntity"/> and <see cref="User"/> related data.
+/// </summary>
+public class UserData
+{
+    /// <summary>
+    /// Gets or sets the custom data key.
+    /// </summary>
+    /// <value>The rating.</value>
+    public required string CustomDataKey { get; set; }
+
+    /// <summary>
+    /// Gets or sets the users 0-10 rating.
+    /// </summary>
+    /// <value>The rating.</value>
+    public double? Rating { get; set; }
+
+    /// <summary>
+    /// Gets or sets the playback position ticks.
+    /// </summary>
+    /// <value>The playback position ticks.</value>
+    public long PlaybackPositionTicks { get; set; }
+
+    /// <summary>
+    /// Gets or sets the play count.
+    /// </summary>
+    /// <value>The play count.</value>
+    public int PlayCount { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether this instance is favorite.
+    /// </summary>
+    /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
+    public bool IsFavorite { get; set; }
+
+    /// <summary>
+    /// Gets or sets the last played date.
+    /// </summary>
+    /// <value>The last played date.</value>
+    public DateTime? LastPlayedDate { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether this <see cref="UserData" /> is played.
+    /// </summary>
+    /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
+    public bool Played { get; set; }
+
+    /// <summary>
+    /// Gets or sets the index of the audio stream.
+    /// </summary>
+    /// <value>The index of the audio stream.</value>
+    public int? AudioStreamIndex { get; set; }
+
+    /// <summary>
+    /// Gets or sets the index of the subtitle stream.
+    /// </summary>
+    /// <value>The index of the subtitle stream.</value>
+    public int? SubtitleStreamIndex { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the item is liked or not.
+    /// This should never be serialized.
+    /// </summary>
+    /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
+    public bool? Likes { get; set; }
+
+    /// <summary>
+    /// Gets or sets the key.
+    /// </summary>
+    /// <value>The key.</value>
+    public required Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the BaseItem.
+    /// </summary>
+    public required BaseItemEntity? Item { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the UserId.
+    /// </summary>
+    public required Guid UserId { get; set; }
+
+    /// <summary>
+    /// Gets or Sets the User.
+    /// </summary>
+    public required User? User { get; set; }
+}

+ 0 - 10
Jellyfin.Data/Enums/ItemSortBy.cs

@@ -154,14 +154,4 @@ public enum ItemSortBy
     /// The index number.
     /// </summary>
     IndexNumber = 29,
-
-    /// <summary>
-    /// The similarity score.
-    /// </summary>
-    SimilarityScore = 30,
-
-    /// <summary>
-    /// The search score.
-    /// </summary>
-    SearchScore = 31,
 }

+ 1 - 1
Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs

@@ -21,7 +21,7 @@ public static class ServiceCollectionExtensions
         serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
         {
             var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
-            opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
+            opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")};Pooling=false");
         });
 
         return serviceCollection;

+ 2176 - 0
Jellyfin.Server.Implementations/Item/BaseItemRepository.cs

@@ -0,0 +1,2176 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+// Do not enforce that because EFCore cannot deal with cultures well.
+#pragma warning disable CA1304 // Specify CultureInfo
+#pragma warning disable CA1311 // Specify a culture or use an invariant version
+#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Common;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Model.Querying;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
+using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity;
+
+namespace Jellyfin.Server.Implementations.Item;
+
+/*
+    All queries in this class and all other nullable enabled EFCore repository classes will make liberal use of the null-forgiving operator "!".
+    This is done as the code isn't actually executed client side, but only the expressions are interpret and the compiler cannot know that.
+    This is your only warning/message regarding this topic.
+*/
+
+/// <summary>
+/// Handles all storage logic for BaseItems.
+/// </summary>
+public sealed class BaseItemRepository
+    : IItemRepository
+{
+    /// <summary>
+    /// This holds all the types in the running assemblies
+    /// so that we can de-serialize properly when we don't have strong types.
+    /// </summary>
+    private static readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+    private readonly IServerApplicationHost _appHost;
+    private readonly IItemTypeLookup _itemTypeLookup;
+    private readonly IServerConfigurationManager _serverConfigurationManager;
+    private readonly ILogger<BaseItemRepository> _logger;
+
+    private static readonly IReadOnlyList<ItemValueType> _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist];
+    private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
+    private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
+    private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
+    private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Studios];
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
+    /// </summary>
+    /// <param name="dbProvider">The db factory.</param>
+    /// <param name="appHost">The Application host.</param>
+    /// <param name="itemTypeLookup">The static type lookup.</param>
+    /// <param name="serverConfigurationManager">The server Configuration manager.</param>
+    /// <param name="logger">System logger.</param>
+    public BaseItemRepository(
+        IDbContextFactory<JellyfinDbContext> dbProvider,
+        IServerApplicationHost appHost,
+        IItemTypeLookup itemTypeLookup,
+        IServerConfigurationManager serverConfigurationManager,
+        ILogger<BaseItemRepository> logger)
+    {
+        _dbProvider = dbProvider;
+        _appHost = appHost;
+        _itemTypeLookup = itemTypeLookup;
+        _serverConfigurationManager = serverConfigurationManager;
+        _logger = logger;
+    }
+
+    /// <inheritdoc />
+    public void DeleteItem(Guid id)
+    {
+        if (id.IsEmpty())
+        {
+            throw new ArgumentException("Guid can't be empty", nameof(id));
+        }
+
+        using var context = _dbProvider.CreateDbContext();
+        using var transaction = context.Database.BeginTransaction();
+        context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
+        context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
+        context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
+        context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
+        context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == 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();
+    }
+
+    /// <inheritdoc />
+    public void UpdateInheritedValues()
+    {
+        using var context = _dbProvider.CreateDbContext();
+        using var transaction = context.Database.BeginTransaction();
+
+        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
+        // ItemValue Inheritance is now correctly mapped via AncestorId on demand
+        context.SaveChanges();
+
+        transaction.Commit();
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter)
+    {
+        ArgumentNullException.ThrowIfNull(filter);
+        PrepareFilterQuery(filter);
+
+        using var context = _dbProvider.CreateDbContext();
+        return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray();
+    }
+
+    /// <inheritdoc />
+    public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter)
+    {
+        return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
+    }
+
+    /// <inheritdoc />
+    public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter)
+    {
+        return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
+    }
+
+    /// <inheritdoc />
+    public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
+    {
+        return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
+    }
+
+    /// <inheritdoc />
+    public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter)
+    {
+        return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
+    }
+
+    /// <inheritdoc />
+    public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter)
+    {
+        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
+    }
+
+    /// <inheritdoc />
+    public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
+    {
+        return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<string> GetStudioNames()
+    {
+        return GetItemValueNames(_getStudiosValueTypes, [], []);
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<string> GetAllArtistNames()
+    {
+        return GetItemValueNames(_getAllArtistsValueTypes, [], []);
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<string> GetMusicGenreNames()
+    {
+        return GetItemValueNames(
+            _getGenreValueTypes,
+            _itemTypeLookup.MusicGenreTypes,
+            []);
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<string> GetGenreNames()
+    {
+        return GetItemValueNames(
+            _getGenreValueTypes,
+            [],
+            _itemTypeLookup.MusicGenreTypes);
+    }
+
+    /// <inheritdoc />
+    public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter)
+    {
+        ArgumentNullException.ThrowIfNull(filter);
+        if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0))
+        {
+            var returnList = GetItemList(filter);
+            return new QueryResult<BaseItemDto>(
+                filter.StartIndex,
+                returnList.Count,
+                returnList);
+        }
+
+        PrepareFilterQuery(filter);
+        var result = new QueryResult<BaseItemDto>();
+
+        using var context = _dbProvider.CreateDbContext();
+
+        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
+
+        dbQuery = TranslateQuery(dbQuery, context, filter);
+        if (filter.EnableTotalRecordCount)
+        {
+            result.TotalRecordCount = dbQuery.Count();
+        }
+
+        dbQuery = ApplyGroupingFilter(dbQuery, filter);
+        dbQuery = ApplyQueryPageing(dbQuery, filter);
+
+        result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
+        result.StartIndex = filter.StartIndex ?? 0;
+        return result;
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<BaseItemDto> GetItemList(InternalItemsQuery filter)
+    {
+        ArgumentNullException.ThrowIfNull(filter);
+        PrepareFilterQuery(filter);
+
+        using var context = _dbProvider.CreateDbContext();
+        IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
+
+        dbQuery = TranslateQuery(dbQuery, context, filter);
+
+        dbQuery = ApplyGroupingFilter(dbQuery, filter);
+        dbQuery = ApplyQueryPageing(dbQuery, filter);
+
+        return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
+    }
+
+    private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
+    {
+        // This whole block is needed to filter duplicate entries on request
+        // for the time beeing it cannot be used because it would destroy the ordering
+        // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but
+        // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
+
+        // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
+        // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
+        // {
+        //     dbQuery = ApplyOrder(dbQuery, filter);
+        //     dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First());
+        // }
+        // else if (enableGroupByPresentationUniqueKey)
+        // {
+        //     dbQuery = ApplyOrder(dbQuery, filter);
+        //     dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First());
+        // }
+        // else if (filter.GroupBySeriesPresentationUniqueKey)
+        // {
+        //     dbQuery = ApplyOrder(dbQuery, filter);
+        //     dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First());
+        // }
+        // else
+        // {
+        //     dbQuery = dbQuery.Distinct();
+        //     dbQuery = ApplyOrder(dbQuery, filter);
+        // }
+        dbQuery = dbQuery.Distinct();
+        dbQuery = ApplyOrder(dbQuery, filter);
+
+        return dbQuery;
+    }
+
+    private IQueryable<BaseItemEntity> ApplyQueryPageing(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
+    {
+        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;
+    }
+
+    private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
+    {
+        dbQuery = TranslateQuery(dbQuery, context, filter);
+        dbQuery = ApplyOrder(dbQuery, filter);
+        dbQuery = ApplyGroupingFilter(dbQuery, filter);
+        dbQuery = ApplyQueryPageing(dbQuery, filter);
+        return dbQuery;
+    }
+
+    private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
+    {
+        IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking().AsSplitQuery()
+            .Include(e => e.TrailerTypes)
+            .Include(e => e.Provider)
+            .Include(e => e.LockedFields);
+
+        if (filter.DtoOptions.EnableImages)
+        {
+            dbQuery = dbQuery.Include(e => e.Images);
+        }
+
+        return dbQuery;
+    }
+
+    /// <inheritdoc/>
+    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.AsNoTracking(), context, filter);
+
+        return dbQuery.Count();
+    }
+
+#pragma warning disable CA1307 // Specify StringComparison for clarity
+    /// <summary>
+    /// Gets the type.
+    /// </summary>
+    /// <param name="typeName">Name of the type.</param>
+    /// <returns>Type.</returns>
+    /// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
+    private static Type? GetType(string typeName)
+    {
+        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));
+    }
+
+    /// <inheritdoc  />
+    public void SaveImages(BaseItemDto item)
+    {
+        ArgumentNullException.ThrowIfNull(item);
+
+        var images = item.ImageInfos.Select(e => Map(item.Id, e));
+        using var context = _dbProvider.CreateDbContext();
+        using var transaction = context.Database.BeginTransaction();
+        context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
+        context.BaseItemImageInfos.AddRange(images);
+        context.SaveChanges();
+        transaction.Commit();
+    }
+
+    /// <inheritdoc  />
+    public void SaveItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
+    {
+        UpdateOrInsertItems(items, cancellationToken);
+    }
+
+    /// <inheritdoc cref="IItemRepository"/>
+    public void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken)
+    {
+        ArgumentNullException.ThrowIfNull(items);
+        cancellationToken.ThrowIfCancellationRequested();
+
+        var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
+        foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()))
+        {
+            var ancestorIds = item.SupportsAncestors ?
+                item.GetAncestorIds().Distinct().ToList() :
+                null;
+
+            var topParent = item.GetTopParent();
+
+            var userdataKey = item.GetUserDataKeys();
+            var inheritedTags = item.GetInheritedTags();
+
+            tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
+        }
+
+        var localItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>();
+
+        using var context = _dbProvider.CreateDbContext();
+        using var transaction = context.Database.BeginTransaction();
+        foreach (var item in tuples)
+        {
+            var entity = Map(item.Item);
+            // TODO: refactor this "inconsistency"
+            entity.TopParentId = item.TopParent?.Id;
+
+            if (!context.BaseItems.Any(e => e.Id == entity.Id))
+            {
+                context.BaseItems.Add(entity);
+            }
+            else
+            {
+                context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+                context.BaseItems.Attach(entity).State = EntityState.Modified;
+            }
+
+            context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+            if (item.Item.SupportsAncestors && item.AncestorIds != null)
+            {
+                foreach (var ancestorId in item.AncestorIds)
+                {
+                    if (!context.BaseItems.Any(f => f.Id == ancestorId))
+                    {
+                        continue;
+                    }
+
+                    context.AncestorIds.Add(new AncestorId()
+                    {
+                        ParentItemId = ancestorId,
+                        ItemId = entity.Id,
+                        Item = null!,
+                        ParentItem = null!
+                    });
+                }
+            }
+
+            // Never save duplicate itemValues as they are now mapped anyway.
+            var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags).DistinctBy(e => (GetCleanValue(e.Value), e.MagicNumber));
+            context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+            foreach (var itemValue in itemValuesToSave)
+            {
+                if (!localItemValueCache.TryGetValue(itemValue, out var refValue))
+                {
+                    refValue = context.ItemValues
+                                .Where(f => f.CleanValue == GetCleanValue(itemValue.Value) && (int)f.Type == itemValue.MagicNumber)
+                                .Select(e => e.ItemValueId)
+                                .FirstOrDefault();
+                }
+
+                if (refValue.IsEmpty())
+                {
+                    context.ItemValues.Add(new ItemValue()
+                    {
+                        CleanValue = GetCleanValue(itemValue.Value),
+                        Type = (ItemValueType)itemValue.MagicNumber,
+                        ItemValueId = refValue = Guid.NewGuid(),
+                        Value = itemValue.Value
+                    });
+                    localItemValueCache[itemValue] = refValue;
+                }
+
+                context.ItemValuesMap.Add(new ItemValueMap()
+                {
+                    Item = null!,
+                    ItemId = entity.Id,
+                    ItemValue = null!,
+                    ItemValueId = refValue
+                });
+            }
+        }
+
+        context.SaveChanges();
+        transaction.Commit();
+    }
+
+    /// <inheritdoc  />
+    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 = PrepareItemQuery(context, new()
+        {
+            DtoOptions = new()
+            {
+                EnableImages = true
+            }
+        }).FirstOrDefault(e => e.Id == id);
+        if (item is null)
+        {
+            return null;
+        }
+
+        return DeserialiseBaseItem(item);
+    }
+
+    /// <summary>
+    /// Maps a Entity to the DTO.
+    /// </summary>
+    /// <param name="entity">The entity.</param>
+    /// <param name="dto">The dto base instance.</param>
+    /// <param name="appHost">The Application server Host.</param>
+    /// <returns>The dto to map.</returns>
+    public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
+    {
+        dto.Id = entity.Id;
+        dto.ParentId = entity.ParentId.GetValueOrDefault();
+        dto.Path = appHost?.ExpandVirtualPath(entity.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.TryParse(entity.ChannelId, out var channelId) ? channelId : Guid.Empty);
+        dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
+        dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
+        dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
+        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 = (ExtraType)entity.ExtraType;
+        }
+
+        if (entity.LockedFields is not null)
+        {
+            dto.LockedFields = entity.LockedFields?.Select(e => (MetadataField)e.Id).ToArray() ?? [];
+        }
+
+        if (entity.Audio is not null)
+        {
+            dto.Audio = (ProgramAudio)entity.Audio;
+        }
+
+        dto.ExtraIds = string.IsNullOrWhiteSpace(entity.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)
+        {
+            trailer.TrailerTypes = entity.TrailerTypes?.Select(e => (TrailerType)e.Id).ToArray() ?? [];
+        }
+
+        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 = entity.Images.Select(e => Map(e, appHost)).ToArray();
+        }
+
+        // dto.Type = entity.Type;
+        // dto.Data = entity.Data;
+        // dto.MediaType = Enum.TryParse<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;
+    }
+
+    /// <summary>
+    /// Maps a Entity to the DTO.
+    /// </summary>
+    /// <param name="dto">The entity.</param>
+    /// <returns>The dto to map.</returns>
+    public BaseItemEntity Map(BaseItemDto dto)
+    {
+        var dtoType = dto.GetType();
+        var entity = new BaseItemEntity()
+        {
+            Type = dtoType.ToString(),
+            Id = dto.Id
+        };
+
+        if (TypeRequiresDeserialization(dtoType))
+        {
+            entity.Data = JsonSerializer.Serialize(dto, dtoType, JsonDefaults.Options);
+        }
+
+        entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
+        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 BaseItemProvider()
+        {
+            Item = entity,
+            ProviderId = e.Key,
+            ProviderValue = e.Value
+        }).ToList();
+
+        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 ? dto.LockedFields
+            .Select(e => new BaseItemMetadataField()
+            {
+                Id = (int)e,
+                Item = entity,
+                ItemId = entity.Id
+            })
+            .ToArray() : 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 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 = 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;
+        // dto.Data = entity.Data;
+        entity.MediaType = dto.MediaType.ToString();
+        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[] GetItemValueNames(IReadOnlyList<ItemValueType> itemValueTypes, IReadOnlyList<string> withItemTypes, IReadOnlyList<string> excludeItemTypes)
+    {
+        using var context = _dbProvider.CreateDbContext();
+
+        var query = context.ItemValuesMap
+            .AsNoTracking()
+            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
+        if (withItemTypes.Count > 0)
+        {
+            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
+        }
+
+        if (excludeItemTypes.Count > 0)
+        {
+            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
+        }
+
+        // query = query.DistinctBy(e => e.CleanValue);
+        return query.Select(e => e.ItemValue.CleanValue).ToArray();
+    }
+
+    private static bool TypeRequiresDeserialization(Type type)
+    {
+        return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
+    }
+
+    private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
+    {
+        ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
+        if (_serverConfigurationManager?.Configuration is null)
+        {
+            throw new InvalidOperationException("Server Configuration manager or configuration is null");
+        }
+
+        var typeToSerialise = GetType(baseItemEntity.Type);
+        return BaseItemRepository.DeserialiseBaseItem(
+            baseItemEntity,
+            _logger,
+            _appHost,
+            skipDeserialization || (_serverConfigurationManager.Configuration.SkipDeserializationForBasicTypes && (typeToSerialise == typeof(Channel) || typeToSerialise == typeof(UserRootFolder))));
+    }
+
+    /// <summary>
+    /// Deserialises a BaseItemEntity and sets all properties.
+    /// </summary>
+    /// <param name="baseItemEntity">The DB entity.</param>
+    /// <param name="logger">Logger.</param>
+    /// <param name="appHost">The application server Host.</param>
+    /// <param name="skipDeserialization">If only mapping should be processed.</param>
+    /// <returns>A mapped BaseItem.</returns>
+    /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
+    public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
+    {
+        var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unkown type.");
+        BaseItemDto? dto = null;
+        if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
+        {
+            try
+            {
+                dto = JsonSerializer.Deserialize(baseItemEntity.Data, type, JsonDefaults.Options) as BaseItemDto;
+            }
+            catch (JsonException ex)
+            {
+                logger.LogError(ex, "Error deserializing item with JSON: {Data}", baseItemEntity.Data);
+            }
+        }
+
+        if (dto is null)
+        {
+            dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unkown type.");
+        }
+
+        return Map(baseItemEntity, dto, appHost);
+    }
+
+    private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> 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.AsNoTracking(), context, innerQuery);
+
+        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))
+        {
+            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 = query.Take(filter.Limit.Value);
+            }
+        }
+
+        var result = new QueryResult<(BaseItemDto, ItemCounts)>();
+        if (filter.EnableTotalRecordCount)
+        {
+            result.TotalRecordCount = query.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()).Count();
+        }
+
+        var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
+        var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie];
+        var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
+        var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum];
+        var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist];
+        var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio];
+        var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer];
+
+        var resultQuery = query.Select(e => new
+        {
+            item = e,
+            // TODO: This is bad refactor!
+            itemCount = new ItemCounts()
+            {
+                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),
+            }
+        });
+
+        result.StartIndex = filter.StartIndex ?? 0;
+        result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e =>
+        {
+            return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
+        }).ToArray();
+
+        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(BaseItemDto item, List<string> 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 static BaseItemImageInfo Map(Guid baseItemId, ItemImageInfo e)
+    {
+        return new BaseItemImageInfo()
+        {
+            ItemId = baseItemId,
+            Id = Guid.NewGuid(),
+            Path = e.Path,
+            Blurhash = e.BlurHash is null ? null : Encoding.UTF8.GetBytes(e.BlurHash),
+            DateModified = e.DateModified,
+            Height = e.Height,
+            Width = e.Width,
+            ImageType = (ImageInfoImageType)e.Type,
+            Item = null!
+        };
+    }
+
+    private static ItemImageInfo Map(BaseItemImageInfo e, IServerApplicationHost? appHost)
+    {
+        return new ItemImageInfo()
+        {
+            Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
+            BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
+            DateModified = e.DateModified,
+            Height = e.Height,
+            Width = e.Width,
+            Type = (ImageType)e.ImageType
+        };
+    }
+
+    private string? GetPathToSave(string path)
+    {
+        if (path is null)
+        {
+            return null;
+        }
+
+        return _appHost.ReverseVirtualPath(path);
+    }
+
+    private List<string> GetItemByNameTypesInQuery(InternalItemsQuery query)
+    {
+        var list = new List<string>();
+
+        if (IsTypeInQuery(BaseItemKind.Person, query))
+        {
+            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]!);
+        }
+
+        if (IsTypeInQuery(BaseItemKind.Genre, query))
+        {
+            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]!);
+        }
+
+        if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
+        {
+            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]!);
+        }
+
+        if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
+        {
+            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!);
+        }
+
+        if (IsTypeInQuery(BaseItemKind.Studio, query))
+        {
+            list.Add(_itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]!);
+        }
+
+        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 Expression<Func<BaseItemEntity, object>> 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 == query.User!.Id)!.LastPlayedDate,
+            ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.PlayCount,
+            ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.IsFavorite,
+            ItemSortBy.IsFolder => e => e.IsFolder,
+            ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
+            ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
+            ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
+            ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+            ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+            ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
+            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<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
+    {
+        var orderBy = filter.OrderBy;
+        var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
+
+        if (hasSearch)
+        {
+            orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy];
+        }
+        else if (orderBy.Count == 0)
+        {
+            return query;
+        }
+
+        IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
+
+        var firstOrdering = orderBy.FirstOrDefault();
+        if (firstOrdering != default)
+        {
+            var expression = MapOrderByField(firstOrdering.OrderBy, filter);
+            if (firstOrdering.SortOrder == SortOrder.Ascending)
+            {
+                orderedQuery = query.OrderBy(expression);
+            }
+            else
+            {
+                orderedQuery = query.OrderByDescending(expression);
+            }
+
+            if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
+            {
+                if (firstOrdering.SortOrder is SortOrder.Ascending)
+                {
+                    orderedQuery = orderedQuery.ThenBy(e => e.Name);
+                }
+                else
+                {
+                    orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
+                }
+            }
+        }
+
+        foreach (var item in orderBy.Skip(1))
+        {
+            var expression = MapOrderByField(item.OrderBy, filter);
+            if (item.SortOrder == SortOrder.Ascending)
+            {
+                orderedQuery = orderedQuery!.ThenBy(expression);
+            }
+            else
+            {
+                orderedQuery = orderedQuery!.ThenByDescending(expression);
+            }
+        }
+
+        return orderedQuery ?? query;
+    }
+
+    private IQueryable<BaseItemEntity> TranslateQuery(
+        IQueryable<BaseItemEntity> 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))
+        {
+            var searchTerm = filter.SearchTerm.ToLower();
+            baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
+        }
+
+        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<string>();
+                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<string>();
+            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 > 0)
+        {
+            var channelIds = filter.ChannelIds.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray();
+            baseQuery = baseQuery.Where(e => channelIds.Contains(e.ChannelId));
+        }
+
+        if (!filter.ParentId.IsEmpty())
+        {
+            baseQuery = baseQuery.Where(e => e.ParentId!.Value == 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)
+        {
+            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)
+        {
+            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.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.People.Name))
+                        .Any(f => f.ItemId == e.Id));
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.Person))
+        {
+            baseQuery = baseQuery.Where(e => e.Peoples!.Any(f => f.People.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!.Contains(nameContains)
+                || e.OriginalTitle!.ToLower().Contains(nameContains!));
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
+        {
+            baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.NameStartsWith));
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
+        {
+            // i hate this
+            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]);
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
+        {
+            // i hate this
+            baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]);
+        }
+
+        if (filter.ImageTypes.Length > 0)
+        {
+            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)
+        {
+            baseQuery = baseQuery
+                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.Rating >= UserItemData.MinLikeValue);
+        }
+
+        if (filter.IsFavoriteOrLiked.HasValue)
+        {
+            baseQuery = baseQuery
+                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavoriteOrLiked);
+        }
+
+        if (filter.IsFavorite.HasValue)
+        {
+            baseQuery = baseQuery
+                .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.IsFavorite == filter.IsFavorite);
+        }
+
+        if (filter.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 (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
+            {
+                baseQuery = baseQuery.Where(e => context.BaseItems
+                    .Where(e => e.IsFolder == false && e.IsVirtualItem == false)
+                    .Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
+                    .Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
+            }
+            else
+            {
+                baseQuery = baseQuery
+                    .Select(e => new
+                    {
+                        IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).FirstOrDefault() ?? false,
+                        Item = e
+                    })
+                    .Where(e => e.IsPlayed == filter.IsPlayed)
+                    .Select(f => f.Item);
+            }
+        }
+
+        if (filter.IsResumable.HasValue)
+        {
+            if (filter.IsResumable.Value)
+            {
+                baseQuery = baseQuery
+                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks > 0);
+            }
+            else
+            {
+                baseQuery = baseQuery
+                       .Where(e => e.UserData!.FirstOrDefault(f => f.UserId == filter.User!.Id)!.PlaybackPositionTicks == 0);
+            }
+        }
+
+        if (filter.ArtistIds.Length > 0)
+        {
+            baseQuery = baseQuery
+                   .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.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId)));
+        }
+
+        if (filter.ContributingArtistIds.Length > 0)
+        {
+            baseQuery = baseQuery
+                   .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId)));
+        }
+
+        if (filter.AlbumIds.Length > 0)
+        {
+            baseQuery = baseQuery.Where(e => context.BaseItems.Where(f => filter.AlbumIds.Contains(f.Id)).Any(f => f.Name == e.Album));
+        }
+
+        if (filter.ExcludeArtistIds.Length > 0)
+        {
+            baseQuery = baseQuery
+                   .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.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.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.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.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
+        }
+
+        if (filter.StudioIds.Length > 0)
+        {
+            baseQuery = baseQuery
+                    .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId)));
+        }
+
+        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)
+        {
+            var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
+            if (filter.MinParentalRating.HasValue)
+            {
+                if (filter.MaxParentalRating.HasValue)
+                {
+                    baseQuery = baseQuery
+                        .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
+                        || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating));
+                }
+                else
+                {
+                    baseQuery = baseQuery
+                        .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
+                        || e.InheritedParentalRatingValue >= filter.MinParentalRating);
+                }
+            }
+            else
+            {
+                baseQuery = baseQuery
+                    .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.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(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filter.HasNoAudioTrackWithLanguage));
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
+        {
+            baseQuery = baseQuery
+                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && f.Language == filter.HasNoInternalSubtitleTrackWithLanguage));
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
+        {
+            baseQuery = baseQuery
+                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && f.Language == filter.HasNoExternalSubtitleTrackWithLanguage));
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
+        {
+            baseQuery = baseQuery
+                .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == filter.HasNoSubtitleTrackWithLanguage));
+        }
+
+        if (filter.HasSubtitles.HasValue)
+        {
+            baseQuery = baseQuery
+                .Where(e => e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) == filter.HasSubtitles.Value);
+        }
+
+        if (filter.HasChapterImages.HasValue)
+        {
+            baseQuery = baseQuery
+                .Where(e => e.Chapters!.Any(f => f.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 == e.ParentId.Value));
+        }
+
+        if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
+        {
+            baseQuery = baseQuery
+                    .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!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1);
+        }
+
+        if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
+        {
+            baseQuery = baseQuery
+                .Where(e => !context.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 > 0)
+        {
+            var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
+            baseQuery = baseQuery
+                .Where(e => mediaTypes.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 == e.TopParentId!.Value));
+            }
+            else
+            {
+                baseQuery = baseQuery.Where(e => queryTopParentIds.Contains(e.TopParentId!.Value));
+            }
+        }
+
+        if (filter.AncestorIds.Length > 0)
+        {
+            baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
+        }
+
+        if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
+        {
+            baseQuery = baseQuery
+                .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.ParentAncestors!.Any(w => w.ItemId == 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(w => w.ItemValue.Type == ItemValueType.InheritedTags)
+                    .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.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(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)
+                        .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.ParentAncestors!
+                        .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.ParentAncestors!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
+            }
+        }
+
+        if (filter.SeriesStatuses.Length > 0)
+        {
+            var seriesStatus = filter.SeriesStatuses.Select(e => e.ToString()).ToArray();
+            baseQuery = baseQuery
+                .Where(e => seriesStatus.Any(f => e.Data!.Contains(f)));
+        }
+
+        if (filter.BoxSetLibraryFolders.Length > 0)
+        {
+            var boxsetFolders = filter.BoxSetLibraryFolders.Select(e => e.ToString("N", CultureInfo.InvariantCulture)).ToArray();
+            baseQuery = baseQuery
+                .Where(e => boxsetFolders.Any(f => e.Data!.Contains(f)));
+        }
+
+        if (filter.VideoTypes.Length > 0)
+        {
+            var videoTypeBs = filter.VideoTypes.Select(e => $"\"VideoType\":\"{e}\"");
+            baseQuery = baseQuery
+                .Where(e => videoTypeBs.Any(f => e.Data!.Contains(f)));
+        }
+
+        if (filter.Is3D.HasValue)
+        {
+            if (filter.Is3D.Value)
+            {
+                baseQuery = baseQuery
+                    .Where(e => e.Data!.Contains("Video3DFormat"));
+            }
+            else
+            {
+                baseQuery = baseQuery
+                    .Where(e => !e.Data!.Contains("Video3DFormat"));
+            }
+        }
+
+        if (filter.IsPlaceHolder.HasValue)
+        {
+            if (filter.IsPlaceHolder.Value)
+            {
+                baseQuery = baseQuery
+                    .Where(e => e.Data!.Contains("IsPlaceHolder\":true"));
+            }
+            else
+            {
+                baseQuery = baseQuery
+                    .Where(e => !e.Data!.Contains("IsPlaceHolder\":true"));
+            }
+        }
+
+        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;
+    }
+}

+ 123 - 0
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;
+
+/// <summary>
+/// The Chapter manager.
+/// </summary>
+public class ChapterRepository : IChapterRepository
+{
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+    private readonly IImageProcessor _imageProcessor;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ChapterRepository"/> class.
+    /// </summary>
+    /// <param name="dbProvider">The EFCore provider.</param>
+    /// <param name="imageProcessor">The Image Processor.</param>
+    public ChapterRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IImageProcessor imageProcessor)
+    {
+        _dbProvider = dbProvider;
+        _imageProcessor = imageProcessor;
+    }
+
+    /// <inheritdoc cref="IChapterRepository"/>
+    public ChapterInfo? GetChapter(BaseItemDto baseItem, int index)
+    {
+        return GetChapter(baseItem.Id, index);
+    }
+
+    /// <inheritdoc cref="IChapterRepository"/>
+    public IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem)
+    {
+        return GetChapters(baseItem.Id);
+    }
+
+    /// <inheritdoc cref="IChapterRepository"/>
+    public ChapterInfo? GetChapter(Guid baseItemId, int index)
+    {
+        using var context = _dbProvider.CreateDbContext();
+        var chapter = context.Chapters.AsNoTracking()
+            .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;
+    }
+
+    /// <inheritdoc cref="IChapterRepository"/>
+    public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
+    {
+        using var context = _dbProvider.CreateDbContext();
+        return context.Chapters.AsNoTracking().Where(e => e.ItemId.Equals(baseItemId))
+            .Select(e => new
+            {
+                chapter = e,
+                baseItemPath = e.Item.Path
+            })
+            .AsEnumerable()
+            .Select(e => Map(e.chapter, e.baseItemPath!))
+            .ToArray();
+    }
+
+    /// <inheritdoc cref="IChapterRepository"/>
+    public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> 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;
+    }
+}

+ 73 - 0
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;
+
+/// <summary>
+/// Manager for handling Media Attachments.
+/// </summary>
+/// <param name="dbProvider">Efcore Factory.</param>
+public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbProvider) : IMediaAttachmentRepository
+{
+    /// <inheritdoc />
+    public void SaveMediaAttachments(
+        Guid id,
+        IReadOnlyList<MediaAttachment> 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();
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery filter)
+    {
+        using var context = dbProvider.CreateDbContext();
+        var query = context.AttachmentStreamInfos.AsNoTracking().Where(e => e.ItemId.Equals(filter.ItemId));
+        if (filter.Index.HasValue)
+        {
+            query = query.Where(e => e.Index == filter.Index);
+        }
+
+        return query.AsEnumerable().Select(Map).ToArray();
+    }
+
+    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!
+        };
+    }
+}

+ 213 - 0
Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs

@@ -0,0 +1,213 @@
+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;
+
+/// <summary>
+/// Repository for obtaining MediaStreams.
+/// </summary>
+public class MediaStreamRepository : IMediaStreamRepository
+{
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+    private readonly IServerApplicationHost _serverApplicationHost;
+    private readonly ILocalizationManager _localization;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MediaStreamRepository"/> class.
+    /// </summary>
+    /// <param name="dbProvider">The EFCore db factory.</param>
+    /// <param name="serverApplicationHost">The Application host.</param>
+    /// <param name="localization">The Localisation Provider.</param>
+    public MediaStreamRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization)
+    {
+        _dbProvider = dbProvider;
+        _serverApplicationHost = serverApplicationHost;
+        _localization = localization;
+    }
+
+    /// <inheritdoc />
+    public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> 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();
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter)
+    {
+        using var context = _dbProvider.CreateDbContext();
+        return TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter).AsEnumerable().Select(Map).ToArray();
+    }
+
+    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<MediaStreamInfo> TranslateQuery(IQueryable<MediaStreamInfo> 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)
+        {
+            var typeValue = (MediaStreamTypeEntity)filter.Type.Value;
+            query = query.Where(e => e.StreamType == typeValue);
+        }
+
+        return query;
+    }
+
+    private MediaStream Map(MediaStreamInfo entity)
+    {
+        var dto = new MediaStream();
+        dto.Index = entity.StreamIndex;
+        dto.Type = (MediaStreamType)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.GetValueOrDefault();
+        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 = (MediaStreamTypeEntity)dto.Type,
+            IsAvc = dto.IsAVC,
+
+            Codec = dto.Codec,
+            Language = dto.Language,
+            ChannelLayout = dto.ChannelLayout,
+            Profile = dto.Profile,
+            AspectRatio = dto.AspectRatio,
+            Path = GetPathToSave(dto.Path) ?? dto.Path,
+            IsInterlaced = dto.IsInterlaced,
+            BitRate = dto.BitRate,
+            Channels = dto.Channels,
+            SampleRate = dto.SampleRate,
+            IsDefault = dto.IsDefault,
+            IsForced = dto.IsForced,
+            IsExternal = dto.IsExternal,
+            Height = dto.Height,
+            Width = dto.Width,
+            AverageFrameRate = dto.AverageFrameRate,
+            RealFrameRate = dto.RealFrameRate,
+            Level = dto.Level.HasValue ? (float)dto.Level : null,
+            PixelFormat = dto.PixelFormat,
+            BitDepth = dto.BitDepth,
+            IsAnamorphic = dto.IsAnamorphic,
+            RefFrames = dto.RefFrames,
+            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,
+            DvVersionMinor = dto.DvVersionMinor,
+            DvProfile = dto.DvProfile,
+            DvLevel = dto.DvLevel,
+            RpuPresentFlag = dto.RpuPresentFlag,
+            ElPresentFlag = dto.ElPresentFlag,
+            BlPresentFlag = dto.BlPresentFlag,
+            DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
+            IsHearingImpaired = dto.IsHearingImpaired,
+            Rotation = dto.Rotation
+        };
+        return entity;
+    }
+}

+ 186 - 0
Jellyfin.Server.Implementations/Item/PeopleRepository.cs

@@ -0,0 +1,186 @@
+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;
+#pragma warning disable RS0030 // Do not use banned APIs
+
+/// <summary>
+/// Manager for handling people.
+/// </summary>
+/// <param name="dbProvider">Efcore Factory.</param>
+/// <param name="itemTypeLookup">Items lookup service.</param>
+/// <remarks>
+/// Initializes a new instance of the <see cref="PeopleRepository"/> class.
+/// </remarks>
+public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IItemTypeLookup itemTypeLookup) : IPeopleRepository
+{
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider = dbProvider;
+
+    /// <inheritdoc/>
+    public IReadOnlyList<PersonInfo> 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.AsEnumerable().Select(Map).ToArray();
+    }
+
+    /// <inheritdoc/>
+    public IReadOnlyList<string> 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).ToArray();
+    }
+
+    /// <inheritdoc />
+    public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
+    {
+        using var context = _dbProvider.CreateDbContext();
+        using var transaction = context.Database.BeginTransaction();
+
+        context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ExecuteDelete();
+        // TODO: yes for __SOME__ reason there can be duplicates.
+        foreach (var item in people.DistinctBy(e => e.Id))
+        {
+            var personEntity = Map(item);
+            var existingEntity = context.Peoples.FirstOrDefault(e => e.Id == personEntity.Id);
+            if (existingEntity is null)
+            {
+                context.Peoples.Add(personEntity);
+                existingEntity = personEntity;
+            }
+
+            context.PeopleBaseItemMap.Add(new PeopleBaseItemMap()
+            {
+                Item = null!,
+                ItemId = itemId,
+                People = existingEntity,
+                PeopleId = existingEntity.Id,
+                ListOrder = item.SortOrder,
+                SortOrder = item.SortOrder,
+                Role = item.Role
+            });
+        }
+
+        context.SaveChanges();
+        transaction.Commit();
+    }
+
+    private PersonInfo Map(People people)
+    {
+        var personInfo = new PersonInfo()
+        {
+            Id = people.Id,
+            Name = people.Name,
+        };
+        if (Enum.TryParse<PersonKind>(people.PersonType, out var kind))
+        {
+            personInfo.Type = kind;
+        }
+
+        return personInfo;
+    }
+
+    private People Map(PersonInfo people)
+    {
+        var personInfo = new People()
+        {
+            Name = people.Name,
+            PersonType = people.Type.ToString(),
+            Id = people.Id,
+        };
+
+        return personInfo;
+    }
+
+    private IQueryable<People> TranslateQuery(IQueryable<People> query, JellyfinDbContext context, InternalPeopleQuery filter)
+    {
+        if (filter.User is not null && filter.IsFavorite.HasValue)
+        {
+            var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
+            query = query.Where(e => e.PersonType == personType)
+                .Where(e => context.BaseItems.Where(d => d.UserData!.Any(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id)))
+                    .Select(f => f.Name).Contains(e.Name));
+        }
+
+        if (!filter.ItemId.IsEmpty())
+        {
+            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
+        }
+
+        if (!filter.AppearsInItemId.IsEmpty())
+        {
+            query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId)));
+        }
+
+        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 && !filter.ItemId.IsEmpty())
+        {
+            query = query.Where(e => e.BaseItems!.First(w => w.ItemId == filter.ItemId).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);
+    }
+}

+ 86 - 10
Jellyfin.Server.Implementations/JellyfinDbContext.cs

@@ -4,20 +4,18 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities.Security;
 using Jellyfin.Data.Interfaces;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Server.Implementations;
 
 /// <inheritdoc/>
-public class JellyfinDbContext : DbContext
+/// <summary>
+/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
+/// </summary>
+/// <param name="options">The database context options.</param>
+/// <param name="logger">Logger.</param>
+public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger) : DbContext(options)
 {
-    /// <summary>
-    /// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
-    /// </summary>
-    /// <param name="options">The database context options.</param>
-    public JellyfinDbContext(DbContextOptions<JellyfinDbContext> options) : base(options)
-    {
-    }
-
     /// <summary>
     /// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
     /// </summary>
@@ -88,6 +86,76 @@ public class JellyfinDbContext : DbContext
     /// </summary>
     public DbSet<MediaSegment> MediaSegments => Set<MediaSegment>();
 
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+    /// </summary>
+    public DbSet<UserData> UserData => Set<UserData>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+    /// </summary>
+    public DbSet<AncestorId> AncestorIds => Set<AncestorId>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+    /// </summary>
+    public DbSet<AttachmentStreamInfo> AttachmentStreamInfos => Set<AttachmentStreamInfo>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+    /// </summary>
+    public DbSet<BaseItemEntity> BaseItems => Set<BaseItemEntity>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+    /// </summary>
+    public DbSet<Chapter> Chapters => Set<Chapter>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<ItemValue> ItemValues => Set<ItemValue>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<ItemValueMap> ItemValuesMap => Set<ItemValueMap>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<MediaStreamInfo> MediaStreamInfos => Set<MediaStreamInfo>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<People> Peoples => Set<People>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => Set<PeopleBaseItemMap>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
+    /// </summary>
+    public DbSet<BaseItemProvider> BaseItemProviders => Set<BaseItemProvider>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<BaseItemImageInfo> BaseItemImageInfos => Set<BaseItemImageInfo>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<BaseItemMetadataField> BaseItemMetadataFields => Set<BaseItemMetadataField>();
+
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/>.
+    /// </summary>
+    public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => Set<BaseItemTrailerType>();
+
     /*public DbSet<Artwork> Artwork => Set<Artwork>();
 
     public DbSet<Book> Books => Set<Book>();
@@ -183,7 +251,15 @@ public class JellyfinDbContext : DbContext
             saveEntity.OnSavingChanges();
         }
 
-        return base.SaveChanges();
+        try
+        {
+            return base.SaveChanges();
+        }
+        catch (Exception e)
+        {
+            logger.LogError(e, "Error trying to save changes.");
+            throw;
+        }
     }
 
     /// <inheritdoc />

+ 1607 - 0
Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs

@@ -0,0 +1,1607 @@
+// <auto-generated />
+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("20241020103111_LibraryDbMigration")]
+    partial class LibraryDbMigration
+    {
+        /// <inheritdoc />
+        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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue");
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "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.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            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", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    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.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            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
+        }
+    }
+}

+ 639 - 0
Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs

@@ -0,0 +1,639 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class LibraryDbMigration : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "BaseItems",
+                columns: table => new
+                {
+                    Id = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Type = table.Column<string>(type: "TEXT", nullable: false),
+                    Data = table.Column<string>(type: "TEXT", nullable: true),
+                    Path = table.Column<string>(type: "TEXT", nullable: true),
+                    StartDate = table.Column<DateTime>(type: "TEXT", nullable: false),
+                    EndDate = table.Column<DateTime>(type: "TEXT", nullable: false),
+                    ChannelId = table.Column<string>(type: "TEXT", nullable: true),
+                    IsMovie = table.Column<bool>(type: "INTEGER", nullable: false),
+                    CommunityRating = table.Column<float>(type: "REAL", nullable: true),
+                    CustomRating = table.Column<string>(type: "TEXT", nullable: true),
+                    IndexNumber = table.Column<int>(type: "INTEGER", nullable: true),
+                    IsLocked = table.Column<bool>(type: "INTEGER", nullable: false),
+                    Name = table.Column<string>(type: "TEXT", nullable: true),
+                    OfficialRating = table.Column<string>(type: "TEXT", nullable: true),
+                    MediaType = table.Column<string>(type: "TEXT", nullable: true),
+                    Overview = table.Column<string>(type: "TEXT", nullable: true),
+                    ParentIndexNumber = table.Column<int>(type: "INTEGER", nullable: true),
+                    PremiereDate = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    ProductionYear = table.Column<int>(type: "INTEGER", nullable: true),
+                    Genres = table.Column<string>(type: "TEXT", nullable: true),
+                    SortName = table.Column<string>(type: "TEXT", nullable: true),
+                    ForcedSortName = table.Column<string>(type: "TEXT", nullable: true),
+                    RunTimeTicks = table.Column<long>(type: "INTEGER", nullable: true),
+                    DateCreated = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    DateModified = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    IsSeries = table.Column<bool>(type: "INTEGER", nullable: false),
+                    EpisodeTitle = table.Column<string>(type: "TEXT", nullable: true),
+                    IsRepeat = table.Column<bool>(type: "INTEGER", nullable: false),
+                    PreferredMetadataLanguage = table.Column<string>(type: "TEXT", nullable: true),
+                    PreferredMetadataCountryCode = table.Column<string>(type: "TEXT", nullable: true),
+                    DateLastRefreshed = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    DateLastSaved = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    IsInMixedFolder = table.Column<bool>(type: "INTEGER", nullable: false),
+                    Studios = table.Column<string>(type: "TEXT", nullable: true),
+                    ExternalServiceId = table.Column<string>(type: "TEXT", nullable: true),
+                    Tags = table.Column<string>(type: "TEXT", nullable: true),
+                    IsFolder = table.Column<bool>(type: "INTEGER", nullable: false),
+                    InheritedParentalRatingValue = table.Column<int>(type: "INTEGER", nullable: true),
+                    UnratedType = table.Column<string>(type: "TEXT", nullable: true),
+                    CriticRating = table.Column<float>(type: "REAL", nullable: true),
+                    CleanName = table.Column<string>(type: "TEXT", nullable: true),
+                    PresentationUniqueKey = table.Column<string>(type: "TEXT", nullable: true),
+                    OriginalTitle = table.Column<string>(type: "TEXT", nullable: true),
+                    PrimaryVersionId = table.Column<string>(type: "TEXT", nullable: true),
+                    DateLastMediaAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    Album = table.Column<string>(type: "TEXT", nullable: true),
+                    LUFS = table.Column<float>(type: "REAL", nullable: true),
+                    NormalizationGain = table.Column<float>(type: "REAL", nullable: true),
+                    IsVirtualItem = table.Column<bool>(type: "INTEGER", nullable: false),
+                    SeriesName = table.Column<string>(type: "TEXT", nullable: true),
+                    SeasonName = table.Column<string>(type: "TEXT", nullable: true),
+                    ExternalSeriesId = table.Column<string>(type: "TEXT", nullable: true),
+                    Tagline = table.Column<string>(type: "TEXT", nullable: true),
+                    ProductionLocations = table.Column<string>(type: "TEXT", nullable: true),
+                    ExtraIds = table.Column<string>(type: "TEXT", nullable: true),
+                    TotalBitrate = table.Column<int>(type: "INTEGER", nullable: true),
+                    ExtraType = table.Column<int>(type: "INTEGER", nullable: true),
+                    Artists = table.Column<string>(type: "TEXT", nullable: true),
+                    AlbumArtists = table.Column<string>(type: "TEXT", nullable: true),
+                    ExternalId = table.Column<string>(type: "TEXT", nullable: true),
+                    SeriesPresentationUniqueKey = table.Column<string>(type: "TEXT", nullable: true),
+                    ShowId = table.Column<string>(type: "TEXT", nullable: true),
+                    OwnerId = table.Column<string>(type: "TEXT", nullable: true),
+                    Width = table.Column<int>(type: "INTEGER", nullable: true),
+                    Height = table.Column<int>(type: "INTEGER", nullable: true),
+                    Size = table.Column<long>(type: "INTEGER", nullable: true),
+                    Audio = table.Column<int>(type: "INTEGER", nullable: true),
+                    ParentId = table.Column<Guid>(type: "TEXT", nullable: true),
+                    TopParentId = table.Column<Guid>(type: "TEXT", nullable: true),
+                    SeasonId = table.Column<Guid>(type: "TEXT", nullable: true),
+                    SeriesId = table.Column<Guid>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_BaseItems", x => x.Id);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "ItemValues",
+                columns: table => new
+                {
+                    ItemValueId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Type = table.Column<int>(type: "INTEGER", nullable: false),
+                    Value = table.Column<string>(type: "TEXT", nullable: false),
+                    CleanValue = table.Column<string>(type: "TEXT", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_ItemValues", x => x.ItemValueId);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Peoples",
+                columns: table => new
+                {
+                    Id = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Name = table.Column<string>(type: "TEXT", nullable: false),
+                    PersonType = table.Column<string>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Peoples", x => x.Id);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "AncestorIds",
+                columns: table => new
+                {
+                    ParentItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    BaseItemEntityId = table.Column<Guid>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_AncestorIds", x => new { x.ItemId, x.ParentItemId });
+                    table.ForeignKey(
+                        name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
+                        column: x => x.BaseItemEntityId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id");
+                    table.ForeignKey(
+                        name: "FK_AncestorIds_BaseItems_ItemId",
+                        column: x => x.ItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                    table.ForeignKey(
+                        name: "FK_AncestorIds_BaseItems_ParentItemId",
+                        column: x => x.ParentItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "AttachmentStreamInfos",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Index = table.Column<int>(type: "INTEGER", nullable: false),
+                    Codec = table.Column<string>(type: "TEXT", nullable: false),
+                    CodecTag = table.Column<string>(type: "TEXT", nullable: true),
+                    Comment = table.Column<string>(type: "TEXT", nullable: true),
+                    Filename = table.Column<string>(type: "TEXT", nullable: true),
+                    MimeType = table.Column<string>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_AttachmentStreamInfos", x => new { x.ItemId, x.Index });
+                    table.ForeignKey(
+                        name: "FK_AttachmentStreamInfos_BaseItems_ItemId",
+                        column: x => x.ItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "BaseItemImageInfos",
+                columns: table => new
+                {
+                    Id = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Path = table.Column<string>(type: "TEXT", nullable: false),
+                    DateModified = table.Column<DateTime>(type: "TEXT", nullable: false),
+                    ImageType = table.Column<int>(type: "INTEGER", nullable: false),
+                    Width = table.Column<int>(type: "INTEGER", nullable: false),
+                    Height = table.Column<int>(type: "INTEGER", nullable: false),
+                    Blurhash = table.Column<byte[]>(type: "BLOB", nullable: true),
+                    ItemId = table.Column<Guid>(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<int>(type: "INTEGER", nullable: false),
+                    ItemId = table.Column<Guid>(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: "BaseItemProviders",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    ProviderId = table.Column<string>(type: "TEXT", nullable: false),
+                    ProviderValue = table.Column<string>(type: "TEXT", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_BaseItemProviders", x => new { x.ItemId, x.ProviderId });
+                    table.ForeignKey(
+                        name: "FK_BaseItemProviders_BaseItems_ItemId",
+                        column: x => x.ItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "BaseItemTrailerTypes",
+                columns: table => new
+                {
+                    Id = table.Column<int>(type: "INTEGER", nullable: false),
+                    ItemId = table.Column<Guid>(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.CreateTable(
+                name: "Chapters",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    ChapterIndex = table.Column<int>(type: "INTEGER", nullable: false),
+                    StartPositionTicks = table.Column<long>(type: "INTEGER", nullable: false),
+                    Name = table.Column<string>(type: "TEXT", nullable: true),
+                    ImagePath = table.Column<string>(type: "TEXT", nullable: true),
+                    ImageDateModified = table.Column<DateTime>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Chapters", x => new { x.ItemId, x.ChapterIndex });
+                    table.ForeignKey(
+                        name: "FK_Chapters_BaseItems_ItemId",
+                        column: x => x.ItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "MediaStreamInfos",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    StreamIndex = table.Column<int>(type: "INTEGER", nullable: false),
+                    StreamType = table.Column<int>(type: "INTEGER", nullable: true),
+                    Codec = table.Column<string>(type: "TEXT", nullable: true),
+                    Language = table.Column<string>(type: "TEXT", nullable: true),
+                    ChannelLayout = table.Column<string>(type: "TEXT", nullable: true),
+                    Profile = table.Column<string>(type: "TEXT", nullable: true),
+                    AspectRatio = table.Column<string>(type: "TEXT", nullable: true),
+                    Path = table.Column<string>(type: "TEXT", nullable: true),
+                    IsInterlaced = table.Column<bool>(type: "INTEGER", nullable: false),
+                    BitRate = table.Column<int>(type: "INTEGER", nullable: false),
+                    Channels = table.Column<int>(type: "INTEGER", nullable: false),
+                    SampleRate = table.Column<int>(type: "INTEGER", nullable: false),
+                    IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
+                    IsForced = table.Column<bool>(type: "INTEGER", nullable: false),
+                    IsExternal = table.Column<bool>(type: "INTEGER", nullable: false),
+                    Height = table.Column<int>(type: "INTEGER", nullable: false),
+                    Width = table.Column<int>(type: "INTEGER", nullable: false),
+                    AverageFrameRate = table.Column<float>(type: "REAL", nullable: false),
+                    RealFrameRate = table.Column<float>(type: "REAL", nullable: false),
+                    Level = table.Column<float>(type: "REAL", nullable: false),
+                    PixelFormat = table.Column<string>(type: "TEXT", nullable: true),
+                    BitDepth = table.Column<int>(type: "INTEGER", nullable: false),
+                    IsAnamorphic = table.Column<bool>(type: "INTEGER", nullable: false),
+                    RefFrames = table.Column<int>(type: "INTEGER", nullable: false),
+                    CodecTag = table.Column<string>(type: "TEXT", nullable: false),
+                    Comment = table.Column<string>(type: "TEXT", nullable: false),
+                    NalLengthSize = table.Column<string>(type: "TEXT", nullable: false),
+                    IsAvc = table.Column<bool>(type: "INTEGER", nullable: false),
+                    Title = table.Column<string>(type: "TEXT", nullable: false),
+                    TimeBase = table.Column<string>(type: "TEXT", nullable: false),
+                    CodecTimeBase = table.Column<string>(type: "TEXT", nullable: false),
+                    ColorPrimaries = table.Column<string>(type: "TEXT", nullable: false),
+                    ColorSpace = table.Column<string>(type: "TEXT", nullable: false),
+                    ColorTransfer = table.Column<string>(type: "TEXT", nullable: false),
+                    DvVersionMajor = table.Column<int>(type: "INTEGER", nullable: false),
+                    DvVersionMinor = table.Column<int>(type: "INTEGER", nullable: false),
+                    DvProfile = table.Column<int>(type: "INTEGER", nullable: false),
+                    DvLevel = table.Column<int>(type: "INTEGER", nullable: false),
+                    RpuPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
+                    ElPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
+                    BlPresentFlag = table.Column<int>(type: "INTEGER", nullable: false),
+                    DvBlSignalCompatibilityId = table.Column<int>(type: "INTEGER", nullable: false),
+                    IsHearingImpaired = table.Column<bool>(type: "INTEGER", nullable: false),
+                    Rotation = table.Column<int>(type: "INTEGER", nullable: false),
+                    KeyFrames = table.Column<string>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_MediaStreamInfos", x => new { x.ItemId, x.StreamIndex });
+                    table.ForeignKey(
+                        name: "FK_MediaStreamInfos_BaseItems_ItemId",
+                        column: x => x.ItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "UserData",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    UserId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Rating = table.Column<double>(type: "REAL", nullable: true),
+                    PlaybackPositionTicks = table.Column<long>(type: "INTEGER", nullable: false),
+                    PlayCount = table.Column<int>(type: "INTEGER", nullable: false),
+                    IsFavorite = table.Column<bool>(type: "INTEGER", nullable: false),
+                    LastPlayedDate = table.Column<DateTime>(type: "TEXT", nullable: true),
+                    Played = table.Column<bool>(type: "INTEGER", nullable: false),
+                    AudioStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
+                    SubtitleStreamIndex = table.Column<int>(type: "INTEGER", nullable: true),
+                    Likes = table.Column<bool>(type: "INTEGER", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_UserData", x => new { x.ItemId, x.UserId });
+                    table.ForeignKey(
+                        name: "FK_UserData_BaseItems_ItemId",
+                        column: x => x.ItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                    table.ForeignKey(
+                        name: "FK_UserData_Users_UserId",
+                        column: x => x.UserId,
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "ItemValuesMap",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    ItemValueId = table.Column<Guid>(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.CreateTable(
+                name: "PeopleBaseItemMap",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    PeopleId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    SortOrder = table.Column<int>(type: "INTEGER", nullable: true),
+                    ListOrder = table.Column<int>(type: "INTEGER", nullable: true),
+                    Role = table.Column<string>(type: "TEXT", nullable: true)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_PeopleBaseItemMap", x => new { x.ItemId, x.PeopleId });
+                    table.ForeignKey(
+                        name: "FK_PeopleBaseItemMap_BaseItems_ItemId",
+                        column: x => x.ItemId,
+                        principalTable: "BaseItems",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                    table.ForeignKey(
+                        name: "FK_PeopleBaseItemMap_Peoples_PeopleId",
+                        column: x => x.PeopleId,
+                        principalTable: "Peoples",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_AncestorIds_BaseItemEntityId",
+                table: "AncestorIds",
+                column: "BaseItemEntityId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_AncestorIds_ParentItemId",
+                table: "AncestorIds",
+                column: "ParentItemId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItemImageInfos_ItemId",
+                table: "BaseItemImageInfos",
+                column: "ItemId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItemMetadataFields_ItemId",
+                table: "BaseItemMetadataFields",
+                column: "ItemId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId",
+                table: "BaseItemProviders",
+                columns: new[] { "ProviderId", "ProviderValue", "ItemId" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem",
+                table: "BaseItems",
+                columns: new[] { "Id", "Type", "IsFolder", "IsVirtualItem" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_IsFolder_TopParentId_IsVirtualItem_PresentationUniqueKey_DateCreated",
+                table: "BaseItems",
+                columns: new[] { "IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_MediaType_TopParentId_IsVirtualItem_PresentationUniqueKey",
+                table: "BaseItems",
+                columns: new[] { "MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_ParentId",
+                table: "BaseItems",
+                column: "ParentId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Path",
+                table: "BaseItems",
+                column: "Path");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_PresentationUniqueKey",
+                table: "BaseItems",
+                column: "PresentationUniqueKey");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_TopParentId_Id",
+                table: "BaseItems",
+                columns: new[] { "TopParentId", "Id" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_IsFolder_IsVirtualItem",
+                table: "BaseItems",
+                columns: new[] { "Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_PresentationUniqueKey_SortName",
+                table: "BaseItems",
+                columns: new[] { "Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Type_TopParentId_Id",
+                table: "BaseItems",
+                columns: new[] { "Type", "TopParentId", "Id" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Type_TopParentId_IsVirtualItem_PresentationUniqueKey_DateCreated",
+                table: "BaseItems",
+                columns: new[] { "Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Type_TopParentId_PresentationUniqueKey",
+                table: "BaseItems",
+                columns: new[] { "Type", "TopParentId", "PresentationUniqueKey" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItems_Type_TopParentId_StartDate",
+                table: "BaseItems",
+                columns: new[] { "Type", "TopParentId", "StartDate" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BaseItemTrailerTypes_ItemId",
+                table: "BaseItemTrailerTypes",
+                column: "ItemId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemValues_Type_CleanValue",
+                table: "ItemValues",
+                columns: new[] { "Type", "CleanValue" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemValuesMap_ItemId",
+                table: "ItemValuesMap",
+                column: "ItemId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MediaStreamInfos_StreamIndex",
+                table: "MediaStreamInfos",
+                column: "StreamIndex");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MediaStreamInfos_StreamIndex_StreamType",
+                table: "MediaStreamInfos",
+                columns: new[] { "StreamIndex", "StreamType" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language",
+                table: "MediaStreamInfos",
+                columns: new[] { "StreamIndex", "StreamType", "Language" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MediaStreamInfos_StreamType",
+                table: "MediaStreamInfos",
+                column: "StreamType");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_PeopleBaseItemMap_ItemId_ListOrder",
+                table: "PeopleBaseItemMap",
+                columns: new[] { "ItemId", "ListOrder" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_PeopleBaseItemMap_ItemId_SortOrder",
+                table: "PeopleBaseItemMap",
+                columns: new[] { "ItemId", "SortOrder" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_PeopleBaseItemMap_PeopleId",
+                table: "PeopleBaseItemMap",
+                column: "PeopleId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Peoples_Name",
+                table: "Peoples",
+                column: "Name");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_UserData_ItemId_UserId_IsFavorite",
+                table: "UserData",
+                columns: new[] { "ItemId", "UserId", "IsFavorite" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_UserData_ItemId_UserId_LastPlayedDate",
+                table: "UserData",
+                columns: new[] { "ItemId", "UserId", "LastPlayedDate" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_UserData_ItemId_UserId_PlaybackPositionTicks",
+                table: "UserData",
+                columns: new[] { "ItemId", "UserId", "PlaybackPositionTicks" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_UserData_ItemId_UserId_Played",
+                table: "UserData",
+                columns: new[] { "ItemId", "UserId", "Played" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_UserData_UserId",
+                table: "UserData",
+                column: "UserId");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "AncestorIds");
+
+            migrationBuilder.DropTable(
+                name: "AttachmentStreamInfos");
+
+            migrationBuilder.DropTable(
+                name: "BaseItemImageInfos");
+
+            migrationBuilder.DropTable(
+                name: "BaseItemMetadataFields");
+
+            migrationBuilder.DropTable(
+                name: "BaseItemProviders");
+
+            migrationBuilder.DropTable(
+                name: "BaseItemTrailerTypes");
+
+            migrationBuilder.DropTable(
+                name: "Chapters");
+
+            migrationBuilder.DropTable(
+                name: "ItemValuesMap");
+
+            migrationBuilder.DropTable(
+                name: "MediaStreamInfos");
+
+            migrationBuilder.DropTable(
+                name: "PeopleBaseItemMap");
+
+            migrationBuilder.DropTable(
+                name: "UserData");
+
+            migrationBuilder.DropTable(
+                name: "ItemValues");
+
+            migrationBuilder.DropTable(
+                name: "Peoples");
+
+            migrationBuilder.DropTable(
+                name: "BaseItems");
+        }
+    }
+}

+ 1610 - 0
Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs

@@ -0,0 +1,1610 @@
+// <auto-generated />
+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("20241111131257_AddedCustomDataKey")]
+    partial class AddedCustomDataKey
+    {
+        /// <inheritdoc />
+        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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue");
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "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.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            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", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    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.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            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
+        }
+    }
+}

+ 28 - 0
Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs

@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class AddedCustomDataKey : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<string>(
+                name: "CustomDataKey",
+                table: "UserData",
+                type: "TEXT",
+                nullable: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "CustomDataKey",
+                table: "UserData");
+        }
+    }
+}

+ 1610 - 0
Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs

@@ -0,0 +1,1610 @@
+// <auto-generated />
+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("20241111135439_AddedCustomDataKeyKey")]
+    partial class AddedCustomDataKeyKey
+    {
+        /// <inheritdoc />
+        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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue");
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "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.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            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", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    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.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            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
+        }
+    }
+}

+ 54 - 0
Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs

@@ -0,0 +1,54 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class AddedCustomDataKeyKey : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_UserData",
+                table: "UserData");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "CustomDataKey",
+                table: "UserData",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_UserData",
+                table: "UserData",
+                columns: new[] { "ItemId", "UserId", "CustomDataKey" });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_UserData",
+                table: "UserData");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "CustomDataKey",
+                table: "UserData",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_UserData",
+                table: "UserData",
+                columns: new[] { "ItemId", "UserId" });
+        }
+    }
+}

+ 1603 - 0
Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs

@@ -0,0 +1,1603 @@
+// <auto-generated />
+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("20241112152323_FixAncestorIdConfig")]
+    partial class FixAncestorIdConfig
+    {
+        /// <inheritdoc />
+        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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ParentItemId");
+
+                    b.HasIndex("ParentItemId");
+
+                    b.ToTable("AncestorIds");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue");
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "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("Children")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany("ParentAncestors")
+                        .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.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            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", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Children");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("ParentAncestors");
+
+                    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.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            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
+        }
+    }
+}

+ 49 - 0
Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs

@@ -0,0 +1,49 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class FixAncestorIdConfig : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropForeignKey(
+                name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
+                table: "AncestorIds");
+
+            migrationBuilder.DropIndex(
+                name: "IX_AncestorIds_BaseItemEntityId",
+                table: "AncestorIds");
+
+            migrationBuilder.DropColumn(
+                name: "BaseItemEntityId",
+                table: "AncestorIds");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<Guid>(
+                name: "BaseItemEntityId",
+                table: "AncestorIds",
+                type: "TEXT",
+                nullable: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_AncestorIds_BaseItemEntityId",
+                table: "AncestorIds",
+                column: "BaseItemEntityId");
+
+            migrationBuilder.AddForeignKey(
+                name: "FK_AncestorIds_BaseItems_BaseItemEntityId",
+                table: "AncestorIds",
+                column: "BaseItemEntityId",
+                principalTable: "BaseItems",
+                principalColumn: "Id");
+        }
+    }
+}

+ 1600 - 0
Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs

@@ -0,0 +1,1600 @@
+// <auto-generated />
+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("20241112232041_FixMediaStreams")]
+    partial class FixMediaStreams
+    {
+        /// <inheritdoc />
+        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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ParentItemId");
+
+                    b.HasIndex("ParentItemId");
+
+                    b.ToTable("AncestorIds");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue");
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "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("Children")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany("ParentAncestors")
+                        .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.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            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", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Children");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("ParentAncestors");
+
+                    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.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            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
+        }
+    }
+}

+ 702 - 0
Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs

@@ -0,0 +1,702 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class FixMediaStreams : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<int>(
+                name: "Width",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Title",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "TimeBase",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "StreamType",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "SampleRate",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "RpuPresentFlag",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "Rotation",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "RefFrames",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<float>(
+                name: "RealFrameRate",
+                table: "MediaStreamInfos",
+                type: "REAL",
+                nullable: true,
+                oldClrType: typeof(float),
+                oldType: "REAL");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Profile",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Path",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "NalLengthSize",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<float>(
+                name: "Level",
+                table: "MediaStreamInfos",
+                type: "REAL",
+                nullable: true,
+                oldClrType: typeof(float),
+                oldType: "REAL");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Language",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsHearingImpaired",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsAvc",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsAnamorphic",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "Height",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "ElPresentFlag",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvVersionMinor",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvVersionMajor",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvProfile",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvLevel",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvBlSignalCompatibilityId",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Comment",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ColorTransfer",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ColorSpace",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ColorPrimaries",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "CodecTimeBase",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "CodecTag",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Codec",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "Channels",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ChannelLayout",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "BlPresentFlag",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "BitRate",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "BitDepth",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<float>(
+                name: "AverageFrameRate",
+                table: "MediaStreamInfos",
+                type: "REAL",
+                nullable: true,
+                oldClrType: typeof(float),
+                oldType: "REAL");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "AspectRatio",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<int>(
+                name: "Width",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Title",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "TimeBase",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "StreamType",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(int),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "SampleRate",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "RpuPresentFlag",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "Rotation",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "RefFrames",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<float>(
+                name: "RealFrameRate",
+                table: "MediaStreamInfos",
+                type: "REAL",
+                nullable: false,
+                defaultValue: 0f,
+                oldClrType: typeof(float),
+                oldType: "REAL",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Profile",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Path",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "NalLengthSize",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<float>(
+                name: "Level",
+                table: "MediaStreamInfos",
+                type: "REAL",
+                nullable: false,
+                defaultValue: 0f,
+                oldClrType: typeof(float),
+                oldType: "REAL",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Language",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsHearingImpaired",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: false,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsAvc",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: false,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsAnamorphic",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: false,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "Height",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "ElPresentFlag",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvVersionMinor",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvVersionMajor",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvProfile",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvLevel",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "DvBlSignalCompatibilityId",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Comment",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ColorTransfer",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ColorSpace",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ColorPrimaries",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "CodecTimeBase",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "CodecTag",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Codec",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "Channels",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ChannelLayout",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<int>(
+                name: "BlPresentFlag",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "BitRate",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<int>(
+                name: "BitDepth",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: 0,
+                oldClrType: typeof(int),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<float>(
+                name: "AverageFrameRate",
+                table: "MediaStreamInfos",
+                type: "REAL",
+                nullable: false,
+                defaultValue: 0f,
+                oldClrType: typeof(float),
+                oldType: "REAL",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "AspectRatio",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+        }
+    }
+}

+ 1594 - 0
Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs

@@ -0,0 +1,1594 @@
+// <auto-generated />
+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("20241112234144_FixMediaStreams2")]
+    partial class FixMediaStreams2
+    {
+        /// <inheritdoc />
+        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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ParentItemId");
+
+                    b.HasIndex("ParentItemId");
+
+                    b.ToTable("AncestorIds");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue");
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "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("Children")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany("ParentAncestors")
+                        .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.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            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", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Children");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("ParentAncestors");
+
+                    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.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            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
+        }
+    }
+}

+ 144 - 0
Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs

@@ -0,0 +1,144 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class FixMediaStreams2 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<string>(
+                name: "Profile",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Path",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Language",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsInterlaced",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: true,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Codec",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ChannelLayout",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "AspectRatio",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(string),
+                oldType: "TEXT");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<string>(
+                name: "Profile",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Path",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Language",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<bool>(
+                name: "IsInterlaced",
+                table: "MediaStreamInfos",
+                type: "INTEGER",
+                nullable: false,
+                defaultValue: false,
+                oldClrType: typeof(bool),
+                oldType: "INTEGER",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Codec",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "ChannelLayout",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<string>(
+                name: "AspectRatio",
+                table: "MediaStreamInfos",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: string.Empty,
+                oldClrType: typeof(string),
+                oldType: "TEXT",
+                oldNullable: true);
+        }
+    }
+}

+ 1595 - 0
Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs

@@ -0,0 +1,1595 @@
+// <auto-generated />
+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("20241113133548_EnforceUniqueItemValue")]
+    partial class EnforceUniqueItemValue
+    {
+        /// <inheritdoc />
+        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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ParentItemId");
+
+                    b.HasIndex("ParentItemId");
+
+                    b.ToTable("AncestorIds");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue")
+                        .IsUnique();
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("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<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("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<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "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("Children")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany("ParentAncestors")
+                        .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.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            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", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Children");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("ParentAncestors");
+
+                    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.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            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
+        }
+    }
+}

+ 37 - 0
Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs

@@ -0,0 +1,37 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class EnforceUniqueItemValue : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropIndex(
+                name: "IX_ItemValues_Type_CleanValue",
+                table: "ItemValues");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemValues_Type_CleanValue",
+                table: "ItemValues",
+                columns: new[] { "Type", "CleanValue" },
+                unique: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropIndex(
+                name: "IX_ItemValues_Type_CleanValue",
+                table: "ItemValues");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemValues_Type_CleanValue",
+                table: "ItemValues",
+                columns: new[] { "Type", "CleanValue" });
+        }
+    }
+}

+ 2 - 1
Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs

@@ -1,5 +1,6 @@
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Jellyfin.Server.Implementations.Migrations
 {
@@ -14,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
             var optionsBuilder = new DbContextOptionsBuilder<JellyfinDbContext>();
             optionsBuilder.UseSqlite("Data Source=jellyfin.db");
 
-            return new JellyfinDbContext(optionsBuilder.Options);
+            return new JellyfinDbContext(optionsBuilder.Options, NullLogger<JellyfinDbContext>.Instance);
         }
     }
 }

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 862 - 82
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs


+ 21 - 0
Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs

@@ -0,0 +1,21 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// AncestorId configuration.
+/// </summary>
+public class AncestorIdConfiguration : IEntityTypeConfiguration<AncestorId>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<AncestorId> builder)
+    {
+        builder.HasKey(e => new { e.ItemId, e.ParentItemId });
+        builder.HasIndex(e => e.ParentItemId);
+        builder.HasOne(e => e.ParentItem).WithMany(e => e.ParentAncestors).HasForeignKey(f => f.ParentItemId);
+        builder.HasOne(e => e.Item).WithMany(e => e.Children).HasForeignKey(f => f.ItemId);
+    }
+}

+ 17 - 0
Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs

@@ -0,0 +1,17 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the AttachmentStreamInfo entity.
+/// </summary>
+public class AttachmentStreamInfoConfiguration : IEntityTypeConfiguration<AttachmentStreamInfo>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<AttachmentStreamInfo> builder)
+    {
+        builder.HasKey(e => new { e.ItemId, e.Index });
+    }
+}

+ 59 - 0
Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs

@@ -0,0 +1,59 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using SQLitePCL;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Configuration for BaseItem.
+/// </summary>
+public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<BaseItemEntity> builder)
+    {
+        builder.HasKey(e => e.Id);
+        // TODO: See rant in entity file.
+        // builder.HasOne(e => e.Parent).WithMany(e => e.DirectChildren).HasForeignKey(e => e.ParentId);
+        // builder.HasOne(e => e.TopParent).WithMany(e => e.AllChildren).HasForeignKey(e => e.TopParentId);
+        // builder.HasOne(e => e.Season).WithMany(e => e.SeasonEpisodes).HasForeignKey(e => e.SeasonId);
+        // builder.HasOne(e => e.Series).WithMany(e => e.SeriesEpisodes).HasForeignKey(e => e.SeriesId);
+        builder.HasMany(e => e.Peoples);
+        builder.HasMany(e => e.UserData);
+        builder.HasMany(e => e.ItemValues);
+        builder.HasMany(e => e.MediaStreams);
+        builder.HasMany(e => e.Chapters);
+        builder.HasMany(e => e.Provider);
+        builder.HasMany(e => e.ParentAncestors);
+        builder.HasMany(e => e.Children);
+        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);
+        builder.HasIndex(e => e.PresentationUniqueKey);
+        builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem });
+
+        // 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 });
+    }
+}

+ 22 - 0
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;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemMetadataFieldConfiguration : IEntityTypeConfiguration<BaseItemMetadataField>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<BaseItemMetadataField> builder)
+    {
+        builder.HasKey(e => new { e.Id, e.ItemId });
+        builder.HasOne(e => e.Item);
+    }
+}

+ 20 - 0
Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs

@@ -0,0 +1,20 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// BaseItemProvider configuration.
+/// </summary>
+public class BaseItemProviderConfiguration : IEntityTypeConfiguration<BaseItemProvider>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<BaseItemProvider> builder)
+    {
+        builder.HasKey(e => new { e.ItemId, e.ProviderId });
+        builder.HasOne(e => e.Item);
+        builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId });
+    }
+}

+ 22 - 0
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;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemTrailerTypeConfiguration : IEntityTypeConfiguration<BaseItemTrailerType>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<BaseItemTrailerType> builder)
+    {
+        builder.HasKey(e => new { e.Id, e.ItemId });
+        builder.HasOne(e => e.Item);
+    }
+}

+ 19 - 0
Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs

@@ -0,0 +1,19 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Chapter configuration.
+/// </summary>
+public class ChapterConfiguration : IEntityTypeConfiguration<Chapter>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<Chapter> builder)
+    {
+        builder.HasKey(e => new { e.ItemId, e.ChapterIndex });
+        builder.HasOne(e => e.Item);
+    }
+}

+ 19 - 0
Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs

@@ -0,0 +1,19 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// itemvalues Configuration.
+/// </summary>
+public class ItemValuesConfiguration : IEntityTypeConfiguration<ItemValue>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<ItemValue> builder)
+    {
+        builder.HasKey(e => e.ItemValueId);
+        builder.HasIndex(e => new { e.Type, e.CleanValue }).IsUnique();
+    }
+}

+ 20 - 0
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;
+
+/// <summary>
+/// itemvalues Configuration.
+/// </summary>
+public class ItemValuesMapConfiguration : IEntityTypeConfiguration<ItemValueMap>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<ItemValueMap> builder)
+    {
+        builder.HasKey(e => new { e.ItemValueId, e.ItemId });
+        builder.HasOne(e => e.Item);
+        builder.HasOne(e => e.ItemValue);
+    }
+}

+ 22 - 0
Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs

@@ -0,0 +1,22 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class MediaStreamInfoConfiguration : IEntityTypeConfiguration<MediaStreamInfo>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<MediaStreamInfo> builder)
+    {
+        builder.HasKey(e => new { e.ItemId, e.StreamIndex });
+        builder.HasIndex(e => e.StreamIndex);
+        builder.HasIndex(e => e.StreamType);
+        builder.HasIndex(e => new { e.StreamIndex, e.StreamType });
+        builder.HasIndex(e => new { e.StreamIndex, e.StreamType, e.Language });
+    }
+}

+ 22 - 0
Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs

@@ -0,0 +1,22 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class PeopleBaseItemMapConfiguration : IEntityTypeConfiguration<PeopleBaseItemMap>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<PeopleBaseItemMap> builder)
+    {
+        builder.HasKey(e => new { e.ItemId, e.PeopleId });
+        builder.HasIndex(e => new { e.ItemId, e.SortOrder });
+        builder.HasIndex(e => new { e.ItemId, e.ListOrder });
+        builder.HasOne(e => e.Item);
+        builder.HasOne(e => e.People);
+    }
+}

+ 20 - 0
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;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class PeopleConfiguration : IEntityTypeConfiguration<People>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<People> builder)
+    {
+        builder.HasKey(e => e.Id);
+        builder.HasIndex(e => e.Name);
+        builder.HasMany(e => e.BaseItems);
+    }
+}

+ 23 - 0
Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs

@@ -0,0 +1,23 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the UserData entity.
+/// </summary>
+public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<UserData> builder)
+    {
+        builder.HasKey(d => new { d.ItemId, d.UserId, d.CustomDataKey });
+        builder.HasIndex(d => new { d.ItemId, d.UserId, d.Played });
+        builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks });
+        builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite });
+        builder.HasIndex(d => new { d.ItemId, d.UserId, d.LastPlayedDate });
+        builder.HasOne(e => e.Item);
+    }
+}

+ 1 - 1
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs

@@ -179,7 +179,7 @@ public class TrickplayManager : ITrickplayManager
             {
                 // Extract images
                 // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
-                var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
+                var mediaSource = video.GetMediaSources(false).FirstOrDefault(source => Guid.Parse(source.Id).Equals(video.Id));
 
                 if (mediaSource is null)
                 {

+ 2 - 1
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -48,7 +48,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.UpdateDefaultPluginRepository),
             typeof(Routines.FixAudioData),
             typeof(Routines.MoveTrickplayFiles),
-            typeof(Routines.RemoveDuplicatePlaylistChildren)
+            typeof(Routines.RemoveDuplicatePlaylistChildren),
+            typeof(Routines.MigrateLibraryDb),
         };
 
         /// <summary>

+ 1201 - 0
Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs

@@ -0,0 +1,1201 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Data;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Emby.Server.Implementations.Data;
+using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
+using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.Item;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Chapter = Jellyfin.Data.Entities.Chapter;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// The migration routine for migrating the userdata database to EF Core.
+/// </summary>
+public class MigrateLibraryDb : IMigrationRoutine
+{
+    private const string DbFilename = "library.db";
+
+    private readonly ILogger<MigrateLibraryDb> _logger;
+    private readonly IServerApplicationPaths _paths;
+    private readonly IDbContextFactory<JellyfinDbContext> _provider;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MigrateLibraryDb"/> class.
+    /// </summary>
+    /// <param name="logger">The logger.</param>
+    /// <param name="provider">The database provider.</param>
+    /// <param name="paths">The server application paths.</param>
+    public MigrateLibraryDb(
+        ILogger<MigrateLibraryDb> logger,
+        IDbContextFactory<JellyfinDbContext> provider,
+        IServerApplicationPaths paths)
+    {
+        _logger = logger;
+        _provider = provider;
+        _paths = paths;
+    }
+
+    /// <inheritdoc/>
+    public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
+
+    /// <inheritdoc/>
+    public string Name => "MigrateLibraryDbData";
+
+    /// <inheritdoc/>
+    public bool PerformOnNewInstall => false; // TODO Change back after testing
+
+    /// <inheritdoc/>
+    public void Perform()
+    {
+        _logger.LogInformation("Migrating the userdata from library.db may take a while, do not stop Jellyfin.");
+
+        var dataPath = _paths.DataPath;
+        var libraryDbPath = Path.Combine(dataPath, DbFilename);
+        using var connection = new SqliteConnection($"Filename={libraryDbPath}");
+        var migrationTotalTime = TimeSpan.Zero;
+
+        var stopwatch = new Stopwatch();
+        stopwatch.Start();
+
+        connection.Open();
+        using var dbContext = _provider.CreateDbContext();
+
+        migrationTotalTime += stopwatch.Elapsed;
+        _logger.LogInformation("Saving UserData entries took {0}.", stopwatch.Elapsed);
+        stopwatch.Restart();
+
+        _logger.LogInformation("Start moving TypedBaseItem.");
+        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,
+         ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, ParentId, TopParentId,
+         Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
+         DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
+         PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
+         ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems
+         """;
+        dbContext.BaseItems.ExecuteDelete();
+
+        var legacyBaseItemWithUserKeys = new Dictionary<string, BaseItemEntity>();
+        foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery))
+        {
+            var baseItem = GetItem(dto);
+            dbContext.BaseItems.Add(baseItem.BaseItem);
+            foreach (var dataKey in baseItem.LegacyUserDataKey)
+            {
+                legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem;
+            }
+        }
+
+        _logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count);
+        dbContext.SaveChanges();
+        migrationTotalTime += stopwatch.Elapsed;
+        _logger.LogInformation("Saving BaseItems entries took {0}.", stopwatch.Elapsed);
+        stopwatch.Restart();
+
+        _logger.LogInformation("Start moving ItemValues.");
+        // do not migrate inherited types as they are now properly mapped in search and lookup.
+        const string itemValueQuery =
+        """
+        SELECT ItemId, Type, Value, CleanValue FROM ItemValues
+                    WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId)
+        """;
+        dbContext.ItemValues.ExecuteDelete();
+
+        // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow.
+        var localItems = new Dictionary<(int Type, string CleanValue), (ItemValue ItemValue, List<Guid> ItemIds)>();
+
+        foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
+        {
+            var itemId = dto.GetGuid(0);
+            var entity = GetItemValue(dto);
+            var key = ((int)entity.Type, entity.CleanValue);
+            if (!localItems.TryGetValue(key, out var existing))
+            {
+                localItems[key] = existing = (entity, []);
+            }
+
+            existing.ItemIds.Add(itemId);
+        }
+
+        foreach (var item in localItems)
+        {
+            dbContext.ItemValues.Add(item.Value.ItemValue);
+            dbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap()
+            {
+                Item = null!,
+                ItemValue = null!,
+                ItemId = f,
+                ItemValueId = item.Value.ItemValue.ItemValueId
+            }));
+        }
+
+        _logger.LogInformation("Try saving {0} ItemValues entries.", dbContext.ItemValues.Local.Count);
+        dbContext.SaveChanges();
+        migrationTotalTime += stopwatch.Elapsed;
+        _logger.LogInformation("Saving People ItemValues took {0}.", stopwatch.Elapsed);
+        stopwatch.Restart();
+
+        _logger.LogInformation("Start moving UserData.");
+        var queryResult = connection.Query("""
+        SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas
+
+        WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key)
+        """);
+
+        dbContext.UserData.ExecuteDelete();
+
+        var users = dbContext.Users.AsNoTracking().ToImmutableArray();
+        var oldUserdata = new Dictionary<string, UserData>();
+
+        foreach (var entity in queryResult)
+        {
+            var userData = GetUserData(users, entity);
+            if (userData is null)
+            {
+                _logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0));
+                continue;
+            }
+
+            if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem))
+            {
+                _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0));
+                continue;
+            }
+
+            userData.ItemId = refItem.Id;
+            dbContext.UserData.Add(userData);
+        }
+
+        _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
+        dbContext.SaveChanges();
+
+        _logger.LogInformation("Start moving MediaStreamInfos.");
+        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,
+        Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer,
+        DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired
+        FROM MediaStreams
+        WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId)
+        """;
+        dbContext.MediaStreamInfos.ExecuteDelete();
+
+        foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery))
+        {
+            dbContext.MediaStreamInfos.Add(GetMediaStream(dto));
+        }
+
+        _logger.LogInformation("Try saving {0} MediaStreamInfos entries.", dbContext.MediaStreamInfos.Local.Count);
+        dbContext.SaveChanges();
+
+        migrationTotalTime += stopwatch.Elapsed;
+        _logger.LogInformation("Saving MediaStreamInfos entries took {0}.", stopwatch.Elapsed);
+        stopwatch.Restart();
+
+        _logger.LogInformation("Start moving People.");
+        const string personsQuery = """
+        SELECT ItemId, Name, Role, PersonType, SortOrder FROM People
+        WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId)
+        """;
+        dbContext.Peoples.ExecuteDelete();
+        dbContext.PeopleBaseItemMap.ExecuteDelete();
+
+        var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
+
+        foreach (SqliteDataReader reader in connection.Query(personsQuery))
+        {
+            var itemId = reader.GetGuid(0);
+            if (!dbContext.BaseItems.Any(f => f.Id == itemId))
+            {
+                _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
+                continue;
+            }
+
+            var entity = GetPerson(reader);
+            if (!peopleCache.TryGetValue(entity.Name, out var personCache))
+            {
+                peopleCache[entity.Name] = personCache = (entity, []);
+            }
+
+            if (reader.TryGetString(2, out var role))
+            {
+            }
+
+            if (reader.TryGetInt32(4, out var sortOrder))
+            {
+            }
+
+            personCache.Items.Add(new PeopleBaseItemMap()
+            {
+                Item = null!,
+                ItemId = itemId,
+                People = null!,
+                PeopleId = personCache.Person.Id,
+                ListOrder = sortOrder,
+                SortOrder = sortOrder,
+                Role = role
+            });
+        }
+
+        foreach (var item in peopleCache)
+        {
+            dbContext.Peoples.Add(item.Value.Person);
+            dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
+        }
+
+        _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count);
+        dbContext.SaveChanges();
+        migrationTotalTime += stopwatch.Elapsed;
+        _logger.LogInformation("Saving People entries took {0}.", stopwatch.Elapsed);
+        stopwatch.Restart();
+
+        _logger.LogInformation("Start moving Chapters.");
+        const string chapterQuery = """
+        SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2
+        WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId)
+        """;
+        dbContext.Chapters.ExecuteDelete();
+
+        foreach (SqliteDataReader dto in connection.Query(chapterQuery))
+        {
+            var chapter = GetChapter(dto);
+            dbContext.Chapters.Add(chapter);
+        }
+
+        _logger.LogInformation("Try saving {0} Chapters entries.", dbContext.Chapters.Local.Count);
+        dbContext.SaveChanges();
+        migrationTotalTime += stopwatch.Elapsed;
+        _logger.LogInformation("Saving Chapters took {0}.", stopwatch.Elapsed);
+        stopwatch.Restart();
+
+        _logger.LogInformation("Start moving AncestorIds.");
+        const string ancestorIdsQuery = """
+        SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds
+        WHERE
+        EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId)
+        AND
+        EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId)
+        """;
+        dbContext.Chapters.ExecuteDelete();
+
+        foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery))
+        {
+            var ancestorId = GetAncestorId(dto);
+            dbContext.AncestorIds.Add(ancestorId);
+        }
+
+        _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.Chapters.Local.Count);
+
+        dbContext.SaveChanges();
+        migrationTotalTime += stopwatch.Elapsed;
+        _logger.LogInformation("Saving AncestorIds took {0}.", stopwatch.Elapsed);
+        stopwatch.Restart();
+
+        connection.Close();
+        _logger.LogInformation("Migration of the Library.db done.");
+        _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old");
+
+        SqliteConnection.ClearAllPools();
+        File.Move(libraryDbPath, libraryDbPath + ".old");
+
+        _logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime);
+
+        if (dbContext.Database.IsSqlite())
+        {
+            _logger.LogInformation("Vaccum and Optimise jellyfin.db now.");
+            dbContext.Database.ExecuteSqlRaw("PRAGMA optimize");
+            dbContext.Database.ExecuteSqlRaw("VACUUM");
+            _logger.LogInformation("jellyfin.db optimized successfully!");
+        }
+        else
+        {
+            _logger.LogInformation("This database doesn't support optimization");
+        }
+    }
+
+    private UserData? GetUserData(ImmutableArray<User> users, SqliteDataReader dto)
+    {
+        var internalUserId = dto.GetInt32(1);
+        var user = users.FirstOrDefault(e => e.InternalId == internalUserId);
+
+        if (user is null)
+        {
+            _logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length);
+            return null;
+        }
+
+        var oldKey = dto.GetString(0);
+
+        return new UserData()
+        {
+            ItemId = Guid.NewGuid(),
+            CustomDataKey = oldKey,
+            UserId = user.Id,
+            Rating = dto.IsDBNull(2) ? null : dto.GetDouble(2),
+            Played = dto.GetBoolean(3),
+            PlayCount = dto.GetInt32(4),
+            IsFavorite = dto.GetBoolean(5),
+            PlaybackPositionTicks = dto.GetInt64(6),
+            LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7),
+            AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8),
+            SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9),
+            Likes = null,
+            User = null!,
+            Item = null!
+        };
+    }
+
+    private AncestorId GetAncestorId(SqliteDataReader reader)
+    {
+        return new AncestorId()
+        {
+            ItemId = reader.GetGuid(0),
+            ParentItemId = reader.GetGuid(1),
+            Item = null!,
+            ParentItem = null!
+        };
+    }
+
+    /// <summary>
+    /// Gets the chapter.
+    /// </summary>
+    /// <param name="reader">The reader.</param>
+    /// <returns>ChapterInfo.</returns>
+    private Chapter GetChapter(SqliteDataReader reader)
+    {
+        var chapter = new Chapter
+        {
+            StartPositionTicks = reader.GetInt64(1),
+            ChapterIndex = reader.GetInt32(5),
+            Item = null!,
+            ItemId = reader.GetGuid(0),
+        };
+
+        if (reader.TryGetString(2, out var chapterName))
+        {
+            chapter.Name = chapterName;
+        }
+
+        if (reader.TryGetString(3, out var imagePath))
+        {
+            chapter.ImagePath = imagePath;
+        }
+
+        if (reader.TryReadDateTime(4, out var imageDateModified))
+        {
+            chapter.ImageDateModified = imageDateModified;
+        }
+
+        return chapter;
+    }
+
+    private ItemValue GetItemValue(SqliteDataReader reader)
+    {
+        return new ItemValue
+        {
+            ItemValueId = Guid.NewGuid(),
+            Type = (ItemValueType)reader.GetInt32(1),
+            Value = reader.GetString(2),
+            CleanValue = reader.GetString(3),
+        };
+    }
+
+    private People GetPerson(SqliteDataReader reader)
+    {
+        var item = new People
+        {
+            Id = Guid.NewGuid(),
+            Name = reader.GetString(1),
+        };
+
+        if (reader.TryGetString(3, out var type))
+        {
+            item.PersonType = type;
+        }
+
+        return item;
+    }
+
+    /// <summary>
+    /// Gets the media stream.
+    /// </summary>
+    /// <param name="reader">The reader.</param>
+    /// <returns>MediaStream.</returns>
+    private MediaStreamInfo GetMediaStream(SqliteDataReader reader)
+    {
+        var item = new MediaStreamInfo
+        {
+            StreamIndex = reader.GetInt32(1),
+            StreamType = Enum.Parse<MediaStreamTypeEntity>(reader.GetString(2)),
+            Item = null!,
+            ItemId = reader.GetGuid(0),
+            AspectRatio = null!,
+            ChannelLayout = null!,
+            Codec = null!,
+            IsInterlaced = false,
+            Language = null!,
+            Path = null!,
+            Profile = null!,
+        };
+
+        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 = 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;
+        }
+
+        if (reader.TryGetInt32(39, out var rpuPresentFlag))
+        {
+            item.RpuPresentFlag = rpuPresentFlag;
+        }
+
+        if (reader.TryGetInt32(40, out var elPresentFlag))
+        {
+            item.ElPresentFlag = elPresentFlag;
+        }
+
+        if (reader.TryGetInt32(41, out var blPresentFlag))
+        {
+            item.BlPresentFlag = blPresentFlag;
+        }
+
+        if (reader.TryGetInt32(42, out var dvBlSignalCompatibilityId))
+        {
+            item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
+        }
+
+        item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
+
+        // if (reader.TryGetInt32(44, out var rotation))
+        // {
+        //     item.Rotation = rotation;
+        // }
+
+        return item;
+    }
+
+    private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader)
+    {
+        var entity = new BaseItemEntity()
+        {
+            Id = reader.GetGuid(0),
+            Type = reader.GetString(1),
+        };
+
+        var index = 2;
+
+        if (reader.TryGetString(index++, out var data))
+        {
+            entity.Data = data;
+        }
+
+        if (reader.TryReadDateTime(index++, out var startDate))
+        {
+            entity.StartDate = startDate;
+        }
+
+        if (reader.TryReadDateTime(index++, out var endDate))
+        {
+            entity.EndDate = endDate;
+        }
+
+        if (reader.TryGetString(index++, out var guid))
+        {
+            entity.ChannelId = guid;
+        }
+
+        if (reader.TryGetBoolean(index++, out var isMovie))
+        {
+            entity.IsMovie = isMovie;
+        }
+
+        if (reader.TryGetBoolean(index++, out var isSeries))
+        {
+            entity.IsSeries = isSeries;
+        }
+
+        if (reader.TryGetString(index++, out var episodeTitle))
+        {
+            entity.EpisodeTitle = episodeTitle;
+        }
+
+        if (reader.TryGetBoolean(index++, out var isRepeat))
+        {
+            entity.IsRepeat = isRepeat;
+        }
+
+        if (reader.TryGetSingle(index++, out var communityRating))
+        {
+            entity.CommunityRating = communityRating;
+        }
+
+        if (reader.TryGetString(index++, out var customRating))
+        {
+            entity.CustomRating = customRating;
+        }
+
+        if (reader.TryGetInt32(index++, out var indexNumber))
+        {
+            entity.IndexNumber = indexNumber;
+        }
+
+        if (reader.TryGetBoolean(index++, out var isLocked))
+        {
+            entity.IsLocked = isLocked;
+        }
+
+        if (reader.TryGetString(index++, out var preferredMetadataLanguage))
+        {
+            entity.PreferredMetadataLanguage = preferredMetadataLanguage;
+        }
+
+        if (reader.TryGetString(index++, out var preferredMetadataCountryCode))
+        {
+            entity.PreferredMetadataCountryCode = preferredMetadataCountryCode;
+        }
+
+        if (reader.TryGetInt32(index++, out var width))
+        {
+            entity.Width = width;
+        }
+
+        if (reader.TryGetInt32(index++, out var height))
+        {
+            entity.Height = height;
+        }
+
+        if (reader.TryReadDateTime(index++, out var dateLastRefreshed))
+        {
+            entity.DateLastRefreshed = dateLastRefreshed;
+        }
+
+        if (reader.TryGetString(index++, out var name))
+        {
+            entity.Name = name;
+        }
+
+        if (reader.TryGetString(index++, out var restorePath))
+        {
+            entity.Path = restorePath;
+        }
+
+        if (reader.TryReadDateTime(index++, out var premiereDate))
+        {
+            entity.PremiereDate = premiereDate;
+        }
+
+        if (reader.TryGetString(index++, out var overview))
+        {
+            entity.Overview = overview;
+        }
+
+        if (reader.TryGetInt32(index++, out var parentIndexNumber))
+        {
+            entity.ParentIndexNumber = parentIndexNumber;
+        }
+
+        if (reader.TryGetInt32(index++, out var productionYear))
+        {
+            entity.ProductionYear = productionYear;
+        }
+
+        if (reader.TryGetString(index++, out var officialRating))
+        {
+            entity.OfficialRating = officialRating;
+        }
+
+        if (reader.TryGetString(index++, out var forcedSortName))
+        {
+            entity.ForcedSortName = forcedSortName;
+        }
+
+        if (reader.TryGetInt64(index++, out var runTimeTicks))
+        {
+            entity.RunTimeTicks = runTimeTicks;
+        }
+
+        if (reader.TryGetInt64(index++, out var size))
+        {
+            entity.Size = size;
+        }
+
+        if (reader.TryReadDateTime(index++, out var dateCreated))
+        {
+            entity.DateCreated = dateCreated;
+        }
+
+        if (reader.TryReadDateTime(index++, out var dateModified))
+        {
+            entity.DateModified = dateModified;
+        }
+
+        if (reader.TryGetString(index++, out var genres))
+        {
+            entity.Genres = genres;
+        }
+
+        if (reader.TryGetGuid(index++, out var parentId))
+        {
+            entity.ParentId = parentId;
+        }
+
+        if (reader.TryGetGuid(index++, out var topParentId))
+        {
+            entity.TopParentId = topParentId;
+        }
+
+        if (reader.TryGetString(index++, out var audioString) && Enum.TryParse<ProgramAudioEntity>(audioString, out var audioType))
+        {
+            entity.Audio = audioType;
+        }
+
+        if (reader.TryGetString(index++, out var serviceName))
+        {
+            entity.ExternalServiceId = serviceName;
+        }
+
+        if (reader.TryGetBoolean(index++, out var isInMixedFolder))
+        {
+            entity.IsInMixedFolder = isInMixedFolder;
+        }
+
+        if (reader.TryReadDateTime(index++, out var dateLastSaved))
+        {
+            entity.DateLastSaved = dateLastSaved;
+        }
+
+        if (reader.TryGetString(index++, out var lockedFields))
+        {
+            entity.LockedFields = lockedFields.Split('|').Select(Enum.Parse<MetadataField>)
+                .Select(e => new BaseItemMetadataField()
+                {
+                    Id = (int)e,
+                    Item = entity,
+                    ItemId = entity.Id
+                })
+                .ToArray();
+        }
+
+        if (reader.TryGetString(index++, out var studios))
+        {
+            entity.Studios = studios;
+        }
+
+        if (reader.TryGetString(index++, out var tags))
+        {
+            entity.Tags = tags;
+        }
+
+        if (reader.TryGetString(index++, out var trailerTypes))
+        {
+            entity.TrailerTypes = trailerTypes.Split('|').Select(Enum.Parse<TrailerType>)
+                .Select(e => new BaseItemTrailerType()
+                {
+                    Id = (int)e,
+                    Item = entity,
+                    ItemId = entity.Id
+                })
+                .ToArray();
+        }
+
+        if (reader.TryGetString(index++, out var originalTitle))
+        {
+            entity.OriginalTitle = originalTitle;
+        }
+
+        if (reader.TryGetString(index++, out var primaryVersionId))
+        {
+            entity.PrimaryVersionId = primaryVersionId;
+        }
+
+        if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))
+        {
+            entity.DateLastMediaAdded = dateLastMediaAdded;
+        }
+
+        if (reader.TryGetString(index++, out var album))
+        {
+            entity.Album = album;
+        }
+
+        if (reader.TryGetSingle(index++, out var lUFS))
+        {
+            entity.LUFS = lUFS;
+        }
+
+        if (reader.TryGetSingle(index++, out var normalizationGain))
+        {
+            entity.NormalizationGain = normalizationGain;
+        }
+
+        if (reader.TryGetSingle(index++, out var criticRating))
+        {
+            entity.CriticRating = criticRating;
+        }
+
+        if (reader.TryGetBoolean(index++, out var isVirtualItem))
+        {
+            entity.IsVirtualItem = isVirtualItem;
+        }
+
+        if (reader.TryGetString(index++, out var seriesName))
+        {
+            entity.SeriesName = seriesName;
+        }
+
+        var userDataKeys = new List<string>();
+        if (reader.TryGetString(index++, out var directUserDataKey))
+        {
+            userDataKeys.Add(directUserDataKey);
+        }
+
+        if (reader.TryGetString(index++, out var seasonName))
+        {
+            entity.SeasonName = seasonName;
+        }
+
+        if (reader.TryGetGuid(index++, out var seasonId))
+        {
+            entity.SeasonId = seasonId;
+        }
+
+        if (reader.TryGetGuid(index++, out var seriesId))
+        {
+            entity.SeriesId = seriesId;
+        }
+
+        if (reader.TryGetString(index++, out var presentationUniqueKey))
+        {
+            entity.PresentationUniqueKey = presentationUniqueKey;
+        }
+
+        if (reader.TryGetInt32(index++, out var parentalRating))
+        {
+            entity.InheritedParentalRatingValue = parentalRating;
+        }
+
+        if (reader.TryGetString(index++, out var externalSeriesId))
+        {
+            entity.ExternalSeriesId = externalSeriesId;
+        }
+
+        if (reader.TryGetString(index++, out var tagLine))
+        {
+            entity.Tagline = tagLine;
+        }
+
+        if (reader.TryGetString(index++, out var providerIds))
+        {
+            entity.Provider = providerIds.Split('|').Select(e => e.Split("="))
+            .Select(e => new BaseItemProvider()
+            {
+                Item = null!,
+                ProviderId = e[0],
+                ProviderValue = e[1]
+            }).ToArray();
+        }
+
+        if (reader.TryGetString(index++, out var imageInfos))
+        {
+            entity.Images = DeserializeImages(imageInfos).Select(f => Map(entity.Id, f)).ToArray();
+        }
+
+        if (reader.TryGetString(index++, out var productionLocations))
+        {
+            entity.ProductionLocations = productionLocations;
+        }
+
+        if (reader.TryGetString(index++, out var extraIds))
+        {
+            entity.ExtraIds = extraIds;
+        }
+
+        if (reader.TryGetInt32(index++, out var totalBitrate))
+        {
+            entity.TotalBitrate = totalBitrate;
+        }
+
+        if (reader.TryGetString(index++, out var extraTypeString) && Enum.TryParse<BaseItemExtraType>(extraTypeString, out var extraType))
+        {
+            entity.ExtraType = extraType;
+        }
+
+        if (reader.TryGetString(index++, out var artists))
+        {
+            entity.Artists = artists;
+        }
+
+        if (reader.TryGetString(index++, out var albumArtists))
+        {
+            entity.AlbumArtists = albumArtists;
+        }
+
+        if (reader.TryGetString(index++, out var externalId))
+        {
+            entity.ExternalId = externalId;
+        }
+
+        if (reader.TryGetString(index++, out var seriesPresentationUniqueKey))
+        {
+            entity.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
+        }
+
+        if (reader.TryGetString(index++, out var showId))
+        {
+            entity.ShowId = showId;
+        }
+
+        if (reader.TryGetString(index++, out var ownerId))
+        {
+            entity.OwnerId = ownerId;
+        }
+
+        if (reader.TryGetString(index++, out var mediaType))
+        {
+            entity.MediaType = mediaType;
+        }
+
+        var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
+        var dataKeys = baseItem.GetUserDataKeys();
+        userDataKeys.AddRange(dataKeys);
+
+        return (entity, userDataKeys.ToArray());
+    }
+
+    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<ItemImageInfo>();
+        }
+
+        // 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<ItemImageInfo>();
+        }
+
+        // Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
+        return result[..position];
+    }
+
+    internal ItemImageInfo? ItemImageInfoFromValueString(ReadOnlySpan<char> value)
+    {
+        const char Delimiter = '*';
+
+        var nextSegment = value.IndexOf(Delimiter);
+        if (nextSegment == -1)
+        {
+            return null;
+        }
+
+        ReadOnlySpan<char> path = value[..nextSegment];
+        value = value[(nextSegment + 1)..];
+        nextSegment = value.IndexOf(Delimiter);
+        if (nextSegment == -1)
+        {
+            return null;
+        }
+
+        ReadOnlySpan<char> dateModified = value[..nextSegment];
+        value = value[(nextSegment + 1)..];
+        nextSegment = value.IndexOf(Delimiter);
+        if (nextSegment == -1)
+        {
+            nextSegment = value.Length;
+        }
+
+        ReadOnlySpan<char> 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<char> widthSpan = value[..nextSegment];
+
+            value = value[(nextSegment + 1)..];
+            nextSegment = value.IndexOf(Delimiter);
+            if (nextSegment == -1)
+            {
+                nextSegment = value.Length;
+            }
+
+            ReadOnlySpan<char> 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<char> 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;
+    }
+}

+ 8 - 0
Jellyfin.Server/Program.cs

@@ -13,6 +13,7 @@ using Jellyfin.Server.Implementations;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using Microsoft.AspNetCore.Hosting;
+using Microsoft.Data.Sqlite;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
@@ -193,6 +194,7 @@ namespace Jellyfin.Server
                 // Don't throw additional exception if startup failed.
                 if (appHost.ServiceProvider is not null)
                 {
+                    var isSqlite = false;
                     _logger.LogInformation("Running query planner optimizations in the database... This might take a while");
                     // Run before disposing the application
                     var context = await appHost.ServiceProvider.GetRequiredService<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
@@ -200,9 +202,15 @@ namespace Jellyfin.Server
                     {
                         if (context.Database.IsSqlite())
                         {
+                            isSqlite = true;
                             await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false);
                         }
                     }
+
+                    if (isSqlite)
+                    {
+                        SqliteConnection.ClearAllPools();
+                    }
                 }
 
                 host?.Dispose();

+ 11 - 0
MediaBrowser.Common/RequiresSourceSerialisationAttribute.cs

@@ -0,0 +1,11 @@
+using System;
+
+namespace MediaBrowser.Common;
+
+/// <summary>
+/// Marks a BaseItem as needing custom serialisation from the Data field of the db.
+/// </summary>
+[System.AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
+public sealed class RequiresSourceSerialisationAttribute : System.Attribute
+{
+}

+ 0 - 19
MediaBrowser.Controller/Chapters/IChapterManager.cs

@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Controller.Chapters
-{
-    /// <summary>
-    /// Interface IChapterManager.
-    /// </summary>
-    public interface IChapterManager
-    {
-        /// <summary>
-        /// Saves the chapters.
-        /// </summary>
-        /// <param name="itemId">The item.</param>
-        /// <param name="chapters">The set of chapters.</param>
-        void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
-    }
-}

+ 49 - 0
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;
+
+/// <summary>
+/// Interface IChapterManager.
+/// </summary>
+public interface IChapterRepository
+{
+    /// <summary>
+    /// Saves the chapters.
+    /// </summary>
+    /// <param name="itemId">The item.</param>
+    /// <param name="chapters">The set of chapters.</param>
+    void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters);
+
+    /// <summary>
+    /// Gets all chapters associated with the baseItem.
+    /// </summary>
+    /// <param name="baseItem">The baseitem.</param>
+    /// <returns>A readonly list of chapter instances.</returns>
+    IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem);
+
+    /// <summary>
+    /// Gets a single chapter of a BaseItem on a specific index.
+    /// </summary>
+    /// <param name="baseItem">The baseitem.</param>
+    /// <param name="index">The index of that chapter.</param>
+    /// <returns>A chapter instance.</returns>
+    ChapterInfo? GetChapter(BaseItemDto baseItem, int index);
+
+    /// <summary>
+    /// Gets all chapters associated with the baseItem.
+    /// </summary>
+    /// <param name="baseItemId">The BaseItems id.</param>
+    /// <returns>A readonly list of chapter instances.</returns>
+    IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId);
+
+    /// <summary>
+    /// Gets a single chapter of a BaseItem on a specific index.
+    /// </summary>
+    /// <param name="baseItemId">The BaseItems id.</param>
+    /// <param name="index">The index of that chapter.</param>
+    /// <returns>A chapter instance.</returns>
+    ChapterInfo? GetChapter(Guid baseItemId, int index);
+}

+ 25 - 0
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
         /// <returns>BlurHash.</returns>
         string GetImageBlurHash(string path, ImageDimensions imageDimensions);
 
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="baseItemPath">The items basePath.</param>
+        /// <param name="imageDateModified">The image last modification date.</param>
+        /// <returns>Guid.</returns>
+        string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified);
+
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="image">The image.</param>
+        /// <returns>Guid.</returns>
+        string? GetImageCacheTag(BaseItemDto item, ChapterInfo image);
+
         /// <summary>
         /// Gets the image cache tag.
         /// </summary>
@@ -65,6 +82,14 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>Guid.</returns>
         string GetImageCacheTag(BaseItem item, ItemImageInfo image);
 
+        /// <summary>
+        /// Gets the image cache tag.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="image">The image.</param>
+        /// <returns>Guid.</returns>
+        string GetImageCacheTag(BaseItemDto item, ItemImageInfo image);
+
         string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
 
         string? GetImageCacheTag(User user);

+ 1 - 1
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities
             return CreateResolveArgs(directoryService, true).FileSystemChildren;
         }
 
-        protected override List<BaseItem> LoadChildren()
+        protected override IReadOnlyList<BaseItem> LoadChildren()
         {
             lock (_childIdsLock)
             {

+ 1 - 0
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// <summary>
     /// Class MusicAlbum.
     /// </summary>
+    [Common.RequiresSourceSerialisation]
     public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
     {
         public MusicAlbum()

+ 2 - 1
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// <summary>
     /// Class MusicArtist.
     /// </summary>
+    [Common.RequiresSourceSerialisation]
     public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
     {
         [JsonIgnore]
@@ -84,7 +85,7 @@ namespace MediaBrowser.Controller.Entities.Audio
             return !IsAccessedByName;
         }
 
-        public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+        public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
         {
             if (query.IncludeItemTypes.Length == 0)
             {

+ 2 - 1
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     /// <summary>
     /// Class MusicGenre.
     /// </summary>
+    [Common.RequiresSourceSerialisation]
     public class MusicGenre : BaseItem, IItemByName
     {
         [JsonIgnore]
@@ -64,7 +65,7 @@ namespace MediaBrowser.Controller.Entities.Audio
             return true;
         }
 
-        public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
+        public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
         {
             query.GenreIds = new[] { Id };
             query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist };

+ 1 - 0
MediaBrowser.Controller/Entities/AudioBook.cs

@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Providers;
 
 namespace MediaBrowser.Controller.Entities
 {
+    [Common.RequiresSourceSerialisation]
     public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo>
     {
         [JsonIgnore]

Vissa filer visades inte eftersom för många filer har ändrats