Merge remote-tracking branch 'upstream/master' into epg-fixes

This commit is contained in:
Shadowghost
2026-05-04 21:26:26 +02:00
233 changed files with 31008 additions and 4377 deletions

View File

@@ -0,0 +1,248 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.MatchCriteria;
namespace Jellyfin.Database.Implementations;
/// <summary>
/// Provides methods for querying item hierarchies using iterative traversal.
/// Uses AncestorIds and LinkedChildren tables for parent-child traversal.
/// </summary>
public static class DescendantQueryHelper
{
/// <summary>
/// Gets a queryable of all descendant IDs for a parent item.
/// Traverses AncestorIds and LinkedChildren to find all descendants.
/// </summary>
/// <param name="context">Database context.</param>
/// <param name="parentId">Parent item ID.</param>
/// <returns>Queryable of descendant item IDs.</returns>
public static IQueryable<Guid> GetAllDescendantIds(JellyfinDbContext context, Guid parentId)
{
ArgumentNullException.ThrowIfNull(context);
var descendants = TraverseHierarchyDown(context, [parentId]);
descendants.Remove(parentId);
return descendants.AsQueryable();
}
/// <summary>
/// Gets a queryable of all owned descendant IDs for a parent item.
/// Traverses only AncestorIds (hierarchical ownership), NOT LinkedChildren (associations).
/// Use this for deletion to avoid destroying items that are merely linked (e.g. movies in a BoxSet).
/// </summary>
/// <param name="context">Database context.</param>
/// <param name="parentId">Parent item ID.</param>
/// <returns>Queryable of owned descendant item IDs.</returns>
public static IQueryable<Guid> GetOwnedDescendantIds(JellyfinDbContext context, Guid parentId)
{
ArgumentNullException.ThrowIfNull(context);
var descendants = TraverseHierarchyDownOwned(context, [parentId]);
descendants.Remove(parentId);
return descendants.AsQueryable();
}
/// <summary>
/// Gets all owned descendant IDs for multiple parent items in a single traversal.
/// More efficient than calling <see cref="GetOwnedDescendantIds"/> per parent because
/// it performs one traversal for all seeds instead of N separate traversals.
/// </summary>
/// <param name="context">Database context.</param>
/// <param name="parentIds">Parent item IDs.</param>
/// <returns>Set of all owned descendant item IDs (excluding the parent IDs themselves).</returns>
public static HashSet<Guid> GetOwnedDescendantIdsBatch(JellyfinDbContext context, IReadOnlyList<Guid> parentIds)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(parentIds);
if (parentIds.Count == 0)
{
return [];
}
var seedSet = new HashSet<Guid>(parentIds);
var descendants = TraverseHierarchyDownOwned(context, seedSet);
// Remove the seed IDs — callers want only descendants
descendants.ExceptWith(seedSet);
return descendants;
}
/// <summary>
/// Gets a queryable of all folder IDs that have any descendant matching the specified criteria.
/// Can be used in LINQ .Contains() expressions.
/// </summary>
/// <param name="context">Database context.</param>
/// <param name="criteria">The matching criteria to apply.</param>
/// <returns>Queryable of folder IDs.</returns>
public static IQueryable<Guid> GetFolderIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(criteria);
var matchingItemIds = criteria switch
{
HasSubtitles => context.MediaStreamInfos
.Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle)
.Select(ms => ms.ItemId)
.Distinct()
.ToHashSet(),
HasChapterImages => context.Chapters
.Where(c => c.ImagePath != null)
.Select(c => c.ItemId)
.Distinct()
.ToHashSet(),
HasMediaStreamType m => GetMatchingMediaStreamItemIds(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)
{
var query = context.MediaStreamInfos
.Where(ms => ms.StreamType == criteria.StreamType && ms.Language == criteria.Language);
if (criteria.IsExternal.HasValue)
{
var isExternal = criteria.IsExternal.Value;
query = query.Where(ms => ms.IsExternal == isExternal);
}
return query.Select(ms => ms.ItemId).Distinct().ToHashSet();
}
/// <summary>
/// Traverses DOWN the hierarchy from parent folders to find all descendants.
/// </summary>
private static HashSet<Guid> TraverseHierarchyDown(JellyfinDbContext context, ICollection<Guid> startIds)
{
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 allChildren = directChildren.Concat(linkedChildren).Distinct().ToArray();
if (allChildren.Length == 0)
{
break;
}
var childFolders = context.BaseItems
.WhereOneOrMany(allChildren, e => e.Id)
.Where(e => e.IsFolder)
.Select(e => e.Id)
.ToHashSet();
foreach (var childId in allChildren)
{
if (visited.Add(childId) && childFolders.Contains(childId))
{
folderStack.Add(childId);
}
}
}
return visited;
}
/// <summary>
/// Traverses DOWN the hierarchy using only AncestorIds (ownership), not LinkedChildren.
/// </summary>
private static HashSet<Guid> TraverseHierarchyDownOwned(JellyfinDbContext context, ICollection<Guid> startIds)
{
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();
if (directChildren.Length == 0)
{
break;
}
var childFolders = context.BaseItems
.WhereOneOrMany(directChildren, e => e.Id)
.Where(e => e.IsFolder)
.Select(e => e.Id)
.ToHashSet();
foreach (var childId in directChildren)
{
if (visited.Add(childId) && childFolders.Contains(childId))
{
folderStack.Add(childId);
}
}
}
return visited;
}
/// <summary>
/// Traverses UP the hierarchy from items to find all ancestor folders.
/// </summary>
private static HashSet<Guid> TraverseHierarchyUp(JellyfinDbContext context, ICollection<Guid> startIds)
{
var ancestors = new HashSet<Guid>();
var itemStack = new HashSet<Guid>(startIds);
while (itemStack.Count != 0)
{
var currentItems = itemStack.ToArray();
itemStack.Clear();
var ancestorParents = context.AncestorIds
.WhereOneOrMany(currentItems, e => e.ItemId)
.Select(e => e.ParentItemId)
.ToArray();
var linkedParents = context.LinkedChildren
.WhereOneOrMany(currentItems, e => e.ChildId)
.Select(e => e.ParentId)
.ToArray();
foreach (var parentId in ancestorParents.Concat(linkedParents))
{
if (ancestors.Add(parentId))
{
itemStack.Add(parentId);
}
}
}
return ancestors;
}
}

