Fix multiple version resolution

This commit is contained in:
Shadowghost
2026-02-08 17:22:52 +01:00
parent bb6c3b4eec
commit 71594b4a9a
7 changed files with 126 additions and 114 deletions

View File

@@ -85,58 +85,6 @@ namespace MediaBrowser.Controller.Entities.Movies
return info;
}
protected override async Task RefreshMetadataForVersions(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
{
var newOptions = new MetadataRefreshOptions(options)
{
SearchResult = null
};
var id = LibraryManager.GetNewItemId(path, typeof(Movie));
// Check if the file still exists
if (!FileSystem.FileExists(path))
{
// File was removed - clean up any orphaned database entry
if (LibraryManager.GetItemById(id) is Movie orphanedMovie && orphanedMovie.OwnerId.Equals(Id))
{
Logger.LogInformation("Alternate version file no longer exists, removing orphaned item: {Path}", path);
LibraryManager.DeleteItem(orphanedMovie, new DeleteOptions { DeleteFileLocation = false });
}
return;
}
if (LibraryManager.GetItemById(id) is not Movie 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;
}
if (movie is null)
{
return;
}
if (movie.OwnerId.Equals(Guid.Empty))
{
movie.OwnerId = Id;
}
await RefreshMetadataForOwnedItem(movie, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false);
// Create LinkedChild entry for this local alternate version
LibraryManager.UpsertLinkedChild(Id, movie.Id, LinkedChildType.LocalAlternateVersion);
}
/// <inheritdoc />
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{

View File

@@ -453,38 +453,45 @@ namespace MediaBrowser.Controller.Entities
// The additional parts won't have additional parts themselves
if (IsFileProtocol && SupportsOwnedItems)
{
if (!IsStacked)
// Check if LinkedChildren are in sync before processing
var existingVersionCount = LibraryManager.GetLocalAlternateVersionIds(this).Count();
var tasks = LocalAlternateVersions
.Select(i => RefreshMetadataForVersions(options, false, i, cancellationToken));
await Task.WhenAll(tasks).ConfigureAwait(false);
if (existingVersionCount != LocalAlternateVersions.Length)
{
RefreshLinkedAlternateVersions();
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);
if (existingLinkCount != LocalAlternateVersions.Length)
{
hasChanges = true;
}
}
hasChanges = true;
}
}
return hasChanges;
}
protected virtual async Task RefreshMetadataForVersions(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
private async Task RefreshMetadataForVersions(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken)
{
// Ensure the alternate version exists with the correct type (e.g. Movie, not Video)
// before refreshing. This must happen here rather than in RefreshMetadataForOwnedVideo
// because that method is also used for stacked parts which should keep their resolved type.
var id = LibraryManager.GetNewItemId(path, GetType());
if (LibraryManager.GetItemById(id) is not Video && FileSystem.FileExists(path))
{
var parentFolder = GetParent() as Folder;
var collectionType = LibraryManager.GetContentType(this);
var altVideo = LibraryManager.ResolveAlternateVersion(path, GetType(), parentFolder, collectionType);
if (altVideo is not null)
{
altVideo.OwnerId = Id;
LibraryManager.CreateItem(altVideo, GetParent());
}
}
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);
@@ -516,18 +523,21 @@ namespace MediaBrowser.Controller.Entities
if (LibraryManager.GetItemById(id) is not Video video)
{
var parentFolder = GetParent() as Folder;
var collectionType = GetParents().OfType<ICollectionFolder>().FirstOrDefault()?.CollectionType;
var collectionType = LibraryManager.GetContentType(this);
video = LibraryManager.ResolvePath(
FileSystem.GetFileSystemInfo(path),
parentFolder,
collectionType: collectionType) as Video;
newOptions.ForceSave = true;
}
if (video is null)
{
return;
}
if (video is null)
{
return;
video.Id = id;
video.OwnerId = Id;
LibraryManager.CreateItem(video, parentFolder);
newOptions.ForceSave = true;
}
if (video.OwnerId.IsEmpty())
@@ -538,18 +548,6 @@ namespace MediaBrowser.Controller.Entities
await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false);
}
private void RefreshLinkedAlternateVersions()
{
foreach (var child in LinkedAlternateVersions)
{
// Reset the cached value
if (child.ItemId.IsNullOrEmpty())
{
child.ItemId = null;
}
}
}
/// <inheritdoc />
public override async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
{

View File

@@ -67,6 +67,22 @@ namespace MediaBrowser.Controller.Library
IDirectoryService? directoryService = null,
CollectionType? collectionType = null);
/// <summary>
/// Resolves a video file as an alternate version of a primary video, ensuring the result
/// has the same concrete type as the primary (e.g. Movie instead of generic Video).
/// Also cleans up any existing item with the wrong type from a previous scan.
/// </summary>
/// <param name="path">The file path of the alternate version.</param>
/// <param name="expectedVideoType">The expected concrete type (same as the primary video).</param>
/// <param name="parent">The parent folder.</param>
/// <param name="collectionType">The collection type of the library.</param>
/// <returns>A correctly-typed Video, or null if resolution fails.</returns>
Video? ResolveAlternateVersion(
string path,
Type expectedVideoType,
Folder? parent,
CollectionType? collectionType);
/// <summary>
/// Resolves a set of files into a list of BaseItem.
/// </summary>