mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-03 23:36:38 +01:00
Fix version resolution and scan handling
This commit is contained in:
@@ -407,7 +407,8 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
// If deleting a primary version video, clear PrimaryVersionId from alternate versions
|
||||
if (item is Video video && string.IsNullOrEmpty(video.PrimaryVersionId))
|
||||
// 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())
|
||||
{
|
||||
var alternateVersions = GetLocalAlternateVersionIds(video)
|
||||
.Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
|
||||
@@ -700,8 +701,8 @@ namespace Emby.Server.Implementations.Library
|
||||
return key.GetMD5();
|
||||
}
|
||||
|
||||
public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null)
|
||||
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
|
||||
public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directoryService = null, CollectionType? collectionType = null)
|
||||
=> ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType);
|
||||
|
||||
private BaseItem? ResolvePath(
|
||||
FileSystemMetadata fileInfo,
|
||||
@@ -2105,6 +2106,8 @@ namespace Emby.Server.Implementations.Library
|
||||
// Resolve and add any local alternate version items that don't exist yet
|
||||
// This ensures they exist in the database when LinkedChildren are processed
|
||||
var allItems = new List<BaseItem>(items);
|
||||
var parentFolder = parent as Folder;
|
||||
var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item is Video video && video.LocalAlternateVersions.Length > 0)
|
||||
@@ -2122,7 +2125,14 @@ namespace Emby.Server.Implementations.Library
|
||||
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
|
||||
{
|
||||
// Alternate version doesn't exist, resolve and create it
|
||||
var altVideo = ResolvePath(_fileSystem.GetFileSystemInfo(path)) as Video;
|
||||
// Pass parent and collectionType so the resolver creates the correct type
|
||||
// (e.g. Movie instead of generic Video)
|
||||
var altVideo = ResolvePath(
|
||||
_fileSystem.GetFileSystemInfo(path),
|
||||
new DirectoryService(_fileSystem),
|
||||
null,
|
||||
parentFolder,
|
||||
parentCollectionType) as Video;
|
||||
if (altVideo is not null)
|
||||
{
|
||||
altVideo.OwnerId = video.Id;
|
||||
@@ -2304,6 +2314,8 @@ namespace Emby.Server.Implementations.Library
|
||||
// Resolve and add any local alternate version items that don't exist yet
|
||||
// This ensures they exist in the database when LinkedChildren are processed
|
||||
var allItems = new List<BaseItem>(items);
|
||||
var parentFolder = parent as Folder;
|
||||
var parentCollectionType = GetTopFolderContentType(parent);
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item is Video video && video.LocalAlternateVersions.Length > 0)
|
||||
@@ -2321,7 +2333,14 @@ namespace Emby.Server.Implementations.Library
|
||||
if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
|
||||
{
|
||||
// Alternate version doesn't exist, resolve and create it
|
||||
var altVideo = ResolvePath(_fileSystem.GetFileSystemInfo(path)) as Video;
|
||||
// Pass parent and collectionType so the resolver creates the correct type
|
||||
// (e.g. Movie instead of generic Video)
|
||||
var altVideo = ResolvePath(
|
||||
_fileSystem.GetFileSystemInfo(path),
|
||||
new DirectoryService(_fileSystem),
|
||||
null,
|
||||
parentFolder,
|
||||
parentCollectionType) as Video;
|
||||
if (altVideo is not null)
|
||||
{
|
||||
altVideo.OwnerId = video.Id;
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Text.Json;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LinkedChildType = Jellyfin.Database.Implementations.Entities.LinkedChildType;
|
||||
@@ -20,13 +21,16 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
|
||||
{
|
||||
private readonly ILogger<MigrateLinkedChildren> _logger;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public MigrateLinkedChildren(
|
||||
ILoggerFactory loggerFactory,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<MigrateLinkedChildren>();
|
||||
_dbProvider = dbProvider;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -219,19 +223,21 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
|
||||
|
||||
_logger.LogInformation("LinkedChildren migration completed. Processed {Count} items.", processedCount);
|
||||
|
||||
UpdateAlternateVersionTypes(context);
|
||||
CleanupWrongTypeAlternateVersions(context);
|
||||
CleanupOrphanedLinkedChildren(context);
|
||||
}
|
||||
|
||||
private void UpdateAlternateVersionTypes(JellyfinDbContext context)
|
||||
private void CleanupWrongTypeAlternateVersions(JellyfinDbContext context)
|
||||
{
|
||||
_logger.LogInformation("Updating alternate version item types to match their parent's type...");
|
||||
_logger.LogInformation("Cleaning up alternate version items with wrong type...");
|
||||
|
||||
// Find all LocalAlternateVersion relationships where the child is a generic Video
|
||||
// but the parent is a more specific type (like Movie)
|
||||
// but the parent is a more specific type (like Movie).
|
||||
// Since IDs are computed from type + path, just updating the Type column would break ID lookups.
|
||||
// Instead, delete them and let the runtime recreate them with the correct type during the next library scan.
|
||||
var genericVideoType = "MediaBrowser.Controller.Entities.Video";
|
||||
|
||||
var alternateVersionsToUpdate = context.LinkedChildren
|
||||
var wrongTypeChildIds = context.LinkedChildren
|
||||
.Where(lc => lc.ChildType == LinkedChildType.LocalAlternateVersion)
|
||||
.Join(
|
||||
context.BaseItems,
|
||||
@@ -242,30 +248,30 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
|
||||
context.BaseItems,
|
||||
x => x.ChildId,
|
||||
child => child.Id,
|
||||
(x, child) => new { x.ChildId, x.ParentType, ChildType = child.Type, Child = child })
|
||||
(x, child) => new { x.ChildId, x.ParentType, ChildType = child.Type })
|
||||
.Where(x => x.ChildType == genericVideoType && x.ParentType != genericVideoType)
|
||||
.Select(x => x.ChildId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (alternateVersionsToUpdate.Count == 0)
|
||||
if (wrongTypeChildIds.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("No alternate version items need type updates.");
|
||||
_logger.LogInformation("No wrong-type alternate version items found.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {Count} alternate version items to update.", alternateVersionsToUpdate.Count);
|
||||
_logger.LogInformation("Found {Count} wrong-type alternate version items to remove.", wrongTypeChildIds.Count);
|
||||
|
||||
foreach (var item in alternateVersionsToUpdate)
|
||||
foreach (var childId in wrongTypeChildIds)
|
||||
{
|
||||
item.Child.Type = item.ParentType;
|
||||
_logger.LogDebug(
|
||||
"Updating item {ChildId} type from {OldType} to {NewType}",
|
||||
item.ChildId,
|
||||
genericVideoType,
|
||||
item.ParentType);
|
||||
var item = _libraryManager.GetItemById(childId);
|
||||
if (item is not null)
|
||||
{
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false });
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
_logger.LogInformation("Successfully updated {Count} alternate version item types.", alternateVersionsToUpdate.Count);
|
||||
_logger.LogInformation("Removed {Count} wrong-type alternate version items. They will be recreated with the correct type on next library scan.", wrongTypeChildIds.Count);
|
||||
}
|
||||
|
||||
private void CleanupOrphanedLinkedChildren(JellyfinDbContext context)
|
||||
|
||||
@@ -109,7 +109,14 @@ namespace MediaBrowser.Controller.Entities.Movies
|
||||
|
||||
if (LibraryManager.GetItemById(id) is not Movie movie)
|
||||
{
|
||||
movie = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Movie;
|
||||
// Pass parent and collectionType so the resolver creates a Movie
|
||||
// instead of a generic Video
|
||||
var parentFolder = GetParent() as Folder;
|
||||
var collectionType = GetParents().OfType<ICollectionFolder>().FirstOrDefault()?.CollectionType;
|
||||
movie = LibraryManager.ResolvePath(
|
||||
FileSystem.GetFileSystemInfo(path),
|
||||
parentFolder,
|
||||
collectionType: collectionType) as Movie;
|
||||
|
||||
newOptions.ForceSave = true;
|
||||
}
|
||||
|
||||
@@ -457,10 +457,20 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
RefreshLinkedAlternateVersions();
|
||||
|
||||
var tasks = LocalAlternateVersions
|
||||
.Select(i => RefreshMetadataForVersions(options, false, i, cancellationToken));
|
||||
if (LocalAlternateVersions.Length > 0)
|
||||
{
|
||||
// Check if LinkedChildren are in sync before processing
|
||||
var existingLinkCount = LibraryManager.GetLocalAlternateVersionIds(this).Count();
|
||||
var tasks = LocalAlternateVersions
|
||||
.Select(i => RefreshMetadataForVersions(options, false, i, cancellationToken));
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
if (existingLinkCount != LocalAlternateVersions.Length)
|
||||
{
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,6 +480,15 @@ namespace MediaBrowser.Controller.Entities
|
||||
protected virtual async Task RefreshMetadataForVersions(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
await RefreshMetadataForOwnedVideo(options, copyTitleMetadata, path, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Create LinkedChild entry for this local alternate version
|
||||
// This ensures the relationship exists in the database even if the alternate version
|
||||
// was created after the primary video was first saved
|
||||
var id = LibraryManager.GetNewItemId(path, GetType());
|
||||
if (LibraryManager.GetItemById(id) is Video video)
|
||||
{
|
||||
LibraryManager.UpsertLinkedChild(Id, video.Id, LinkedChildType.LocalAlternateVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private new async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
|
||||
@@ -496,7 +515,12 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
if (LibraryManager.GetItemById(id) is not Video video)
|
||||
{
|
||||
video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video;
|
||||
var parentFolder = GetParent() as Folder;
|
||||
var collectionType = GetParents().OfType<ICollectionFolder>().FirstOrDefault()?.CollectionType;
|
||||
video = LibraryManager.ResolvePath(
|
||||
FileSystem.GetFileSystemInfo(path),
|
||||
parentFolder,
|
||||
collectionType: collectionType) as Video;
|
||||
|
||||
newOptions.ForceSave = true;
|
||||
}
|
||||
@@ -512,11 +536,6 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
|
||||
await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Create LinkedChild entry for this local alternate version
|
||||
// This ensures the relationship exists in the database even if the alternate version
|
||||
// was created after the primary video was first saved
|
||||
LibraryManager.UpsertLinkedChild(Id, video.Id, LinkedChildType.LocalAlternateVersion);
|
||||
}
|
||||
|
||||
private void RefreshLinkedAlternateVersions()
|
||||
|
||||
@@ -59,11 +59,13 @@ namespace MediaBrowser.Controller.Library
|
||||
/// <param name="fileInfo">The file information.</param>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <param name="directoryService">An instance of <see cref="IDirectoryService"/>.</param>
|
||||
/// <param name="collectionType">The collection type of the library containing this item.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
BaseItem? ResolvePath(
|
||||
FileSystemMetadata fileInfo,
|
||||
Folder? parent = null,
|
||||
IDirectoryService? directoryService = null);
|
||||
IDirectoryService? directoryService = null,
|
||||
CollectionType? collectionType = null);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a set of files into a list of BaseItem.
|
||||
|
||||
@@ -77,7 +77,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)),
|
||||
|
||||
Reference in New Issue
Block a user