View File

@@ -96,7 +96,7 @@ public class BaseItemEntity
public string? OriginalTitle { get; set; }
public string? PrimaryVersionId { get; set; }
public Guid? PrimaryVersionId { get; set; }
public DateTime? DateLastMediaAdded { get; set; }
@@ -118,8 +118,6 @@ public class BaseItemEntity
public string? ProductionLocations { get; set; }
public string? ExtraIds { get; set; }
public int? TotalBitrate { get; set; }
public BaseItemExtraType? ExtraType { get; set; }
@@ -134,7 +132,17 @@ public class BaseItemEntity
public string? ShowId { get; set; }
public string? OwnerId { get; set; }
public Guid? OwnerId { get; set; }
/// <summary>
/// Gets or sets the owner item (for extras like trailers, theme songs, etc.).
/// </summary>
public BaseItemEntity? Owner { get; set; }
/// <summary>
/// Gets or sets the extras owned by this item (trailers, theme songs, behind the scenes, etc.).
/// </summary>
public ICollection<BaseItemEntity>? Extras { get; set; }
public int? Width { get; set; }
@@ -178,6 +186,16 @@ public class BaseItemEntity
public ICollection<BaseItemImageInfo>? Images { get; set; }
/// <summary>
/// Gets or sets the linked children (for BoxSets, Playlists, etc.).
/// </summary>
public ICollection<LinkedChildEntity>? LinkedChildEntities { get; set; }
/// <summary>
/// Gets or sets the items this entity is linked to as a child.
/// </summary>
public ICollection<LinkedChildEntity>? LinkedChildOfEntities { get; set; }
// those are references to __LOCAL__ ids not DB ids ... TODO: Bring the whole folder structure into the DB
// public ICollection<BaseItemEntity>? SeriesEpisodes { get; set; }
// public BaseItemEntity? Series { get; set; }

View File

