aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs17
-rw-r--r--Jellyfin.Data/Entities/AncestorId.cs14
-rw-r--r--Jellyfin.Data/Entities/BaseItemEntity.cs2
-rw-r--r--Jellyfin.Data/Entities/ItemValue.cs18
-rw-r--r--Jellyfin.Data/Entities/ItemValueMap.cs30
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs153
-rw-r--r--Jellyfin.Server.Implementations/JellyfinDbContext.cs5
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs1582
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs133
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs60
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs2
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs4
-rw-r--r--Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs20
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs30
14 files changed, 1967 insertions, 103 deletions
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index 4516b89dc..932bd2b05 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -1,10 +1,13 @@
#pragma warning disable CS1591
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Server.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
@@ -13,11 +16,16 @@ namespace Emby.Server.Implementations.Data
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<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 941a8eb2e..ef0fe0ba7 100644
--- a/Jellyfin.Data/Entities/AncestorId.cs
+++ b/Jellyfin.Data/Entities/AncestorId.cs
@@ -8,12 +8,22 @@ namespace Jellyfin.Data.Entities;
public class AncestorId
{
/// <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 cd1991891..7670c1893 100644
--- a/Jellyfin.Data/Entities/BaseItemEntity.cs
+++ b/Jellyfin.Data/Entities/BaseItemEntity.cs
@@ -158,7 +158,7 @@ public class BaseItemEntity
public ICollection<UserData>? 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 bfa53cd46..7b1048c10 100644
--- a/Jellyfin.Data/Entities/ItemValue.cs
+++ b/Jellyfin.Data/Entities/ItemValue.cs
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
namespace Jellyfin.Data.Entities;
@@ -11,14 +9,9 @@ namespace Jellyfin.Data.Entities;
public class ItemValue
{
/// <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 000000000..94db6a011
--- /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 86c682027..d7de7e9bd 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -69,10 +69,11 @@ public sealed class BaseItemRepository(IDbContextFactory<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 406230a70..284897c99 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -117,6 +117,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
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>
public DbSet<MediaStreamInfo> MediaStreamInfos => Set<MediaStreamInfo>();
diff --git a/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs
new file mode 100644
index 000000000..00a943f79
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.Designer.cs
@@ -0,0 +1,1582 @@
+// <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 000000000..9b1985254
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20241010142722_FixedItemValueReferenceStyle.cs
@@ -0,0 +1,133 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <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 49abeef5c..20d7cf3dd 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -683,26 +683,43 @@ namespace Jellyfin.Server.Implementations.Migrations
modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
{
- b.Property<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 0e90b8d82..fe5cf30ac 100644
--- a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
@@ -15,5 +15,7 @@ public class AncestorIdConfiguration : IEntityTypeConfiguration<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 c39854f5a..7dfa2032e 100644
--- a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
@@ -13,7 +13,7 @@ public class ItemValuesConfiguration : IEntityTypeConfiguration<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 000000000..9c22b114c
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
@@ -0,0 +1,20 @@
+using System;
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <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 85d537380..294c4e8a6 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -113,12 +113,31 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.SaveChanges();
- var itemValueQuery = "select ItemId, Type, Value, CleanValue FROM ItemValues";
+ // do not migrate inherited types as they are now properly mapped in search and lookup.
+ var itemValueQuery = "select ItemId, Type, Value, CleanValue FROM ItemValues WHERE Type <> 6";
dbContext.ItemValues.ExecuteDelete();
foreach (SqliteDataReader dto in connection.Query(itemValueQuery))
{
- dbContext.ItemValues.Add(GetItemValue(dto));
+ var itemId = dto.GetGuid(0);
+ var entity = GetItemValue(dto);
+ var existingItemValue = dbContext.ItemValues.FirstOrDefault(f => f.Type == entity.Type && f.Value == entity.Value);
+ if (existingItemValue is null)
+ {
+ dbContext.ItemValues.Add(entity);
+ }
+ else
+ {
+ entity = existingItemValue;
+ }
+
+ dbContext.ItemValuesMap.Add(new ItemValueMap()
+ {
+ Item = null!,
+ ItemValue = null!,
+ ItemId = itemId,
+ ItemValueId = entity.ItemValueId
+ });
}
dbContext.SaveChanges();
@@ -185,7 +204,9 @@ public class MigrateLibraryDb : IMigrationRoutine
return new AncestorId()
{
ItemId = reader.GetGuid(0),
- ParentItemId = reader.GetGuid(1)
+ ParentItemId = reader.GetGuid(1),
+ Item = null!,
+ ParentItem = null!
};
}
@@ -226,11 +247,10 @@ public class MigrateLibraryDb : IMigrationRoutine
{
return new ItemValue
{
- ItemId = reader.GetGuid(0),
+ ItemValueId = Guid.NewGuid(),
Type = (ItemValueType)reader.GetInt32(1),
Value = reader.GetString(2),
CleanValue = reader.GetString(3),
- Item = null!
};
}