From ee0dad6f432e5bfdda074e3f006f4c4d3c418d08 Mon Sep 17 00:00:00 2001
From: JPVenson <github@jpb.email>
Date: Thu, 10 Oct 2024 14:32:49 +0000
Subject: [PATCH] Refactored ItemValue structure

---
 .../Data/CleanDatabaseScheduledTask.cs        |   17 +-
 Jellyfin.Data/Entities/AncestorId.cs          |   14 +-
 Jellyfin.Data/Entities/BaseItemEntity.cs      |    2 +-
 Jellyfin.Data/Entities/ItemValue.cs           |   18 +-
 Jellyfin.Data/Entities/ItemValueMap.cs        |   30 +
 .../Item/BaseItemRepository.cs                |  153 +-
 .../JellyfinDbContext.cs                      |    5 +
 ...2_FixedItemValueReferenceStyle.Designer.cs | 1582 +++++++++++++++++
 ...1010142722_FixedItemValueReferenceStyle.cs |  133 ++
 .../Migrations/JellyfinDbModelSnapshot.cs     |   60 +-
 .../AncestorIdConfiguration.cs                |    2 +
 .../ItemValuesConfiguration.cs                |    4 +-
 .../ItemValuesMapConfiguration.cs             |   20 +
 .../Migrations/Routines/MigrateLibraryDb.cs   |   30 +-
 14 files changed, 1967 insertions(+), 103 deletions(-)
 create mode 100644 Jellyfin.Data/Entities/ItemValueMap.cs
 create mode 100644 Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs
 create mode 100644 Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs
 create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs

diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 4516b89dc2..932bd2b05a 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,10 +1,13 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Data
@@ -13,11 +16,16 @@ namespace Emby.Server.Implementations.Data
     {
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger<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)
@@ -34,7 +42,7 @@ namespace Emby.Server.Implementations.Data
             });
 
             var numComplete = 0;
-            var numItems = itemIds.Count;
+            var numItems = itemIds.Count + 1;
 
             _logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
 
@@ -60,6 +68,11 @@ namespace Emby.Server.Implementations.Data
                 progress.Report(percent * 100);
             }
 
+            using var context = _dbProvider.CreateDbContext();
+            using var transaction = context.Database.BeginTransaction();
+            context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
+            transaction.Commit();
+
             progress.Report(100);
         }
     }
diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Data/Entities/AncestorId.cs
index 941a8eb2e1..ef0fe0ba71 100644
--- a/Jellyfin.Data/Entities/AncestorId.cs
+++ b/Jellyfin.Data/Entities/AncestorId.cs
@@ -8,12 +8,22 @@ namespace Jellyfin.Data.Entities;
 public class AncestorId
 {
     /// <summary>
-    /// Gets or Sets the AncestorId that may or may not be an database managed Item or an materialised local item.
+    /// Gets or Sets the AncestorId.
     /// </summary>
     public required Guid ParentItemId { get; set; }
 
     /// <summary>
-    /// Gets or Sets the related that may or may not be an database managed Item or an materialised local item.
+    /// Gets or Sets the related BaseItem.
     /// </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; }
 }
diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Data/Entities/BaseItemEntity.cs
index cd1991891f..7670c18930 100644
--- a/Jellyfin.Data/Entities/BaseItemEntity.cs
+++ b/Jellyfin.Data/Entities/BaseItemEntity.cs
@@ -158,7 +158,7 @@ public class BaseItemEntity
 
     public ICollection<UserData>? UserData { get; set; }
 
-    public ICollection<ItemValue>? ItemValues { get; set; }
+    public ICollection<ItemValueMap>? ItemValues { get; set; }
 
     public ICollection<MediaStreamInfo>? MediaStreams { get; set; }
 
diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Data/Entities/ItemValue.cs
index bfa53cd465..7b1048c10c 100644
--- a/Jellyfin.Data/Entities/ItemValue.cs
+++ b/Jellyfin.Data/Entities/ItemValue.cs
@@ -1,7 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
 
 namespace Jellyfin.Data.Entities;
 
