diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
index bfd0fac34a..9a42c86f7d 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
@@ -8,7 +8,7 @@ using Jellyfin.Database.Implementations.MatchCriteria;
namespace Jellyfin.Database.Implementations;
///
-/// Provides methods for querying item hierarchies using iterative traversal.
+/// Provides methods for querying item hierarchies.
/// Uses AncestorIds and LinkedChildren tables for parent-child traversal.
///
public static class DescendantQueryHelper
@@ -32,11 +32,18 @@ public static class DescendantQueryHelper
{
ArgumentNullException.ThrowIfNull(context);
- var descendants = TraverseHierarchyDown(context, [parentId]);
+ var (closureRoots, linkRoots) = ResolveLinkedRoots(context, parentId);
- descendants.Remove(parentId);
+ var hierarchyDescendants = ClosureDescendants(context, closureRoots);
- return descendants.AsQueryable();
+ var linkedDescendants = context.LinkedChildren
+ .WhereOneOrMany(linkRoots, e => e.ParentId)
+ .Select(e => e.ChildId);
+
+ return hierarchyDescendants
+ .Concat(linkedDescendants)
+ .Where(e => !e.Equals(parentId))
+ .Distinct();
}
///
@@ -51,11 +58,9 @@ public static class DescendantQueryHelper
{
ArgumentNullException.ThrowIfNull(context);
- var descendants = TraverseHierarchyDownOwned(context, [parentId]);
-
- descendants.Remove(parentId);
-
- return descendants.AsQueryable();
+ return ClosureDescendants(context, [parentId])
+ .Where(e => !e.Equals(parentId))
+ .Distinct();
}
///
@@ -76,11 +81,12 @@ public static class DescendantQueryHelper
return [];
}
- var seedSet = new HashSet(parentIds);
- var descendants = TraverseHierarchyDownOwned(context, seedSet);
+ var descendants = ClosureDescendants(context, parentIds)
+ .Distinct()
+ .ToHashSet();
- // Remove the seed IDs — callers want only descendants
- descendants.ExceptWith(seedSet);
+ // The callers want only descendants, and an item is never its own descendant.
+ descendants.ExceptWith(parentIds);
return descendants;
}
@@ -96,28 +102,48 @@ public static class DescendantQueryHelper
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(criteria);
+
var matchingItemIds = criteria switch
{
HasSubtitles => context.MediaStreamInfos
.Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle)
- .Select(ms => ms.ItemId)
- .Distinct()
- .ToHashSet(),
+ .Select(ms => ms.ItemId),
HasChapterImages => context.Chapters
.Where(c => c.ImagePath != null)
- .Select(c => c.ItemId)
- .Distinct()
- .ToHashSet(),
+ .Select(c => c.ItemId),
HasMediaStreamType m => GetMatchingMediaStreamItemIds(context, m),
_ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Name}")
};
- var ancestors = TraverseHierarchyUp(context, matchingItemIds);
+ // One hop up the closure covers every ancestor level.
+ var hierarchyAncestors = context.AncestorIds
+ .Where(e => matchingItemIds.Contains(e.ItemId))
+ .Select(e => e.ParentItemId);
- return ancestors.AsQueryable();
+ var linkParents = ResolveLinkParents(context, matchingItemIds, hierarchyAncestors);
+
+ // The link parents are resolved ids, so they are read back as a sub-select to keep the result
+ // composable. An id without a BaseItem row could never match a caller's row anyway.
+ var linkedParents = context.BaseItems
+ .WhereOneOrMany(linkParents, e => e.Id)
+ .Select(e => e.Id);
+
+ var linkedParentAncestors = context.AncestorIds
+ .WhereOneOrMany(linkParents, e => e.ItemId)
+ .Select(e => e.ParentItemId);
+
+ var seamAncestors = context.AncestorIds
+ .Where(e => hierarchyAncestors.Contains(e.ItemId) || linkedParentAncestors.Contains(e.ItemId))
+ .Select(e => e.ParentItemId);
+
+ return hierarchyAncestors
+ .Concat(linkedParents)
+ .Concat(linkedParentAncestors)
+ .Concat(seamAncestors)
+ .Distinct();
}
- private static HashSet GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria)
+ private static IQueryable GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria)
{
var query = context.MediaStreamInfos
.Where(ms => ms.StreamType == criteria.StreamType
@@ -130,130 +156,131 @@ public static class DescendantQueryHelper
query = query.Where(ms => ms.IsExternal == isExternal);
}
- return query.Select(ms => ms.ItemId).Distinct().ToHashSet();
+ return query.Select(ms => ms.ItemId);
+ }
+
+ private static IQueryable ClosureDescendants(JellyfinDbContext context, IReadOnlyList roots)
+ {
+ var direct = context.AncestorIds
+ .WhereOneOrMany(roots, e => e.ParentItemId)
+ .Select(e => e.ItemId);
+
+ // An item carries its own chain plus its collection folders, never the UserRootFolder.
+ var indirect = context.AncestorIds
+ .Where(e => direct.Contains(e.ParentItemId))
+ .Select(e => e.ItemId);
+
+ return direct.Concat(indirect);
}
///
- /// Traverses DOWN the hierarchy from parent folders to find all descendants.
+ /// Resolves every folder that reaches one of the matching items through a linked edge.
///
- private static HashSet TraverseHierarchyDown(JellyfinDbContext context, ICollection startIds)
+ /// The ids of the folders whose linked children lead, at any depth, to a matching item.
+ private static List ResolveLinkParents(JellyfinDbContext context, IQueryable matchingItemIds, IQueryable ancestorsOfMatches)
{
- var visited = new HashSet(startIds);
- var folderStack = new HashSet(startIds);
+ // A link sits above the closure as well as above another link: a BoxSet holds a Series whose
+ // episode matches, and another BoxSet holds that BoxSet. So the hop repeats until it stops
+ // finding anything new, and each hop takes the links landing on the set itself or on a folder
+ // that contains it. Only folders owning linked children are ever collected, which bounds this
+ // by the number of BoxSets and Playlists rather than by the item count.
+ var resolved = context.LinkedChildren
+ .Where(e => matchingItemIds.Contains(e.ChildId) || ancestorsOfMatches.Contains(e.ChildId))
+ .Select(e => e.ParentId)
+ .Distinct()
+ .ToHashSet();
- while (folderStack.Count != 0)
+ var frontier = resolved.ToList();
+
+ while (frontier.Count != 0)
{
- var currentFolders = folderStack.ToArray();
- folderStack.Clear();
+ var containingFolders = context.AncestorIds
+ .WhereOneOrMany(frontier, e => e.ItemId)
+ .Select(e => e.ParentItemId);
- var directChildren = context.AncestorIds
- .WhereOneOrMany(currentFolders, e => e.ParentItemId)
- .Select(e => e.ItemId)
+ var directLinkParents = context.LinkedChildren
+ .WhereOneOrMany(frontier, e => e.ChildId)
+ .Select(e => e.ParentId);
+
+ var indirectLinkParents = context.LinkedChildren
+ .Where(e => containingFolders.Contains(e.ChildId))
+ .Select(e => e.ParentId);
+
+ var next = directLinkParents
+ .Concat(indirectLinkParents)
+ .Distinct()
.ToArray();
- var linkedChildren = context.LinkedChildren
- .WhereOneOrMany(currentFolders, e => e.ParentId)
- .Select(e => e.ChildId)
- .ToArray();
-
- var allChildren = directChildren.Concat(linkedChildren).Distinct().ToArray();
-
- if (allChildren.Length == 0)
+ frontier = [];
+ foreach (var id in next)
{
- break;
+ // Cyclic links (a BoxSet holding itself, directly or not) terminate on the resolved set.
+ if (resolved.Add(id))
+ {
+ frontier.Add(id);
+ }
}
+ }
- var childFolders = context.BaseItems
- .WhereOneOrMany(allChildren, e => e.Id)
- .Where(e => e.IsFolder)
+ return [.. resolved];
+ }
+
+ ///
+ /// Resolves the roots the descendant sub-selects have to be anchored on.
+ ///
+ ///
+ /// The roots whose AncestorIds closure belongs to the result, and the roots whose LinkedChildren
+ /// belong to the result.
+ ///
+ private static (List ClosureRoots, List LinkRoots) ResolveLinkedRoots(JellyfinDbContext context, Guid parentId)
+ {
+ // A folder found through the closure needs no closure hop of its own.
+ var closureRoots = new List { parentId };
+ var linkRoots = new List { parentId };
+ var visited = new HashSet { parentId };
+ var frontier = new List { parentId };
+
+ while (frontier.Count != 0)
+ {
+ var closureIds = ClosureDescendants(context, frontier);
+
+ var linkedIds = context.LinkedChildren
+ .WhereOneOrMany(frontier, e => e.ParentId)
+ .Select(e => e.ChildId);
+
+ // Folders that own linked children, i.e. the only items whose links are worth following.
+ var linkOwners = context.BaseItems
+ .Where(e => e.IsFolder
+ && (closureIds.Contains(e.Id) || linkedIds.Contains(e.Id))
+ && context.LinkedChildren.Any(l => l.ParentId.Equals(e.Id)))
+ .Select(e => e.Id)
+ .ToArray();
+
+ var linkedFolders = context.BaseItems
+ .Where(e => e.IsFolder && linkedIds.Contains(e.Id))
.Select(e => e.Id)
.ToHashSet();
- foreach (var childId in allChildren)
+ frontier = [];
+ foreach (var id in linkOwners.Concat(linkedFolders))
{
- if (visited.Add(childId) && childFolders.Contains(childId))
+ if (!visited.Add(id))
{
- folderStack.Add(childId);
+ continue;
+ }
+
+ frontier.Add(id);
+ linkRoots.Add(id);
+
+ // Only a folder reached through a link contributes a closure that is not covered by
+ // the roots already collected.
+ if (linkedFolders.Contains(id))
+ {
+ closureRoots.Add(id);
}
}
}
- return visited;
- }
-
- ///
- /// Traverses DOWN the hierarchy using only AncestorIds (ownership), not LinkedChildren.
- ///
- private static HashSet TraverseHierarchyDownOwned(JellyfinDbContext context, ICollection startIds)
- {
- var visited = new HashSet(startIds);
- var folderStack = new HashSet(startIds);
-
- while (folderStack.Count != 0)
- {
- var currentFolders = folderStack.ToArray();
- folderStack.Clear();
-
- var directChildren = context.AncestorIds
- .WhereOneOrMany(currentFolders, e => e.ParentItemId)
- .Select(e => e.ItemId)
- .ToArray();
-
- if (directChildren.Length == 0)
- {
- break;
- }
-
- var childFolders = context.BaseItems
- .WhereOneOrMany(directChildren, e => e.Id)
- .Where(e => e.IsFolder)
- .Select(e => e.Id)
- .ToHashSet();
-
- foreach (var childId in directChildren)
- {
- if (visited.Add(childId) && childFolders.Contains(childId))
- {
- folderStack.Add(childId);
- }
- }
- }
-
- return visited;
- }
-
- ///
- /// Traverses UP the hierarchy from items to find all ancestor folders.
- ///
- private static HashSet TraverseHierarchyUp(JellyfinDbContext context, ICollection startIds)
- {
- var ancestors = new HashSet();
- var itemStack = new HashSet(startIds);
-
- while (itemStack.Count != 0)
- {
- var currentItems = itemStack.ToArray();
- itemStack.Clear();
-
- var ancestorParents = context.AncestorIds
- .WhereOneOrMany(currentItems, e => e.ItemId)
- .Select(e => e.ParentItemId)
- .ToArray();
-
- var linkedParents = context.LinkedChildren
- .WhereOneOrMany(currentItems, e => e.ChildId)
- .Select(e => e.ParentId)
- .ToArray();
-
- foreach (var parentId in ancestorParents.Concat(linkedParents))
- {
- if (ancestors.Add(parentId))
- {
- itemStack.Add(parentId);
- }
- }
- }
-
- return ancestors;
+ return (closureRoots, linkRoots);
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
index afa9eee363..fc4e96cf8a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
@@ -13,5 +13,6 @@ public class MediaStreamInfoConfiguration : IEntityTypeConfiguration builder)
{
builder.HasKey(e => new { e.ItemId, e.StreamIndex });
+ builder.HasIndex(e => new { e.StreamType, e.ItemId });
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260811145224_AddMediaStreamTypeItemIdIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260811145224_AddMediaStreamTypeItemIdIndex.Designer.cs
new file mode 100644
index 0000000000..79362985be
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260811145224_AddMediaStreamTypeItemIdIndex.Designer.cs
@@ -0,0 +1,1813 @@
+//
+using System;
+using Jellyfin.Database.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Sqlite.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20260811145224_AddMediaStreamTypeItemIdIndex")]
+ partial class AddMediaStreamTypeItemIdIndex
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.10");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Album")
+ .HasColumnType("TEXT");
+
+ b.Property("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property("Data")
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
+ b.Property("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("PrimaryVersionId")
+ .HasFilter("\"PrimaryVersionId\" IS NOT NULL");
+
+ b.HasIndex("SeasonId");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("SeriesName");
+
+ b.HasIndex("ExtraType", "OwnerId");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "CleanName");
+
+ b.HasIndex("TopParentId", "Type", "IsVirtualItem")
+ .HasFilter("\"PrimaryVersionId\" IS NULL AND (\"OwnerId\" IS NULL OR \"ExtraType\" IS NOT NULL)");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "SortName");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "IsFolder", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("TopParentId", "MediaType", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("TopParentId", "Type", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "ParentIndexNumber", "IndexNumber");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ IsFolder = false,
+ IsInMixedFolder = false,
+ IsLocked = false,
+ IsMovie = false,
+ IsRepeat = false,
+ IsSeries = false,
+ IsVirtualItem = false,
+ Name = "This is a placeholder item for UserData that has been detached from its original item",
+ Type = "PLACEHOLDER"
+ });
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Property("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.Property("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChildType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ParentId", "SortOrder");
+
+ b.HasIndex("ChildId", "ChildType");
+
+ b.HasIndex("ParentId", "ChildType");
+
+ b.ToTable("LinkedChildren", (string)null);
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("Level")
+ .HasColumnType("REAL");
+
+ b.Property("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.Property("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamType", "ItemId");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property