diff options
| author | Niels van Velzen <nielsvanvelzen@users.noreply.github.com> | 2026-05-04 18:00:50 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-04 18:00:50 +0200 |
| commit | 5d5ae271a59886a76d5b699eb651081a97a89c8b (patch) | |
| tree | 687bb9a3cadf42881e7a48c4d7932ab4e802d9e2 | |
| parent | d707a9dba106e78d1720214daf905eb39e5a3b30 (diff) | |
| parent | f9a7cd745757daa0215242c06ca6348f8e8c7bf9 (diff) | |
Merge pull request #16702 from itz4blitz/blitz/issue-13197-nfo-season
Honor episode NFO season during metadata merge
| -rw-r--r-- | MediaBrowser.Providers/TV/EpisodeMetadataService.cs | 10 | ||||
| -rw-r--r-- | tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs | 110 |
2 files changed, 120 insertions, 0 deletions
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<Episode, EpisodeInfo> { 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<Episode> + { + Item = new Episode + { + ParentIndexNumber = 2 + } + }; + + var target = new MetadataResult<Episode> + { + 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<Episode> + { + Item = new Episode + { + ParentIndexNumber = 1 + } + }; + + var temp = new MetadataResult<Episode> + { + 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<Episode> + { + Item = new Episode() + }; + + var target = new MetadataResult<Episode> + { + 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<IServerConfigurationManager>(), + NullLogger<EpisodeMetadataService>.Instance, + Mock.Of<IProviderManager>(), + Mock.Of<IFileSystem>(), + Mock.Of<ILibraryManager>(), + Mock.Of<IExternalDataManager>(), + Mock.Of<IItemRepository>()) + { + } + + public void Merge(MetadataResult<Episode> source, MetadataResult<Episode> target, bool replaceData, bool mergeMetadataSettings) + { + MergeData(source, target, Array.Empty<MetadataField>(), replaceData, mergeMetadataSettings); + } + } +} |