@@ -0,0 +1,39 @@
using System;
namespace Jellyfin.Database.Implementations.Entities;
/// <summary>
/// Represents a linked child relationship between items (e.g., BoxSet to Movies, Playlist to tracks).
/// </summary>
public class LinkedChildEntity
{
/// <summary>
/// Gets or sets the parent item ID (BoxSet, Playlist, etc.).
/// </summary>
public required Guid ParentId { get; set; }
/// <summary>
/// Gets or sets the child item ID.
/// </summary>
public required Guid ChildId { get; set; }
/// <summary>
/// Gets or sets the type of linked child (Manual or Shortcut).
/// </summary>
public required LinkedChildType ChildType { get; set; }
/// <summary>
/// Gets or sets the sort order.
/// </summary>
public int? SortOrder { get; set; }
/// <summary>
/// Gets or sets the parent item navigation property.
/// </summary>
public BaseItemEntity? Parent { get; set; }
/// <summary>
/// Gets or sets the child item navigation property.
/// </summary>
public BaseItemEntity? Child { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace Jellyfin.Database.Implementations.Entities;
/// <summary>
/// The linked child type.
/// </summary>
public enum LinkedChildType
{
/// <summary>
/// Manually linked child.
/// </summary>
Manual = 0,
/// <summary>
/// Shortcut linked child.
/// </summary>
Shortcut = 1,
/// <summary>
/// Local alternate version (same item, different file path).
/// </summary>
LocalAlternateVersion = 2,
/// <summary>
/// Linked alternate version (different item ID).
/// </summary>
LinkedAlternateVersion = 3
}

View File

@@ -143,6 +143,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
/// </summary>
public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => Set<PeopleBaseItemMap>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing linked children relationships.
/// </summary>
public DbSet<LinkedChildEntity> LinkedChildren => Set<LinkedChildEntity>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
/// </summary>

View File

@@ -0,0 +1,6 @@
namespace Jellyfin.Database.Implementations.MatchCriteria;
/// <summary>
/// Base type for folder matching criteria using discriminated union pattern.
/// </summary>
public abstract record FolderMatchCriteria;

View File

@@ -0,0 +1,6 @@
namespace Jellyfin.Database.Implementations.MatchCriteria;
/// <summary>
/// Matches folders containing descendants with chapter images.
/// </summary>
public sealed record HasChapterImages : FolderMatchCriteria;

View File

@@ -0,0 +1,14 @@
using Jellyfin.Database.Implementations.Entities;
namespace Jellyfin.Database.Implementations.MatchCriteria;
/// <summary>
/// Matches folders containing descendants with a specific media stream type and language.
/// </summary>
/// <param name="StreamType">The type of media stream to match (Audio, Subtitle, etc.).</param>
/// <param name="Language">The language to match.</param>
/// <param name="IsExternal">If not null, filters by internal (false) or external (true) streams. Only applicable to subtitles.</param>
public sealed record HasMediaStreamType(
MediaStreamTypeEntity StreamType,
string Language,
bool? IsExternal = null) : FolderMatchCriteria;

View File

@@ -0,0 +1,6 @@
namespace Jellyfin.Database.Implementations.MatchCriteria;
/// <summary>
/// Matches folders containing descendants with subtitles.
/// </summary>
public sealed record HasSubtitles : FolderMatchCriteria;

View File

@@ -28,15 +28,17 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
builder.HasMany(e => e.Parents);
builder.HasMany(e => e.Children);
builder.HasMany(e => e.DirectChildren).WithOne(e => e.DirectParent).HasForeignKey(e => e.ParentId).OnDelete(DeleteBehavior.Cascade);
builder.HasMany(e => e.Extras).WithOne(e => e.Owner).HasForeignKey(e => e.OwnerId).OnDelete(DeleteBehavior.NoAction);
builder.HasMany(e => e.LockedFields);
builder.HasMany(e => e.TrailerTypes);
builder.HasMany(e => e.Images);
builder.HasIndex(e => e.Path);
builder.HasIndex(e => e.ParentId);
builder.HasIndex(e => e.OwnerId);
builder.HasIndex(e => e.Name);
builder.HasIndex(e => new { e.ExtraType, e.OwnerId });
builder.HasIndex(e => e.PresentationUniqueKey);
builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem });
// covering index
builder.HasIndex(e => new { e.TopParentId, e.Id });
// series
@@ -53,14 +55,33 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
// latest items
builder.HasIndex(e => new { e.Type, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
// latest items - optimized for sorting by DateCreated (no PresentationUniqueKey breaking the sort)
builder.HasIndex(e => new { e.TopParentId, e.Type, e.IsVirtualItem, e.DateCreated });
builder.HasIndex(e => new { e.TopParentId, e.IsFolder, e.IsVirtualItem, e.DateCreated });
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 });
// 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)
builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.ParentIndexNumber, e.IndexNumber });
// ByName queries: WHERE Type = X AND CleanName IN (...)
builder.HasIndex(e => new { e.Type, e.CleanName });
// Latest TV: GROUP BY SeriesName
builder.HasIndex(e => e.SeriesName);
// Latest TV: episode count per season, season count per series
builder.HasIndex(e => e.SeasonId);
builder.HasIndex(e => e.SeriesId);
// Items/Counts: SELECT Type, COUNT(*) GROUP BY Type filtered by TopParentId.
builder.HasIndex(e => new { e.TopParentId, e.Type, e.IsVirtualItem })
.HasFilter("\"PrimaryVersionId\" IS NULL AND (\"OwnerId\" IS NULL OR \"ExtraType\" IS NOT NULL)");
builder.HasData(new BaseItemEntity()
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
Type = "PLACEHOLDER",
Name = "This is a placeholder item for UserData that has been detacted from its original item",
Name = "This is a placeholder item for UserData that has been detached from its original item",
});
}
}

View File

@@ -0,0 +1,21 @@
using Jellyfin.Database.Implementations.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Jellyfin.Database.Implementations.ModelConfiguration;
/// <summary>
/// FluentAPI configuration for the BaseItemImageInfo entity.
/// </summary>
public class BaseItemImageInfoConfiguration : IEntityTypeConfiguration<BaseItemImageInfo>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<BaseItemImageInfo> builder)
{
builder.HasKey(e => e.Id);
builder.HasOne(e => e.Item).WithMany(e => e.Images).HasForeignKey(e => e.ItemId);
// Composite index for filtering by item and image type (also covers ItemId-only lookups)
builder.HasIndex(e => new { e.ItemId, e.ImageType });
}
}

