mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-03 23:36:38 +01:00
Add LinkedChildren database table for normalized relationships
Introduces a new database table to store linked child relationships for boxsets, playlists, and video alternate versions. This replaces the JSON-serialized Data column approach with a proper relational structure. - Add LinkedChildEntity and LinkedChildType enum - Add entity configuration with proper foreign keys - Add EF Core migration for SQLite
This commit is contained in:
@@ -178,6 +178,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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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 => e.ParentId);
|
||||
builder.HasIndex(e => 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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.2");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
|
||||
{
|
||||
@@ -782,6 +782,37 @@ 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");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
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")
|
||||
@@ -1580,6 +1611,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")
|
||||
@@ -1668,6 +1718,10 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
|
||||
b.Navigation("ItemValues");
|
||||
|
||||
b.Navigation("LinkedChildEntities");
|
||||
|
||||
b.Navigation("LinkedChildOfEntities");
|
||||
|
||||
b.Navigation("LockedFields");
|
||||
|
||||
b.Navigation("MediaStreams");
|
||||
|
||||
Reference in New Issue
Block a user