@@ -11,14 +9,9 @@ namespace Jellyfin.Data.Entities;
 public class ItemValue
 {
     /// <summary>
-    /// Gets or Sets the reference ItemId.
+    /// Gets or Sets the ItemValueId.
     /// </summary>
-    public required Guid ItemId { get; set; }
-
-    /// <summary>
-    /// Gets or Sets the referenced BaseItem.
-    /// </summary>
-    public required BaseItemEntity Item { get; set; }
+    public required Guid ItemValueId { get; set; }
 
     /// <summary>
     /// Gets or Sets the Type.
@@ -34,4 +27,11 @@ public class ItemValue
     /// 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
 }
diff --git a/Jellyfin.Data/Entities/ItemValueMap.cs b/Jellyfin.Data/Entities/ItemValueMap.cs
new file mode 100644
index 0000000000..94db6a011b
--- /dev/null
+++ b/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; }
+}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 86c6820275..d7de7e9bda 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -69,10 +69,11 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
         context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
         context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
         context.AncestorIds.Where(e => e.ItemId == id).ExecuteDelete();
-        context.ItemValues.Where(e => e.ItemId == id).ExecuteDelete();
-        context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
+        context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
+        context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
         context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
         context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
+        context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
         context.SaveChanges();
         transaction.Commit();
     }
@@ -83,25 +84,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
         using var context = dbProvider.CreateDbContext();
         using var transaction = context.Database.BeginTransaction();
 
-        context.ItemValues.Where(e => e.Type == ItemValueType.InheritedTags).ExecuteDelete();
-        context.ItemValues.AddRange(context.ItemValues.Where(e => e.Type == ItemValueType.Tags).Select(e => new ItemValue()
-        {
-            CleanValue = e.CleanValue,
-            ItemId = e.ItemId,
-            Type = ItemValueType.InheritedTags,
-            Value = e.Value,
-            Item = null!
-        }));
-
-        context.ItemValues.AddRange(
-            context.AncestorIds.Join(context.ItemValues.Where(e => e.Value != null && e.Type == ItemValueType.Tags), e => e.ParentItemId, e => e.ItemId, (e, f) => new ItemValue()
-            {
-                CleanValue = f.CleanValue,
-                ItemId = e.ItemId,
-                Item = null!,
-                Type = ItemValueType.InheritedTags,
-                Value = f.Value
-            }));
+        context.ItemValuesMap.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags).ExecuteDelete();
+        // ItemValue Inheritence is now correctly mapped via AncestorId on demand
         context.SaveChanges();
 
         transaction.Commit();
@@ -717,24 +701,22 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
             }
         }
 
-        var artistQuery = context.BaseItems.Where(w => filter.ArtistIds.Contains(w.Id));
-
         if (filter.ArtistIds.Length > 0)
         {
             baseQuery = baseQuery
-                   .Where(e => e.ItemValues!.Any(f => f.Type <= ItemValueType.Artist && artistQuery.Any(w => w.CleanName == f.CleanValue)));
+                   .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type <= ItemValueType.Artist && filter.ArtistIds.Contains(f.ItemId)));
         }
 
         if (filter.AlbumArtistIds.Length > 0)
         {
             baseQuery = baseQuery
-               .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Artist && artistQuery.Any(w => w.CleanName == f.CleanValue)));
+                   .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId)));
         }
 
         if (filter.ContributingArtistIds.Length > 0)
         {
-            var contributingArtists = context.BaseItems.Where(e => filter.ContributingArtistIds.Contains(e.Id));
-            baseQuery = baseQuery.Where(e => e.ItemValues!.Any(f => f.Type == 0 && contributingArtists.Any(w => w.CleanName == f.CleanValue)));
+            baseQuery = baseQuery
+                   .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId)));
         }
 
         if (filter.AlbumIds.Length > 0)