View File

@@ -14,6 +14,6 @@ public class BaseItemProviderConfiguration : IEntityTypeConfiguration<BaseItemPr
{
builder.HasKey(e => new { e.ItemId, e.ProviderId });
builder.HasOne(e => e.Item);
builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId });
builder.HasIndex(e => new { e.ProviderId, e.ItemId, e.ProviderValue });
}
}

View File

@@ -20,9 +20,6 @@ namespace Jellyfin.Database.Implementations.ModelConfiguration
builder
.HasIndex(entity => new { entity.UserId, entity.DeviceId });
builder
.HasIndex(entity => entity.DeviceId);
}
}
}

View File

@@ -0,0 +1,31 @@
using Jellyfin.Database.Implementations.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Jellyfin.Database.Implementations.ModelConfiguration;
/// <summary>
/// LinkedChildEntity configuration.
/// </summary>
public class LinkedChildConfiguration : IEntityTypeConfiguration<LinkedChildEntity>
{
/// <inheritdoc/>
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.HasIndex(e => new { e.ParentId, e.ChildType });
builder.HasIndex(e => new { e.ChildId, e.ChildType });
builder.HasOne(e => e.Parent)
.WithMany(e => e.LinkedChildEntities)
.HasForeignKey(e => e.ParentId)
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(e => e.Child)
.WithMany(e => e.LinkedChildOfEntities)
.HasForeignKey(e => e.ChildId)
.OnDelete(DeleteBehavior.NoAction);
}
}

View File

@@ -13,9 +13,5 @@ public class MediaStreamInfoConfiguration : IEntityTypeConfiguration<MediaStream
public void Configure(EntityTypeBuilder<MediaStreamInfo> builder)
{
builder.HasKey(e => new { e.ItemId, e.StreamIndex });
builder.HasIndex(e => e.StreamIndex);
builder.HasIndex(e => e.StreamType);
builder.HasIndex(e => new { e.StreamIndex, e.StreamType });
builder.HasIndex(e => new { e.StreamIndex, e.StreamType, e.Language });
}
}

View File

@@ -15,6 +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.HasOne(e => e.Item);
builder.HasOne(e => e.People);
}

View File

@@ -17,6 +17,9 @@ public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks });
builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite });
builder.HasIndex(d => new { d.ItemId, d.UserId, d.LastPlayedDate });
builder.HasIndex(d => new { d.UserId, d.ItemId, d.LastPlayedDate });
builder.HasIndex(d => new { d.UserId, d.Played, d.ItemId });
builder.HasIndex(d => new { d.UserId, d.IsFavorite, d.ItemId });
builder.HasOne(e => e.Item).WithMany(e => e.UserData);
}
}

View File

