aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Entities/Video.cs
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-05-04 21:26:26 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-05-04 21:26:26 +0200
commit57c0fcd674c659c658369f0aebfd5d9d6787a9d4 (patch)
tree7aff23d6f54e913a6a34cb5a2568a07298582444 /MediaBrowser.Controller/Entities/Video.cs
parent68ab58589444091925c15ad20d36f935b7bc2e21 (diff)
parentec04313317bed62728b059108cd232e9744f6354 (diff)
Merge remote-tracking branch 'upstream/master' into epg-fixes
Diffstat (limited to 'MediaBrowser.Controller/Entities/Video.cs')
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs202
1 files changed, 152 insertions, 50 deletions
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 1043029c6e..80bcd62dcd 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -19,6 +19,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities
{
@@ -40,7 +41,7 @@ namespace MediaBrowser.Controller.Entities
}
[JsonIgnore]
- public string PrimaryVersionId { get; set; }
+ public Guid? PrimaryVersionId { get; set; }
public string[] AdditionalParts { get; set; }
@@ -160,7 +161,7 @@ namespace MediaBrowser.Controller.Entities
public bool IsStacked => AdditionalParts.Length > 0;
[JsonIgnore]
- public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
+ public override bool HasLocalAlternateVersions => LibraryManager.GetLocalAlternateVersionIds(this).Any();
public static IRecordingsManager RecordingsManager { get; set; }
@@ -253,14 +254,17 @@ 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))
{
- return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
+ // Count alternate versions using LibraryManager
+ var linkedCount = LibraryManager.GetLinkedAlternateVersions(video).Count();
+ var localCount = LibraryManager.GetLocalAlternateVersionIds(video).Count();
+ return linkedCount + localCount + 1;
}
callstack.Add(video.Id);
@@ -268,7 +272,10 @@ namespace MediaBrowser.Controller.Entities
}
}
- return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
+ // Count alternate versions using LibraryManager
+ var linkedVersionCount = LibraryManager.GetLinkedAlternateVersions(this).Count();
+ var localVersionCount = LibraryManager.GetLocalAlternateVersionIds(this).Count();
+ return linkedVersionCount + localVersionCount + 1;
}
public override List<string> GetUserDataKeys()
@@ -310,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();
@@ -364,11 +363,6 @@ namespace MediaBrowser.Controller.Entities
return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
}
- public IEnumerable<Guid> GetLocalAlternateVersionIds()
- {
- return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
- }
-
private string GetUserDataKey(string providerId)
{
var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant();
@@ -382,15 +376,6 @@ namespace MediaBrowser.Controller.Entities
return key;
}
- public IEnumerable<Video> GetLinkedAlternateVersions()
- {
- return LinkedAlternateVersions
- .Select(GetLinkedChild)
- .Where(i => i is not null)
- .OfType<Video>()
- .OrderBy(i => i.SortName);
- }
-
/// <summary>
/// Gets the additional parts.
/// </summary>
@@ -436,10 +421,21 @@ namespace MediaBrowser.Controller.Entities
{
var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
+ // Clean up LocalAlternateVersions - remove paths that no longer exist
+ if (LocalAlternateVersions.Length > 0)
+ {
+ var validPaths = LocalAlternateVersions.Where(FileSystem.FileExists).ToArray();
+ if (validPaths.Length != LocalAlternateVersions.Length)
+ {
+ LocalAlternateVersions = validPaths;
+ hasChanges = true;
+ }
+ }
+
if (IsStacked)
{
var tasks = AdditionalParts
- .Select(i => RefreshMetadataForOwnedVideo(options, true, i, cancellationToken));
+ .Select(i => RefreshMetadataForOwnedVideo(options, true, i, typeof(Video), cancellationToken));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
@@ -449,30 +445,134 @@ namespace MediaBrowser.Controller.Entities
// The additional parts won't have additional parts themselves
if (IsFileProtocol && SupportsOwnedItems)
{
- if (!IsStacked)
- {
- RefreshLinkedAlternateVersions();
+ // Check if LinkedChildren are in sync before processing
+ var existingVersionCount = LibraryManager.GetLocalAlternateVersionIds(this).Count();
+ var tasks = LocalAlternateVersions
+ .Select(i => RefreshMetadataForVersions(options, false, i, cancellationToken));
- var tasks = LocalAlternateVersions
- .Select(i => RefreshMetadataForOwnedVideo(options, false, i, cancellationToken));
+ await Task.WhenAll(tasks).ConfigureAwait(false);
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ if (existingVersionCount != LocalAlternateVersions.Length)
+ {
+ hasChanges = true;
}
}
return hasChanges;
}
- private void RefreshLinkedAlternateVersions()
+ 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;
+ altVideo.SetPrimaryVersionId(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
+ 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);
+ }
+ }
+ }
+
+ private new Task RefreshMetadataForOwnedVideo(
+ MetadataRefreshOptions options,
+ bool copyTitleMetadata,
+ string path,
+ CancellationToken cancellationToken)
+ => RefreshMetadataForOwnedVideo(options, copyTitleMetadata, path, GetType(), cancellationToken);
+
+ private async Task RefreshMetadataForOwnedVideo(
+ MetadataRefreshOptions options,
+ bool copyTitleMetadata,
+ string path,
+ Type itemType,
+ CancellationToken cancellationToken)
{
- foreach (var child in LinkedAlternateVersions)
+ var newOptions = new MetadataRefreshOptions(options)
{
- // Reset the cached value
- if (child.ItemId.IsNullOrEmpty())
+ SearchResult = null
+ };
+
+ var id = LibraryManager.GetNewItemId(path, itemType);
+
+ // Check if the file still exists
+ if (!FileSystem.FileExists(path))
+ {
+ // File was removed - clean up any orphaned database entry
+ if (LibraryManager.GetItemById(id) is Video orphanedVideo && orphanedVideo.OwnerId.Equals(Id))
{
- child.ItemId = null;
+ Logger.LogInformation("Owned video file no longer exists, removing orphaned item: {Path}", path);
+ LibraryManager.DeleteItem(orphanedVideo, new DeleteOptions { DeleteFileLocation = false });
}
+
+ return;
}
+
+ if (LibraryManager.GetItemById(id) is not Video video)
+ {
+ var parentFolder = GetParent() as Folder;
+ var collectionType = LibraryManager.GetContentType(this);
+ video = LibraryManager.ResolvePath(
+ FileSystem.GetFileSystemInfo(path),
+ parentFolder,
+ collectionType: collectionType) as Video;
+
+ if (video is null)
+ {
+ return;
+ }
+
+ // Ensure parts use the expected base type (e.g. Video, not Movie)
+ if (video.GetType() != itemType && Activator.CreateInstance(itemType) is Video correctVideo)
+ {
+ correctVideo.Path = video.Path;
+ correctVideo.Name = video.Name;
+ correctVideo.VideoType = video.VideoType;
+ correctVideo.ProductionYear = video.ProductionYear;
+ correctVideo.ExtraType = video.ExtraType;
+ video = correctVideo;
+ }
+
+ video.Id = id;
+ video.OwnerId = Id;
+ LibraryManager.CreateItem(video, parentFolder);
+ newOptions.ForceSave = true;
+ }
+
+ if (video.OwnerId.IsEmpty())
+ {
+ video.OwnerId = Id;
+ }
+
+ await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -480,7 +580,7 @@ namespace MediaBrowser.Controller.Entities
{
await base.UpdateToRepositoryAsync(updateReason, cancellationToken).ConfigureAwait(false);
- var localAlternates = GetLocalAlternateVersionIds()
+ var localAlternates = LibraryManager.GetLocalAlternateVersionIds(this)
.Select(i => LibraryManager.GetItemById(i))
.Where(i => i is not null);
@@ -537,22 +637,24 @@ namespace MediaBrowser.Controller.Entities
(this, MediaSourceType.Default)
};
- list.AddRange(GetLinkedAlternateVersions().Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
+ 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));
- list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
+ list.AddRange(LibraryManager.GetLinkedAlternateVersions(primary).Where(i => !existingIds.Contains(i.Id)).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
}
}
var localAlternates = list
.SelectMany(i =>
{
- return i.Item1 is Video video ? video.GetLocalAlternateVersionIds() : Enumerable.Empty<Guid>();
+ return i.Item1 is Video video ? LibraryManager.GetLocalAlternateVersionIds(video) : Enumerable.Empty<Guid>();
})
.Select(LibraryManager.GetItemById)
.Where(i => i is not null)