@@ -744,42 +726,41 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
 
         if (filter.ExcludeArtistIds.Length > 0)
         {
-            var excludeArtistQuery = context.BaseItems.Where(w => filter.ExcludeArtistIds.Contains(w.Id));
             baseQuery = baseQuery
-                   .Where(e => !e.ItemValues!.Any(f => f.Type <= ItemValueType.Artist && artistQuery.Any(w => w.CleanName == f.CleanValue)));
+                   .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ExcludeArtistIds.Contains(f.ItemId)));
         }
 
         if (filter.GenreIds.Count > 0)
         {
             baseQuery = baseQuery
-                   .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Genre && context.BaseItems.Where(w => filter.GenreIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue)));
+                   .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && filter.GenreIds.Contains(f.ItemId)));
         }
 
         if (filter.Genres.Count > 0)
         {
             var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray();
             baseQuery = baseQuery
-                   .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Genre && cleanGenres.Contains(f.CleanValue)));
+                    .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && cleanGenres.Contains(f.ItemValue.CleanValue)));
         }
 
         if (tags.Count > 0)
         {
             var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray();
             baseQuery = baseQuery
-                   .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Tags && cleanValues.Contains(f.CleanValue)));
+                    .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
         }
 
         if (excludeTags.Count > 0)
         {
             var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray();
             baseQuery = baseQuery
-                   .Where(e => !e.ItemValues!.Any(f => f.Type == ItemValueType.Tags && cleanValues.Contains(f.CleanValue)));
+                    .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
         }
 
         if (filter.StudioIds.Length > 0)
         {
             baseQuery = baseQuery
-                   .Where(e => e.ItemValues!.Any(f => f.Type == ItemValueType.Studios && context.BaseItems.Where(w => filter.StudioIds.Contains(w.Id)).Any(w => w.CleanName == f.CleanValue)));
+                    .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId)));
         }
 
         if (filter.OfficialRatings.Length > 0)
@@ -936,13 +917,13 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
         if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
         {
             baseQuery = baseQuery
-                .Where(e => !e.ItemValues!.Any(f => (f.Type == ItemValueType.Artist || f.Type == ItemValueType.AlbumArtist) && f.CleanValue == e.CleanName));
+                    .Where(e => e.ItemValues!.Count(f => (f.ItemValue.Type == ItemValueType.Artist || f.ItemValue.Type == ItemValueType.AlbumArtist)) == 1);
         }
 
         if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
         {
             baseQuery = baseQuery
-                .Where(e => !e.ItemValues!.Any(f => f.Type == ItemValueType.Studios && f.CleanValue == e.CleanName));
+                    .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1);
         }
 
         if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
@@ -1081,8 +1062,8 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
         if (filter.ExcludeInheritedTags.Length > 0)
         {
             baseQuery = baseQuery
-                .Where(e => !e.ItemValues!.Where(e => e.Type == ItemValueType.InheritedTags)
-                    .Any(f => filter.ExcludeInheritedTags.Contains(f.CleanValue)));
+                .Where(e => !e.ItemValues!.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags)
+                    .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
         }
 
         if (filter.IncludeInheritedTags.Length > 0)
@@ -1092,26 +1073,25 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
             if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
             {
                 baseQuery = baseQuery
-                    .Where(e => e.ItemValues!.Where(e => e.Type == ItemValueType.InheritedTags)
-                        .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue))
+                    .Where(e => e.ItemValues!.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags)
+                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
                         ||
-                        (e.ParentId.HasValue && context.ItemValues.Where(w => w.ItemId == e.ParentId.Value)!.Where(e => e.Type == ItemValueType.InheritedTags)
-                        .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue))));
+                        (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(e => e.ItemValue.Type == ItemValueType.InheritedTags)
+                        .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
             }
 
             // A playlist should be accessible to its owner regardless of allowed tags.
             else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
             {
                 baseQuery = baseQuery
-                    .Where(e => e.ItemValues!.Where(e => e.Type == ItemValueType.InheritedTags)
-                        .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
-                // d                                                                      ^^ this is stupid it hate this.
+                    .Where(e => e.AncestorIds!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
+                         || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
+                // d        ^^ this is stupid it hate this.
             }
             else
             {
                 baseQuery = baseQuery
-                    .Where(e => e.ItemValues!.Where(e => e.Type == ItemValueType.InheritedTags)
-                        .Any(f => filter.IncludeInheritedTags.Contains(f.CleanValue)));
+                    .Where(e => e.AncestorIds!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
             }
         }
 
@@ -1277,25 +1257,48 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
                         entity.AncestorIds.Add(new AncestorId()
                         {
                             ParentItemId = ancestorId,
-                            ItemId = entity.Id
+                            ItemId = entity.Id,
+                            Item = null!,
+                            ParentItem = null!
                         });
                     }
                 }
 