@@ -0,0 +1,126 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddLinkedChildrenTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "LinkedChildren",
columns: table => new
{
ParentId = table.Column<Guid>(type: "TEXT", nullable: false),
ChildId = table.Column<Guid>(type: "TEXT", nullable: false),
ChildType = table.Column<int>(type: "INTEGER", nullable: false),
SortOrder = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_LinkedChildren", x => new { x.ParentId, x.ChildId });
table.ForeignKey(
name: "FK_LinkedChildren_BaseItems_ChildId",
column: x => x.ChildId,
principalTable: "BaseItems",
principalColumn: "Id");
table.ForeignKey(
name: "FK_LinkedChildren_BaseItems_ParentId",
column: x => x.ParentId,
principalTable: "BaseItems",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_LinkedChildren_ChildId",
table: "LinkedChildren",
column: "ChildId");
migrationBuilder.CreateIndex(
name: "IX_LinkedChildren_ChildId_ChildType",
table: "LinkedChildren",
columns: new[] { "ChildId", "ChildType" });
migrationBuilder.CreateIndex(
name: "IX_LinkedChildren_ParentId",
table: "LinkedChildren",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_LinkedChildren_ParentId_ChildType",
table: "LinkedChildren",
columns: new[] { "ParentId", "ChildType" });
migrationBuilder.CreateIndex(
name: "IX_LinkedChildren_ParentId_SortOrder",
table: "LinkedChildren",
columns: new[] { "ParentId", "SortOrder" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Re-populate LinkedChildren data back into the JSON Data column before dropping the table
migrationBuilder.Sql(
@"UPDATE BaseItems
SET Data = CASE
WHEN Data IS NULL OR Data = '' THEN
json_object('LinkedChildren', (
SELECT json_group_array(
json_object(
'Path', Child.Path,
'Type', CASE LC.ChildType
WHEN 0 THEN 'Manual'
WHEN 1 THEN 'Shortcut'
ELSE 'Manual'
END,
'ItemId', LOWER(REPLACE(LC.ChildId, '-', ''))
)
)
FROM LinkedChildren LC
INNER JOIN BaseItems Child ON LC.ChildId = Child.Id
WHERE LC.ParentId = BaseItems.Id
ORDER BY LC.SortOrder
))
ELSE
json_set(
Data,
'$.LinkedChildren',
(
SELECT json_group_array(
json_object(
'Path', Child.Path,
'Type', CASE LC.ChildType
WHEN 0 THEN 'Manual'
WHEN 1 THEN 'Shortcut'
ELSE 'Manual'
END,
'ItemId', LOWER(REPLACE(LC.ChildId, '-', ''))
)
)
FROM LinkedChildren LC
INNER JOIN BaseItems Child ON LC.ChildId = Child.Id
WHERE LC.ParentId = BaseItems.Id
ORDER BY LC.SortOrder
)
)
END
WHERE EXISTS (
SELECT 1
FROM LinkedChildren LC
WHERE LC.ParentId = BaseItems.Id
)");
migrationBuilder.DropTable(
name: "LinkedChildren");
migrationBuilder.Sql(
@"DELETE FROM __EFMigrationsHistory
WHERE MigrationId = '20260113120000_MigrateLinkedChildren'");
}
}
}

View File

@@ -0,0 +1,156 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class ChangeOwnerIdToGuid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Normalize OwnerId to uppercase GUID format
migrationBuilder.Sql(
@"UPDATE BaseItems
SET OwnerId = UPPER(OwnerId)
WHERE OwnerId IS NOT NULL");
// Clear invalid OwnerId values (not 36 characters = not a valid GUID)
migrationBuilder.Sql(
@"UPDATE BaseItems
SET OwnerId = null
WHERE OwnerId IS NOT NULL AND length(OwnerId) != 36");
// Clear placeholder/empty GUIDs
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "OwnerId",
keyValue: new Guid("00000000-0000-0000-0000-000000000000"),
column: "OwnerId",
value: null);
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "OwnerId",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "OwnerId",
value: null);
migrationBuilder.AddColumn<Guid>(
name: "BaseItemEntityId",
table: "BaseItems",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
columns: new[] { "BaseItemEntityId", "Name", "OwnerId" },
values: new object[] { null, "This is a placeholder item for UserData that has been detached from its original item", null });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_ExtraType",
table: "BaseItems",
column: "ExtraType");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_ExtraType_OwnerId",
table: "BaseItems",
columns: new[] { "ExtraType", "OwnerId" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_OwnerId",
table: "BaseItems",
column: "OwnerId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_IsFolder_IsVirtualItem_DateCreated",
table: "BaseItems",
columns: new[] { "TopParentId", "IsFolder", "IsVirtualItem", "DateCreated" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_MediaType_IsVirtualItem_DateCreated",
table: "BaseItems",
columns: new[] { "TopParentId", "MediaType", "IsVirtualItem", "DateCreated" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_Type_IsVirtualItem_DateCreated",
table: "BaseItems",
columns: new[] { "TopParentId", "Type", "IsVirtualItem", "DateCreated" });
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId",
principalTable: "BaseItems",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "OwnerId",
value: null);
migrationBuilder.Sql(
@"UPDATE BaseItems
SET OwnerId = LOWER(OwnerId)
WHERE OwnerId IS NOT NULL");
migrationBuilder.DropForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_ExtraType",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_ExtraType_OwnerId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_OwnerId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_TopParentId_IsFolder_IsVirtualItem_DateCreated",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_TopParentId_MediaType_IsVirtualItem_DateCreated",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_TopParentId_Type_IsVirtualItem_DateCreated",
table: "BaseItems");
migrationBuilder.DropColumn(
name: "BaseItemEntityId",
table: "BaseItems");
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
columns: new[] { "Name", "OwnerId" },
values: new object[] { "This is a placeholder item for UserData that has been detacted from its original item", null });
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddForeignKeyToOwnerId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems");
migrationBuilder.DropColumn(
name: "BaseItemEntityId",
table: "BaseItems");
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_OwnerId",
table: "BaseItems",
column: "OwnerId",
principalTable: "BaseItems",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_BaseItems_BaseItems_OwnerId",
table: "BaseItems");
migrationBuilder.AddColumn<Guid>(
name: "BaseItemEntityId",
table: "BaseItems",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "BaseItemEntityId",
value: null);
migrationBuilder.CreateIndex(
name: "IX_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId");
migrationBuilder.AddForeignKey(
name: "FK_BaseItems_BaseItems_BaseItemEntityId",
table: "BaseItems",
column: "BaseItemEntityId",
principalTable: "BaseItems",
principalColumn: "Id");
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class DropExtraIdsColumn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ExtraIds",
table: "BaseItems");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ExtraIds",
table: "BaseItems",
type: "TEXT",
nullable: true);
migrationBuilder.UpdateData(
table: "BaseItems",
keyColumn: "Id",
keyValue: new Guid("00000000-0000-0000-0000-000000000001"),
column: "ExtraIds",
value: null);
}
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddLatestItemsDateCreatedIndexes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_UserData_UserId",
table: "UserData");
migrationBuilder.CreateIndex(
name: "IX_UserData_UserId_ItemId_LastPlayedDate",
table: "UserData",
columns: new[] { "UserId", "ItemId", "LastPlayedDate" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_UserData_UserId_ItemId_LastPlayedDate",
table: "UserData");
migrationBuilder.CreateIndex(
name: "IX_UserData_UserId",
table: "UserData",
column: "UserId");
}
}
}

