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); + } + } +}