-                var itemValues = GetItemValuesToSave(item.Item, item.InheritedTags);
-                context.ItemValues.Where(e => e.ItemId == entity.Id).ExecuteDelete();
-                entity.ItemValues = new List<ItemValue>();
+                var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags);
+                var itemValues = itemValuesToSave.Select(e => e.Value).ToArray();
+                context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+                entity.ItemValues = new List<ItemValueMap>();
+                var referenceValues = context.ItemValues.Where(e => itemValues.Any(f => f == e.CleanValue)).ToArray();
 
-                foreach (var itemValue in itemValues)
+                foreach (var itemValue in itemValuesToSave)
                 {
-                    entity.ItemValues.Add(new()
+                    var refValue = referenceValues.FirstOrDefault(f => f.CleanValue == itemValue.Value && (int)f.Type == itemValue.MagicNumber);
+                    if (refValue is not null)
                     {
-                        Item = entity,
-                        Type = (ItemValueType)itemValue.MagicNumber,
-                        Value = itemValue.Value,
-                        CleanValue = GetCleanValue(itemValue.Value),
-                        ItemId = entity.Id
-                    });
+                        entity.ItemValues.Add(new ItemValueMap()
+                        {
+                            Item = entity,
+                            ItemId = entity.Id,
+                            ItemValue = null!,
+                            ItemValueId = refValue.ItemValueId
+                        });
+                    }
+                    else
+                    {
+                        entity.ItemValues.Add(new ItemValueMap()
+                        {
+                            Item = entity,
+                            ItemId = entity.Id,
+                            ItemValue = new ItemValue()
+                            {
+                                CleanValue = GetCleanValue(itemValue.Value),
+                                Type = (ItemValueType)itemValue.MagicNumber,
+                                ItemValueId = Guid.NewGuid(),
+                                Value = itemValue.Value
+                            },
+                            ItemValueId = Guid.Empty
+                        });
+                    }
                 }
             }
 
@@ -1652,21 +1655,21 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
     {
         using var context = dbProvider.CreateDbContext();
 
-        var query = context.ItemValues
+        var query = context.ItemValuesMap
             .AsNoTracking()
-            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.Type));
+            .Where(e => itemValueTypes.Any(w => (ItemValueType)w == e.ItemValue.Type));
         if (withItemTypes.Count > 0)
         {
-            query = query.Where(e => context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId == e.ItemId)));
+            query = query.Where(e => withItemTypes.Contains(e.Item.Type));
         }
 
         if (excludeItemTypes.Count > 0)
         {
-            query = query.Where(e => !context.BaseItems.Where(e => withItemTypes.Contains(e.Type)).Any(f => f.ItemValues!.Any(w => w.ItemId == e.ItemId)));
+            query = query.Where(e => !excludeItemTypes.Contains(e.Item.Type));
         }
 
         // query = query.DistinctBy(e => e.CleanValue);
-        return query.Select(e => e.CleanValue).ToImmutableArray();
+        return query.Select(e => e.ItemValue.CleanValue).ToImmutableArray();
     }
 
     private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity)
@@ -1705,7 +1708,7 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
         };
         var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery);
 
-        query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.Type)));
+        query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
 
         if (filter.OrderBy.Count != 0
             || !string.IsNullOrEmpty(filter.SearchTerm))