View File

@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddIndicesToImageInfo : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_BaseItemImageInfos_ItemId_ImageType",
table: "BaseItemImageInfos",
columns: new[] { "ItemId", "ImageType" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_BaseItemImageInfos_ItemId_ImageType",
table: "BaseItemImageInfos");
}
}
}

View File

@@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddBaseItemNameIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_UserData_UserId_IsFavorite_ItemId",
table: "UserData",
columns: new[] { "UserId", "IsFavorite", "ItemId" });
migrationBuilder.CreateIndex(
name: "IX_UserData_UserId_Played_ItemId",
table: "UserData",
columns: new[] { "UserId", "Played", "ItemId" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Name",
table: "BaseItems",
column: "Name");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_UserData_UserId_IsFavorite_ItemId",
table: "UserData");
migrationBuilder.DropIndex(
name: "IX_UserData_UserId_Played_ItemId",
table: "UserData");
migrationBuilder.DropIndex(
name: "IX_BaseItems_Name",
table: "BaseItems");
}
}
}

View File

@@ -0,0 +1,171 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class IndexOptimizations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_MediaStreamInfos_StreamIndex",
table: "MediaStreamInfos");
migrationBuilder.DropIndex(
name: "IX_MediaStreamInfos_StreamIndex_StreamType",
table: "MediaStreamInfos");
migrationBuilder.DropIndex(
name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language",
table: "MediaStreamInfos");
migrationBuilder.DropIndex(
name: "IX_MediaStreamInfos_StreamType",
table: "MediaStreamInfos");
migrationBuilder.DropIndex(
name: "IX_LinkedChildren_ChildId",
table: "LinkedChildren");
migrationBuilder.DropIndex(
name: "IX_LinkedChildren_ParentId",
table: "LinkedChildren");
migrationBuilder.DropIndex(
name: "IX_Devices_DeviceId",
table: "Devices");
migrationBuilder.DropIndex(
name: "IX_BaseItems_ExtraType",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId",
table: "BaseItemProviders");
migrationBuilder.DropIndex(
name: "IX_BaseItemImageInfos_ItemId",
table: "BaseItemImageInfos");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_SeasonId",
table: "BaseItems",
column: "SeasonId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_SeriesId",
table: "BaseItems",
column: "SeriesId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_SeriesName",
table: "BaseItems",
column: "SeriesName");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_ParentIndexNumber_IndexNumber",
table: "BaseItems",
columns: new[] { "Type", "SeriesPresentationUniqueKey", "ParentIndexNumber", "IndexNumber" });
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_TopParentId_SortName",
table: "BaseItems",
columns: new[] { "Type", "TopParentId", "SortName" });
migrationBuilder.CreateIndex(
name: "IX_BaseItemProviders_ProviderId_ItemId_ProviderValue",
table: "BaseItemProviders",
columns: new[] { "ProviderId", "ItemId", "ProviderValue" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_BaseItems_SeasonId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_SeriesId",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_SeriesName",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_ParentIndexNumber_IndexNumber",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItems_Type_TopParentId_SortName",
table: "BaseItems");
migrationBuilder.DropIndex(
name: "IX_BaseItemProviders_ProviderId_ItemId_ProviderValue",
table: "BaseItemProviders");
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamIndex",
table: "MediaStreamInfos",
column: "StreamIndex");
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamIndex_StreamType",
table: "MediaStreamInfos",
columns: new[] { "StreamIndex", "StreamType" });
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language",
table: "MediaStreamInfos",
columns: new[] { "StreamIndex", "StreamType", "Language" });
migrationBuilder.CreateIndex(
name: "IX_MediaStreamInfos_StreamType",
table: "MediaStreamInfos",
column: "StreamType");
migrationBuilder.CreateIndex(
name: "IX_LinkedChildren_ChildId",
table: "LinkedChildren",
column: "ChildId");
migrationBuilder.CreateIndex(
name: "IX_LinkedChildren_ParentId",
table: "LinkedChildren",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_Devices_DeviceId",
table: "Devices",
column: "DeviceId");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_ExtraType",
table: "BaseItems",
column: "ExtraType");
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem",
table: "BaseItems",
columns: new[] { "Id", "Type", "IsFolder", "IsVirtualItem" });
migrationBuilder.CreateIndex(
name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId",
table: "BaseItemProviders",
columns: new[] { "ProviderId", "ProviderValue", "ItemId" });
migrationBuilder.CreateIndex(
name: "IX_BaseItemImageInfos_ItemId",
table: "BaseItemImageInfos",
column: "ItemId");
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class ChangePrimaryVersionIdToGuid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Convert "N" format (32 chars, no hyphens) to standard GUID format (36 chars with hyphens)
migrationBuilder.Sql(
@"UPDATE BaseItems
SET PrimaryVersionId = UPPER(
SUBSTR(PrimaryVersionId,1,8)||'-'||
SUBSTR(PrimaryVersionId,9,4)||'-'||
SUBSTR(PrimaryVersionId,13,4)||'-'||
SUBSTR(PrimaryVersionId,17,4)||'-'||
SUBSTR(PrimaryVersionId,21,12))
WHERE PrimaryVersionId IS NOT NULL AND LENGTH(PrimaryVersionId) = 32");
// Normalize existing standard-format values to uppercase
migrationBuilder.Sql(
@"UPDATE BaseItems
SET PrimaryVersionId = UPPER(PrimaryVersionId)
WHERE PrimaryVersionId IS NOT NULL");
// Clear invalid values (not 36 characters = not a valid GUID)
migrationBuilder.Sql(
@"UPDATE BaseItems
SET PrimaryVersionId = NULL
WHERE PrimaryVersionId IS NOT NULL AND LENGTH(PrimaryVersionId) != 36");
// Clear placeholder/empty GUIDs
migrationBuilder.Sql(
@"UPDATE BaseItems
SET PrimaryVersionId = NULL
WHERE PrimaryVersionId = '00000000-0000-0000-0000-000000000000'");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Convert standard GUID format back to "N" format (remove hyphens, lowercase)
migrationBuilder.Sql(
@"UPDATE BaseItems
SET PrimaryVersionId = LOWER(REPLACE(PrimaryVersionId, '-', ''))
WHERE PrimaryVersionId IS NOT NULL");
}
}
}

View File

@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddTypeCleanNameIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_BaseItems_Type_CleanName",
table: "BaseItems",
columns: new[] { "Type", "CleanName" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_BaseItems_Type_CleanName",
table: "BaseItems");
}
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
/// <inheritdoc />
public partial class AddPartialIndexForItemCounts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_BaseItems_TopParentId_Type_IsVirtualItem",
table: "BaseItems",
columns: new[] { "TopParentId", "Type", "IsVirtualItem" },
filter: "\"PrimaryVersionId\" IS NULL AND (\"OwnerId\" IS NULL OR \"ExtraType\" IS NOT NULL)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_BaseItems_TopParentId_Type_IsVirtualItem",
table: "BaseItems");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.9");
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
@@ -207,9 +207,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("ExternalServiceId")
.HasColumnType("TEXT");
b.Property<string>("ExtraIds")
.HasColumnType("TEXT");
b.Property<int?>("ExtraType")
.HasColumnType("INTEGER");
@@ -273,7 +270,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Overview")
.HasColumnType("TEXT");
b.Property<string>("OwnerId")
b.Property<Guid?>("OwnerId")
.HasColumnType("TEXT");
b.Property<Guid?>("ParentId")
@@ -297,7 +294,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("PresentationUniqueKey")
.HasColumnType("TEXT");
b.Property<string>("PrimaryVersionId")
b.Property<Guid?>("PrimaryVersionId")
.HasColumnType("TEXT");
b.Property<string>("ProductionLocations")
@@ -363,26 +360,51 @@ namespace Jellyfin.Server.Implementations.Migrations
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", "StartDate");
b.HasIndex("Type", "TopParentId", "SortName");
b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
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");
@@ -404,7 +426,7 @@ namespace Jellyfin.Server.Implementations.Migrations
IsRepeat = false,
IsSeries = false,
IsVirtualItem = false,
Name = "This is a placeholder item for UserData that has been detacted from its original item",
Name = "This is a placeholder item for UserData that has been detached from its original item",
Type = "PLACEHOLDER"
});
});
@@ -439,7 +461,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("ItemId");
b.HasIndex("ItemId", "ImageType");
b.ToTable("BaseItemImageInfos");
@@ -477,7 +499,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "ProviderId");
b.HasIndex("ProviderId", "ProviderValue", "ItemId");
b.HasIndex("ProviderId", "ItemId", "ProviderValue");
b.ToTable("BaseItemProviders");
@@ -782,6 +804,33 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b =>
{
b.Property<Guid>("ParentId")
.HasColumnType("TEXT");
b.Property<Guid>("ChildId")
.HasColumnType("TEXT");
b.Property<int>("ChildType")
.HasColumnType("INTEGER");
b.Property<int?>("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<Guid>("Id")
@@ -956,14 +1005,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "StreamIndex");
b.HasIndex("StreamIndex");
b.HasIndex("StreamType");
b.HasIndex("StreamIndex", "StreamType");
b.HasIndex("StreamIndex", "StreamType", "Language");
b.ToTable("MediaStreamInfos");
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
@@ -1167,8 +1208,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
@@ -1396,8 +1435,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasKey("ItemId", "UserId", "CustomDataKey");
b.HasIndex("UserId");
b.HasIndex("ItemId", "UserId", "IsFavorite");
b.HasIndex("ItemId", "UserId", "LastPlayedDate");
@@ -1406,6 +1443,12 @@ namespace Jellyfin.Server.Implementations.Migrations
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);
@@ -1452,12 +1495,19 @@ namespace Jellyfin.Server.Implementations.Migrations
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 =>
@@ -1580,6 +1630,25 @@ namespace Jellyfin.Server.Implementations.Migrations
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")
@@ -1664,10 +1733,16 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("DirectChildren");
b.Navigation("Extras");
b.Navigation("Images");
b.Navigation("ItemValues");
b.Navigation("LinkedChildEntities");
b.Navigation("LinkedChildOfEntities");
b.Navigation("LockedFields");
b.Navigation("MediaStreams");

