aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-07-25 17:17:30 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-07-25 17:17:30 +0200
commit3c9727d36cdf6749106ad398835484810d0c6a22 (patch)
tree957daad77089f41da653c31f84415ba258447c92
parent6382563440b69d816639abc2b4d03fca1d35eca2 (diff)
Always inherit from owner item and add tests
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs59
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs12
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs77
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs60
4 files changed, 189 insertions, 19 deletions
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 41f7e796e8..cc3cba41c2 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1533,15 +1533,27 @@ namespace MediaBrowser.Controller.Entities
var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
var newExtraIds = Array.ConvertAll(extras, x => x.Id);
- var currentExtraIds = LibraryManager.GetItemList(new InternalItemsQuery()
+ var currentExtras = LibraryManager.GetItemList(new InternalItemsQuery()
{
OwnerIds = [item.Id]
- }).Select(e => e.Id).ToArray();
+ });
+
+ var currentExtraIds = currentExtras.Select(e => e.Id).ToArray();
var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh)
{
+ // The owner's dates may only have become known after its extras were created, so keep
+ // them in sync even when there is nothing to refresh.
+ foreach (var extra in currentExtras)
+ {
+ if (extra.ExtraType is not null && InheritDatesFromOwner(item, extra))
+ {
+ await extra.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
return false;
}
@@ -1558,18 +1570,6 @@ namespace MediaBrowser.Controller.Entities
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
- // Extras (e.g. trailers) frequently have no reliable date metadata of their own and
- // would otherwise fall back to the file's container creation date. Inherit the owner's
- // year/premiere date when the extra doesn't have one, so it stays consistent with the
- // media it belongs to. Setting it before the refresh means the media info provider
- // won't overwrite it from the file creation date.
- if (i.ProductionYear is null && item.ProductionYear is not null)
- {
- i.ProductionYear = item.ProductionYear;
- i.PremiereDate ??= item.PremiereDate;
- subOptions.ForceSave = true;
- }
-
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
@@ -2652,6 +2652,32 @@ namespace MediaBrowser.Controller.Entities
}
}
+ /// <summary>
+ /// Applies the owner's premiere date and production year to an owned item, returning whether anything changed.
+ /// </summary>
+ /// <param name="owner">The owner.</param>
+ /// <param name="ownedItem">The owned item.</param>
+ /// <returns><c>true</c> if the owned item was changed, else <c>false</c>.</returns>
+ internal static bool InheritDatesFromOwner(BaseItem owner, BaseItem ownedItem)
+ {
+ // Extras have no release date of their own, so the owner's is authoritative.
+ var changed = false;
+
+ if (owner.ProductionYear is not null && ownedItem.ProductionYear != owner.ProductionYear)
+ {
+ ownedItem.ProductionYear = owner.ProductionYear;
+ changed = true;
+ }
+
+ if (owner.PremiereDate is not null && ownedItem.PremiereDate != owner.PremiereDate)
+ {
+ ownedItem.PremiereDate = owner.PremiereDate;
+ changed = true;
+ }
+
+ return changed;
+ }
+
protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
var newOptions = new MetadataRefreshOptions(options)
@@ -2711,6 +2737,11 @@ namespace MediaBrowser.Controller.Entities
ownedItem.CustomRating = item.CustomRating;
newOptions.ForceSave = true;
}
+
+ if (InheritDatesFromOwner(item, ownedItem))
+ {
+ newOptions.ForceSave = true;
+ }
}
await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index ff14f87923..1cb8e09414 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -386,7 +386,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
+ internal void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
{
var replaceData = refreshOptions.ReplaceAllMetadata;
@@ -432,7 +432,9 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- if (data.ProductionYear is not null)
+ // Extras have no release date of their own, they inherit it from the item they belong to.
+ var useContainerDates = video.ExtraType is null;
+ if (useContainerDates && data.ProductionYear is not null)
{
if (video.ProductionYear is null || replaceData)
{
@@ -440,9 +442,9 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- if (data.PremiereDate.HasValue)
+ if (useContainerDates && data.PremiereDate is not null)
{
- if (!video.PremiereDate.HasValue || replaceData)
+ if (video.PremiereDate is null || replaceData)
{
video.PremiereDate = data.PremiereDate;
}
@@ -482,7 +484,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
// If we don't have a ProductionYear try and get it from PremiereDate
- if (video.PremiereDate is not null && video.ProductionYear is null)
+ if (useContainerDates && video.PremiereDate is not null && video.ProductionYear is null)
{
video.ProductionYear = video.PremiereDate.Value.ToLocalTime().Year;
}
diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
index c0a2b0ecca..6234365ca3 100644
--- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
+++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
@@ -5,6 +5,7 @@ using System.Reflection;
using System.Threading;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaSegments;
@@ -335,4 +336,80 @@ public class BaseItemTests
Assert.Contains(alt2.Id, ids);
}
}
+
+ [Fact]
+ public void InheritDatesFromOwner_OwnerHasDates_OverwritesOwnedItemDates()
+ {
+ var owner = new Movie
+ {
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ // 2016 is what the container creation date of a re-encoded trailer would have yielded.
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = 2016,
+ PremiereDate = new DateTime(2016, 5, 4, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ Assert.True(BaseItem.InheritDatesFromOwner(owner, trailer));
+ Assert.Equal(owner.ProductionYear, trailer.ProductionYear);
+ Assert.Equal(owner.PremiereDate, trailer.PremiereDate);
+ }
+
+ [Fact]
+ public void InheritDatesFromOwner_OwnerHasNoDates_KeepsOwnedItemDates()
+ {
+ var owner = new Movie();
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ Assert.False(BaseItem.InheritDatesFromOwner(owner, trailer));
+ Assert.Equal(1982, trailer.ProductionYear);
+ Assert.Equal(new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc), trailer.PremiereDate);
+ }
+
+ [Fact]
+ public void InheritDatesFromOwner_DatesAlreadyMatch_ReportsNoChange()
+ {
+ var owner = new Movie
+ {
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = owner.ProductionYear,
+ PremiereDate = owner.PremiereDate
+ };
+
+ Assert.False(BaseItem.InheritDatesFromOwner(owner, trailer));
+ }
+
+ [Fact]
+ public void InheritDatesFromOwner_OwnedItemHasNoDates_TakesOwnerDates()
+ {
+ var owner = new Movie
+ {
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer
+ };
+
+ Assert.True(BaseItem.InheritDatesFromOwner(owner, trailer));
+ Assert.Equal(1982, trailer.ProductionYear);
+ Assert.Equal(new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc), trailer.PremiereDate);
+ }
}
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
index 2438ef06d1..59d3f42edf 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
@@ -3,7 +3,9 @@ using AutoFixture;
using AutoFixture.AutoMoq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.MediaInfo;
using Moq;
using Xunit;
@@ -75,4 +77,62 @@ public class FFProbeVideoInfoTests
Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime));
}
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void FetchEmbeddedInfo_NoExtra_AppliesContainerDates(bool replaceAllMetadata)
+ {
+ var video = new Video();
+
+ _fFProbeVideoInfo.FetchEmbeddedInfo(video, CreateMediaInfoWithDates(), CreateRefreshOptions(replaceAllMetadata), new LibraryOptions());
+
+ Assert.Equal(2016, video.ProductionYear);
+ Assert.Equal(new DateTime(2016, 5, 4, 0, 0, 0, DateTimeKind.Utc), video.PremiereDate);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void FetchEmbeddedInfo_Extra_IgnoresContainerDates(bool replaceAllMetadata)
+ {
+ var video = new Video
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ _fFProbeVideoInfo.FetchEmbeddedInfo(video, CreateMediaInfoWithDates(), CreateRefreshOptions(replaceAllMetadata), new LibraryOptions());
+
+ Assert.Equal(1982, video.ProductionYear);
+ Assert.Equal(new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc), video.PremiereDate);
+ }
+
+ [Fact]
+ public void FetchEmbeddedInfo_ExtraWithoutDates_StaysWithoutDates()
+ {
+ var video = new Video
+ {
+ ExtraType = ExtraType.Trailer
+ };
+
+ _fFProbeVideoInfo.FetchEmbeddedInfo(video, CreateMediaInfoWithDates(), CreateRefreshOptions(false), new LibraryOptions());
+
+ Assert.Null(video.ProductionYear);
+ Assert.Null(video.PremiereDate);
+ }
+
+ private static MediaBrowser.Model.MediaInfo.MediaInfo CreateMediaInfoWithDates()
+ => new()
+ {
+ ProductionYear = 2016,
+ PremiereDate = new DateTime(2016, 5, 4, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ private static MetadataRefreshOptions CreateRefreshOptions(bool replaceAllMetadata)
+ => new(Mock.Of<IDirectoryService>())
+ {
+ ReplaceAllMetadata = replaceAllMetadata
+ };
}