@@ -1745,13 +1748,13 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
             // TODO: This is bad refactor!
             itemCount = new ItemCounts()
             {
-                SeriesCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Series).FullName)),
-                EpisodeCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Episode).FullName)),
-                MovieCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Data.Entities.Libraries.Movie).FullName)),
-                AlbumCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(MusicAlbum).FullName)),
-                ArtistCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(MusicArtist).FullName)),
-                SongCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Audio).FullName)),
-                TrailerCount = context.ItemValues.Count(f => e.ItemValues!.Any(w => w.Type == f.Type && w.CleanValue == f.CleanValue && f.Item.Type == typeof(Trailer).FullName)),
+                SeriesCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Series).FullName),
+                EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Data.Entities.Libraries.Movie).FullName),
+                MovieCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Series).FullName),
+                AlbumCount = e.ItemValues!.Count(f => f.Item.Type == typeof(MusicAlbum).FullName),
+                ArtistCount = e.ItemValues!.Count(f => f.Item.Type == typeof(MusicArtist).FullName),
+                SongCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Audio).FullName),
+                TrailerCount = e.ItemValues!.Count(f => f.Item.Type == typeof(Trailer).FullName),
             }
         });
 
@@ -1981,9 +1984,9 @@ public sealed class BaseItemRepository(IDbContextFactory<JellyfinDbContext> dbPr
             ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.Played,
             ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id && f.Key == e.UserDataKey)!.Played,
             ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
-            ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.Type == 0).Select(f => f.CleanValue),
-            ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.Type == ItemValueType.AlbumArtist).Select(f => f.CleanValue),
-            ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.Type == ItemValueType.Studios).Select(f => f.CleanValue),
+            ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue),
+            ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue),
+            ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue),
             ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue,
             // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
             ItemSortBy.SeriesSortName => e => e.SeriesName,
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index 406230a70a..284897c994 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -116,6 +116,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
     /// </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}"/> containing the user data.
     /// </summary>