View File

@@ -60,11 +60,13 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
var sqliteConnectionBuilder = new SqliteConnectionStringBuilder();
sqliteConnectionBuilder.DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyfin.db"));
sqliteConnectionBuilder.Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Default);
sqliteConnectionBuilder.Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true);
sqliteConnectionBuilder.DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 30);
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),
Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true),
DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 60)
};
var connectionString = sqliteConnectionBuilder.ToString();
@@ -77,7 +79,8 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly))
// TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released
.ConfigureWarnings(warnings =>
warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning))
warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning)
.Ignore(RelationalEventId.MultipleCollectionIncludeWarning))
.AddInterceptors(new PragmaConnectionInterceptor(
_logger,
GetOption<int?>(customOptions, "cacheSize", e => int.Parse(e, CultureInfo.InvariantCulture)),
@@ -155,7 +158,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
/// <inheritdoc />
public Task RestoreBackupFast(string key, CancellationToken cancellationToken)
{
// ensure there are absolutly no dangling Sqlite connections.
// ensure there are absolutely no dangling Sqlite connections.
SqliteConnection.ClearAllPools();
var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db");
var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db");

View File

@@ -17,7 +17,7 @@ with SQLite currently being the only supported provider, you need to run the Ent
The example is made from the root folder of the project e.g for codespaces `/workspaces/jellyfin`
```cmd
dotnet ef migrations add {MIGRATION_NAME} --project "src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite" -- --migration-provider Jellyfin-SQLite
dotnet ef migrations add {MIGRATION_NAME} --project "src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite" --output-dir Migrations -- --migration-provider Jellyfin-SQLite
```
If you get the error: `Run "dotnet tool restore" to make the "dotnet-ef" command available.` Run `dotnet restore`.

View File

@@ -148,5 +148,30 @@ namespace Jellyfin.Extensions
{
return string.IsNullOrEmpty(text) ? text : text.AsSpan().LeftPart('\0').ToString();
}
/// <summary>
/// Normalizes a string for comparison by removing diacritics, converting to lowercase,
/// replacing punctuation/special characters with spaces, and collapsing whitespace.
/// </summary>
/// <param name="value">The string to normalize.</param>
/// <returns>The normalized string, or the original if null/whitespace.</returns>
public static string GetCleanValue(this string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}
// Remove diacritics and convert to lowercase
var cleaned = value.RemoveDiacritics().ToLowerInvariant();
// Replace all punctuation and special characters with spaces
cleaned = Regex.Replace(cleaned, @"[^\p{L}\p{N}\s]", " ");
// Collapse multiple spaces into single space and trim
cleaned = Regex.Replace(cleaned, @"\s+", " ").Trim();
return cleaned;
}
}
}

View File

@@ -156,7 +156,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
{
if (keyframeData.KeyframeTicks.Count > 0 && keyframeData.TotalDuration < keyframeData.KeyframeTicks[^1])
{
throw new ArgumentException("Invalid duration in keyframe data", nameof(keyframeData));
keyframeData = new KeyframeData(keyframeData.KeyframeTicks[^1], keyframeData.KeyframeTicks);
}
long lastKeyframe = 0;
@@ -176,7 +176,12 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
}
}
result.Add(TimeSpan.FromTicks(keyframeData.TotalDuration - lastKeyframe).TotalSeconds);
var remaining = keyframeData.TotalDuration - lastKeyframe;
if (remaining > 0)
{
result.Add(TimeSpan.FromTicks(remaining).TotalSeconds);
}
return result;
}