diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 9ffccaa9e9..51d4081ecf 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
+using Jellyfin.Data;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
@@ -103,6 +104,7 @@ public class PersonsController : BaseJellyfinApiController
personTypes,
excludePersonTypes)
{
+ AccessFilter = BuildAccessFilter(user),
NameContains = searchTerm,
NameStartsWith = nameStartsWith,
NameLessThan = nameLessThan,
@@ -123,6 +125,20 @@ public class PersonsController : BaseJellyfinApiController
.ToArray());
}
+ // People are not owned by a library, so nothing in the Peoples table says which of them a user is
+ // allowed to see; that only follows from the items they are credited on.
+ private InternalItemsQuery? BuildAccessFilter(User? user)
+ {
+ if (user is null || !user.HasContentRestrictions())
+ {
+ return null;
+ }
+
+ var accessFilter = new InternalItemsQuery(user) { IncludeOwnedItems = true };
+ _libraryManager.ConfigureUserAccess(accessFilter, user);
+ return accessFilter;
+ }
+
///
/// Get person by name.
///
diff --git a/Jellyfin.Data/UserEntityExtensions.cs b/Jellyfin.Data/UserEntityExtensions.cs
index 0fc8d3cd25..c6468daa2e 100644
--- a/Jellyfin.Data/UserEntityExtensions.cs
+++ b/Jellyfin.Data/UserEntityExtensions.cs
@@ -162,6 +162,23 @@ public static class UserEntityExtensions
return Array.IndexOf(GetPreferenceValues(entity, PreferenceKind.GroupedFolders), id) != -1;
}
+ ///
+ /// Checks whether any library, parental rating or tag rule keeps content from this user.
+ ///
+ /// The user to check.
+ /// True if some content in the library is hidden from this user.
+ public static bool HasContentRestrictions(this User entity)
+ {
+ ArgumentNullException.ThrowIfNull(entity);
+
+ return !entity.HasPermission(PermissionKind.EnableAllFolders)
+ || entity.GetPreference(PreferenceKind.BlockedMediaFolders).Length > 0
+ || entity.MaxParentalRatingScore.HasValue
+ || entity.GetPreference(PreferenceKind.BlockedTags).Length > 0
+ || entity.GetPreference(PreferenceKind.AllowedTags).Length > 0
+ || entity.GetPreference(PreferenceKind.BlockUnratedItems).Length > 0;
+ }
+
///
/// Initializes the default permissions for a user. Should only be called on user creation.
///
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
index 47f8a40b9c..8b0b7f37f8 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
@@ -1050,14 +1050,29 @@ public sealed partial class BaseItemRepository
{
var includedItemByNameTypes = GetItemByNameTypesInQuery(filter);
var enableItemsByName = (filter.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
- if (enableItemsByName && includedItemByNameTypes.Count > 0)
+
+ // A by-name item belongs to no library, so it has no TopParentId to test and the filter
+ // below would drop it. Items-by-name queries exempt the whole group; a query that names a
+ // by-name type explicitly gets the same exemption, since it is asking for those items.
+ var exemptedItemByNameTypes = enableItemsByName
+ ? includedItemByNameTypes
+ : _itemByNameKinds.Where(filter.IncludeItemTypes.Contains).Select(e => _itemTypeLookup.BaseItemKindNames[e]!).ToList();
+
+ if (exemptedItemByNameTypes.Count > 0)
{
- baseQuery = baseQuery.Where(e => includedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.TopParentId!.Value));
+ baseQuery = baseQuery.Where(e => exemptedItemByNameTypes.Contains(e.Type) || queryTopParentIds.Any(w => w == e.TopParentId!.Value));
}
else
{
baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
}
+
+ // That exemption is what lets a by-name item from a library the user cannot open show up
+ // in search. Decide those on the items behind the name instead.
+ if (filter.UserHasContentRestrictions && exemptedItemByNameTypes.Count > 0)
+ {
+ baseQuery = ApplyItemByNameAccessFiltering(baseQuery, context, filter, exemptedItemByNameTypes, queryTopParentIds);
+ }
}
if (filter.AncestorIds.Length > 0)
@@ -1272,4 +1287,47 @@ public sealed partial class BaseItemRepository
return baseQuery;
}
+
+ ///
+ /// Keeps a by-name row only when at least one item behind its name is reachable for the user.
+ ///
+ private IQueryable ApplyItemByNameAccessFiltering(
+ IQueryable baseQuery,
+ JellyfinDbContext context,
+ InternalItemsQuery filter,
+ IReadOnlyList itemByNameTypes,
+ Guid[] topParentIds)
+ {
+ // IncludeOwnedItems: a credit on an alternate version of a reachable movie still counts.
+ var accessibleItems = ApplyAccessFiltering(
+ context,
+ context.BaseItems.AsNoTracking(),
+ new InternalItemsQuery(filter.User) { TopParentIds = topParentIds, IncludeOwnedItems = true });
+
+ // Each predicate is written outside-in - name row, then link table, then item - and with nested
+ // Any() rather than a Contains over the accessible ids, which would materialise all of them
+ // before the first row. That keeps every step an index seek.
+ var personType = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Person];
+ if (itemByNameTypes.Contains(personType))
+ {
+ baseQuery = baseQuery.Where(e => e.Type != personType
+ || context.Peoples.Any(p => p.Name == e.Name
+ && context.PeopleBaseItemMap.Any(m => m.PeopleId == p.Id && accessibleItems.Any(i => i.Id == m.ItemId))));
+ }
+
+ foreach (var (kind, valueTypes) in _itemByNameValueTypes)
+ {
+ var typeName = _itemTypeLookup.BaseItemKindNames[kind];
+ if (!itemByNameTypes.Contains(typeName))
+ {
+ continue;
+ }
+
+ baseQuery = baseQuery.Where(e => e.Type != typeName
+ || context.ItemValues.Any(v => valueTypes.Contains(v.Type) && v.CleanValue == e.CleanName
+ && context.ItemValuesMap.Any(m => m.ItemValueId == v.ItemValueId && accessibleItems.Any(i => i.Id == m.ItemId))));
+ }
+
+ return baseQuery;
+ }
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 57041276b7..4c3dba1b47 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -46,6 +46,23 @@ public sealed partial class BaseItemRepository
private static readonly IReadOnlyList _getStudiosValueTypes = [ItemValueType.Studios];
private static readonly IReadOnlyList _getGenreValueTypes = [ItemValueType.Genre];
+ private static readonly BaseItemKind[] _itemByNameKinds =
+ [
+ BaseItemKind.Person,
+ BaseItemKind.Genre,
+ BaseItemKind.MusicGenre,
+ BaseItemKind.MusicArtist,
+ BaseItemKind.Studio
+ ];
+
+ private static readonly (BaseItemKind Kind, IReadOnlyList ValueTypes)[] _itemByNameValueTypes =
+ [
+ (BaseItemKind.Genre, _getGenreValueTypes),
+ (BaseItemKind.MusicGenre, _getGenreValueTypes),
+ (BaseItemKind.MusicArtist, _getAllArtistsValueTypes),
+ (BaseItemKind.Studio, _getStudiosValueTypes)
+ ];
+
///
/// Initializes a new instance of the class.
///
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index 9a5bc0b1c2..6637d99afa 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -21,10 +21,11 @@ namespace Jellyfin.Server.Implementations.Item;
///
/// Efcore Factory.
/// Items lookup service.
+/// Shared item query helpers.
///
/// Initializes a new instance of the class.
///
-public class PeopleRepository(IDbContextFactory dbProvider, IItemTypeLookup itemTypeLookup) : IPeopleRepository
+public class PeopleRepository(IDbContextFactory dbProvider, IItemTypeLookup itemTypeLookup, IItemQueryHelpers queryHelpers) : IPeopleRepository
{
private readonly IDbContextFactory _dbProvider = dbProvider;
@@ -54,10 +55,18 @@ public class PeopleRepository(IDbContextFactory dbProvider, I
.Where(p => !candidates.Any(other => other.Name.ToLower() == p.Name.ToLower() && other.Id < p.Id))
.OrderBy(e => e.Name.ToLower());
- distinctNameCount = candidates.Select(e => e.Name.ToLower()).Distinct().Count();
+ if (filter.EnableTotalRecordCount)
+ {
+ distinctNameCount = candidates.Select(e => e.Name.ToLower()).Distinct().Count();
+ }
+ }
+
+ var count = 0;
+ if (filter.EnableTotalRecordCount)
+ {
+ count = distinctNameCount ?? dbQuery.Count();
}
- var count = distinctNameCount ?? dbQuery.Count();
if (filter.StartIndex.HasValue && filter.StartIndex > 0)
{
dbQuery = dbQuery.Skip(filter.StartIndex.Value);
@@ -250,6 +259,14 @@ public class PeopleRepository(IDbContextFactory dbProvider, I
.AsNoTracking();
}
+ if (filter.AccessFilter is not null)
+ {
+ // Keep only people credited on at least one item the user can see.
+ var accessibleItems = queryHelpers.ApplyAccessFiltering(context, context.BaseItems.AsNoTracking(), filter.AccessFilter);
+ query = query.Where(e => context.PeopleBaseItemMap
+ .Any(m => m.PeopleId == e.Id && accessibleItems.Any(i => i.Id == m.ItemId)));
+ }
+
if (!filter.ItemId.IsEmpty())
{
query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId)));
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 3b1f6a961f..e85f86b72f 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -496,6 +496,12 @@ namespace MediaBrowser.Controller.Entities
public IReadOnlyList SubtitleLanguages { get; set; }
+ ///
+ /// Gets a value indicating whether some content in the library is hidden from .
+ /// Filters that only exist to hide content can be skipped entirely when this is false.
+ ///
+ public bool UserHasContentRestrictions { get; private set; }
+
public void SetUser(User user)
{
var maxRating = user.MaxParentalRatingScore;
@@ -519,6 +525,7 @@ namespace MediaBrowser.Controller.Entities
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
.ToArray();
+ UserHasContentRestrictions = user.HasContentRestrictions();
User = user;
}
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index e12ba22343..8d2a959f4d 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -19,8 +19,16 @@ namespace MediaBrowser.Controller.Entities
{
PersonTypes = personTypes;
ExcludePersonTypes = excludePersonTypes;
+ EnableTotalRecordCount = true;
}
+ ///
+ /// Gets or sets a value indicating whether to count the matching people. Under an
+ /// the count is the expensive half of the query: the page walk stops
+ /// at the limit, the count has to check every person.
+ ///
+ public bool EnableTotalRecordCount { get; set; }
+
public int? StartIndex { get; set; }
///
@@ -51,5 +59,11 @@ namespace MediaBrowser.Controller.Entities
public User User { get; set; }
public bool? IsFavorite { get; set; }
+
+ ///
+ /// Gets or sets the item query whose access settings (library access, parental rating, tags)
+ /// people must satisfy through at least one of the items they are credited on.
+ ///
+ public InternalItemsQuery AccessFilter { get; set; }
}
}
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 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.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..414210f444
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260728182152_AddPeopleItemMapCoveringIndex.Designer.cs
@@ -0,0 +1,1813 @@
+//
+using System;
+using Jellyfin.Database.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Sqlite.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20260728182152_AddPeopleItemMapCoveringIndex")]
+ partial class AddPeopleItemMapCoveringIndex
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.10");
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ParentItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ParentItemId");
+
+ b.HasIndex("ParentItemId");
+
+ b.ToTable("AncestorIds");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property("Filename")
+ .HasColumnType("TEXT");
+
+ b.Property("MimeType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "Index");
+
+ b.ToTable("AttachmentStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Album")
+ .HasColumnType("TEXT");
+
+ b.Property("AlbumArtists")
+ .HasColumnType("TEXT");
+
+ b.Property("Artists")
+ .HasColumnType("TEXT");
+
+ b.Property("Audio")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChannelId")
+ .HasColumnType("TEXT");
+
+ b.Property("CleanName")
+ .HasColumnType("TEXT");
+
+ b.Property("CommunityRating")
+ .HasColumnType("REAL");
+
+ b.Property("CriticRating")
+ .HasColumnType("REAL");
+
+ b.Property("CustomRating")
+ .HasColumnType("TEXT");
+
+ b.Property("Data")
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastMediaAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastRefreshed")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastSaved")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("EndDate")
+ .HasColumnType("TEXT");
+
+ b.Property("EpisodeTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("ExternalId")
+ .HasColumnType("TEXT");
+
+ b.Property("ExternalSeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property("ExternalServiceId")
+ .HasColumnType("TEXT");
+
+ b.Property("ExtraType")
+ .HasColumnType("INTEGER");
+
+ b.Property("ForcedSortName")
+ .HasColumnType("TEXT");
+
+ b.Property("Genres")
+ .HasColumnType("TEXT");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("InheritedParentalRatingSubValue")
+ .HasColumnType("INTEGER");
+
+ b.Property("InheritedParentalRatingValue")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsInMixedFolder")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsMovie")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsRepeat")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSeries")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsVirtualItem")
+ .HasColumnType("INTEGER");
+
+ b.Property("LUFS")
+ .HasColumnType("REAL");
+
+ b.Property("MediaType")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizationGain")
+ .HasColumnType("REAL");
+
+ b.Property("OfficialRating")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasColumnType("TEXT");
+
+ b.Property("OwnerId")
+ .HasColumnType("TEXT");
+
+ b.Property("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property("ParentIndexNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredMetadataCountryCode")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredMetadataLanguage")
+ .HasColumnType("TEXT");
+
+ b.Property("PremiereDate")
+ .HasColumnType("TEXT");
+
+ b.Property("PresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property("PrimaryVersionId")
+ .HasColumnType("TEXT");
+
+ b.Property("ProductionLocations")
+ .HasColumnType("TEXT");
+
+ b.Property("ProductionYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("RunTimeTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeasonId")
+ .HasColumnType("TEXT");
+
+ b.Property("SeasonName")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesName")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesPresentationUniqueKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ShowId")
+ .HasColumnType("TEXT");
+
+ b.Property("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property("StartDate")
+ .HasColumnType("TEXT");
+
+ b.Property("Studios")
+ .HasColumnType("TEXT");
+
+ b.Property("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property("Tags")
+ .HasColumnType("TEXT");
+
+ b.Property("TopParentId")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UnratedType")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Path");
+
+ b.HasIndex("PresentationUniqueKey");
+
+ b.HasIndex("PrimaryVersionId")
+ .HasFilter("\"PrimaryVersionId\" IS NOT NULL");
+
+ b.HasIndex("SeasonId");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("SeriesName");
+
+ b.HasIndex("ExtraType", "OwnerId");
+
+ b.HasIndex("TopParentId", "Id");
+
+ b.HasIndex("Type", "CleanName");
+
+ b.HasIndex("TopParentId", "Type", "IsVirtualItem")
+ .HasFilter("\"PrimaryVersionId\" IS NULL AND (\"OwnerId\" IS NULL OR \"ExtraType\" IS NOT NULL)");
+
+ b.HasIndex("Type", "TopParentId", "Id");
+
+ b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+ b.HasIndex("Type", "TopParentId", "SortName");
+
+ b.HasIndex("Type", "TopParentId", "StartDate");
+
+ b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+ b.HasIndex("TopParentId", "IsFolder", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("TopParentId", "MediaType", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("TopParentId", "Type", "IsVirtualItem", "DateCreated");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "ParentIndexNumber", "IndexNumber");
+
+ b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+ b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+ b.ToTable("BaseItems");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ IsFolder = false,
+ IsInMixedFolder = false,
+ IsLocked = false,
+ IsMovie = false,
+ IsRepeat = false,
+ IsSeries = false,
+ IsVirtualItem = false,
+ Name = "This is a placeholder item for UserData that has been detached from its original item",
+ Type = "PLACEHOLDER"
+ });
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Blurhash")
+ .HasColumnType("BLOB");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("ImageType")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ItemId", "ImageType");
+
+ b.ToTable("BaseItemImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemMetadataFields");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderId")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemId", "ProviderId");
+
+ b.HasIndex("ProviderId", "ItemId", "ProviderValue");
+
+ b.ToTable("BaseItemProviders");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("BaseItemTrailerTypes");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChapterIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property("ImageDateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("ImagePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("StartPositionTicks")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "ChapterIndex");
+
+ b.ToTable("Chapters");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b =>
+ {
+ b.Property("ItemValueId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CleanValue")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId");
+
+ b.HasIndex("Type", "CleanValue");
+
+ b.HasIndex("Type", "Value")
+ .IsUnique();
+
+ b.ToTable("ItemValues");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b =>
+ {
+ b.Property("ItemValueId")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("ItemValueId", "ItemId");
+
+ b.HasIndex("ItemId");
+
+ b.ToTable("ItemValuesMap");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.PrimitiveCollection("KeyframeTicks")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalDuration")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId");
+
+ b.ToTable("KeyframeData");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
+ {
+ b.Property("ParentId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChildId")
+ .HasColumnType("TEXT");
+
+ b.Property("ChildType")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ParentId", "ChildId");
+
+ b.HasIndex("ChildId", "ChildType");
+
+ b.HasIndex("ParentId", "ChildType");
+
+ b.HasIndex("ParentId", "SortOrder");
+
+ b.ToTable("LinkedChildren", (string)null);
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("EndTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("SegmentProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("StartTicks")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSegments");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("StreamIndex")
+ .HasColumnType("INTEGER");
+
+ b.Property("AspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property("AverageFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property("BitRate")
+ .HasColumnType("INTEGER");
+
+ b.Property("BlPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChannelLayout")
+ .HasColumnType("TEXT");
+
+ b.Property("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property("CodecTag")
+ .HasColumnType("TEXT");
+
+ b.Property("CodecTimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property("DvBlSignalCompatibilityId")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvLevel")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvProfile")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvVersionMajor")
+ .HasColumnType("INTEGER");
+
+ b.Property("DvVersionMinor")
+ .HasColumnType("INTEGER");
+
+ b.Property("ElPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("Hdr10PlusPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsAnamorphic")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsAvc")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsExternal")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsForced")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsHearingImpaired")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsInterlaced")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsOriginal")
+ .HasColumnType("INTEGER");
+
+ b.Property("KeyFrames")
+ .HasColumnType("TEXT");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("Level")
+ .HasColumnType("REAL");
+
+ b.Property("NalLengthSize")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.Property("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property("RealFrameRate")
+ .HasColumnType("REAL");
+
+ b.Property("RefFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rotation")
+ .HasColumnType("INTEGER");
+
+ b.Property("RpuPresentFlag")
+ .HasColumnType("INTEGER");
+
+ b.Property("SampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property("StreamType")
+ .HasColumnType("INTEGER");
+
+ b.Property("TimeBase")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "StreamIndex");
+
+ b.ToTable("MediaStreamInfos");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("PersonType")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("Peoples");
+
+ b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
+ });
+
+ modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("PeopleId")
+ .HasColumnType("TEXT");
+
+ b.Property("Role")
+ .HasColumnType("TEXT");
+
+ b.Property("ListOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property