diff --git a/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs
new file mode 100644
index 0000000000..00a943f794
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs
@@ -0,0 +1,1582 @@
+// <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("20241010142722_FixedItemValueReferenceStyle")]
+    partial class FixedItemValueReferenceStyle
+    {
+        /// <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<string>("UserDataKey")
+                        .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("UserDataKey", "Type");
+
+                    b.HasIndex("Type", "TopParentId", "Id");
+
+                    b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "TopParentId", "StartDate");
+
+                    b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+                    b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.ToTable("BaseItems");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+                {
+                    b.Property<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>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Role", "ListOrder");
+
+                    b.HasIndex("Name");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<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<string>("Key")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("BaseItemEntityId")
+                        .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("Key", "UserId");
+
+                    b.HasIndex("BaseItemEntityId");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("Key", "UserId", "IsFavorite");
+
+                    b.HasIndex("Key", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("Key", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("Key", "UserId", "Played");
+
+                    b.ToTable("UserData");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null)
+                        .WithMany("AncestorIds")
+                        .HasForeignKey("BaseItemEntityId");
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany()
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany()
+                        .HasForeignKey("ParentItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ParentItem");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany()
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Images")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("LockedFields")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Provider")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("TrailerTypes")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("ItemValues")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+                        .WithMany("BaseItemsMap")
+                        .HasForeignKey("ItemValueId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ItemValue");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("MediaStreams")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null)
+                        .WithMany("UserData")
+                        .HasForeignKey("BaseItemEntityId");
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("AncestorIds");
+
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("Peoples");
+
+                    b.Navigation("Provider");
+
+                    b.Navigation("TrailerTypes");
+
+                    b.Navigation("UserData");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Navigation("HomeSections");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Navigation("BaseItemsMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Navigation("AccessSchedules");
+
+                    b.Navigation("DisplayPreferences");
+
+                    b.Navigation("ItemDisplayPreferences");
+
+                    b.Navigation("Permissions");
+
+                    b.Navigation("Preferences");
+
+                    b.Navigation("ProfileImage");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs
new file mode 100644
index 0000000000..9b1985254f
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs
@@ -0,0 +1,133 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class FixedItemValueReferenceStyle : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropForeignKey(
+                name: "FK_ItemValues_BaseItems_ItemId",
+                table: "ItemValues");
+
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_ItemValues",
+                table: "ItemValues");
+
+            migrationBuilder.DropIndex(
+                name: "IX_ItemValues_ItemId_Type_CleanValue",
+                table: "ItemValues");
+
+            migrationBuilder.RenameColumn(
+                name: "ItemId",
+                table: "ItemValues",
+                newName: "ItemValueId");
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_ItemValues",
+                table: "ItemValues",
+                column: "ItemValueId");
+
+            migrationBuilder.CreateTable(
+                name: "ItemValuesMap",
+                columns: table => new
+                {
+                    ItemId = table.Column<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.CreateIndex(
+                name: "IX_ItemValues_Type_CleanValue",
+                table: "ItemValues",
+                columns: new[] { "Type", "CleanValue" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemValuesMap_ItemId",
+                table: "ItemValuesMap",
+                column: "ItemId");
+
+            migrationBuilder.AddForeignKey(
+                name: "FK_AncestorIds_BaseItems_ItemId",
+                table: "AncestorIds",
+                column: "ItemId",
+                principalTable: "BaseItems",
+                principalColumn: "Id",
+                onDelete: ReferentialAction.Cascade);
+
+            migrationBuilder.AddForeignKey(
+                name: "FK_AncestorIds_BaseItems_ParentItemId",
+                table: "AncestorIds",
+                column: "ParentItemId",
+                principalTable: "BaseItems",
+                principalColumn: "Id",
+                onDelete: ReferentialAction.Cascade);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropForeignKey(
+                name: "FK_AncestorIds_BaseItems_ItemId",
+                table: "AncestorIds");
+
+            migrationBuilder.DropForeignKey(
+                name: "FK_AncestorIds_BaseItems_ParentItemId",
+                table: "AncestorIds");
+
+            migrationBuilder.DropTable(
+                name: "ItemValuesMap");
+
+            migrationBuilder.DropPrimaryKey(
+                name: "PK_ItemValues",
+                table: "ItemValues");
+
+            migrationBuilder.DropIndex(
+                name: "IX_ItemValues_Type_CleanValue",
+                table: "ItemValues");
+
+            migrationBuilder.RenameColumn(
+                name: "ItemValueId",
+                table: "ItemValues",
+                newName: "ItemId");
+
+            migrationBuilder.AddPrimaryKey(
+                name: "PK_ItemValues",
+                table: "ItemValues",
+                columns: new[] { "ItemId", "Type", "Value" });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_ItemValues_ItemId_Type_CleanValue",
+                table: "ItemValues",
+                columns: new[] { "ItemId", "Type", "CleanValue" });
+
+            migrationBuilder.AddForeignKey(
+                name: "FK_ItemValues_BaseItems_ItemId",
+                table: "ItemValues",
+                column: "ItemId",
+                principalTable: "BaseItems",
+                principalColumn: "Id",
+                onDelete: ReferentialAction.Cascade);
+        }
+    }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index 49abeef5cc..20d7cf3dda 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -683,26 +683,43 @@ namespace Jellyfin.Server.Implementations.Migrations
 
             modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
                 {
-                    b.Property<Guid>("ItemId")
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
                         .HasColumnType("TEXT");
 
                     b.Property<int>("Type")
                         .HasColumnType("INTEGER");
 
                     b.Property<string>("Value")
-                        .HasColumnType("TEXT");
-
-                    b.Property<string>("CleanValue")
                         .IsRequired()
                         .HasColumnType("TEXT");
 
-                    b.HasKey("ItemId", "Type", "Value");
+                    b.HasKey("ItemValueId");
 
-                    b.HasIndex("ItemId", "Type", "CleanValue");
+                    b.HasIndex("Type", "CleanValue");
 
                     b.ToTable("ItemValues");
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<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")
@@ -1307,6 +1324,22 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", null)
                         .WithMany("AncestorIds")
                         .HasForeignKey("BaseItemEntityId");
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany()
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany()
+                        .HasForeignKey("ParentItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ParentItem");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
@@ -1410,7 +1443,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsRequired();
                 });
 
