diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-07-25 17:17:30 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-07-25 17:17:30 +0200 |
| commit | 3c9727d36cdf6749106ad398835484810d0c6a22 (patch) | |
| tree | 957daad77089f41da653c31f84415ba258447c92 | |
| parent | 6382563440b69d816639abc2b4d03fca1d35eca2 (diff) | |
Always inherit from owner item and add tests
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 + }; } |
