Migrate PrimaryVersionId to GUID and fix assignment

This commit is contained in:
Shadowghost
2026-02-15 23:12:52 +01:00
parent 837c7d4ed3
commit 3439d3c017
10 changed files with 1880 additions and 34 deletions

View File

@@ -408,7 +408,7 @@ namespace Emby.Server.Implementations.Library
// If deleting a primary version video, clear PrimaryVersionId from alternate versions
// OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
if (item is Video video && string.IsNullOrEmpty(video.PrimaryVersionId) && video.OwnerId.IsEmpty())
if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
{
var alternateVersions = GetLocalAlternateVersionIds(video)
.Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
@@ -435,16 +435,15 @@ namespace Emby.Server.Implementations.Library
// Update remaining alternates to point to new primary
foreach (var alternate in alternateVersions.Skip(1))
{
alternate.SetPrimaryVersionId(newPrimary.Id.ToString("N", CultureInfo.InvariantCulture));
alternate.SetPrimaryVersionId(newPrimary.Id);
alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
}
}
else if (item is Video alternateVideo && !string.IsNullOrEmpty(alternateVideo.PrimaryVersionId)
&& Guid.TryParse(alternateVideo.PrimaryVersionId, out var primaryId))
else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
{
// If deleting an alternate version, re-route references to its primary
_itemRepository.RerouteLinkedChildren(alternateVideo.Id, primaryId);
_itemRepository.RerouteLinkedChildren(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value);
}
var children = item.IsFolder
@@ -2181,6 +2180,7 @@ namespace Emby.Server.Implementations.Library
if (altVideo is not null)
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
allItems.Add(altVideo);
}
}
@@ -2383,6 +2383,7 @@ namespace Emby.Server.Implementations.Library
if (altVideo is not null)
{
altVideo.OwnerId = video.Id;
altVideo.SetPrimaryVersionId(video.Id);
allItems.Add(altVideo);
}
}

View File

@@ -147,9 +147,9 @@ public class VideosController : BaseJellyfinApiController
return NotFound();
}
if (item.LinkedAlternateVersions.Length == 0)
if (item.LinkedAlternateVersions.Length == 0 && item.PrimaryVersionId.HasValue)
{
item = _libraryManager.GetItemById<Video>(Guid.Parse(item.PrimaryVersionId));
item = _libraryManager.GetItemById<Video>(item.PrimaryVersionId.Value);
}
if (item is null)
@@ -197,7 +197,7 @@ public class VideosController : BaseJellyfinApiController
return BadRequest("Please supply at least two videos to merge.");
}
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && !i.PrimaryVersionId.HasValue);
if (primaryVersion is null)
{
primaryVersion = items
@@ -218,7 +218,7 @@ public class VideosController : BaseJellyfinApiController
foreach (var item in items.Where(i => !i.Id.Equals(primaryVersion.Id)))
{
item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture));
item.SetPrimaryVersionId(primaryVersion.Id);
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);

View File

@@ -284,7 +284,7 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine
lc => lc.ChildId,
item => item.Id,
(lc, item) => new { lc.ParentId, lc.ChildId, item.PrimaryVersionId })
.Where(x => x.PrimaryVersionId == null || x.PrimaryVersionId != x.ParentId.ToString())
.Where(x => !x.PrimaryVersionId.HasValue || !x.PrimaryVersionId.Value.Equals(x.ParentId))
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
@@ -307,7 +307,7 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine
if (childItem is not null)
{
childItem.PrimaryVersionId = link.ParentId.ToString();
childItem.PrimaryVersionId = link.ParentId;
updatedCount++;
}
}

View File

@@ -1074,9 +1074,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
entity.OriginalTitle = originalTitle;
}
if (reader.TryGetString(index++, out var primaryVersionId))
if (reader.TryGetString(index++, out var primaryVersionId) && Guid.TryParse(primaryVersionId, out var primaryVersionGuid))
{
entity.PrimaryVersionId = primaryVersionId;
entity.PrimaryVersionId = primaryVersionGuid;
}
if (reader.TryReadDateTime(index++, out var dateLastMediaAdded))

View File

@@ -480,7 +480,7 @@ namespace MediaBrowser.Controller.Entities
if (item is Video video)
{
// Check via PrimaryVersionId
if (!string.IsNullOrEmpty(video.PrimaryVersionId))
if (video.PrimaryVersionId.HasValue)
{
Logger.LogDebug("Item is an alternate version (via PrimaryVersionId), skipping deletion: {Path}", item.Path ?? item.Name);
continue;

View File

@@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Entities
}
[JsonIgnore]
public string PrimaryVersionId { get; set; }
public Guid? PrimaryVersionId { get; set; }
public string[] AdditionalParts { get; set; }
@@ -254,9 +254,9 @@ namespace MediaBrowser.Controller.Entities
private int GetMediaSourceCount(HashSet<Guid> callstack = null)
{
callstack ??= new();
if (!string.IsNullOrEmpty(PrimaryVersionId))
if (PrimaryVersionId.HasValue)
{
var item = LibraryManager.GetItemById(PrimaryVersionId);
var item = LibraryManager.GetItemById(PrimaryVersionId.Value);
if (item is Video video)
{
if (callstack.Contains(video.Id))
@@ -317,25 +317,17 @@ namespace MediaBrowser.Controller.Entities
return list;
}
public void SetPrimaryVersionId(string id)
public void SetPrimaryVersionId(Guid? id)
{
if (string.IsNullOrEmpty(id))
{
PrimaryVersionId = null;
}
else
{
PrimaryVersionId = id;
}
PrimaryVersionId = id;
PresentationUniqueKey = CreatePresentationUniqueKey();
}
public override string CreatePresentationUniqueKey()
{
if (!string.IsNullOrEmpty(PrimaryVersionId))
if (PrimaryVersionId.HasValue)
{
return PrimaryVersionId;
return PrimaryVersionId.Value.ToString("N", CultureInfo.InvariantCulture);
}
return base.CreatePresentationUniqueKey();
@@ -483,6 +475,7 @@ namespace MediaBrowser.Controller.Entities
if (altVideo is not null)
{
altVideo.OwnerId = Id;
altVideo.SetPrimaryVersionId(Id);
LibraryManager.CreateItem(altVideo, GetParent());
}
}
@@ -495,6 +488,13 @@ namespace MediaBrowser.Controller.Entities
if (LibraryManager.GetItemById(id) is Video video)
{
LibraryManager.UpsertLinkedChild(Id, video.Id, LinkedChildType.LocalAlternateVersion);
// Ensure PrimaryVersionId is set for existing alternate versions that may not have it
if (!video.PrimaryVersionId.HasValue)
{
video.SetPrimaryVersionId(Id);
await video.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -612,9 +612,9 @@ namespace MediaBrowser.Controller.Entities
list.AddRange(LibraryManager.GetLinkedAlternateVersions(this).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
if (!string.IsNullOrEmpty(PrimaryVersionId))
if (PrimaryVersionId.HasValue)
{
if (LibraryManager.GetItemById(PrimaryVersionId) is Video primary)
if (LibraryManager.GetItemById(PrimaryVersionId.Value) is Video primary)
{
var existingIds = list.Select(i => i.Item1.Id).ToList();
list.Add((primary, MediaSourceType.Grouping));

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; }

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

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
modelBuilder.HasAnnotation("ProductVersion", "10.0.3");
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
{
@@ -294,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")