-            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
                 {
                     b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
                         .WithMany("ItemValues")
@@ -1418,7 +1451,15 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .OnDelete(DeleteBehavior.Cascade)
                         .IsRequired();
 
+                    b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+                        .WithMany("BaseItemsMap")
+                        .HasForeignKey("ItemValueId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
                     b.Navigation("Item");
+
+                    b.Navigation("ItemValue");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
@@ -1513,6 +1554,11 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Navigation("HomeSections");
                 });
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Navigation("BaseItemsMap");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
                 {
                     b.Navigation("AccessSchedules");
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
index 0e90b8d820..fe5cf30ac4 100644
--- a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
@@ -15,5 +15,7 @@ public class AncestorIdConfiguration : IEntityTypeConfiguration<AncestorId>
     {
         builder.HasKey(e => new { e.ItemId, e.ParentItemId });
         builder.HasIndex(e => e.ParentItemId);
+        builder.HasOne(e => e.ParentItem);
+        builder.HasOne(e => e.Item);
     }
 }
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
index c39854f5ac..7dfa2032e2 100644
--- a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
@@ -13,7 +13,7 @@ public class ItemValuesConfiguration : IEntityTypeConfiguration<ItemValue>
     /// <inheritdoc/>
     public void Configure(EntityTypeBuilder<ItemValue> builder)
     {
-        builder.HasKey(e => new { e.ItemId, e.Type, e.Value });
-        builder.HasIndex(e => new { e.ItemId, e.Type, e.CleanValue });
+        builder.HasKey(e => e.ItemValueId);
+        builder.HasIndex(e => new { e.Type, e.CleanValue });
     }
 }
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
new file mode 100644
index 0000000000..9c22b114c7
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
@@ -0,0 +1,20 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <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);
+    }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 85d537380b..294c4e8a68 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -113,12 +113,31 @@ public class MigrateLibraryDb : IMigrationRoutine
 
         dbContext.SaveChanges();
 
-        var itemValueQuery = "select ItemId, Type, Value, CleanValue FROM ItemValues";
+        // do not migrate inherited types as they are now properly mapped in search and lookup.
+        var itemValueQuery = "select ItemId, Type, Value, CleanValue FROM ItemValues WHERE Type <> 6";
         dbContext.ItemValues.ExecuteDelete();
 
         foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
         {
-            dbContext.ItemValues.Add(GetItemValue(dto));
+            var itemId = dto.GetGuid(0);
+            var entity = GetItemValue(dto);
+            var existingItemValue = dbContext.ItemValues.FirstOrDefault(f => f.Type == entity.Type && f.Value == entity.Value);
+            if (existingItemValue is null)
+            {
+                dbContext.ItemValues.Add(entity);
+            }
+            else
+            {
+                entity = existingItemValue;
+            }
+
+            dbContext.ItemValuesMap.Add(new ItemValueMap()
+            {
+                Item = null!,
+                ItemValue = null!,
+                ItemId = itemId,
+                ItemValueId = entity.ItemValueId
+            });
         }
 
         dbContext.SaveChanges();
@@ -185,7 +204,9 @@ public class MigrateLibraryDb : IMigrationRoutine
         return new AncestorId()
         {
             ItemId = reader.GetGuid(0),
-            ParentItemId = reader.GetGuid(1)
+            ParentItemId = reader.GetGuid(1),
+            Item = null!,
+            ParentItem = null!
         };
     }
 
@@ -226,11 +247,10 @@ public class MigrateLibraryDb : IMigrationRoutine
     {
         return new ItemValue
         {
-            ItemId = reader.GetGuid(0),
+            ItemValueId = Guid.NewGuid(),
             Type = (ItemValueType)reader.GetInt32(1),
             Value = reader.GetString(2),
             CleanValue = reader.GetString(3),
-            Item = null!
         };
     }