aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs409
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LinkedChildEntity.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs8
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj1
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs274
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/LeafPlayedState.cs10
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs10
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs5
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LinkedChildConfiguration.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs4
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs2
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs1
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs33
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs31
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs8
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs37
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs33
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs34
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs55
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs3
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.Designer.cs1808
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.cs89
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.Designer.cs1811
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.cs28
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.Designer.cs1811
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.cs25
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.Designer.cs1811
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.cs36
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.Designer.cs1813
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.cs27
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.Designer.cs1807
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.cs120
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs43
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs187
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs126
-rw-r--r--src/Jellyfin.Drawing.Skia/SvgSecurityValidator.cs339
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs35
-rw-r--r--src/Jellyfin.Drawing/Properties/AssemblyInfo.cs2
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj2
-rw-r--r--src/Jellyfin.Extensions/PathHelper.cs77
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs37
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs43
-rw-r--r--src/Jellyfin.LiveTv/Guide/GuideManager.cs70
-rw-r--r--src/Jellyfin.LiveTv/IO/EncodedRecorder.cs4
-rw-r--r--src/Jellyfin.LiveTv/Listings/ListingsManager.cs7
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs84
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs126
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvProgramEtag.cs184
-rw-r--r--src/Jellyfin.LiveTv/LiveTvChannelImageHelper.cs33
-rw-r--r--src/Jellyfin.LiveTv/LiveTvManager.cs10
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs8
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs2
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs26
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj12
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Properties/AssemblyInfo.cs3
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs93
77 files changed, 13285 insertions, 506 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
index 88a2c684ff..2ba3faf5f7 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs
@@ -1,18 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Linq.Expressions;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.MatchCriteria;
namespace Jellyfin.Database.Implementations;
/// <summary>
-/// Provides methods for querying item hierarchies using iterative traversal.
+/// Provides methods for querying item hierarchies.
/// Uses AncestorIds and LinkedChildren tables for parent-child traversal.
/// </summary>
public static class DescendantQueryHelper
{
/// <summary>
+ /// Gets the predicate identifying items that count toward played/total aggregation:
+ /// real leaf media, i.e. neither folders nor virtual items (missing or unaired episodes).
+ /// Shared by the per-item and batched count paths so they cannot diverge.
+ /// </summary>
+ public static Expression<Func<BaseItemEntity, bool>> IsCountableLeaf { get; } =
+ b => !b.IsFolder && !b.IsVirtualItem;
+
+ /// <summary>
+ /// Gets the predicate identifying the items that stand on their own in a library. An alternate
+ /// version is a second file for the item that links it rather than an item beside it, and an owned
+ /// item belongs to its owner unless it is an extra (a trailer and the like, which carries both an
+ /// owner and an extra type). Nothing here turns on who is asking, so a count that applies it
+ /// answers the same with a user and without one.
+ /// </summary>
+ public static Expression<Func<BaseItemEntity, bool>> IsDistinctLibraryItem { get; } =
+ b => !b.PrimaryVersionId.HasValue && (!b.OwnerId.HasValue || b.ExtraType != null);
+
+ /// <summary>
+ /// Builds the predicate identifying the items a user has played, counting a multi-version item as
+ /// played when any of its alternate versions is. Mirrors the aggregation
+ /// <c>VersionResumeData.ApplyTo</c> performs on the played flag a single item reports, so that a
+ /// folder's unplayed count cannot disagree with the watched state its members render with.
+ /// </summary>
+ /// <param name="userId">The id of the user whose played state to test.</param>
+ /// <returns>The predicate matching the items that user has played.</returns>
+ public static Expression<Func<BaseItemEntity, bool>> IsPlayedBy(Guid userId) =>
+ b => b.UserData!.Any(u => u.UserId.Equals(userId) && u.Played)
+ || b.LinkedChildEntities!.Any(lc =>
+ (lc.ChildType == LinkedChildType.LocalAlternateVersion || lc.ChildType == LinkedChildType.LinkedAlternateVersion)
+ && lc.Child!.UserData!.Any(u => u.UserId.Equals(userId) && u.Played));
+
+ /// <summary>
+ /// Builds the projection pairing an item's id with <see cref="IsPlayedBy"/> evaluated on that same
+ /// row. A caller that needs the flag alongside the id composes it rather than testing membership of
+ /// the played set: as a sub-select the set is unbounded by whatever the caller joins it to, so the
+ /// database builds it from the whole table once per place it appears.
+ /// </summary>
+ /// <param name="userId">The id of the user whose played state to test.</param>
+ /// <returns>The projection of each item onto its id and that user's played state.</returns>
+ public static Expression<Func<BaseItemEntity, LeafPlayedState>> PlayedStateBy(Guid userId)
+ {
+ var played = IsPlayedBy(userId);
+ var item = played.Parameters[0];
+
+ // Named members, as the compiler emits for an anonymous type: without them the query provider
+ // cannot read a later `x.Id` back to the column it was built from and gives up translating.
+ return Expression.Lambda<Func<BaseItemEntity, LeafPlayedState>>(
+ Expression.New(
+ typeof(LeafPlayedState).GetConstructor([typeof(Guid), typeof(bool)])!,
+ [Expression.Property(item, nameof(BaseItemEntity.Id)), played.Body],
+ [typeof(LeafPlayedState).GetProperty(nameof(LeafPlayedState.Id))!, typeof(LeafPlayedState).GetProperty(nameof(LeafPlayedState.Played))!]),
+ item);
+ }
+
+ /// <summary>
+ /// Builds the negation of <see cref="IsPlayedBy"/>, so a caller filtering for unplayed items reads
+ /// the same definition of played as one filtering for played items.
+ /// </summary>
+ /// <param name="userId">The id of the user whose played state to test.</param>
+ /// <returns>The predicate matching the items that user has not played.</returns>
+ public static Expression<Func<BaseItemEntity, bool>> IsUnplayedBy(Guid userId)
+ {
+ var played = IsPlayedBy(userId);
+ return Expression.Lambda<Func<BaseItemEntity, bool>>(Expression.Not(played.Body), played.Parameters);
+ }
+
+ /// <summary>
/// Gets a queryable of all descendant IDs for a parent item.
/// Traverses AncestorIds and LinkedChildren to find all descendants.
/// </summary>
@@ -23,11 +91,36 @@ public static class DescendantQueryHelper
{
ArgumentNullException.ThrowIfNull(context);
- var descendants = TraverseHierarchyDown(context, [parentId]);
+ return AllDescendants(context, [parentId])
+ .Where(e => !e.Equals(parentId))
+ .Distinct();
+ }
+
+ /// <summary>
+ /// Gets all descendant IDs for multiple parent items in a single traversal.
+ /// Traverses AncestorIds and LinkedChildren, like <see cref="GetAllDescendantIds"/>, but resolves
+ /// the roots once for all seeds instead of once per seed.
+ /// </summary>
+ /// <param name="context">Database context.</param>
+ /// <param name="parentIds">Parent item IDs.</param>
+ /// <returns>Set of all descendant item IDs (excluding the parent IDs themselves).</returns>
+ public static HashSet<Guid> GetAllDescendantIdsBatch(JellyfinDbContext context, IReadOnlyList<Guid> parentIds)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(parentIds);
+
+ if (parentIds.Count == 0)
+ {
+ return [];
+ }
- descendants.Remove(parentId);
+ var descendants = AllDescendants(context, parentIds)
+ .Distinct()
+ .ToHashSet();
- return descendants.AsQueryable();
+ descendants.ExceptWith(parentIds);
+
+ return descendants;
}
/// <summary>
@@ -42,11 +135,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();
}
/// <summary>
@@ -67,11 +158,11 @@ public static class DescendantQueryHelper
return [];
}
- var seedSet = new HashSet<Guid>(parentIds);
- var descendants = TraverseHierarchyDownOwned(context, seedSet);
+ var descendants = ClosureDescendants(context, parentIds)
+ .Distinct()
+ .ToHashSet();
- // Remove the seed IDs — callers want only descendants
- descendants.ExceptWith(seedSet);
+ descendants.ExceptWith(parentIds);
return descendants;
}
@@ -87,28 +178,106 @@ public static class DescendantQueryHelper
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(criteria);
- var matchingItemIds = criteria switch
+
+ // Both sides of a version group can hold a folder a caller would see as matching: the
+ // alternate carries its own AncestorIds rows and may sit in a different library than the
+ // primary it is reported against, and the primary is the item that becomes visible.
+ var reportedItemIds = MatchingMediaOwnerIds(context, criteria)
+ .Concat(GetPrimaryVersionIdsMatching(context, criteria))
+ .Distinct();
+
+ // One hop up the closure covers every ancestor level.
+ var hierarchyAncestors = context.AncestorIds
+ .Where(e => reportedItemIds.Contains(e.ItemId))
+ .Select(e => e.ParentItemId);
+
+ var linkParents = ResolveLinkParents(context, reportedItemIds, hierarchyAncestors);
+
+ // Read back as a sub-select so the result stays composable. Off the primary key, which is one
+ // row per id: LinkedChildren would yield one row per link and lean on the outer Distinct.
+ 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);
+
+ // The chain an item carries stops at its collection folders, so this hop crosses that seam to
+ // the UserRootFolder above them. One statement for both sides beats a sub-select per side.
+ 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();
+ }
+
+ /// <summary>
+ /// Gets a queryable of the IDs of the primary versions whose alternate version's media matches the
+ /// criteria.
+ /// </summary>
+ /// <param name="context">Database context.</param>
+ /// <param name="criteria">The matching criteria to apply.</param>
+ /// <returns>Queryable of primary version item IDs.</returns>
+ /// <remarks>
+ /// For callers that already test an item's own media with their own indexed predicate: this covers
+ /// exactly what such a predicate misses, and the filtered PrimaryVersionId index keeps it to the few
+ /// items that have versions at all.
+ /// </remarks>
+ public static IQueryable<Guid> GetPrimaryVersionIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(criteria);
+
+ // Anchored on the alternates rather than on the matches: "has a primary version" is served by
+ // the partial PrimaryVersionId index, which holds only the few items that are second files, so
+ // this costs a seek each into the stream index instead of a second pass over every stream row.
+ var alternates = context.BaseItems.Where(v => v.PrimaryVersionId.HasValue);
+
+ if (criteria is HasChapterImages)
{
- HasSubtitles => context.MediaStreamInfos
- .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle)
- .Select(ms => ms.ItemId)
- .Distinct()
- .ToHashSet(),
- HasChapterImages => context.Chapters
+ return alternates
+ .Where(v => context.Chapters.Any(c => c.ItemId.Equals(v.Id) && c.ImagePath != null))
+ .Select(v => v.PrimaryVersionId!.Value);
+ }
+
+ var matchingStreams = MatchingMediaStreams(context, criteria);
+
+ return alternates
+ .Where(v => matchingStreams.Any(ms => ms.ItemId.Equals(v.Id)))
+ .Select(v => v.PrimaryVersionId!.Value);
+ }
+
+ // The ids of the items whose own media matches. Kept to the stream and chapter tables so their
+ // covering indexes answer this outright: projecting the BaseItems navigation instead would add a
+ // primary-key lookup per stream row rather than one per matching item, and the leading key of both
+ // indexes leaves the ids already grouped, so the Distinct costs no sort.
+ private static IQueryable<Guid> MatchingMediaOwnerIds(JellyfinDbContext context, FolderMatchCriteria criteria)
+ => criteria is HasChapterImages
+ ? context.Chapters
.Where(c => c.ImagePath != null)
.Select(c => c.ItemId)
.Distinct()
- .ToHashSet(),
- HasMediaStreamType m => GetMatchingMediaStreamItemIds(context, m),
+ : MatchingMediaStreams(context, criteria)
+ .Select(ms => ms.ItemId)
+ .Distinct();
+
+ // The stream rows a criteria matches. One definition, so the owner projection and the alternate
+ // projection cannot drift apart despite reading it from opposite ends.
+ private static IQueryable<MediaStreamInfo> MatchingMediaStreams(JellyfinDbContext context, FolderMatchCriteria criteria)
+ => criteria switch
+ {
+ HasSubtitles => context.MediaStreamInfos
+ .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle),
+ HasMediaStreamType m => GetMatchingMediaStreams(context, m),
_ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Name}")
};
- var ancestors = TraverseHierarchyUp(context, matchingItemIds);
-
- return ancestors.AsQueryable();
- }
-
- private static HashSet<Guid> GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria)
+ private static IQueryable<MediaStreamInfo> GetMatchingMediaStreams(JellyfinDbContext context, HasMediaStreamType criteria)
{
var query = context.MediaStreamInfos
.Where(ms => ms.StreamType == criteria.StreamType
@@ -121,130 +290,140 @@ public static class DescendantQueryHelper
query = query.Where(ms => ms.IsExternal == isExternal);
}
- return query.Select(ms => ms.ItemId).Distinct().ToHashSet();
+ return query;
}
- /// <summary>
- /// Traverses DOWN the hierarchy from parent folders to find all descendants.
- /// </summary>
- private static HashSet<Guid> TraverseHierarchyDown(JellyfinDbContext context, ICollection<Guid> startIds)
+ private static IQueryable<Guid> AllDescendants(JellyfinDbContext context, IReadOnlyList<Guid> parentIds)
{
- var visited = new HashSet<Guid>(startIds);
- var folderStack = new HashSet<Guid>(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();
-
- var linkedChildren = context.LinkedChildren
- .WhereOneOrMany(currentFolders, e => e.ParentId)
- .Select(e => e.ChildId)
- .ToArray();
+ var (closureRoots, linkRoots) = ResolveLinkedRoots(context, parentIds);
- var allChildren = directChildren.Concat(linkedChildren).Distinct().ToArray();
+ var linkedDescendants = context.LinkedChildren
+ .WhereOneOrMany(linkRoots, e => e.ParentId)
+ .Select(e => e.ChildId);
- if (allChildren.Length == 0)
- {
- break;
- }
+ return ClosureDescendants(context, closureRoots)
+ .Concat(linkedDescendants);
+ }
- var childFolders = context.BaseItems
- .WhereOneOrMany(allChildren, e => e.Id)
- .Where(e => e.IsFolder)
- .Select(e => e.Id)
- .ToHashSet();
+ private static IQueryable<Guid> ClosureDescendants(JellyfinDbContext context, IReadOnlyList<Guid> roots)
+ {
+ var direct = context.AncestorIds
+ .WhereOneOrMany(roots, e => e.ParentItemId)
+ .Select(e => e.ItemId);
- foreach (var childId in allChildren)
- {
- if (visited.Add(childId) && childFolders.Contains(childId))
- {
- folderStack.Add(childId);
- }
- }
- }
+ // 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 visited;
+ return direct.Concat(indirect);
}
- /// <summary>
- /// Traverses DOWN the hierarchy using only AncestorIds (ownership), not LinkedChildren.
- /// </summary>
- private static HashSet<Guid> TraverseHierarchyDownOwned(JellyfinDbContext context, ICollection<Guid> startIds)
+ // Resolves the folders whose linked children lead, at any depth, to a matching item.
+ private static List<Guid> ResolveLinkParents(JellyfinDbContext context, IQueryable<Guid> matchingItemIds, IQueryable<Guid> ancestorsOfMatches)
{
- var visited = new HashSet<Guid>(startIds);
- var folderStack = new HashSet<Guid>(startIds);
-
- while (folderStack.Count != 0)
+ // An alternate version is a second file for the item that links it, not a child of it, so that
+ // edge is not walked. It is also the one link a non-folder owns, and there is one per remuxed
+ // movie: walking it would swell this list from the BoxSet and Playlist count to the item count,
+ // and the list is bound into every statement the returned queryable is embedded in.
+ var containerLinks = context.LinkedChildren
+ .Where(e => e.ChildType != LinkedChildType.LocalAlternateVersion
+ && e.ChildType != LinkedChildType.LinkedAlternateVersion);
+
+ // A link sits above the closure and above another link alike, so the hop repeats until nothing
+ // new turns up.
+ var resolved = containerLinks
+ .Where(e => matchingItemIds.Contains(e.ChildId) || ancestorsOfMatches.Contains(e.ChildId))
+ .Select(e => e.ParentId)
+ .Distinct()
+ .ToHashSet();
+
+ 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)
- .ToArray();
+ var directLinkParents = containerLinks
+ .WhereOneOrMany(frontier, e => e.ChildId)
+ .Select(e => e.ParentId);
- if (directChildren.Length == 0)
- {
- break;
- }
+ var indirectLinkParents = containerLinks
+ .Where(e => containingFolders.Contains(e.ChildId))
+ .Select(e => e.ParentId);
- var childFolders = context.BaseItems
- .WhereOneOrMany(directChildren, e => e.Id)
- .Where(e => e.IsFolder)
- .Select(e => e.Id)
- .ToHashSet();
+ var next = directLinkParents
+ .Concat(indirectLinkParents)
+ .Distinct()
+ .ToArray();
- foreach (var childId in directChildren)
+ frontier = [];
+ foreach (var id in next)
{
- if (visited.Add(childId) && childFolders.Contains(childId))
+ // Cyclic links terminate on the resolved set.
+ if (resolved.Add(id))
{
- folderStack.Add(childId);
+ frontier.Add(id);
}
}
}
- return visited;
+ return [.. resolved];
}
- /// <summary>
- /// Traverses UP the hierarchy from items to find all ancestor folders.
- /// </summary>
- private static HashSet<Guid> TraverseHierarchyUp(JellyfinDbContext context, ICollection<Guid> startIds)
+ // Resolves the roots the descendant sub-selects are anchored on: those contributing their closure,
+ // and those contributing their linked children.
+ private static (List<Guid> ClosureRoots, List<Guid> LinkRoots) ResolveLinkedRoots(JellyfinDbContext context, IReadOnlyList<Guid> parentIds)
{
- var ancestors = new HashSet<Guid>();
- var itemStack = new HashSet<Guid>(startIds);
+ var visited = new HashSet<Guid>(parentIds);
+ var closureRoots = visited.ToList();
+ var linkRoots = visited.ToList();
+ var frontier = visited.ToList();
- while (itemStack.Count != 0)
+ while (frontier.Count != 0)
{
- var currentItems = itemStack.ToArray();
- itemStack.Clear();
+ var closureIds = ClosureDescendants(context, frontier);
- var ancestorParents = context.AncestorIds
- .WhereOneOrMany(currentItems, e => e.ItemId)
- .Select(e => e.ParentItemId)
- .ToArray();
+ var linkedIds = context.LinkedChildren
+ .WhereOneOrMany(frontier, e => e.ParentId)
+ .Select(e => e.ChildId);
- var linkedParents = context.LinkedChildren
- .WhereOneOrMany(currentItems, e => e.ChildId)
+ var linkedFolders = context.BaseItems
+ .Where(e => e.IsFolder && linkedIds.Contains(e.Id))
+ .Select(e => e.Id)
+ .ToHashSet();
+
+ // Folders whose own links have to be followed. Driven off LinkedChildren because owning a
+ // link is the rare property, so the folder check only reaches rows that can qualify. That
+ // check stays: a non-folder owns links too (a movie and its alternate versions).
+ var linkOwners = context.LinkedChildren
+ .Where(e => (closureIds.Contains(e.ParentId) || linkedIds.Contains(e.ParentId))
+ && e.Parent!.IsFolder)
.Select(e => e.ParentId)
+ .Distinct()
.ToArray();
- foreach (var parentId in ancestorParents.Concat(linkedParents))
+ frontier = [];
+ foreach (var id in linkOwners.Concat(linkedFolders))
{
- if (ancestors.Add(parentId))
+ if (!visited.Add(id))
+ {
+ continue;
+ }
+
+ frontier.Add(id);
+ linkRoots.Add(id);
+
+ // Only a folder reached through a link adds a closure the roots so far do not cover.
+ if (linkedFolders.Contains(id))
{
- itemStack.Add(parentId);
+ closureRoots.Add(id);
}
}
}
- return ancestors;
+ return (closureRoots, linkRoots);
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LinkedChildEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LinkedChildEntity.cs
index 7361775711..be315f1b2c 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LinkedChildEntity.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LinkedChildEntity.cs
@@ -25,7 +25,7 @@ public class LinkedChildEntity
/// <summary>
/// Gets or sets the sort order.
/// </summary>
- public int? SortOrder { get; set; }
+ public int SortOrder { get; set; }
/// <summary>
/// Gets or sets the parent item navigation property.
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs
index 84b86574cc..eae02dda1c 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs
@@ -37,7 +37,7 @@ namespace Jellyfin.Database.Implementations.Entities
/// <summary>
/// Gets or sets the id of the associated user.
/// </summary>
- public Guid? UserId { get; set; }
+ public Guid UserId { get; set; }
/// <summary>
/// Gets the type of this permission.
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs
index c02ea7375a..9bd159f2cf 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs
@@ -35,7 +35,7 @@ namespace Jellyfin.Database.Implementations.Entities
/// <summary>
/// Gets or sets the id of the associated user.
/// </summary>
- public Guid? UserId { get; set; }
+ public Guid UserId { get; set; }
/// <summary>
/// Gets the type of this preference.
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
index b10e210e5d..bf6568d10a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Database.Implementations.Interfaces;
@@ -326,7 +325,6 @@ namespace Jellyfin.Database.Implementations.Entities
/// <summary>
/// Gets the list of permissions this user has.
/// </summary>
- [ForeignKey("Permission_Permissions_Guid")]
public virtual ICollection<Permission> Permissions { get; private set; }
/*
@@ -339,7 +337,6 @@ namespace Jellyfin.Database.Implementations.Entities
/// <summary>
/// Gets the list of preferences this user has.
/// </summary>
- [ForeignKey("Preference_Preferences_Guid")]
public virtual ICollection<Preference> Preferences { get; private set; }
/// <inheritdoc/>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
index 27dbeaba6a..0a72287ba9 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
@@ -37,14 +37,18 @@ public interface IJellyfinDatabaseProvider
void ConfigureConventions(ModelConfigurationBuilder configurationBuilder);
/// <summary>
- /// If supported this should run any periodic maintaince tasks.
+ /// If supported this should run any periodic maintaince tasks, reclaiming unused space and refreshing the query
+ /// planner statistics. Also used after migrations have modified the database.
/// </summary>
/// <param name="cancellationToken">The token to abort the operation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task RunScheduledOptimisation(CancellationToken cancellationToken);
/// <summary>
- /// If supported this should perform any actions that are required on stopping the jellyfin server.
+ /// If supported this should perform any actions that are required on stopping the jellyfin server. This runs
+ /// against a deadline imposed by the service manager, so unlike
+ /// <see cref="RunScheduledOptimisation(CancellationToken)"/> it should only do work whose cost does not grow with
+ /// the size of the database.
/// </summary>
/// <param name="cancellationToken">The token that will be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
index 0b29a71cbd..887ba114fc 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
@@ -13,7 +13,6 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Database.Implementations</PackageId>
- <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs
index 1af7460540..0dfce732ce 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs
@@ -14,11 +14,17 @@ namespace Jellyfin.Database.Implementations;
/// <summary>
/// Contains a number of query related extensions.
/// </summary>
+/// <remarks>
+/// Every helper here binds its values through <see cref="EF.Parameter{T}(T)"/>. Values embedded as bare
+/// constants are inlined into the SQL as literals, which gives each distinct value its own entry in EF's
+/// compiled query cache and its own statement for the database to plan.
+/// </remarks>
public static class JellyfinQueryHelperExtensions
{
private static readonly MethodInfo _containsMethodGenericCache = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static).First(m => m.Name == nameof(Enumerable.Contains) && m.GetParameters().Length == 2);
private static readonly MethodInfo _efParameterInstruction = typeof(EF).GetMethod(nameof(EF.Parameter), BindingFlags.Public | BindingFlags.Static)!;
private static readonly ConcurrentDictionary<Type, MethodInfo> _containsQueryCache = new();
+ private static readonly ConcurrentDictionary<Type, MethodInfo> _efParameterCache = new();
/// <summary>
/// Builds an optimised query checking one property against a list of values while maintaining an optimal query.
@@ -26,15 +32,69 @@ public static class JellyfinQueryHelperExtensions
/// <typeparam name="TEntity">The entity.</typeparam>
/// <typeparam name="TProperty">The property type to compare.</typeparam>
/// <param name="query">The source query.</param>
- /// <param name="oneOf">The list of items to check.</param>
+ /// <param name="oneOf">The list of items to check. An empty list matches nothing.</param>
/// <param name="property">Property expression.</param>
/// <returns>A Query.</returns>
- public static IQueryable<TEntity> WhereOneOrMany<TEntity, TProperty>(this IQueryable<TEntity> query, IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
+ public static IQueryable<TEntity> WhereOneOrMany<TEntity, TProperty>(this IQueryable<TEntity> query, IReadOnlyList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
{
return query.Where(OneOrManyExpressionBuilder(oneOf, property));
}
/// <summary>
+ /// Builds an optimised query expression checking one property against a list of values while maintaining an optimal query.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity.</typeparam>
+ /// <typeparam name="TProperty">The property type to compare.</typeparam>
+ /// <param name="oneOf">The list of items to check. An empty list matches nothing.</param>
+ /// <param name="property">Property expression.</param>
+ /// <returns>A Query.</returns>
+ public static Expression<Func<TEntity, bool>> OneOrManyExpressionBuilder<TEntity, TProperty>(this IReadOnlyList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
+ {
+ ArgumentNullException.ThrowIfNull(oneOf);
+ ArgumentNullException.ThrowIfNull(property);
+
+ var parameter = Expression.Parameter(typeof(TEntity), "item");
+ property = ParameterReplacer.Replace<Func<TEntity, TProperty>, Func<TEntity, TProperty>>(property, property.Parameters[0], parameter);
+
+ if (oneOf.Count == 0)
+ {
+ // Fail closed, and without asking the database to unpack an empty collection to prove it.
+ return Expression.Lambda<Func<TEntity, bool>>(Expression.Constant(false), parameter);
+ }
+
+ if (oneOf.Count == 1)
+ {
+ var value = Expression.Call(
+ null,
+ EfParameterFor(typeof(TProperty)),
+ Expression.Constant(oneOf[0], typeof(TProperty)));
+
+ return Expression.Lambda<Func<TEntity, bool>>(
+ typeof(TProperty).IsValueType
+ ? Expression.Equal(property.Body, value)
+ : Expression.ReferenceEqual(property.Body, value),
+ parameter);
+ }
+
+ var containsMethodInfo = _containsQueryCache.GetOrAdd(typeof(TProperty), static (key) => _containsMethodGenericCache.MakeGenericMethod(key));
+
+ // Binding the whole collection as one parameter keeps the statement identical for any element
+ // count, instead of emitting one placeholder per element.
+ return Expression.Lambda<Func<TEntity, bool>>(
+ Expression.Call(
+ null,
+ containsMethodInfo,
+ Expression.Call(null, EfParameterFor(oneOf.GetType()), Expression.Constant(oneOf)),
+ property.Body),
+ parameter);
+ }
+
+ private static MethodInfo EfParameterFor(Type type)
+ {
+ return _efParameterCache.GetOrAdd(type, static (key) => _efParameterInstruction.MakeGenericMethod(key));
+ }
+
+ /// <summary>
/// Builds a query that checks referenced ItemValues for a cross BaseItem lookup.
/// </summary>
/// <param name="baseQuery">The source query.</param>
@@ -47,191 +107,161 @@ public static class JellyfinQueryHelperExtensions
this IQueryable<BaseItemEntity> baseQuery,
JellyfinDbContext context,
ItemValueType itemValueType,
- IList<Guid> referenceIds,
+ IReadOnlyList<Guid> referenceIds,
bool invert = false)
{
- return baseQuery.Where(ReferencedItemFilterExpressionBuilder(context, itemValueType, referenceIds, invert));
+ return baseQuery.WhereReferencedItem(context, [itemValueType], referenceIds, invert);
}
/// <summary>
- /// Builds a query that checks referenced ItemValues for a cross BaseItem lookup.
+ /// Builds a query that checks referenced ItemValues of any of the given types for a cross BaseItem lookup.
/// </summary>
/// <param name="baseQuery">The source query.</param>
/// <param name="context">The database context.</param>
- /// <param name="itemValueTypes">The type of item value to reference.</param>
+ /// <param name="itemValueTypes">The types of item value to reference.</param>
/// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
/// <param name="invert">If set an exclusion check is performed instead.</param>
/// <returns>A Query.</returns>
- public static IQueryable<BaseItemEntity> WhereReferencedItemMultipleTypes(
+ /// <remarks>
+ /// Matching is on CleanName alone. Genre/artist/album etc items do not set an ItemValue of their own
+ /// type, so the referenced item's Type is never consulted and ids whose names clean to the same value
+ /// are interchangeable across types.
+ /// </remarks>
+ public static IQueryable<BaseItemEntity> WhereReferencedItem(
this IQueryable<BaseItemEntity> baseQuery,
JellyfinDbContext context,
- IList<ItemValueType> itemValueTypes,
- IList<Guid> referenceIds,
+ IReadOnlyList<ItemValueType> itemValueTypes,
+ IReadOnlyList<Guid> referenceIds,
bool invert = false)
{
- var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
- var typeFilter = OneOrManyExpressionBuilder<ItemValue, ItemValueType>(itemValueTypes, iv => iv.Type);
-
- return baseQuery.Where(item =>
- context.ItemValues
- .Where(typeFilter)
- .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map })
- .Any(val =>
- context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue)
- && val.map.ItemId == item.Id) == EF.Constant(!invert));
- }
+ ArgumentNullException.ThrowIfNull(context);
- /// <summary>
- /// Builds a query expression that checks referenced ItemValues for a cross BaseItem lookup.
- /// </summary>
- /// <param name="context">The database context.</param>
- /// <param name="itemValueType">The type of item value to reference.</param>
- /// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
- /// <param name="invert">If set an exclusion check is performed instead.</param>
- /// <returns>A Query.</returns>
- public static Expression<Func<BaseItemEntity, bool>> ReferencedItemFilterExpressionBuilder(
- this JellyfinDbContext context,
- ItemValueType itemValueType,
- IList<Guid> referenceIds,
- bool invert = false)
- {
- // Well genre/artist/album etc items do not actually set the ItemValue of thier specitic types so we cannot match it that way.
- /*
- "(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreIds and Type=2)))"
- */
-
- var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
-
- return item =>
- context.ItemValues
- .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (item, map) => new { item, map })
- .Any(val =>
- val.item.Type == itemValueType
- && context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.item.CleanValue)
- && val.map.ItemId == item.Id) == EF.Constant(!invert);
+ // Flat sub-selects rather than a correlated .Any(...Any(...)).
+ var referencedCleanValues = context.BaseItems
+ .Where(OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, e => e.Id))
+ .Select(e => e.CleanName);
+
+ var matchingItemIds = context.ItemValuesMap
+ .Where(OneOrManyExpressionBuilder<ItemValueMap, ItemValueType>(itemValueTypes, m => m.ItemValue.Type))
+ .Where(m => referencedCleanValues.Contains(m.ItemValue.CleanValue))
+ .Select(m => m.ItemId);
+
+ return invert
+ ? baseQuery.Where(e => !matchingItemIds.Contains(e.Id))
+ : baseQuery.Where(e => matchingItemIds.Contains(e.Id));
}
/// <summary>
- /// Filters items that match any of the specified (provider name, value) pairs.
+ /// Filters items that have any of the specified providers, optionally restricted to given values.
/// </summary>
/// <param name="baseQuery">The source query.</param>
- /// <param name="providerIds">Dictionary mapping provider names to arrays of values to match.</param>
+ /// <param name="providerIds">Dictionary mapping provider names to values to match. An empty value array matches any value for that provider.</param>
/// <returns>A filtered query.</returns>
public static IQueryable<BaseItemEntity> WhereHasAnyProviderIds(
this IQueryable<BaseItemEntity> baseQuery,
IReadOnlyDictionary<string, string[]> providerIds)
{
- var providerKeys = providerIds
- .SelectMany(kvp => kvp.Value.Select(v => $"{kvp.Key}:{v}"))
- .ToList();
-
- if (providerKeys.Count == 0)
- {
- return baseQuery;
- }
-
- return baseQuery.Where(e => e.Provider!.Any(p => providerKeys.Contains(p.ProviderId + ":" + p.ProviderValue)));
+ return baseQuery.WhereProviderMatch(Flatten(providerIds), false);
}
/// <summary>
- /// Filters items that have any of the specified providers. Empty/null values match any value for that provider.
+ /// Filters items that have any of the specified providers, optionally restricted to a given value.
/// </summary>
/// <param name="baseQuery">The source query.</param>
- /// <param name="providerIds">Dictionary mapping provider names to optional values.</param>
+ /// <param name="providerIds">Dictionary mapping provider names to optional values. An empty value matches any value for that provider.</param>
/// <returns>A filtered query.</returns>
public static IQueryable<BaseItemEntity> WhereHasAnyProviderId(
this IQueryable<BaseItemEntity> baseQuery,
IReadOnlyDictionary<string, string> providerIds)
{
- var existenceOnly = providerIds
- .Where(e => string.IsNullOrEmpty(e.Value))
- .Select(e => e.Key)
- .ToList();
-
- var specificValues = providerIds
- .Where(e => !string.IsNullOrEmpty(e.Value))
- .Select(e => $"{e.Key}:{e.Value}")
- .ToList();
-
- if (existenceOnly.Count == 0 && specificValues.Count == 0)
- {
- return baseQuery;
- }
-
- if (existenceOnly.Count == 0)
- {
- return baseQuery.Where(e => e.Provider!.Any(p =>
- specificValues.Contains(p.ProviderId + ":" + p.ProviderValue)));
- }
-
- if (specificValues.Count == 0)
- {
- return baseQuery.Where(e => e.Provider!.Any(p => existenceOnly.Contains(p.ProviderId)));
- }
-
- // Single EXISTS over Provider with both predicates OR'd, instead of two separate subqueries.
- return baseQuery.Where(e => e.Provider!.Any(p =>
- existenceOnly.Contains(p.ProviderId) ||
- specificValues.Contains(p.ProviderId + ":" + p.ProviderValue)));
+ return baseQuery.WhereProviderMatch(providerIds, false);
}
/// <summary>
- /// Excludes items that match any of the specified (provider name, value) pairs.
+ /// Excludes items that have any of the specified providers, optionally restricted to a given value.
/// </summary>
/// <param name="baseQuery">The source query.</param>
- /// <param name="providerIds">Dictionary mapping provider names to values to exclude.</param>
+ /// <param name="providerIds">Dictionary mapping provider names to optional values. An empty value excludes any value for that provider.</param>
/// <returns>A filtered query.</returns>
public static IQueryable<BaseItemEntity> WhereExcludeProviderIds(
this IQueryable<BaseItemEntity> baseQuery,
IReadOnlyDictionary<string, string> providerIds)
{
- var excludeKeys = providerIds
- .Select(e => $"{e.Key}:{e.Value}")
- .ToList();
+ return baseQuery.WhereProviderMatch(providerIds, true);
+ }
+
+ private static IEnumerable<KeyValuePair<string, string>> Flatten(IReadOnlyDictionary<string, string[]> providerIds)
+ {
+ ArgumentNullException.ThrowIfNull(providerIds);
- if (excludeKeys.Count == 0)
+ foreach (var (provider, values) in providerIds)
{
- return baseQuery;
- }
+ if (values is null || values.Length == 0)
+ {
+ yield return new KeyValuePair<string, string>(provider, string.Empty);
+ continue;
+ }
- return baseQuery.Where(e => e.Provider!.All(p => !excludeKeys.Contains(p.ProviderId + ":" + p.ProviderValue)));
+ foreach (var value in values)
+ {
+ yield return new KeyValuePair<string, string>(provider, value);
+ }
+ }
}
/// <summary>
- /// Builds an optimised query expression checking one property against a list of values while maintaining an optimal query.
+ /// Matches items against a set of (provider, value) pairs, where an empty value means any value for
+ /// that provider. Emits a single EXISTS over the provider collection with the predicates OR'd, rather
+ /// than one subquery per predicate group.
/// </summary>
- /// <typeparam name="TEntity">The entity.</typeparam>
- /// <typeparam name="TProperty">The property type to compare.</typeparam>
- /// <param name="oneOf">The list of items to check.</param>
- /// <param name="property">Property expression.</param>
- /// <returns>A Query.</returns>
- public static Expression<Func<TEntity, bool>> OneOrManyExpressionBuilder<TEntity, TProperty>(this IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
+ private static IQueryable<BaseItemEntity> WhereProviderMatch(
+ this IQueryable<BaseItemEntity> baseQuery,
+ IEnumerable<KeyValuePair<string, string>> providerIds,
+ bool invert)
{
- var parameter = Expression.Parameter(typeof(TEntity), "item");
- property = ParameterReplacer.Replace<Func<TEntity, TProperty>, Func<TEntity, TProperty>>(property, property.Parameters[0], parameter);
- if (oneOf.Count == 1)
+ ArgumentNullException.ThrowIfNull(providerIds);
+
+ var existenceOnly = new List<string>();
+ var specificValues = new List<string>();
+ foreach (var (provider, value) in providerIds)
{
- var value = oneOf[0];
- if (typeof(TProperty).IsValueType)
+ if (string.IsNullOrEmpty(value))
{
- return Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(property.Body, Expression.Constant(value)), parameter);
+ existenceOnly.Add(provider);
}
else
{
- return Expression.Lambda<Func<TEntity, bool>>(Expression.ReferenceEqual(property.Body, Expression.Constant(value)), parameter);
+ specificValues.Add(provider + ":" + value);
}
}
- var containsMethodInfo = _containsQueryCache.GetOrAdd(typeof(TProperty), static (key) => _containsMethodGenericCache.MakeGenericMethod(key));
-
- // Threshold picked from microbenchmarks on SQLite: inline IN(const,...) beats a
- // parameterized array lookup by ~5-10% up to ~32 elements.
- if (oneOf.Count <= 32)
+ if (existenceOnly.Count == 0 && specificValues.Count == 0)
{
- return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Constant(oneOf), property.Body), parameter);
+ return baseQuery;
}
- return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Call(null, _efParameterInstruction.MakeGenericMethod(oneOf.GetType()), Expression.Constant(oneOf)), property.Body), parameter);
+ var predicate = ProviderPredicate(existenceOnly, specificValues);
+
+ // NOT EXISTS rather than NOT IN: the latter yields no rows at all if the subquery can produce NULL.
+ return invert
+ ? baseQuery.Where(e => !e.Provider!.AsQueryable().Any(predicate))
+ : baseQuery.Where(e => e.Provider!.AsQueryable().Any(predicate));
+ }
+
+ private static Expression<Func<BaseItemProvider, bool>> ProviderPredicate(
+ IReadOnlyList<string> existenceOnly,
+ IReadOnlyList<string> specificValues)
+ {
+ var byProvider = existenceOnly.OneOrManyExpressionBuilder<BaseItemProvider, string>(p => p.ProviderId);
+ var byPair = specificValues.OneOrManyExpressionBuilder<BaseItemProvider, string>(p => p.ProviderId + ":" + p.ProviderValue);
+
+ // Both builders mint their own parameter; rebind so the two bodies can share one lambda.
+ var parameter = byProvider.Parameters[0];
+ var reboundPair = ParameterReplacer.Replace<Func<BaseItemProvider, bool>, Func<BaseItemProvider, bool>>(byPair, byPair.Parameters[0], parameter);
+
+ return Expression.Lambda<Func<BaseItemProvider, bool>>(
+ Expression.OrElse(byProvider.Body, reboundPair.Body),
+ parameter);
}
internal static class ParameterReplacer
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/LeafPlayedState.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/LeafPlayedState.cs
new file mode 100644
index 0000000000..0846013c45
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/LeafPlayedState.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Jellyfin.Database.Implementations;
+
+/// <summary>
+/// An item's id paired with whether a given user has played it.
+/// </summary>
+/// <param name="Id">The id of the item.</param>
+/// <param name="Played">Whether the user has played the item.</param>
+public readonly record struct LeafPlayedState(Guid Id, bool Played);
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs
index 76ffa5a9ea..29a073ff74 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs
@@ -88,13 +88,13 @@ public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
/// <inheritdoc/>
public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
{
- _writePolicy.ExecuteAndCapture(saveChanges);
+ _writePolicy.Execute(saveChanges);
}
/// <inheritdoc/>
public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
{
- await _writeAsyncPolicy.ExecuteAndCaptureAsync(saveChanges).ConfigureAwait(false);
+ await _writeAsyncPolicy.ExecuteAsync(saveChanges).ConfigureAwait(false);
}
private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs
index 404292e8eb..e7a7d5a53f 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs
@@ -17,6 +17,13 @@ namespace Jellyfin.Database.Implementations.Locking;
/// <summary>
/// A locking behavior that will always block any operation while a write is requested. Mimicks the old SqliteRepository behavior.
/// </summary>
+/// <remarks>
+/// Unsafe with asynchronous transactions; because <see cref="ReaderWriterLockSlim"/> is
+/// thread-affine, holding it from <c>TransactionStarting</c> to <c>TransactionCommitted</c>
+/// works only while continuations resume inline. A genuinely-async continuation inside a
+/// transaction releases on another thread, throwing
+/// <see cref="SynchronizationLockException"/> or deadlocking a later write.
+/// </remarks>
public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
{
private readonly ILogger<PessimisticLockBehavior> _logger;
@@ -47,7 +54,8 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
/// <inheritdoc/>
public void Initialise(DbContextOptionsBuilder optionsBuilder)
{
- _logger.LogInformation("The database locking mode has been set to: Pessimistic.");
+ _logger.LogWarning(
+ "The database locking mode has been set to: Pessimistic. This mode is not safe with asynchronous transactions and can deadlock.");
optionsBuilder.AddInterceptors(new CommandLockingInterceptor(_loggerFactory.CreateLogger<CommandLockingInterceptor>()));
optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_loggerFactory.CreateLogger<TransactionLockingInterceptor>()));
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
index 8556fb7bb3..ee36be035b 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
@@ -61,6 +61,11 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
builder.HasIndex(e => new { e.TopParentId, e.MediaType, e.IsVirtualItem, e.DateCreated });
// resume
builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey });
+ // alternate versions of an item, e.g. resolving the played date of a version onto its primary.
+ // Filtered: almost no item has a primary version, and an index covering those rows too would tempt
+ // the planner into serving "PrimaryVersionId IS NULL" - true for the whole library - out of it.
+ builder.HasIndex(e => e.PrimaryVersionId)
+ .HasFilter("\"PrimaryVersionId\" IS NOT NULL");
// sorted library queries (e.g., Series sorted by SortName)
builder.HasIndex(e => new { e.Type, e.TopParentId, e.SortName });
// NextUp: per-series episode ordering (index seek + range scan on season/episode)
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LinkedChildConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LinkedChildConfiguration.cs
index 2abccd41f0..b4013a394f 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LinkedChildConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LinkedChildConfiguration.cs
@@ -13,8 +13,7 @@ public class LinkedChildConfiguration : IEntityTypeConfiguration<LinkedChildEnti
public void Configure(EntityTypeBuilder<LinkedChildEntity> builder)
{
builder.ToTable("LinkedChildren");
- builder.HasKey(e => new { e.ParentId, e.ChildId });
- builder.HasIndex(e => new { e.ParentId, e.SortOrder });
+ builder.HasKey(e => new { e.ParentId, e.SortOrder });
builder.HasIndex(e => new { e.ParentId, e.ChildType });
builder.HasIndex(e => new { e.ChildId, e.ChildType });
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
index afa9eee363..48c537bbd3 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
@@ -13,5 +13,9 @@ public class MediaStreamInfoConfiguration : IEntityTypeConfiguration<MediaStream
public void Configure(EntityTypeBuilder<MediaStreamInfo> builder)
{
builder.HasKey(e => new { e.ItemId, e.StreamIndex });
+
+ // Covering index for the stream filters. ItemId comes second because it is what they project and
+ // dedupe on; Language and IsExternal follow only to keep their predicates off the table.
+ builder.HasIndex(e => new { e.StreamType, e.ItemId, e.Language, e.IsExternal });
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
index 32ede86c96..7ebdbf4e8b 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
@@ -15,7 +15,7 @@ public class PeopleBaseItemMapConfiguration : IEntityTypeConfiguration<PeopleBas
builder.HasKey(e => new { e.ItemId, e.PeopleId, e.Role });
builder.HasIndex(e => new { e.ItemId, e.SortOrder });
builder.HasIndex(e => new { e.ItemId, e.ListOrder });
- builder.HasIndex(e => e.PeopleId);
+ builder.HasIndex(e => new { e.PeopleId, e.ItemId });
builder.HasOne(e => e.Item);
builder.HasOne(e => e.People);
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs
index d2aed54eb1..ae53a36724 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs
@@ -14,10 +14,8 @@ namespace Jellyfin.Database.Implementations.ModelConfiguration
{
// Used to get a user's permissions or a specific permission for a user.
// Also prevents multiple values being created for a user.
- // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
builder
.HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
.IsUnique();
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs
index 207051bcd1..5306078ed4 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs
@@ -14,7 +14,6 @@ namespace Jellyfin.Database.Implementations.ModelConfiguration
{
builder
.HasIndex(p => new { p.UserId, p.Kind })
- .HasFilter("[UserId] IS NOT NULL")
.IsUnique();
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
index 05f2c80a25..72805ca19a 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs
@@ -117,8 +117,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
- b.HasIndex("UserId");
-
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
@@ -176,8 +174,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
- b.HasIndex("UserId");
-
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
@@ -291,12 +287,17 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsConcurrencyToken()
.HasColumnType("INTEGER");
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
- b.HasIndex("Permission_Permissions_Guid");
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
@@ -317,6 +318,9 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsConcurrencyToken()
.HasColumnType("INTEGER");
+ b.Property<Guid?>("UserId")
+ .HasColumnType("TEXT");
+
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
@@ -324,7 +328,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
- b.HasIndex("Preference_Preferences_Guid");
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
@@ -431,10 +437,14 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
- .HasColumnType("TEXT");
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
b.HasKey("Id");
+ b.HasIndex("Username")
+ .IsUnique();
+
b.ToTable("Users");
});
@@ -469,7 +479,8 @@ namespace Jellyfin.Server.Implementations.Migrations
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
- .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
@@ -485,14 +496,16 @@ namespace Jellyfin.Server.Implementations.Migrations
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
- .HasForeignKey("Permission_Permissions_Guid");
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
- .HasForeignKey("Preference_Preferences_Guid");
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs
index ddea37f6dd..a1e6604fe1 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs
@@ -445,6 +445,37 @@ namespace Jellyfin.Server.Implementations.Migrations
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")
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs
index ab7065ee65..8518b5720c 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs
@@ -285,6 +285,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
+ b.Property<string>("SegmentProviderId")
+ .HasColumnType("TEXT");
+
b.Property<long>("StartTicks")
.HasColumnType("INTEGER");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs
index 9b72d9688a..2c0b18577e 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs
@@ -1247,8 +1247,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
- .HasColumnType("TEXT")
- .UseCollation("NOCASE");
+ .HasColumnType("TEXT");
b.HasKey("Id");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs
index f5cfe86c44..8d787678b5 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs
@@ -1247,8 +1247,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
- .HasColumnType("TEXT")
- .UseCollation("NOCASE");
+ .HasColumnType("TEXT");
b.HasKey("Id");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs
index 434ea820af..83608cd26c 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs
@@ -229,6 +229,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("IndexNumber")
.HasColumnType("INTEGER");
+ b.Property<int?>("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
b.Property<int?>("InheritedParentalRatingValue")
.HasColumnType("INTEGER");
@@ -1275,7 +1278,10 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
- b.Property<int?>("MaxParentalAgeRating")
+ b.Property<int?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs
index bad01778da..0ed52b2514 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs
@@ -229,6 +229,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("IndexNumber")
.HasColumnType("INTEGER");
+ b.Property<int?>("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
b.Property<int?>("InheritedParentalRatingValue")
.HasColumnType("INTEGER");
@@ -748,6 +751,24 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
{
b.Property<Guid>("Id")
@@ -1260,7 +1281,10 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
- b.Property<int?>("MaxParentalAgeRating")
+ b.Property<int?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
@@ -1519,6 +1543,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("ItemValue");
});
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
{
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs
index d668eea92f..e4bbb199b2 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs
@@ -750,6 +750,24 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
{
b.Property<Guid>("Id")
@@ -847,6 +865,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("ElPresentFlag")
.HasColumnType("INTEGER");
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
b.Property<int?>("Height")
.HasColumnType("INTEGER");
@@ -1521,6 +1542,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("ItemValue");
});
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
{
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs
index d7672b1379..86fd7be9d8 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs
@@ -123,7 +123,6 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("INTEGER");
b.Property<string>("Codec")
- .IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CodecTag")
@@ -751,6 +750,24 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
{
b.Property<Guid>("Id")
@@ -848,6 +865,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("ElPresentFlag")
.HasColumnType("INTEGER");
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
b.Property<int?>("Height")
.HasColumnType("INTEGER");
@@ -1522,6 +1542,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("ItemValue");
});
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
{
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs
index 4ba3352edc..035b574fb8 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs
@@ -183,9 +183,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<DateTime?>("DateCreated")
.HasColumnType("TEXT");
- b.Property<DateTime?>("DateCreatedFilesystem")
- .HasColumnType("TEXT");
-
b.Property<DateTime?>("DateLastMediaAdded")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs
index a7f5e369ab..156f553fb3 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs
@@ -11,27 +11,19 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
-DELETE FROM BaseItems
- WHERE
- ParentId IS NOT NULL
- AND
- NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
-DELETE FROM BaseItems
- WHERE
- ParentId IS NOT NULL
- AND
- NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
-DELETE FROM BaseItems
- WHERE
- ParentId IS NOT NULL
- AND
- NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
-DELETE FROM BaseItems
- WHERE
- ParentId IS NOT NULL
- AND
- NOT EXISTS(SELECT 1 FROM BaseItems parent WHERE parent.Id = BaseItems.ParentId);
-""");
+ WITH RECURSIVE Orphan ("Id") AS (
+ SELECT Child."Id"
+ FROM "BaseItems" AS Child
+ WHERE Child."ParentId" IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM "BaseItems" AS Parent WHERE Parent."Id" = Child."ParentId")
+ UNION
+ SELECT Descendant."Id"
+ FROM "BaseItems" AS Descendant
+ INNER JOIN Orphan ON Descendant."ParentId" = Orphan."Id"
+ )
+ DELETE FROM "BaseItems" WHERE "Id" IN (SELECT "Id" FROM Orphan);
+ """);
+
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_ParentId",
table: "BaseItems",
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs
index 4927b0e78d..379da0e9be 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs
@@ -11,6 +11,61 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
+ migrationBuilder.Sql(
+ """
+ DROP TABLE IF EXISTS "OrphanedBaseItemIds";
+ CREATE TEMPORARY TABLE "OrphanedBaseItemIds" ("Id" TEXT NOT NULL PRIMARY KEY);
+
+ INSERT INTO "OrphanedBaseItemIds" ("Id")
+ WITH RECURSIVE Orphan ("Id") AS (
+ SELECT Child."Id"
+ FROM "BaseItems" AS Child
+ WHERE Child."ParentId" IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM "BaseItems" AS Parent WHERE Parent."Id" = Child."ParentId")
+ UNION
+ SELECT Descendant."Id"
+ FROM "BaseItems" AS Descendant
+ INNER JOIN Orphan ON Descendant."ParentId" = Orphan."Id"
+ )
+ SELECT "Id" FROM Orphan;
+
+ -- Keep the play state of the doomed items the way ItemPersistenceService does when it
+ -- deletes an item: reattach it to the placeholder item instead of letting the
+ -- FK_UserData_BaseItems_ItemId cascade wipe it. The placeholder can only hold one row
+ -- per (UserId, CustomDataKey), so resolve collisions before repointing anything.
+ DELETE FROM "UserData"
+ WHERE "ItemId" = '00000000-0000-0000-0000-000000000001'
+ AND EXISTS (
+ SELECT 1
+ FROM "UserData" AS Doomed
+ INNER JOIN "OrphanedBaseItemIds" AS Orphan ON Orphan."Id" = Doomed."ItemId"
+ WHERE Doomed."UserId" = "UserData"."UserId"
+ AND Doomed."CustomDataKey" = "UserData"."CustomDataKey");
+
+ DELETE FROM "UserData"
+ WHERE "ItemId" IN (SELECT "Id" FROM "OrphanedBaseItemIds")
+ AND "rowid" NOT IN (
+ SELECT MIN("rowid")
+ FROM "UserData"
+ WHERE "ItemId" IN (SELECT "Id" FROM "OrphanedBaseItemIds")
+ GROUP BY "UserId", "CustomDataKey");
+
+ UPDATE "UserData"
+ SET "ItemId" = '00000000-0000-0000-0000-000000000001',
+ "RetentionDate" = datetime('now')
+ WHERE "ItemId" IN (SELECT "Id" FROM "OrphanedBaseItemIds");
+
+ -- FK_LinkedChildren_BaseItems_{ParentId,ChildId} are NO ACTION, so these rows have to
+ -- go by hand or the delete below fails on them.
+ DELETE FROM "LinkedChildren"
+ WHERE "ParentId" IN (SELECT "Id" FROM "OrphanedBaseItemIds")
+ OR "ChildId" IN (SELECT "Id" FROM "OrphanedBaseItemIds");
+
+ DELETE FROM "BaseItems" WHERE "Id" IN (SELECT "Id" FROM "OrphanedBaseItemIds");
+
+ DROP TABLE "OrphanedBaseItemIds";
+ """);
+
// Normalize OwnerId to uppercase GUID format
migrationBuilder.Sql(
@"UPDATE BaseItems
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs
index f9cb9aa736..db60cdabb4 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs
@@ -955,6 +955,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs
index 29874264af..2419977cfc 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs
@@ -952,6 +952,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs
index 8282a8a582..5b961bdd5b 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs
@@ -952,6 +952,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs
index 5541a0191b..4d7e68d45f 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs
@@ -954,6 +954,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs
index f6fd1db21e..33e7ebef95 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs
@@ -956,6 +956,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs
index 5f7131ff65..6131100f4f 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs
@@ -956,6 +956,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs
index 0499921fec..0e63fe75dc 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs
@@ -956,6 +956,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs
index bf46ad9b39..52c07c717d 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs
@@ -958,6 +958,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs
index fc5c7afa0e..e6f24c1d95 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs
@@ -961,6 +961,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs
index 63f858bc98..d69d880e7f 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260522092303_AddNormalizedUsername.Designer.cs
@@ -961,6 +961,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs
index a1f555a59b..871d5d2110 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260524120336_AddUniqueNormalizedUsernameIndex.Designer.cs
@@ -961,6 +961,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<bool?>("IsInterlaced")
.HasColumnType("INTEGER");
+ b.Property<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
b.Property<string>("KeyFrames")
.HasColumnType("TEXT");
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.Designer.cs
new file mode 100644
index 0000000000..d5b6bd1d51
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.Designer.cs
@@ -0,0 +1,1808 @@
+// <auto-generated />
+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("20260723111547_AllowDuplicatePlaylistChildren")]
+ partial class AllowDuplicatePlaylistChildren
+ {
+ /// <inheritdoc />
+ 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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<Guid?>("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<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?>("InheritedParentalRatingSubValue")
+ .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>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("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<Guid?>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ 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<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", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property<Guid>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .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<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId", "Role");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ 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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.HasIndex("UserId", "IsFavorite", "ItemId");
+
+ b.HasIndex("UserId", "ItemId", "LastPlayedDate");
+
+ b.HasIndex("UserId", "Played", "ItemId");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
+ .WithMany("Extras")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.NoAction);
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
+ .WithMany("DirectChildren")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("DirectParent");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child")
+ .WithMany("LinkedChildOfEntities")
+ .HasForeignKey("ChildId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent")
+ .WithMany("LinkedChildEntities")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("Child");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("DirectChildren");
+
+ b.Navigation("Extras");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LinkedChildEntities");
+
+ b.Navigation("LinkedChildOfEntities");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.cs
new file mode 100644
index 0000000000..173213034e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260723111547_AllowDuplicatePlaylistChildren.cs
@@ -0,0 +1,89 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Sqlite.Migrations
+{
+ /// <inheritdoc />
+ public partial class AllowDuplicatePlaylistChildren : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ // Rows that predate the composite (ParentId, SortOrder) primary key stored a null SortOrder
+ // (e.g. BoxSet and Collection children). Assign each such row a stable 0-based position within
+ // its parent so the rows stay unique once SortOrder becomes part of the primary key; otherwise
+ // they would all collapse to the column default (0) and collide during the table rebuild.
+ migrationBuilder.Sql(
+ @"UPDATE ""LinkedChildren""
+ SET ""SortOrder"" = (
+ SELECT COUNT(*)
+ FROM ""LinkedChildren"" AS lc2
+ WHERE lc2.""ParentId"" = ""LinkedChildren"".""ParentId""
+ AND lc2.""rowid"" < ""LinkedChildren"".""rowid""
+ )
+ WHERE ""SortOrder"" IS NULL;");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_LinkedChildren",
+ table: "LinkedChildren");
+
+ migrationBuilder.DropIndex(
+ name: "IX_LinkedChildren_ParentId_SortOrder",
+ table: "LinkedChildren");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "SortOrder",
+ table: "LinkedChildren",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(int),
+ oldType: "INTEGER",
+ oldNullable: true);
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_LinkedChildren",
+ table: "LinkedChildren",
+ columns: new[] { "ParentId", "SortOrder" });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ // The (ParentId, ChildId) primary key cannot represent the same child more than once per
+ // parent. Drop any duplicate entries (keeping the first by SortOrder) that may have been
+ // created while duplicates were allowed, so the old key can be restored. This is lossy by
+ // nature — duplicate playlist entries cannot survive a downgrade.
+ migrationBuilder.Sql(
+ @"DELETE FROM ""LinkedChildren""
+ WHERE ""rowid"" NOT IN (
+ SELECT MIN(""rowid"")
+ FROM ""LinkedChildren""
+ GROUP BY ""ParentId"", ""ChildId""
+ );");
+
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_LinkedChildren",
+ table: "LinkedChildren");
+
+ migrationBuilder.AlterColumn<int>(
+ name: "SortOrder",
+ table: "LinkedChildren",
+ type: "INTEGER",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "INTEGER");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_LinkedChildren",
+ table: "LinkedChildren",
+ columns: new[] { "ParentId", "ChildId" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_LinkedChildren_ParentId_SortOrder",
+ table: "LinkedChildren",
+ columns: new[] { "ParentId", "SortOrder" });
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.Designer.cs
new file mode 100644
index 0000000000..a8da9b4c23
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.Designer.cs
@@ -0,0 +1,1811 @@
+// <auto-generated />
+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.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20260724185102_AddPrimaryVersionIdIndex")]
+ partial class AddPrimaryVersionIdIndex
+ {
+ /// <inheritdoc />
+ 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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<Guid?>("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<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?>("InheritedParentalRatingSubValue")
+ .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>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("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<Guid?>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("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<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", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property<Guid>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .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<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId", "Role");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ 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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.HasIndex("UserId", "IsFavorite", "ItemId");
+
+ b.HasIndex("UserId", "ItemId", "LastPlayedDate");
+
+ b.HasIndex("UserId", "Played", "ItemId");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
+ .WithMany("Extras")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.NoAction);
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
+ .WithMany("DirectChildren")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("DirectParent");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child")
+ .WithMany("LinkedChildOfEntities")
+ .HasForeignKey("ChildId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent")
+ .WithMany("LinkedChildEntities")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("Child");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("DirectChildren");
+
+ b.Navigation("Extras");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LinkedChildEntities");
+
+ b.Navigation("LinkedChildOfEntities");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.cs
new file mode 100644
index 0000000000..069104244c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260724185102_AddPrimaryVersionIdIndex.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddPrimaryVersionIdIndex : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_BaseItems_PrimaryVersionId",
+ table: "BaseItems",
+ column: "PrimaryVersionId",
+ filter: "\"PrimaryVersionId\" IS NOT NULL");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_BaseItems_PrimaryVersionId",
+ table: "BaseItems");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.Designer.cs
new file mode 100644
index 0000000000..39713d639c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.Designer.cs
@@ -0,0 +1,1811 @@
+// <auto-generated />
+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.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20260728170000_AddPeopleNameLowerIndex")]
+ partial class AddPeopleNameLowerIndex
+ {
+ /// <inheritdoc />
+ 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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<Guid?>("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<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?>("InheritedParentalRatingSubValue")
+ .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>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("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<Guid?>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("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<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", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property<Guid>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .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<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId", "Role");
+
+ b.HasIndex("PeopleId");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ 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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.HasIndex("UserId", "IsFavorite", "ItemId");
+
+ b.HasIndex("UserId", "ItemId", "LastPlayedDate");
+
+ b.HasIndex("UserId", "Played", "ItemId");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
+ .WithMany("Extras")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.NoAction);
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
+ .WithMany("DirectChildren")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("DirectParent");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child")
+ .WithMany("LinkedChildOfEntities")
+ .HasForeignKey("ChildId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent")
+ .WithMany("LinkedChildEntities")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("Child");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("DirectChildren");
+
+ b.Navigation("Extras");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LinkedChildEntities");
+
+ b.Navigation("LinkedChildOfEntities");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.cs
new file mode 100644
index 0000000000..1f59630fe9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728170000_AddPeopleNameLowerIndex.cs
@@ -0,0 +1,25 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddPeopleNameLowerIndex : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ // Expression index, so it cannot be declared on the entity type. /Persons collapses the
+ // one-row-per-(Name, PersonType) table to one row per lowercased name; without this index
+ // that dedup scans and groups the whole table on every request.
+ migrationBuilder.Sql("CREATE INDEX IF NOT EXISTS \"IX_Peoples_NameLower\" ON \"Peoples\" (lower(\"Name\"));");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("DROP INDEX IF EXISTS \"IX_Peoples_NameLower\";");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.Designer.cs
new file mode 100644
index 0000000000..6aacbab798
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.Designer.cs
@@ -0,0 +1,1811 @@
+// <auto-generated />
+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("20260728182152_AddPeopleItemMapCoveringIndex")]
+ partial class AddPeopleItemMapCoveringIndex
+ {
+ /// <inheritdoc />
+ 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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<Guid?>("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<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?>("InheritedParentalRatingSubValue")
+ .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>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("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<Guid?>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("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<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", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property<Guid>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .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<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId", "Role");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.HasIndex("PeopleId", "ItemId");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ 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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.HasIndex("UserId", "IsFavorite", "ItemId");
+
+ b.HasIndex("UserId", "ItemId", "LastPlayedDate");
+
+ b.HasIndex("UserId", "Played", "ItemId");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
+ .WithMany("Extras")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.NoAction);
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
+ .WithMany("DirectChildren")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("DirectParent");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child")
+ .WithMany("LinkedChildOfEntities")
+ .HasForeignKey("ChildId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent")
+ .WithMany("LinkedChildEntities")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("Child");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("DirectChildren");
+
+ b.Navigation("Extras");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LinkedChildEntities");
+
+ b.Navigation("LinkedChildOfEntities");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.cs
new file mode 100644
index 0000000000..9be7953ada
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Sqlite.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddPeopleItemMapCoveringIndex : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_PeopleBaseItemMap_PeopleId",
+ table: "PeopleBaseItemMap");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_PeopleBaseItemMap_PeopleId_ItemId",
+ table: "PeopleBaseItemMap",
+ columns: new[] { "PeopleId", "ItemId" });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_PeopleBaseItemMap_PeopleId_ItemId",
+ table: "PeopleBaseItemMap");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_PeopleBaseItemMap_PeopleId",
+ table: "PeopleBaseItemMap",
+ column: "PeopleId");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.Designer.cs
new file mode 100644
index 0000000000..afa6840a97
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.Designer.cs
@@ -0,0 +1,1813 @@
+// <auto-generated />
+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("20260812050902_AddMediaStreamFilterIndex")]
+ partial class AddMediaStreamFilterIndex
+ {
+ /// <inheritdoc />
+ 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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<Guid?>("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<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?>("InheritedParentalRatingSubValue")
+ .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>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("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<Guid?>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("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<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", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property<Guid>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .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<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamType", "ItemId", "Language", "IsExternal");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId", "Role");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.HasIndex("PeopleId", "ItemId");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ 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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.HasIndex("UserId", "IsFavorite", "ItemId");
+
+ b.HasIndex("UserId", "ItemId", "LastPlayedDate");
+
+ b.HasIndex("UserId", "Played", "ItemId");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
+ .WithMany("Extras")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.NoAction);
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
+ .WithMany("DirectChildren")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("DirectParent");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child")
+ .WithMany("LinkedChildOfEntities")
+ .HasForeignKey("ChildId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent")
+ .WithMany("LinkedChildEntities")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("Child");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("DirectChildren");
+
+ b.Navigation("Extras");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LinkedChildEntities");
+
+ b.Navigation("LinkedChildOfEntities");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.cs
new file mode 100644
index 0000000000..75187c6c4b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260812050902_AddMediaStreamFilterIndex.cs
@@ -0,0 +1,27 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Sqlite.Migrations
+{
+ /// <inheritdoc />
+ public partial class AddMediaStreamFilterIndex : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_MediaStreamInfos_StreamType_ItemId_Language_IsExternal",
+ table: "MediaStreamInfos",
+ columns: new[] { "StreamType", "ItemId", "Language", "IsExternal" });
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_MediaStreamInfos_StreamType_ItemId_Language_IsExternal",
+ table: "MediaStreamInfos");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.Designer.cs
new file mode 100644
index 0000000000..c41b8b0106
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.Designer.cs
@@ -0,0 +1,1807 @@
+// <auto-generated />
+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("20260815063607_RemoveOrphanedUserPermissionsAndPreferences")]
+ partial class RemoveOrphanedUserPermissionsAndPreferences
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.11");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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<Guid?>("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<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?>("InheritedParentalRatingSubValue")
+ .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>("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("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<Guid?>("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property<long?>("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid?>("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property<long?>("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime?>("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("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<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", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection<string>("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property<long>("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property<Guid>("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<Guid>("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("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<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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<int>("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool?>("Hdr10PlusPresentFlag")
+ .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<bool>("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Language")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("Level")
+ .HasColumnType("REAL");
+
+ b.Property<string>("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Path")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property<float?>("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property<int?>("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Title")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.HasIndex("StreamType", "ItemId", "Language", "IsExternal");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property<string>("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Role")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "PeopleId", "Role");
+
+ b.HasIndex("ItemId", "ListOrder");
+
+ b.HasIndex("ItemId", "SortOrder");
+
+ b.HasIndex("PeopleId", "ItemId");
+
+ b.ToTable("PeopleBaseItemMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ 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();
+
+ b.ToTable("Permissions");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.Property<int>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("Kind")
+ .HasColumnType("INTEGER");
+
+ 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();
+
+ b.ToTable("Preferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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?>("MaxParentalRatingScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<int?>("MaxParentalRatingSubScore")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("NormalizedUsername")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ 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");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedUsername")
+ .IsUnique();
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.Property<Guid>("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("CustomDataKey")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("AudioStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("IsFavorite")
+ .HasColumnType("INTEGER");
+
+ b.Property<DateTime?>("LastPlayedDate")
+ .HasColumnType("TEXT");
+
+ b.Property<bool?>("Likes")
+ .HasColumnType("INTEGER");
+
+ b.Property<int>("PlayCount")
+ .HasColumnType("INTEGER");
+
+ b.Property<long>("PlaybackPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property<bool>("Played")
+ .HasColumnType("INTEGER");
+
+ b.Property<double?>("Rating")
+ .HasColumnType("REAL");
+
+ b.Property<DateTime?>("RetentionDate")
+ .HasColumnType("TEXT");
+
+ b.Property<int?>("SubtitleStreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+ b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+ b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+ b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+ b.HasIndex("ItemId", "UserId", "Played");
+
+ b.HasIndex("UserId", "IsFavorite", "ItemId");
+
+ b.HasIndex("UserId", "ItemId", "LastPlayedDate");
+
+ b.HasIndex("UserId", "Played", "ItemId");
+
+ b.ToTable("UserData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Parents")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem")
+ .WithMany("Children")
+ .HasForeignKey("ParentItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ParentItem");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner")
+ .WithMany("Extras")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.NoAction);
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent")
+ .WithMany("DirectChildren")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("DirectParent");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Images")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("LockedFields")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Provider")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("TrailerTypes")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Chapters")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("ItemValues")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue")
+ .WithMany("BaseItemsMap")
+ .HasForeignKey("ItemValueId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("ItemValue");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany()
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child")
+ .WithMany("LinkedChildOfEntities")
+ .HasForeignKey("ChildId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent")
+ .WithMany("LinkedChildEntities")
+ .HasForeignKey("ParentId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("Child");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("MediaStreams")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("Peoples")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People")
+ .WithMany("BaseItems")
+ .HasForeignKey("PeopleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("People");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b =>
+ {
+ b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
+ .WithMany("UserData")
+ .HasForeignKey("ItemId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Item");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Navigation("Chapters");
+
+ b.Navigation("Children");
+
+ b.Navigation("DirectChildren");
+
+ b.Navigation("Extras");
+
+ b.Navigation("Images");
+
+ b.Navigation("ItemValues");
+
+ b.Navigation("LinkedChildEntities");
+
+ b.Navigation("LinkedChildOfEntities");
+
+ b.Navigation("LockedFields");
+
+ b.Navigation("MediaStreams");
+
+ b.Navigation("Parents");
+
+ b.Navigation("Peoples");
+
+ b.Navigation("Provider");
+
+ b.Navigation("TrailerTypes");
+
+ b.Navigation("UserData");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Navigation("BaseItemsMap");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Navigation("BaseItems");
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.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/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.cs
new file mode 100644
index 0000000000..2530e84af6
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260815063607_RemoveOrphanedUserPermissionsAndPreferences.cs
@@ -0,0 +1,120 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Sqlite.Migrations
+{
+ /// <inheritdoc />
+ public partial class RemoveOrphanedUserPermissionsAndPreferences : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("DELETE FROM Permissions WHERE UserId IS NULL OR UserId NOT IN (SELECT Id FROM Users);");
+ migrationBuilder.Sql("DELETE FROM Preferences WHERE UserId IS NULL OR UserId NOT IN (SELECT Id FROM Users);");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Preferences_UserId_Kind",
+ table: "Preferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Permissions_UserId_Kind",
+ table: "Permissions");
+
+ migrationBuilder.DropColumn(
+ name: "Preference_Preferences_Guid",
+ table: "Preferences");
+
+ migrationBuilder.DropColumn(
+ name: "Permission_Permissions_Guid",
+ table: "Permissions");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "UserId",
+ table: "Preferences",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "UserId",
+ table: "Permissions",
+ type: "TEXT",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
+ oldClrType: typeof(Guid),
+ oldType: "TEXT",
+ oldNullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Preferences_UserId_Kind",
+ table: "Preferences",
+ columns: ["UserId", "Kind"],
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Permissions_UserId_Kind",
+ table: "Permissions",
+ columns: ["UserId", "Kind"],
+ unique: true);
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_Preferences_UserId_Kind",
+ table: "Preferences");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Permissions_UserId_Kind",
+ table: "Permissions");
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "UserId",
+ table: "Preferences",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "TEXT");
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "Preference_Preferences_Guid",
+ table: "Preferences",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.AlterColumn<Guid>(
+ name: "UserId",
+ table: "Permissions",
+ type: "TEXT",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "TEXT");
+
+ migrationBuilder.AddColumn<Guid>(
+ name: "Permission_Permissions_Guid",
+ table: "Permissions",
+ type: "TEXT",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Preferences_UserId_Kind",
+ table: "Preferences",
+ columns: ["UserId", "Kind"],
+ unique: true,
+ filter: "[UserId] IS NOT NULL");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Permissions_UserId_Kind",
+ table: "Permissions",
+ columns: ["UserId", "Kind"],
+ unique: true,
+ filter: "[UserId] IS NOT NULL");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
index fd18c035e6..0c35ccc992 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "10.0.12");
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.11");
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
@@ -373,6 +373,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("PresentationUniqueKey");
+ b.HasIndex("PrimaryVersionId")
+ .HasFilter("\"PrimaryVersionId\" IS NOT NULL");
+
b.HasIndex("SeasonId");
b.HasIndex("SeriesId");
@@ -812,23 +815,21 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<Guid>("ParentId")
.HasColumnType("TEXT");
+ b.Property<int>("SortOrder")
+ .HasColumnType("INTEGER");
+
b.Property<Guid>("ChildId")
.HasColumnType("TEXT");
b.Property<int>("ChildType")
.HasColumnType("INTEGER");
- b.Property<int?>("SortOrder")
- .HasColumnType("INTEGER");
-
- b.HasKey("ParentId", "ChildId");
+ b.HasKey("ParentId", "SortOrder");
b.HasIndex("ChildId", "ChildType");
b.HasIndex("ParentId", "ChildType");
- b.HasIndex("ParentId", "SortOrder");
-
b.ToTable("LinkedChildren", (string)null);
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
@@ -1011,6 +1012,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "StreamIndex");
+ b.HasIndex("StreamType", "ItemId", "Language", "IsExternal");
+
b.ToTable("MediaStreamInfos");
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
@@ -1057,12 +1060,12 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "PeopleId", "Role");
- b.HasIndex("PeopleId");
-
b.HasIndex("ItemId", "ListOrder");
b.HasIndex("ItemId", "SortOrder");
+ b.HasIndex("PeopleId", "ItemId");
+
b.ToTable("PeopleBaseItemMap");
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
@@ -1077,14 +1080,11 @@ namespace Jellyfin.Server.Implementations.Migrations
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")
+ b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<bool>("Value")
@@ -1093,8 +1093,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
- .IsUnique()
- .HasFilter("[UserId] IS NOT NULL");
+ .IsUnique();
b.ToTable("Permissions");
@@ -1110,14 +1109,11 @@ namespace Jellyfin.Server.Implementations.Migrations
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")
+ b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
@@ -1128,8 +1124,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
- .IsUnique()
- .HasFilter("[UserId] IS NOT NULL");
+ .IsUnique();
b.ToTable("Preferences");
@@ -1698,7 +1693,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
- .OnDelete(DeleteBehavior.Cascade);
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b =>
@@ -1706,7 +1702,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasOne("Jellyfin.Database.Implementations.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
- .OnDelete(DeleteBehavior.Cascade);
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b =>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
index 044fd0131f..3330b64b69 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Data.Common;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -63,7 +64,11 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
var sqliteConnectionBuilder = new SqliteConnectionStringBuilder
{
DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyfin.db")),
- Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Default),
+ // Private, not Default: sqlite3_enable_shared_cache is process-global, so a plugin
+ // enabling it makes these connections share a cache too. Contention then surfaces as
+ // SQLITE_LOCKED ("database table is locked"), which the busy handler does not cover,
+ // so busy_timeout is skipped and the command fails at CommandTimeout instead.
+ Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Private),
Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true),
DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 60)
};
@@ -99,17 +104,9 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
}
/// <inheritdoc/>
- public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
+ public Task RunScheduledOptimisation(CancellationToken cancellationToken)
{
- var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
- await using (context.ConfigureAwait(false))
- {
- await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false);
- await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
- await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
- await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false);
- _logger.LogInformation("jellyfin.db optimized successfully!");
- }
+ return OptimizeAsync(cancellationToken);
}
/// <inheritdoc/>
@@ -121,19 +118,109 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
/// <inheritdoc/>
public async Task RunShutdownTask(CancellationToken cancellationToken)
{
+ // Run before disposing the application. Only a checkpoint: stopping is on a deadline.
+
+ // Empty the pool first. Anything still parked in it can start reading again between here and the
+ // checkpoint, and a reader that holds the write-ahead log open is exactly what makes the truncation
+ // fail. Connections handed out already cannot be taken away, but they get disposed on return.
+ SqliteConnection.ClearAllPools();
+
+ try
+ {
+ if (DbContextFactory is not null)
+ {
+ var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // A missed checkpoint only leaves a write-ahead log for the next start to replay, so never fail the
+ // shutdown over this.
+ _logger.LogError(ex, "Error while checkpointing jellyfin.db");
+ }
+
+ // The checkpointing connection went back into the pool, so retire that one as well.
+ SqliteConnection.ClearAllPools();
+ }
+
+ private async Task OptimizeAsync(CancellationToken cancellationToken)
+ {
if (DbContextFactory is null)
{
return;
}
- // Run before disposing the application
var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
- await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
+ await context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ long? tempStore;
+ long? analysisLimit;
+ var pragmaCommand = context.Database.GetDbConnection().CreateCommand();
+ await using (pragmaCommand.ConfigureAwait(false))
+ {
+ pragmaCommand.CommandText = "PRAGMA temp_store";
+ tempStore = await ReadPragmaValueAsync(pragmaCommand, cancellationToken).ConfigureAwait(false);
+ pragmaCommand.CommandText = "PRAGMA analysis_limit";
+ analysisLimit = await ReadPragmaValueAsync(pragmaCommand, cancellationToken).ConfigureAwait(false);
+ }
+
+ await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false);
+
+ _logger.LogDebug(
+ "Rebuilding jellyfin.db on disk, scratch space goes to {TempDirectory}",
+ Environment.GetEnvironmentVariable("SQLITE_TMPDIR") ?? "SQLite's default temporary directory");
+ await context.Database.ExecuteSqlRawAsync("PRAGMA temp_store=1", cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ // The connection goes back to the pool, so hand it over the way it was handed to us.
+ if (tempStore is not null)
+ {
+ await context.Database.ExecuteSqlRawAsync(
+ FormattableString.Invariant($"PRAGMA temp_store={tempStore.Value}"),
+ CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+
+ await context.Database.ExecuteSqlRawAsync("PRAGMA analysis_limit=0", cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await context.Database.ExecuteSqlRawAsync("ANALYZE", cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ if (analysisLimit is not null)
+ {
+ await context.Database.ExecuteSqlRawAsync(
+ FormattableString.Invariant($"PRAGMA analysis_limit={analysisLimit.Value}"),
+ CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+
+ await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("jellyfin.db optimized successfully!");
+ }
+ finally
+ {
+ await context.Database.CloseConnectionAsync().ConfigureAwait(false);
+ }
}
+ }
- SqliteConnection.ClearAllPools();
+ private static async Task<long?> ReadPragmaValueAsync(DbCommand command, CancellationToken cancellationToken)
+ {
+ var value = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
+ return value is null or DBNull ? null : Convert.ToInt64(value, CultureInfo.InvariantCulture);
}
/// <inheritdoc/>
@@ -143,16 +230,31 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
}
/// <inheritdoc />
- public Task<string> MigrationBackupFast(CancellationToken cancellationToken)
+ public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
{
- var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
- var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName);
- Directory.CreateDirectory(backupFile);
+ var backupFolder = Path.Combine(_applicationPaths.DataPath, BackupFolderName);
+ Directory.CreateDirectory(backupFolder);
+
+ if (DbContextFactory is not null && File.Exists(path))
+ {
+ var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ await context.Database.ExecuteSqlRawAsync("PRAGMA wal_checkpoint(TRUNCATE)", cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
+ var backupFile = Path.Combine(backupFolder, $"{key}_jellyfin.db");
+ for (var attempt = 1; File.Exists(backupFile); attempt++)
+ {
+ key = string.Create(CultureInfo.InvariantCulture, $"{DateTime.UtcNow:yyyyMMddHHmmss}_{attempt}");
+ backupFile = Path.Combine(backupFolder, $"{key}_jellyfin.db");
+ }
- backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db");
File.Copy(path, backupFile);
- return Task.FromResult(key);
+ return key;
}
/// <inheritdoc />
@@ -169,10 +271,55 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
return Task.CompletedTask;
}
+ if (!TryRetireWriteAheadLog(path))
+ {
+ _logger.LogCritical(
+ "Refusing to restore jellyfin.db: the write-ahead log at {WriteAheadLog} could not be retired, which "
+ + "means the database is still open and replacing it now would silently bring back the data this "
+ + "rollback is undoing. Stop the server and copy {Backup} over {Path} by hand.",
+ path + "-wal",
+ backupFile,
+ path);
+ return Task.CompletedTask;
+ }
+
File.Copy(backupFile, path, true);
+
return Task.CompletedTask;
}
+ private bool TryRetireWriteAheadLog(string path)
+ {
+ var writeAheadLogPath = path + "-wal";
+ if (!File.Exists(path) || !File.Exists(writeAheadLogPath))
+ {
+ return true;
+ }
+
+ try
+ {
+ var connectionString = new SqliteConnectionStringBuilder
+ {
+ DataSource = path,
+ Mode = SqliteOpenMode.ReadWrite,
+ Pooling = false
+ }.ToString();
+
+ using var connection = new SqliteConnection(connectionString);
+ connection.Open();
+ using var command = connection.CreateCommand();
+ command.CommandText = "PRAGMA wal_checkpoint(TRUNCATE)";
+ command.ExecuteNonQuery();
+ }
+ catch (SqliteException ex)
+ {
+ // Either something else holds the database or it is too damaged to open. The check below covers both.
+ _logger.LogError(ex, "Could not open jellyfin.db to retire its write-ahead log");
+ }
+
+ return !File.Exists(writeAheadLogPath);
+ }
+
/// <inheritdoc />
public Task DeleteBackup(string key)
{
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index b6d2914efa..b8d40614d2 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -1,8 +1,10 @@
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using BlurHashSharp.SkiaSharp;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
@@ -11,6 +13,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing;
using Microsoft.Extensions.Logging;
using SkiaSharp;
+using Svg;
using Svg.Skia;
namespace Jellyfin.Drawing.Skia;
@@ -21,22 +24,15 @@ namespace Jellyfin.Drawing.Skia;
public class SkiaEncoder : IImageEncoder
{
private const string SvgFormat = "svg";
+
+ // The light sharpening kernel applied after resizing, see ResizeImage.
+ private const float SharpenCenterWeight = 1.4f;
+ private const float SharpenNeighborWeight = -0.1f;
+
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths;
private static readonly SKTypeface?[] _typefaces = InitializeTypefaces();
- private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution(
- new SKSizeI(3, 3),
- [
- 0, -.1f, 0,
- -.1f, 1.4f, -.1f,
- 0, -.1f, 0
- ],
- 1f,
- 0f,
- new SKPointI(1, 1),
- SKShaderTileMode.Clamp,
- true);
/// <summary>
/// The default sampling options, equivalent to old high quality filter settings when upscaling.
@@ -48,6 +44,13 @@ public class SkiaEncoder : IImageEncoder
/// </summary>
public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
+ static SkiaEncoder()
+ {
+ SvgDocument.ResolveExternalElements = ExternalType.None;
+ SvgDocument.ResolveExternalImages = ExternalType.None;
+ SvgDocument.ResolveExternalXmlEntites = ExternalType.None;
+ }
+
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
/// </summary>
@@ -183,6 +186,12 @@ public class SkiaEncoder : IImageEncoder
var extension = Path.GetExtension(path.AsSpan());
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
{
+ if (!SvgSecurityValidator.IsSafe(path, out var reason))
+ {
+ _logger.LogError("Refusing to determine dimensions for SVG {FilePath}: {Reason}", path, reason);
+ return default;
+ }
+
using var svg = new SKSvg();
try
{
@@ -445,6 +454,12 @@ public class SkiaEncoder : IImageEncoder
throw new FileNotFoundException("File not found", path);
}
+ if (!SvgSecurityValidator.IsSafe(path, out var reason))
+ {
+ _logger.LogError("Refusing to render SVG {FilePath}: {Reason}", path, reason);
+ return null;
+ }
+
using var svg = SKSvg.CreateFromFile(path);
if (svg.Drawable is null)
{
@@ -541,8 +556,15 @@ public class SkiaEncoder : IImageEncoder
/// <returns>The resized image.</returns>
internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
{
- using var surface = SKSurface.Create(targetInfo);
- using var canvas = surface.Canvas;
+ if (source.Width == targetInfo.Width && source.Height == targetInfo.Height)
+ {
+ return SKImage.FromBitmap(source);
+ }
+
+ var isDownscale = source.Width > targetInfo.Width || source.Height > targetInfo.Height;
+
+ using var target = new SKBitmap(targetInfo);
+ using var canvas = new SKCanvas(target);
using var paint = new SKPaint();
paint.IsAntialias = isAntialias;
paint.IsDither = isDither;
@@ -550,11 +572,10 @@ public class SkiaEncoder : IImageEncoder
// Historically, kHigh implied cubic filtering, but only when upsampling.
// If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipmaps).
// With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation.
- var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height
+ var samplingOptions = isDownscale
? DefaultSamplingOptions
: UpscaleSamplingOptions;
- paint.ImageFilter = _imageFilter;
canvas.DrawBitmap(
source,
SKRect.Create(0, 0, source.Width, source.Height),
@@ -562,7 +583,78 @@ public class SkiaEncoder : IImageEncoder
samplingOptions,
paint);
- return surface.Snapshot();
+ if (isDownscale)
+ {
+ SharpenInPlace(target);
+ }
+
+ return SKImage.FromBitmap(target);
+ }
+
+ /// <summary>
+ /// Applies the light 3x3 sharpening kernel to the bitmap in place.
+ ///
+ /// This is equivalent to the SKImageFilter.CreateMatrixConvolution paint filter that
+ /// was previously part of the resize draw call. Since the SkiaSharp 3 update that
+ /// filter no longer has a fast CPU path and takes multiple seconds per image on the
+ /// software rasterizer, so the same kernel is applied directly instead.
+ /// </summary>
+ /// <param name="bitmap">The bitmap to sharpen. Must use a color type with four bytes per pixel; other color types are returned unchanged.</param>
+ internal static void SharpenInPlace(SKBitmap bitmap)
+ {
+ if (bitmap.BytesPerPixel != 4)
+ {
+ return;
+ }
+
+ var width = bitmap.Width;
+ var height = bitmap.Height;
+ var stride = bitmap.RowBytes;
+ var pixels = bitmap.GetPixels();
+ if (width == 0 || height == 0 || pixels == IntPtr.Zero)
+ {
+ return;
+ }
+
+ var length = stride * height;
+ var source = ArrayPool<byte>.Shared.Rent(length);
+ var result = ArrayPool<byte>.Shared.Rent(length);
+ try
+ {
+ Marshal.Copy(pixels, source, 0, length);
+
+ for (var y = 0; y < height; y++)
+ {
+ // The kernel clamps at the edges: out-of-bounds taps reuse the edge pixel.
+ var row = y * stride;
+ var up = y == 0 ? row : row - stride;
+ var down = y == height - 1 ? row : row + stride;
+
+ for (var x = 0; x < width; x++)
+ {
+ var col = x * 4;
+ var left = x == 0 ? col : col - 4;
+ var right = x == width - 1 ? col : col + 4;
+
+ for (var channel = 0; channel < 4; channel++)
+ {
+ var value = (SharpenCenterWeight * source[row + col + channel])
+ + (SharpenNeighborWeight * (source[up + col + channel]
+ + source[down + col + channel]
+ + source[row + left + channel]
+ + source[row + right + channel]));
+ result[row + col + channel] = (byte)Math.Clamp((int)(value + 0.5f), 0, 255);
+ }
+ }
+ }
+
+ Marshal.Copy(result, 0, pixels, length);
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(source);
+ ArrayPool<byte>.Shared.Return(result);
+ }
}
/// <inheritdoc/>
diff --git a/src/Jellyfin.Drawing.Skia/SvgSecurityValidator.cs b/src/Jellyfin.Drawing.Skia/SvgSecurityValidator.cs
new file mode 100644
index 0000000000..65f35643b2
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SvgSecurityValidator.cs
@@ -0,0 +1,339 @@
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Compression;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Xml;
+
+[assembly: InternalsVisibleTo("Jellyfin.Drawing.Skia.Tests")]
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Validates that an SVG document does not reference external resources before it is rasterized.
+/// </summary>
+internal static class SvgSecurityValidator
+{
+ // Guards against a chain of nested data:image/svg+xml payloads.
+ private const int MaxDataUriDepth = 4;
+
+ // Upper bound for a decompressed svgz payload carried inside a data URI, to guard against decompression bombs.
+ private const int MaxDecompressedBytes = 16 * 1024 * 1024;
+
+ private const int DecompressBufferSize = 81920;
+
+ private static readonly XmlReaderSettings _scanSettings = new()
+ {
+ DtdProcessing = DtdProcessing.Parse,
+ XmlResolver = null,
+ MaxCharactersFromEntities = 1024 * 1024,
+ IgnoreComments = true,
+ IgnoreProcessingInstructions = true,
+ IgnoreWhitespace = true,
+ CloseInput = false
+ };
+
+ /// <summary>
+ /// Determines whether the SVG at the given path is safe to rasterize, i.e. contains no references
+ /// to external resources.
+ /// </summary>
+ /// <param name="path">The path to the SVG file.</param>
+ /// <param name="reason">When this method returns <c>false</c>, the reason the document was rejected.</param>
+ /// <returns><c>true</c> if the document is free of external references; otherwise <c>false</c>.</returns>
+ public static bool IsSafe(string path, [NotNullWhen(false)] out string? reason)
+ {
+ try
+ {
+ using var stream = File.OpenRead(path);
+ reason = Validate(stream, 0);
+ }
+ catch (IOException ex)
+ {
+ reason = "Unable to read the file for validation: " + ex.Message;
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ reason = "Unable to read the file for validation: " + ex.Message;
+ }
+
+ return reason is null;
+ }
+
+ /// <summary>
+ /// Determines whether the SVG in the given stream is safe to rasterize.
+ /// </summary>
+ /// <param name="stream">The stream containing the SVG document.</param>
+ /// <param name="reason">When this method returns <c>false</c>, the reason the document was rejected.</param>
+ /// <returns><c>true</c> if the document is free of external references; otherwise <c>false</c>.</returns>
+ public static bool IsSafe(Stream stream, [NotNullWhen(false)] out string? reason)
+ {
+ reason = Validate(stream, 0);
+ return reason is null;
+ }
+
+ private static string? Validate(Stream stream, int depth)
+ {
+ try
+ {
+ using var reader = XmlReader.Create(stream, _scanSettings);
+ while (reader.Read())
+ {
+ switch (reader.NodeType)
+ {
+ case XmlNodeType.DocumentType:
+ {
+ var subset = reader.Value;
+ if (!string.IsNullOrEmpty(subset)
+ && (subset.Contains("SYSTEM", StringComparison.OrdinalIgnoreCase)
+ || subset.Contains("PUBLIC", StringComparison.OrdinalIgnoreCase)))
+ {
+ return "The document declares an external DTD entity";
+ }
+
+ break;
+ }
+
+ case XmlNodeType.Element when reader.HasAttributes:
+ {
+ for (var i = 0; i < reader.AttributeCount; i++)
+ {
+ reader.MoveToAttribute(i);
+ var isHref = reader.LocalName.Equals("href", StringComparison.OrdinalIgnoreCase);
+ var reason = isHref
+ ? ValidateReference(reader.Value, depth, "href")
+ : ValidateCss(reader.Value, depth);
+ if (reason is not null)
+ {
+ return reason;
+ }
+ }
+
+ reader.MoveToElement();
+ break;
+ }
+
+ case XmlNodeType.Text:
+ case XmlNodeType.CDATA:
+ {
+ var reason = ValidateCss(reader.Value, depth);
+ if (reason is not null)
+ {
+ return reason;
+ }
+
+ break;
+ }
+ }
+ }
+
+ return null;
+ }
+ catch (XmlException ex)
+ {
+ // Malformed markup, a forbidden DTD construct or an unresolved external entity: refuse to render.
+ return "The document could not be safely parsed: " + ex.Message;
+ }
+ }
+
+ private static string? ValidateReference(ReadOnlySpan<char> value, int depth, string context)
+ {
+ var trimmed = value.Trim();
+ if (trimmed.IsEmpty || trimmed[0] == '#')
+ {
+ return null;
+ }
+
+ if (trimmed.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
+ {
+ return ValidateDataUri(trimmed, depth, context);
+ }
+
+ return "An external resource is referenced via " + context;
+ }
+
+ private static string? ValidateDataUri(ReadOnlySpan<char> dataUri, int depth, string context)
+ {
+ // "data:[<mediatype>][;base64],<payload>" (mirrors Svg.Model's data URI parsing).
+ var comma = dataUri.IndexOf(',');
+ if (comma < 0)
+ {
+ return "A malformed data URI is referenced via " + context;
+ }
+
+ var header = dataUri[5..comma];
+ var firstSeparator = header.IndexOf(';');
+ var mediaType = (firstSeparator < 0 ? header : header[..firstSeparator]).Trim();
+
+ // Only "image/svg+xml" is re-parsed as SVG by the renderer; any other type is treated as raster data.
+ if (!mediaType.Contains('/') || !mediaType.Equals("image/svg+xml", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ if (depth >= MaxDataUriDepth)
+ {
+ return "Nested data URIs exceed the allowed depth";
+ }
+
+ var lastSeparator = header.LastIndexOf(';');
+ var isBase64 = lastSeparator >= 0
+ && header[(lastSeparator + 1)..].Trim().Equals("base64", StringComparison.OrdinalIgnoreCase);
+
+ var payload = dataUri[(comma + 1)..].Trim();
+ byte[]? buffer = null;
+ try
+ {
+ int length;
+ if (isBase64)
+ {
+ buffer = ArrayPool<byte>.Shared.Rent((payload.Length / 4 * 3) + 3);
+ if (!Convert.TryFromBase64Chars(payload, buffer, out length))
+ {
+ return "An undecodable data URI is referenced via " + context;
+ }
+ }
+ else
+ {
+ var unescaped = Uri.UnescapeDataString(payload.ToString());
+ buffer = ArrayPool<byte>.Shared.Rent(Encoding.UTF8.GetMaxByteCount(unescaped.Length));
+ length = Encoding.UTF8.GetBytes(unescaped, buffer);
+ }
+
+ if (length > 2 && buffer[0] == 0x1F && buffer[1] == 0x8B)
+ {
+ using var decompressed = Decompress(buffer, length);
+ return Validate(decompressed, depth + 1);
+ }
+
+ using var stream = new MemoryStream(buffer, 0, length, false);
+ return Validate(stream, depth + 1);
+ }
+ catch (FormatException ex)
+ {
+ return "An undecodable data URI is referenced via " + context + ": " + ex.Message;
+ }
+ catch (InvalidDataException ex)
+ {
+ return "An invalid compressed data URI is referenced via " + context + ": " + ex.Message;
+ }
+ finally
+ {
+ if (buffer is not null)
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
+ }
+ }
+
+ private static MemoryStream Decompress(byte[] compressed, int length)
+ {
+ using var input = new MemoryStream(compressed, 0, length, false);
+ using var gzip = new GZipStream(input, CompressionMode.Decompress);
+ var output = new MemoryStream();
+ var buffer = ArrayPool<byte>.Shared.Rent(DecompressBufferSize);
+ try
+ {
+ var total = 0;
+ int read;
+ while ((read = gzip.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ total += read;
+ if (total > MaxDecompressedBytes)
+ {
+ throw new InvalidDataException("Compressed data URI exceeds the allowed size");
+ }
+
+ output.Write(buffer, 0, read);
+ }
+ }
+ catch
+ {
+ output.Dispose();
+ throw;
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
+
+ output.Position = 0;
+ return output;
+ }
+
+ private static string? ValidateCss(ReadOnlySpan<char> value, int depth)
+ {
+ if (value.IsEmpty)
+ {
+ return null;
+ }
+
+ var index = 0;
+ while (true)
+ {
+ var found = value[index..].IndexOf("url(", StringComparison.OrdinalIgnoreCase);
+ if (found < 0)
+ {
+ break;
+ }
+
+ var start = index + found + 4;
+ var close = value[start..].IndexOf(')');
+ if (close < 0)
+ {
+ break;
+ }
+
+ var target = value.Slice(start, close).Trim();
+ target = target.Trim('\'');
+ target = target.Trim('"').Trim();
+ var reason = ValidateReference(target, depth, "url()");
+ if (reason is not null)
+ {
+ return reason;
+ }
+
+ index = start + close + 1;
+ if (index >= value.Length)
+ {
+ break;
+ }
+ }
+
+ // Handle the bare "@import '...';" form (the "@import url(...)" form is covered above).
+ index = 0;
+ while (true)
+ {
+ var found = value[index..].IndexOf("@import", StringComparison.OrdinalIgnoreCase);
+ if (found < 0)
+ {
+ break;
+ }
+
+ var rest = value[(index + found + 7)..];
+ var quote = rest.IndexOfAny('\'', '"');
+ if (quote >= 0)
+ {
+ var afterQuote = rest[(quote + 1)..];
+ var end = afterQuote.IndexOfAny('\'', '"');
+ if (end >= 0)
+ {
+ var reason = ValidateReference(afterQuote[..end], depth, "@import");
+ if (reason is not null)
+ {
+ return reason;
+ }
+ }
+ }
+
+ index = index + found + 7;
+ if (index >= value.Length)
+ {
+ break;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 6ffb022842..ad1b216970 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -31,7 +31,7 @@ namespace Jellyfin.Drawing;
public sealed class ImageProcessor : IImageProcessor, IDisposable
{
// Increment this when there's a change requiring caches to be invalidated
- private const char Version = '3';
+ private const char Version = '4';
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif", ".svg" };
@@ -251,6 +251,33 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <summary>
/// Gets the cache file path based on a set of parameters.
/// </summary>
+ /// <param name="originalPath">The original image path.</param>
+ /// <param name="dateModified">The source image modification date.</param>
+ /// <param name="format">The output format.</param>
+ /// <param name="options">The image processing options.</param>
+ /// <returns>The transformed image cache path.</returns>
+ internal string GetCacheFilePath(
+ string originalPath,
+ DateTime dateModified,
+ ImageFormat format,
+ ImageProcessingOptions options)
+ => GetCacheFilePath(
+ originalPath,
+ options.Width,
+ options.Height,
+ options.MaxWidth,
+ options.MaxHeight,
+ options.FillWidth,
+ options.FillHeight,
+ options.Quality,
+ dateModified,
+ format,
+ options.PercentPlayed,
+ options.UnplayedCount,
+ options.Blur,
+ options.BackgroundColor,
+ options.ForegroundLayer);
+
private string GetCacheFilePath(
string originalPath,
int? width,
@@ -318,13 +345,13 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
if (percentPlayed > 0)
{
- filename.Append(",p=");
- filename.Append(percentPlayed);
+ filename.Append(",pp=");
+ filename.Append(percentPlayed.ToString(CultureInfo.InvariantCulture));
}
if (unwatchedCount.HasValue)
{
- filename.Append(",p=");
+ filename.Append(",uc=");
filename.Append(unwatchedCount.Value);
}
diff --git a/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs b/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs
index 3851bf9241..3d39372313 100644
--- a/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs
+++ b/src/Jellyfin.Drawing/Properties/AssemblyInfo.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -12,6 +13,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
+[assembly: InternalsVisibleTo("Jellyfin.Server.Integration.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 5518d9b954..bdffecdc8a 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
- <VersionPrefix>12.0.0</VersionPrefix>
+ <VersionPrefix>13.0.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/src/Jellyfin.Extensions/PathHelper.cs b/src/Jellyfin.Extensions/PathHelper.cs
new file mode 100644
index 0000000000..ab74a7749d
--- /dev/null
+++ b/src/Jellyfin.Extensions/PathHelper.cs
@@ -0,0 +1,77 @@
+using System;
+using System.IO;
+
+namespace Jellyfin.Extensions;
+
+/// <summary>
+/// Helpers for safely composing filesystem paths from untrusted input.
+/// </summary>
+/// <remarks>
+/// <see cref="Path.Combine(string, string)"/> has two issues that matter in
+/// any code that joins a trusted directory with an externally-supplied name:
+/// it neither normalises <c>..</c> nor rejects a rooted second argument
+/// (a rooted second arg silently discards the first). Use the helpers below
+/// any time the name comes from media metadata, request input, archive
+/// entries, or any other channel that can be influenced by a third party.
+/// </remarks>
+public static class PathHelper
+{
+ /// <summary>
+ /// Reduces a possibly-untrusted file name to a safe leaf-only name with no
+ /// directory components.
+ /// </summary>
+ /// <param name="fileName">The candidate file name.</param>
+ /// <returns>
+ /// The leaf component of <paramref name="fileName"/>, or <c>null</c> if
+ /// the input has no usable leaf (empty, <c>.</c>, or <c>..</c>).
+ /// </returns>
+ public static string? GetSafeLeafFileName(string? fileName)
+ {
+ if (string.IsNullOrEmpty(fileName))
+ {
+ return null;
+ }
+
+ var leaf = Path.GetFileName(fileName);
+ if (string.IsNullOrEmpty(leaf) || string.Equals(leaf, ".", StringComparison.Ordinal) || string.Equals(leaf, "..", StringComparison.Ordinal))
+ {
+ return null;
+ }
+
+ return leaf;
+ }
+
+ /// <summary>
+ /// Returns whether <paramref name="candidate"/> resolves to a path that
+ /// equals or is contained inside <paramref name="root"/>.
+ /// </summary>
+ /// <param name="root">The directory the candidate must remain inside.</param>
+ /// <param name="candidate">The candidate absolute or relative path.</param>
+ /// <returns><c>true</c> if the candidate is inside or equal to root; otherwise <c>false</c>.</returns>
+ /// <remarks>
+ /// Both arguments are resolved via <see cref="Path.GetFullPath(string)"/>
+ /// so <c>..</c> segments are collapsed before the comparison. The root is
+ /// compared with a trailing directory separator to prevent prefix
+ /// collisions (e.g. <c>/var/data</c> must not be accepted as a parent of
+ /// <c>/var/dataset</c>).
+ /// </remarks>
+ public static bool IsContainedIn(string root, string candidate)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(root);
+ ArgumentException.ThrowIfNullOrEmpty(candidate);
+
+ var fullRoot = Path.GetFullPath(root);
+ var fullCandidate = Path.GetFullPath(candidate);
+
+ if (string.Equals(fullCandidate, fullRoot, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ var rootWithSep = fullRoot.EndsWith(Path.DirectorySeparatorChar)
+ ? fullRoot
+ : fullRoot + Path.DirectorySeparatorChar;
+
+ return fullCandidate.StartsWith(rootWithSep, StringComparison.Ordinal);
+ }
+}
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 906efbcbcc..38f1cf738f 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using System.Text.RegularExpressions;
using ICU4N.Text;
@@ -173,5 +174,41 @@ namespace Jellyfin.Extensions
return cleaned;
}
+
+ /// <summary>
+ /// Escapes an argument so that it survives command line parsing as a single argument when it is wrapped in double quotes by the caller.
+ /// </summary>
+ /// <param name="value">The argument to escape.</param>
+ /// <returns>The escaped argument.</returns>
+ public static string EscapeProcessArgument(this string value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ var span = value.AsSpan();
+ if (!span.Contains('"'))
+ {
+ var trailing = span.Length - span.TrimEnd('\\').Length;
+ return trailing == 0 ? value : string.Concat(value, new string('\\', trailing));
+ }
+
+ var escaped = new StringBuilder(value.Length + 8);
+ var backslashes = 0;
+
+ foreach (var character in span)
+ {
+ if (character == '\\')
+ {
+ backslashes++;
+ continue;
+ }
+
+ escaped
+ .Append('\\', character == '"' ? (backslashes * 2) + 1 : backslashes)
+ .Append(character);
+ backslashes = 0;
+ }
+
+ return escaped.Append('\\', backslashes * 2).ToString();
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index e421601092..256a652fd2 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -14,7 +14,6 @@ using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
-using Jellyfin.LiveTv;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -373,9 +372,7 @@ namespace Jellyfin.LiveTv.Channels
{
IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item);
- return results
- .Select(i => NormalizeMediaSource(item, i))
- .ToList();
+ return NormalizeMediaSources(item, results);
}
/// <summary>
@@ -401,9 +398,7 @@ namespace Jellyfin.LiveTv.Channels
results = Enumerable.Empty<MediaSourceInfo>();
}
- return results
- .Select(i => NormalizeMediaSource(item, i))
- .ToList();
+ return NormalizeMediaSources(item, results);
}
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
@@ -421,11 +416,36 @@ namespace Jellyfin.LiveTv.Channels
return list;
}
- private static MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info)
+ private static IReadOnlyList<MediaSourceInfo> NormalizeMediaSources(BaseItem item, IEnumerable<MediaSourceInfo> infos)
{
- info.RunTimeTicks ??= item.RunTimeTicks;
+ var list = infos.ToList();
+ var itemId = item.Id.ToString("N", CultureInfo.InvariantCulture);
+ var hasDefaultSource = list.Any(i => string.Equals(i.Id, itemId, StringComparison.OrdinalIgnoreCase));
- return info;
+ for (var index = 0; index < list.Count; index++)
+ {
+ var info = list[index];
+ info.RunTimeTicks ??= item.RunTimeTicks;
+
+ if (!string.IsNullOrEmpty(info.Id))
+ {
+ continue;
+ }
+
+ if (!hasDefaultSource)
+ {
+ // The source carrying the item id sorts first and becomes the client's default.
+ info.Id = itemId;
+ hasDefaultSource = true;
+ continue;
+ }
+
+ // Remaining sources need ids that are distinct but stable, as clients send them back to request playback.
+ var key = string.IsNullOrEmpty(info.Path) ? index.ToString(CultureInfo.InvariantCulture) : info.Path;
+ info.Id = (itemId + key).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ }
+
+ return list;
}
private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken)
@@ -1110,8 +1130,9 @@ namespace Jellyfin.LiveTv.Channels
item.Path = mediaSource?.Path;
}
- if (LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(item, null, info.ImageUrl))
+ if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
{
+ item.SetImagePath(ImageType.Primary, info.ImageUrl);
_logger.LogDebug("Forcing update due to ImageUrl {0}", item.Name);
forceUpdate = true;
}
diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
index b8545cbb64..a11f83f2f8 100644
--- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs
+++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
@@ -5,8 +5,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
-using Jellyfin.LiveTv;
using Jellyfin.LiveTv.Configuration;
+using Jellyfin.LiveTv.Listings;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -125,12 +125,16 @@ public class GuideManager : IGuideManager
{
var innerProgress = new Progress<double>(p => progress.Report(p * progressPerService));
- var idList = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(false);
+ var (channelIds, programIds, hasErrors) = await RefreshChannelsInternal(service, innerProgress, cancellationToken).ConfigureAwait(false);
- newChannelIdList.AddRange(idList.Item1);
- newProgramIdList.AddRange(idList.Item2);
+ newChannelIdList.AddRange(channelIds);
+ newProgramIdList.AddRange(programIds);
+
+ // The channels that failed did not report any programs, so cleaning the database
+ // would delete every program they provide instead of keeping the previous ones.
+ cleanDatabase &= !hasErrors;
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
@@ -172,10 +176,12 @@ public class GuideManager : IGuideManager
: 7;
}
- private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, IProgress<double> progress, CancellationToken cancellationToken)
+ private async Task<(List<Guid> ChannelIds, List<Guid> ProgramIds, bool HasErrors)> RefreshChannelsInternal(ILiveTvService service, IProgress<double> progress, CancellationToken cancellationToken)
{
progress.Report(10);
+ var hasErrors = false;
+
var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
.Select(i => new Tuple<string, ChannelInfo>(service.Name, i))
.ToList();
@@ -195,12 +201,13 @@ public class GuideManager : IGuideManager
list.Add(item);
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
+ hasErrors = true;
_logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
}
@@ -314,12 +321,13 @@ public class GuideManager : IGuideManager
},
cancellationToken).ConfigureAwait(false);
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
+ hasErrors = true;
_logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
}
@@ -330,7 +338,7 @@ public class GuideManager : IGuideManager
}
progress.Report(100);
- return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
+ return (channels, programIds, hasErrors);
}
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
@@ -449,9 +457,23 @@ public class GuideManager : IGuideManager
item.Name = channelInfo.Name;
- if (LiveTvChannelImageHelper.UpdateChannelImageIfNeeded(item, channelInfo.ImagePath, channelInfo.ImageUrl))
+ var currentPrimary = item.GetImageInfo(ImageType.Primary, 0);
+ var imageUrlIsNull = string.IsNullOrWhiteSpace(channelInfo.ImageUrl);
+
+ // Update channel image if image URL has changed
+ if (currentPrimary is null
+ || (!imageUrlIsNull && !string.Equals(currentPrimary.Path, channelInfo.ImageUrl, StringComparison.Ordinal)))
{
- forceUpdate = true;
+ if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath))
+ {
+ item.SetImagePath(ImageType.Primary, channelInfo.ImagePath);
+ forceUpdate = true;
+ }
+ else if (!imageUrlIsNull)
+ {
+ item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl);
+ forceUpdate = true;
+ }
}
if (isNew)
@@ -486,8 +508,13 @@ public class GuideManager : IGuideManager
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
};
-
- item.TrySetProviderId(EtagKey, info.Etag);
+ }
+ else if (XmlTvProgramEtag.MatchesStored(info.Etag, item.GetProviderId(EtagKey)))
+ {
+ // XMLTV ETags are generated from the final ProgramInfo fields Jellyfin consumes,
+ // so an exact match means nothing relevant changed. Other providers stay on the
+ // field-by-field update path.
+ return (item, false, false);
}
if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
@@ -613,13 +640,9 @@ public class GuideManager : IGuideManager
forceUpdate |= UpdateImages(item, info);
- if (isNew)
- {
- item.OnMetadataChanged();
-
- return (item, true, false);
- }
-
+ // Restore the etag wiped by `item.ProviderIds = info.ProviderIds` above and
+ // persist it on new items so they join the fast path on the next refresh
+ // instead of taking an extra full processing cycle.
var isUpdated = forceUpdate;
var etag = info.Etag;
if (string.IsNullOrWhiteSpace(etag))
@@ -632,6 +655,13 @@ public class GuideManager : IGuideManager
isUpdated = true;
}
+ if (isNew)
+ {
+ item.OnMetadataChanged();
+
+ return (item, true, false);
+ }
+
if (isUpdated)
{
item.OnMetadataChanged();
diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
index 19c4514766..633c4f95ed 100644
--- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
+++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
@@ -188,8 +188,8 @@ namespace Jellyfin.LiveTv.IO
var commandLineArgs = string.Format(
CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
- inputTempFile,
- targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
+ inputTempFile.EscapeProcessArgument(),
+ targetFile.EscapeProcessArgument(),
videoArgs,
GetAudioArgs(mediaSource),
subtitleArgs,
diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
index 15e20d6f64..7274a3030c 100644
--- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
+++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
@@ -352,9 +352,12 @@ public class ListingsManager : IListingsManager
var xmltvCacheFile = Path.Combine(cachePath, "xmltv", safeId + ".xml");
try
{
- File.Delete(xmltvCacheFile);
+ if (File.Exists(xmltvCacheFile))
+ {
+ File.Delete(xmltvCacheFile);
+ }
}
- catch (IOException ex)
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
_logger.LogWarning(ex, "Error deleting XMLTV cache file for provider {ProviderId}", safeId);
}
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
index d456bea469..40ec99e9a5 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
@@ -491,6 +491,12 @@ namespace Jellyfin.LiveTv.Listings
var results = new List<ShowImagesDto>();
for (int i = 0; i < programIds.Count; i += BatchSize)
{
+ // The daily image limit may be surfaced mid-batch.
+ if (IsImageDailyLimitActive())
+ {
+ break;
+ }
+
var batch = programIds.Skip(i).Take(BatchSize);
using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs/");
@@ -511,6 +517,18 @@ namespace Jellyfin.LiveTv.Listings
entry.ProgramId,
entry.Code,
entry.Message);
+
+ // The image download limit can be reported per-entry inside an
+ // otherwise successful (HTTP 200) response when the limit is hit
+ // mid-batch. Back off so we stop requesting images until SD resets.
+ if (entry.Code is (int)SdErrorCode.MaxImageDownloads or (int)SdErrorCode.MaxImageDownloadsTrial)
+ {
+ _logger.LogError(
+ "Schedules Direct image download limit hit (code {Code}). Disabling image acquisition until SD reset.",
+ entry.Code);
+ SetImageLimitHit();
+ }
+
continue;
}
@@ -531,46 +549,52 @@ namespace Jellyfin.LiveTv.Listings
{
var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
- var lineups = new List<NameIdPair>();
-
if (string.IsNullOrWhiteSpace(token))
{
- return lineups;
+ throw new AuthenticationException("Could not authenticate with Schedules Direct");
}
+ var lineups = new List<NameIdPair>();
+
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location);
options.Headers.TryAddWithoutValidation("token", token);
- try
+ var root = await Request<IReadOnlyList<HeadendsDto>>(options, false, info, cancellationToken).ConfigureAwait(false);
+ foreach (HeadendsDto headend in root ?? [])
{
- var root = await Request<IReadOnlyList<HeadendsDto>>(options, false, info, cancellationToken).ConfigureAwait(false);
- if (root is not null)
+ foreach (LineupDto lineup in headend.Lineups ?? [])
{
- foreach (HeadendsDto headend in root)
+ lineups.Add(new NameIdPair
{
- foreach (LineupDto lineup in headend.Lineups)
- {
- lineups.Add(new NameIdPair
- {
- Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
- Id = lineup.Uri?[18..]
- });
- }
- }
- }
- else
- {
- _logger.LogInformation("No lineups available");
+ Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
+ Id = string.IsNullOrWhiteSpace(lineup.Lineup) ? lineup.Uri?.Split('/')[^1] : lineup.Lineup
+ });
}
}
- catch (Exception ex)
+
+ if (lineups.Count == 0)
{
- _logger.LogError(ex, "Error getting headends");
+ _logger.LogWarning(
+ "Schedules Direct has no lineups for country {Country} and postal code {PostalCode}",
+ country,
+ location);
}
return lineups;
}
+ private void ResetErrorState(ListingsProviderInfo info)
+ {
+ _accountError = false;
+ Interlocked.Exchange(ref _lastErrorResponseTicks, 0);
+
+ // Only the account being saved is retried, the tokens of the other accounts stay valid.
+ if (!string.IsNullOrWhiteSpace(info.Username))
+ {
+ _tokens.TryRemove(info.Username, out _);
+ }
+ }
+
private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken)
{
var username = info.Username;
@@ -587,15 +611,19 @@ namespace Jellyfin.LiveTv.Listings
return null;
}
- // Permanent account error — SD is disabled for this server lifetime.
+ // Account error — SD stays disabled until the provider is saved again or the server restarts.
if (_accountError)
{
+ _logger.LogWarning("Skipping Schedules Direct request because of an earlier account error. Save the listings provider again to retry.");
+
return null;
}
// Avoid hammering SD after transient login failures (e.g. max attempts / temporary lockout)
if ((DateTime.UtcNow - new DateTime(Interlocked.Read(ref _lastErrorResponseTicks), DateTimeKind.Utc)).TotalMinutes < 30)
{
+ _logger.LogWarning("Skipping Schedules Direct request because of a recent login failure. Retrying no earlier than 30 minutes after it.");
+
return null;
}
@@ -758,7 +786,7 @@ namespace Jellyfin.LiveTv.Listings
return root.Token;
}
- throw new AuthenticationException("Could not authenticate with Schedules Direct Error: " + root.Message);
+ throw new AuthenticationException("Could not authenticate with Schedules Direct Error: " + (root?.Message ?? "empty response"));
}
private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -974,10 +1002,18 @@ namespace Jellyfin.LiveTv.Listings
public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
{
+ ResetErrorState(info);
+
if (validateLogin)
{
ArgumentException.ThrowIfNullOrEmpty(info.Username);
ArgumentException.ThrowIfNullOrEmpty(info.Password);
+
+ var token = await GetToken(info, CancellationToken.None).ConfigureAwait(false);
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ throw new AuthenticationException("Could not authenticate with Schedules Direct");
+ }
}
if (validateListings)
diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
index 7088d1f0bf..f78d464659 100644
--- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -27,11 +28,14 @@ namespace Jellyfin.LiveTv.Listings
public class XmlTvListingsProvider : IListingsProvider
{
private static readonly TimeSpan _maxCacheAge = TimeSpan.FromHours(1);
+ private static readonly TimeSpan _downloadTimeout = TimeSpan.FromMinutes(15);
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<XmlTvListingsProvider> _logger;
+ private readonly ConcurrentDictionary<string, DateTime> _lastDownloadFailures = new(StringComparer.Ordinal);
+
public XmlTvListingsProvider(
IServerConfigurationManager config,
IHttpClientFactory httpClientFactory,
@@ -64,32 +68,51 @@ namespace Jellyfin.LiveTv.Listings
string cacheDir = Path.Join(_config.ApplicationPaths.CachePath, "xmltv");
string cacheFile = Path.Join(cacheDir, cacheFilename);
- if (File.Exists(cacheFile))
+ if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
{
- if (File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
+ return cacheFile;
+ }
+
+ var isRemote = info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
+
+ if (isRemote
+ && _lastDownloadFailures.TryGetValue(info.Path, out var lastFailure)
+ && DateTime.UtcNow - lastFailure < _maxCacheAge)
+ {
+ if (File.Exists(cacheFile))
{
return cacheFile;
}
- File.Delete(cacheFile);
- }
- else
- {
- Directory.CreateDirectory(cacheDir);
+ throw new InvalidOperationException("Skipping the XMLTV download after a recent failure: " + info.Path);
}
+ Directory.CreateDirectory(cacheDir);
+
+ var tempFile = cacheFile + ".tmp";
+
try
{
- if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ using var timeout = new CancellationTokenSource(_downloadTimeout);
+ using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeout.Token);
+ var downloadCancellationToken = linkedTokenSource.Token;
+
+ if (isRemote)
{
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
+ var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
+ httpClient.Timeout = _downloadTimeout;
+
+ using var response = await httpClient
+ .GetAsync(info.Path, HttpCompletionOption.ResponseHeadersRead, downloadCancellationToken)
+ .ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
var redirectedUrl = response.RequestMessage?.RequestUri?.ToString() ?? info.Path;
- var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var stream = await response.Content.ReadAsStreamAsync(downloadCancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
- return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).ConfigureAwait(false);
+ await UnzipIfNeededAndCopy(redirectedUrl, stream, tempFile, downloadCancellationToken).ConfigureAwait(false);
}
}
else
@@ -97,28 +120,63 @@ namespace Jellyfin.LiveTv.Listings
var stream = AsyncFile.OpenRead(info.Path);
await using (stream.ConfigureAwait(false))
{
- return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
+ await UnzipIfNeededAndCopy(info.Path, stream, tempFile, downloadCancellationToken).ConfigureAwait(false);
}
}
+
+ File.Move(tempFile, cacheFile, true);
+ _lastDownloadFailures.TryRemove(info.Path, out _);
+
+ return cacheFile;
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ TryDeleteTempFile(tempFile);
+
+ throw;
}
catch (Exception ex)
{
+ TryDeleteTempFile(tempFile);
+ _lastDownloadFailures[info.Path] = DateTime.UtcNow;
+
_logger.LogError(ex, "Error downloading or processing XMLTV file from {Path}", info.Path);
if (File.Exists(cacheFile))
{
- File.Delete(cacheFile);
+ _logger.LogWarning("Falling back to the previously downloaded XMLTV file for {Path}", info.Path);
+
+ return cacheFile;
+ }
+
+ if (ex is OperationCanceledException)
+ {
+ throw new TimeoutException(
+ string.Format(CultureInfo.InvariantCulture, "Timed out downloading the XMLTV file from {0}", info.Path),
+ ex);
}
throw;
}
}
- private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToken cancellationToken)
+ private void TryDeleteTempFile(string tempFile)
+ {
+ try
+ {
+ File.Delete(tempFile);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ _logger.LogWarning(ex, "Error deleting temporary XMLTV file {File}", tempFile);
+ }
+ }
+
+ private async Task UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToken cancellationToken)
{
var fileStream = new FileStream(
file,
- FileMode.CreateNew,
+ FileMode.Create,
FileAccess.Write,
FileShare.None,
IODefaults.FileStreamBufferSize,
@@ -148,15 +206,8 @@ namespace Jellyfin.LiveTv.Listings
var fileInfo = new FileInfo(file);
if (!fileInfo.Exists || fileInfo.Length == 0)
{
- if (fileInfo.Exists)
- {
- File.Delete(file);
- }
-
throw new InvalidOperationException("Downloaded XMLTV file is empty: " + originalUrl);
}
-
- return file;
}
public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
@@ -173,7 +224,29 @@ namespace Jellyfin.LiveTv.Listings
var reader = new XmlTvReader(path, GetLanguage(info));
return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken)
- .Select(p => GetProgramInfo(p, info));
+ .Select(p => GetProgramInfoWithEtag(p, info));
+ }
+
+ private ProgramInfo GetProgramInfoWithEtag(XmlTvProgram program, ListingsProviderInfo info)
+ {
+ var programInfo = GetProgramInfo(program, info);
+
+ if (XmlTvProgramEtag.TryCreate(programInfo, out var etag, out var reason))
+ {
+ programInfo.Etag = etag;
+ }
+ else
+ {
+ _logger.LogDebug(
+ "Unable to create XMLTV program ETag for program {ProgramId} on channel {ChannelId} from {StartDate} to {EndDate}: {Reason}. The program will be treated as updated on each guide refresh.",
+ programInfo.Id,
+ programInfo.ChannelId,
+ programInfo.StartDate,
+ programInfo.EndDate,
+ reason);
+ }
+
+ return programInfo;
}
private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
@@ -259,6 +332,13 @@ namespace Jellyfin.LiveTv.Listings
public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings)
{
+ // Saving the provider is an explicit retry, so the download backoff has to be dropped
+ // together with the cached file the listings manager deletes.
+ if (!string.IsNullOrEmpty(info.Path))
+ {
+ _lastDownloadFailures.TryRemove(info.Path, out _);
+ }
+
// Assume all urls are valid. check files for existence
if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path))
{
diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvProgramEtag.cs b/src/Jellyfin.LiveTv/Listings/XmlTvProgramEtag.cs
new file mode 100644
index 0000000000..b5ddb1530f
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Listings/XmlTvProgramEtag.cs
@@ -0,0 +1,184 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using MediaBrowser.Controller.LiveTv;
+
+namespace Jellyfin.LiveTv.Listings
+{
+ internal static class XmlTvProgramEtag
+ {
+ internal const string Prefix = "xmltv-sha256-v1:";
+
+ internal static bool IsXmlTvEtag(string? etag)
+ => !string.IsNullOrWhiteSpace(etag)
+ && etag.StartsWith(Prefix, StringComparison.Ordinal);
+
+ // Returns true only when the incoming etag is XMLTV-style AND equals the stored value.
+ // The IsXmlTvEtag gate keeps other providers (e.g. Schedules Direct) on the
+ // field-by-field update path even if their etag strings happen to match.
+ internal static bool MatchesStored(string? incomingEtag, string? storedEtag)
+ => IsXmlTvEtag(incomingEtag)
+ && string.Equals(incomingEtag, storedEtag, StringComparison.OrdinalIgnoreCase);
+
+ internal static bool TryCreate(ProgramInfo programInfo, out string? etag, out string? reason)
+ {
+ etag = null;
+
+ if (string.IsNullOrWhiteSpace(programInfo.Id))
+ {
+ reason = "program id is empty";
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(programInfo.ChannelId))
+ {
+ reason = "channel id is empty";
+ return false;
+ }
+
+ if (programInfo.StartDate == default)
+ {
+ reason = "start date is empty";
+ return false;
+ }
+
+ if (programInfo.EndDate == default)
+ {
+ reason = "end date is empty";
+ return false;
+ }
+
+ if (programInfo.EndDate <= programInfo.StartDate)
+ {
+ reason = "end date is not after start date";
+ return false;
+ }
+
+ var builder = new StringBuilder(1024);
+
+ // Keep this list aligned with the ProgramInfo fields consumed by GuideManager.
+ AppendValue(builder, "schema", "xmltv-programinfo-v1");
+ AppendValue(builder, nameof(programInfo.Id), programInfo.Id);
+ AppendValue(builder, nameof(programInfo.ChannelId), programInfo.ChannelId);
+ AppendValue(builder, nameof(programInfo.Name), programInfo.Name);
+ AppendValue(builder, nameof(programInfo.OfficialRating), programInfo.OfficialRating);
+ AppendValue(builder, nameof(programInfo.Overview), programInfo.Overview);
+ AppendValue(builder, nameof(programInfo.StartDate), programInfo.StartDate);
+ AppendValue(builder, nameof(programInfo.EndDate), programInfo.EndDate);
+ AppendList(builder, nameof(programInfo.Genres), programInfo.Genres);
+ AppendValue(builder, nameof(programInfo.OriginalAirDate), programInfo.OriginalAirDate);
+ AppendValue(builder, nameof(programInfo.IsHD), programInfo.IsHD);
+ AppendValue(builder, nameof(programInfo.Audio), programInfo.Audio?.ToString());
+ AppendValue(builder, nameof(programInfo.CommunityRating), programInfo.CommunityRating);
+ AppendValue(builder, nameof(programInfo.IsRepeat), programInfo.IsRepeat);
+ AppendValue(builder, nameof(programInfo.EpisodeTitle), programInfo.EpisodeTitle);
+ AppendValue(builder, nameof(programInfo.ImagePath), programInfo.ImagePath);
+ AppendValue(builder, nameof(programInfo.ImageUrl), programInfo.ImageUrl);
+ AppendValue(builder, nameof(programInfo.ThumbImageUrl), programInfo.ThumbImageUrl);
+ AppendValue(builder, nameof(programInfo.LogoImageUrl), programInfo.LogoImageUrl);
+ AppendValue(builder, nameof(programInfo.BackdropImageUrl), programInfo.BackdropImageUrl);
+ AppendValue(builder, nameof(programInfo.IsMovie), programInfo.IsMovie);
+ AppendValue(builder, nameof(programInfo.IsSports), programInfo.IsSports);
+ AppendValue(builder, nameof(programInfo.IsSeries), programInfo.IsSeries);
+ AppendValue(builder, nameof(programInfo.IsLive), programInfo.IsLive);
+ AppendValue(builder, nameof(programInfo.IsNews), programInfo.IsNews);
+ AppendValue(builder, nameof(programInfo.IsKids), programInfo.IsKids);
+ AppendValue(builder, nameof(programInfo.IsPremiere), programInfo.IsPremiere);
+ AppendValue(builder, nameof(programInfo.ProductionYear), programInfo.ProductionYear);
+ AppendValue(builder, nameof(programInfo.SeriesId), programInfo.SeriesId);
+ AppendValue(builder, nameof(programInfo.ShowId), programInfo.ShowId);
+ AppendValue(builder, nameof(programInfo.SeasonNumber), programInfo.SeasonNumber);
+ AppendValue(builder, nameof(programInfo.EpisodeNumber), programInfo.EpisodeNumber);
+ AppendDictionary(builder, nameof(programInfo.ProviderIds), programInfo.ProviderIds);
+ AppendDictionary(builder, nameof(programInfo.SeriesProviderIds), programInfo.SeriesProviderIds);
+
+ var hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
+ etag = Prefix + Convert.ToHexString(hash);
+ reason = null;
+ return true;
+ }
+
+ private static void AppendValue(StringBuilder builder, string name, string? value)
+ {
+ builder.Append(name).Append('|');
+ if (value is null)
+ {
+ builder.Append('N').Append("|0|");
+ }
+ else
+ {
+ builder.Append('S')
+ .Append('|')
+ .Append(value.Length.ToString(CultureInfo.InvariantCulture))
+ .Append('|')
+ .Append(value);
+ }
+
+ builder.Append('\n');
+ }
+
+ private static void AppendValue(StringBuilder builder, string name, DateTime value)
+ => AppendValue(builder, name, FormatDateTime(value));
+
+ private static void AppendValue(StringBuilder builder, string name, DateTime? value)
+ => AppendValue(builder, name, value.HasValue ? FormatDateTime(value.Value) : null);
+
+ private static void AppendValue(StringBuilder builder, string name, bool value)
+ => AppendValue(builder, name, value ? "true" : "false");
+
+ private static void AppendValue(StringBuilder builder, string name, bool? value)
+ => AppendValue(builder, name, value switch { true => "true", false => "false", null => null });
+
+ private static void AppendValue(StringBuilder builder, string name, int? value)
+ => AppendValue(builder, name, value?.ToString(CultureInfo.InvariantCulture));
+
+ private static void AppendValue(StringBuilder builder, string name, float? value)
+ => AppendValue(builder, name, value?.ToString("R", CultureInfo.InvariantCulture));
+
+ // Treat Unspecified as UTC so the etag does not vary with the server's local timezone.
+ private static string FormatDateTime(DateTime value)
+ {
+ var utc = value.Kind switch
+ {
+ DateTimeKind.Utc => value,
+ DateTimeKind.Unspecified => DateTime.SpecifyKind(value, DateTimeKind.Utc),
+ _ => value.ToUniversalTime(),
+ };
+
+ return utc.ToString("O", CultureInfo.InvariantCulture);
+ }
+
+ private static void AppendList(StringBuilder builder, string name, IReadOnlyList<string> values)
+ {
+ AppendValue(builder, name + ".Count", values.Count.ToString(CultureInfo.InvariantCulture));
+ for (var i = 0; i < values.Count; i++)
+ {
+ AppendValue(builder, $"{name}[{i}]", values[i]);
+ }
+ }
+
+ private static void AppendDictionary(StringBuilder builder, string name, IReadOnlyDictionary<string, string?> values)
+ {
+ AppendValue(builder, name + ".Count", values.Count.ToString(CultureInfo.InvariantCulture));
+ if (values.Count == 0)
+ {
+ return;
+ }
+
+ var index = 0;
+ foreach (var (key, value) in values
+ .OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(i => i.Key, StringComparer.Ordinal))
+ {
+ AppendValue(builder, $"{name}[{index}].Key", key);
+ AppendValue(builder, $"{name}[{index}].Value", value);
+ index++;
+ }
+ }
+ }
+}
diff --git a/src/Jellyfin.LiveTv/LiveTvChannelImageHelper.cs b/src/Jellyfin.LiveTv/LiveTvChannelImageHelper.cs
deleted file mode 100644
index a590193b5f..0000000000
--- a/src/Jellyfin.LiveTv/LiveTvChannelImageHelper.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Entities;
-
-namespace Jellyfin.LiveTv;
-
-/// <summary>
-/// Helpers for keeping Live TV channel icons in sync with guide data.
-/// </summary>
-internal static class LiveTvChannelImageHelper
-{
- /// <summary>
- /// Applies the channel icon from guide or tuner metadata.
- /// Called on each guide refresh so remote icons are re-downloaded even when the URL is unchanged.
- /// </summary>
- /// <param name="item">The channel item.</param>
- /// <param name="imagePath">The local image path from the tuner, if any.</param>
- /// <param name="imageUrl">The remote image URL from the guide provider, if any.</param>
- /// <returns><c>true</c> when the item image metadata was updated.</returns>
- internal static bool UpdateChannelImageIfNeeded(BaseItem item, string? imagePath, string? imageUrl)
- {
- var newImageSource = !string.IsNullOrWhiteSpace(imagePath)
- ? imagePath
- : imageUrl;
-
- if (string.IsNullOrWhiteSpace(newImageSource))
- {
- return false;
- }
-
- item.SetImagePath(ImageType.Primary, newImageSource);
- return true;
- }
-}
diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs
index 173d3c3e8e..93f9083178 100644
--- a/src/Jellyfin.LiveTv/LiveTvManager.cs
+++ b/src/Jellyfin.LiveTv/LiveTvManager.cs
@@ -1229,6 +1229,14 @@ namespace Jellyfin.LiveTv
.Where(IsLiveTvEnabled);
}
+ /// <inheritdoc />
+ public bool IsEnabledForUser(User user)
+ {
+ ArgumentNullException.ThrowIfNull(user);
+
+ return IsLiveTvEnabled(user);
+ }
+
/// <summary>
/// Resets the tuner.
/// </summary>
@@ -1262,7 +1270,7 @@ namespace Jellyfin.LiveTv
public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken)
{
- var name = _localization.GetLocalizedString("HeaderLiveTV");
+ var name = _localization.GetServerLocalizedString("HeaderLiveTV");
return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
}
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
index 846f9baf71..8a5aeccba5 100644
--- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
@@ -286,7 +286,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
if (requiresRefresh)
{
- await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
+ _libraryManager.QueueLibraryScan();
}
}
@@ -497,7 +497,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
// trim trailing period from the folder name
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim().TrimEnd('.').Trim();
- if (metadata is not null && metadata.ProductionYear.HasValue)
+ if (metadata is not null && metadata.ProductionYear is not null)
{
folderName += " (" + metadata.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
@@ -532,7 +532,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
}
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
- if (timer.ProductionYear.HasValue)
+ if (timer.ProductionYear is not null)
{
folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
@@ -550,7 +550,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
}
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
- if (timer.ProductionYear.HasValue)
+ if (timer.ProductionYear is not null)
{
folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
index 7e68dbb547..e0e5a00fb9 100644
--- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
@@ -290,7 +290,7 @@ public class RecordingsMetadataManager
null,
DateTime.UtcNow.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false);
- if (item.ProductionYear.HasValue)
+ if (item.ProductionYear is not null)
{
await writer.WriteElementStringAsync(null, "year", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
diff --git a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index e1f87a7bd4..15b1368939 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -326,7 +326,7 @@ namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
BufferMs = 0,
Container = "ts",
Id = id,
- SupportsDirectPlay = false,
+ SupportsDirectPlay = true,
SupportsDirectStream = true,
SupportsTranscoding = true,
IsInfiniteStream = true,
diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
index fb606be0ef..902ca76af8 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
@@ -32,6 +32,7 @@ namespace Jellyfin.LiveTv.TunerHosts
{
private static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"];
private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"];
+ private static readonly string[] _manifestExtensions = [".m3u8", ".m3u", ".mpd"];
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
@@ -151,11 +152,20 @@ namespace Jellyfin.LiveTv.TunerHosts
var protocol = _mediaSourceManager.GetPathProtocol(path);
var isRemote = true;
- if (Uri.TryCreate(path, UriKind.Absolute, out var uri))
+ Uri.TryCreate(path, UriKind.Absolute, out var uri);
+ if (uri is not null)
{
isRemote = !_networkManager.IsInLocalNetwork(uri.Host);
}
+ // A manifest is not a byte stream. Serving one directly hands the client a playlist whose
+ // variant and segment URIs are relative to the origin, and those do not resolve against the
+ // Jellyfin url the client fetched it from. Remux or transcode these instead.
+ if (IsManifest(path, uri))
+ {
+ supportsDirectPlay = false;
+ }
+
var httpHeaders = new Dictionary<string, string>();
if (protocol == MediaProtocol.Http)
@@ -210,6 +220,20 @@ namespace Jellyfin.LiveTv.TunerHosts
return mediaSource;
}
+ /// <summary>
+ /// Determines whether a channel path points at an HLS or DASH manifest rather than at a byte stream.
+ /// </summary>
+ /// <param name="path">The channel path.</param>
+ /// <param name="uri">The channel path parsed as an absolute uri, or <c>null</c> if it is not one.</param>
+ /// <returns><c>true</c> if the path names a streaming manifest.</returns>
+ private static bool IsManifest(string path, Uri uri)
+ {
+ // Use the uri path when there is one so that a query string does not hide the extension.
+ var extension = Path.GetExtension(uri is null ? path : uri.AbsolutePath);
+
+ return _manifestExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
+ }
+
public Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
{
return Task.FromResult(new List<TunerHostInfo>());
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 5e7e2090cd..55068a5e7e 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -2,13 +2,17 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
+ <ItemGroup>
+ <Compile Include="..\..\SharedVersion.cs" />
+ </ItemGroup>
+
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.MediaEncoding.Keyframes</PackageId>
- <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
@@ -22,10 +26,4 @@
<PackageReference Include="NEbml" />
</ItemGroup>
- <ItemGroup>
- <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
- <_Parameter1>Jellyfin.MediaEncoding.Keyframes.Tests</_Parameter1>
- </AssemblyAttribute>
- </ItemGroup>
-
</Project>
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Properties/AssemblyInfo.cs b/src/Jellyfin.MediaEncoding.Keyframes/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..70cc8a4648
--- /dev/null
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Jellyfin.MediaEncoding.Keyframes.Tests")]
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index 4559f68ce8..496c108cfd 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -491,6 +491,7 @@ public class NetworkManager : INetworkManager, IDisposable
startupOverrideKey,
true,
true));
+ WarnIfPublishedUrlBasePathDiffers(publishedServerUrls, config.BaseUrl);
_publishedServerUrls = publishedServerUrls;
return;
}
@@ -580,10 +581,53 @@ public class NetworkManager : INetworkManager, IDisposable
}
}
+ WarnIfPublishedUrlBasePathDiffers(publishedServerUrls, config.BaseUrl);
_publishedServerUrls = publishedServerUrls;
}
}
+ /// <summary>
+ /// Warns when a full-URL published server override uses a public path that differs from the configured base
+ /// URL. Jellyfin appends the base URL to generated Live TV client URLs in this case, which can conflict with
+ /// reverse proxies that translate public request paths. Bare host/IP overrides are exempt because the base URL
+ /// is appended when the API URL is built from them.
+ /// </summary>
+ /// <param name="publishedServerUrls">The parsed published server URL overrides.</param>
+ /// <param name="baseUrl">The configured base URL, if any.</param>
+ private void WarnIfPublishedUrlBasePathDiffers(List<PublishedServerUriOverride> publishedServerUrls, string baseUrl)
+ {
+ if (string.IsNullOrEmpty(baseUrl))
+ {
+ return;
+ }
+
+ foreach (var overrideUri in publishedServerUrls.Select(x => x.OverrideUri).Distinct(StringComparer.OrdinalIgnoreCase))
+ {
+ if (!overrideUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
+ && !overrideUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (!Uri.TryCreate(overrideUri, UriKind.Absolute, out var uri))
+ {
+ continue;
+ }
+
+ var path = Uri.UnescapeDataString(uri.AbsolutePath).TrimEnd('/');
+ if (path.EndsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ var publishedServerHost = uri.GetComponents(UriComponents.HostAndPort, UriFormat.Unescaped);
+ _logger.LogWarning(
+ "The published server URL for host '{PublishedServerHost}' does not end with the configured base URL '{BaseUrl}'. Jellyfin will append this base URL when generating Live TV client URLs. If your reverse proxy translates public paths, this may cause Live TV playback to fail. Update the Published Server URIs setting on the Networking page of the admin dashboard, the JELLYFIN_PublishedServerUrl environment variable / --published-server-url option, or the reverse proxy path mapping accordingly.",
+ publishedServerHost,
+ baseUrl);
+ }
+ }
+
private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
{
if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
@@ -851,7 +895,7 @@ public class NetworkManager : INetworkManager, IDisposable
bool isExternal = !IsInLocalNetwork(source);
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
- if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
+ if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result, out port))
{
return result;
}
@@ -1017,11 +1061,12 @@ public class NetworkManager : INetworkManager, IDisposable
/// <param name="source">IP source address to use.</param>
/// <param name="isInExternalSubnet">True if the source is in an external subnet.</param>
/// <param name="bindPreference">The published server URL that matches the source address.</param>
+ /// <param name="port">The explicit port parsed from the override, if any.</param>
/// <returns><c>true</c> if a match is found, <c>false</c> otherwise.</returns>
- private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference)
+ private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference, out int? port)
{
bindPreference = string.Empty;
- int? port = null;
+ port = null;
// Only consider subnets including the source IP, preferring specific overrides
List<PublishedServerUriOverride> validPublishedServerUrls;
@@ -1063,25 +1108,43 @@ public class NetworkManager : INetworkManager, IDisposable
return false;
}
- // Handle override specifying port
- var parts = bindPreference.Split(':');
- if (parts.Length > 1)
+ // Handle override specifying an explicit port.
+ (bindPreference, port) = ParseHostAndPort(bindPreference);
+
+ if (port.HasValue)
{
- if (int.TryParse(parts[1], out int p))
- {
- bindPreference = parts[0];
- port = p;
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
- return true;
- }
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+ }
+ else
+ {
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
}
-
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true;
}
/// <summary>
+ /// Splits a published server URL override into its host and explicit port, if any.
+ /// Full URLs (containing "://") are returned whole, with any port left embedded.
+ /// </summary>
+ /// <param name="value">The override value, e.g. "host:port", "[::1]:port", or a full URL.</param>
+ /// <returns>The parsed host (or the original value if not split) and the explicit port, if any.</returns>
+ private static (string Host, int? Port) ParseHostAndPort(string value)
+ {
+ if (value.Contains("://", StringComparison.Ordinal))
+ {
+ return (value, null);
+ }
+
+ if (Uri.TryCreate("any://" + value, UriKind.Absolute, out var parsed) && parsed.Port != -1)
+ {
+ return (parsed.DnsSafeHost, parsed.Port);
+ }
+
+ return (value, null);
+ }
+
+ /// <summary>
/// Attempts to match the source against the user defined bind interfaces.
/// </summary>
/// <param name="source">IP source address to use.</param>