From f9a7cd745757daa0215242c06ca6348f8e8c7bf9 Mon Sep 17 00:00:00 2001 From: itz4blitz Date: Sat, 25 Apr 2026 18:38:07 -0400 Subject: [PATCH] Honor episode NFO season during metadata merge Path-derived season numbers could win over explicit provider metadata during episode refresh, causing episodes to appear in the wrong season. Prefer provider-supplied season numbers in the provider merge phases without letting later backfill merges clobber them. --- .../TV/EpisodeMetadataService.cs | 10 ++ .../TV/EpisodeMetadataServiceTests.cs | 110 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs index 31f0687114..596ca8d201 100644 --- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs +++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs @@ -109,5 +109,15 @@ public class EpisodeMetadataService : MetadataService { targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd; } + + // Episode season numbers can be set from path parsing before local metadata is merged. + // When a provider supplies an explicit season, prefer it during provider->temp and temp->item merges, + // but avoid clobbering provider data when existing metadata is backfilled into temp. + if (mergeMetadataSettings + && sourceItem.ParentIndexNumber.HasValue + && targetItem.ParentIndexNumber != sourceItem.ParentIndexNumber) + { + targetItem.ParentIndexNumber = sourceItem.ParentIndexNumber; + } } } diff --git a/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs new file mode 100644 index 0000000000..8f5b1b3c48 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs @@ -0,0 +1,110 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Providers.Manager; +using MediaBrowser.Providers.TV; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.TV; + +public class EpisodeMetadataServiceTests +{ + private readonly TestEpisodeMetadataService _service = new(); + + [Fact] + public void MergeData_ProviderSeasonOverridesPathDerivedSeason() + { + var source = new MetadataResult + { + Item = new Episode + { + ParentIndexNumber = 2 + } + }; + + var target = new MetadataResult + { + Item = new Episode + { + ParentIndexNumber = 1 + } + }; + + _service.Merge(source, target, replaceData: false, mergeMetadataSettings: true); + + Assert.Equal(2, target.Item.ParentIndexNumber); + } + + [Fact] + public void MergeData_BackfillExistingMetadata_DoesNotOverrideProviderSeason() + { + var existingMetadata = new MetadataResult + { + Item = new Episode + { + ParentIndexNumber = 1 + } + }; + + var temp = new MetadataResult + { + Item = new Episode + { + ParentIndexNumber = 2 + } + }; + + _service.Merge(existingMetadata, temp, replaceData: false, mergeMetadataSettings: false); + + Assert.Equal(2, temp.Item.ParentIndexNumber); + } + + [Fact] + public void MergeData_MissingProviderSeasonKeepsExistingSeason() + { + var source = new MetadataResult + { + Item = new Episode() + }; + + var target = new MetadataResult + { + Item = new Episode + { + ParentIndexNumber = 1 + } + }; + + _service.Merge(source, target, replaceData: false, mergeMetadataSettings: true); + + Assert.Equal(1, target.Item.ParentIndexNumber); + } + + private sealed class TestEpisodeMetadataService : EpisodeMetadataService + { + public TestEpisodeMetadataService() + : base( + Mock.Of(), + NullLogger.Instance, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()) + { + } + + public void Merge(MetadataResult source, MetadataResult target, bool replaceData, bool mergeMetadataSettings) + { + MergeData(source, target, Array.Empty(), replaceData, mergeMetadataSettings); + } + } +}