diff options
24 files changed, 273 insertions, 31 deletions
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 21638ba9e7..0d93403758 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -57,7 +57,6 @@ namespace Emby.Naming.Common ".nrg", ".nsv", ".nuv", - ".ogg", ".ogm", ".ogv", ".pva", @@ -362,7 +361,10 @@ namespace Emby.Naming.Common // Not a Kodi rule as well, but the expression below also causes false positives, // so we make sure this one gets tested first. // "Foo Bar 889" - new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$") + // Names carrying an SxxEyy marker are excluded because the Kodi expression above already covers them. + // Without that guard this expression reads digits out of the title instead, turning + // "S01E01 1-23-45 [Bluray-1080p]" into episodes 1 through 45. + new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?![^\\\/]*[Ss][0-9]+[][ ._-]*[Ee][0-9]+)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$") { IsNamed = true }, diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 52569ed66a..983ecced02 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3199,11 +3199,11 @@ namespace Emby.Server.Implementations.Library } } - if (!episode.ProductionYear.HasValue) + if (episode.ProductionYear is null) { episode.ProductionYear = episodeInfo.Year; - if (episode.ProductionYear.HasValue) + if (episode.ProductionYear is not null) { changed = true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index b9f9f29723..6ba4a7bce6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -32,8 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers : base(logger, namingOptions, directoryService) { _namingOptions = namingOptions; - _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) }; - _videoResolvers = new IItemResolver[] { this }; + _trailerResolvers = [new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService, parseName: true)]; + _videoResolvers = [this]; } protected override Video Resolve(ItemResolveArgs args) diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs index ba320266a4..b3bdea704a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs @@ -2,6 +2,7 @@ using Emby.Naming.Common; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using Microsoft.Extensions.Logging; @@ -14,15 +15,25 @@ namespace Emby.Server.Implementations.Library.Resolvers public class GenericVideoResolver<T> : BaseVideoResolver<T> where T : Video, new() { + private readonly bool _parseName; + /// <summary> /// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class. /// </summary> /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> /// <param name="directoryService">The directory service.</param> - public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService) + /// <param name="parseName">Whether to parse the file name for metadata such as the year.</param> + public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService, bool parseName = false) : base(logger, namingOptions, directoryService) { + _parseName = parseName; + } + + /// <inheritdoc /> + protected override T Resolve(ItemResolveArgs args) + { + return ResolveVideo<T>(args, _parseName); } } } diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs index 8c8b8824f3..30b268bb60 100644 --- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Sorting return x.PremiereDate.Value; } - if (x.ProductionYear.HasValue) + if (x.ProductionYear is not null) { try { diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs index 9aec87f183..8774bd8d4f 100644 --- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs +++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Sorting return 0; } - if (x.ProductionYear.HasValue) + if (x.ProductionYear is not null) { return x.ProductionYear.Value; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 21a726aaec..209feac702 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1546,15 +1546,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; } @@ -1570,6 +1582,7 @@ namespace MediaBrowser.Controller.Entities i.OwnerId = ownerId; i.ParentId = Guid.Empty; + return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken); }); @@ -2652,6 +2665,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 +2750,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.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index e8817a29cf..8c3ce2ff58 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -90,7 +90,7 @@ namespace MediaBrowser.Controller.Entities.Movies { var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); - if (!ProductionYear.HasValue) + if (ProductionYear is null) { var info = LibraryManager.ParseName(Name); diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 237ad5198c..effbf98820 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Entities { var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); - if (!ProductionYear.HasValue) + if (ProductionYear is null) { var info = LibraryManager.ParseName(Name); diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 952187c6e1..d9f300ad20 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -507,7 +507,7 @@ namespace MediaBrowser.Controller.Entities.TV { var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); - if (!ProductionYear.HasValue) + if (ProductionYear is null) { var info = LibraryManager.ParseName(Name); diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 939709215c..a2465eedf0 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -49,7 +49,7 @@ namespace MediaBrowser.Controller.Entities { var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata); - if (!ProductionYear.HasValue) + if (ProductionYear is null) { var info = LibraryManager.ParseName(Name); diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index c57ed2faf8..9ba103cc8b 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -730,7 +730,7 @@ namespace MediaBrowser.Controller.Entities // Apply year filter if (query.Years.Length > 0) { - if (!(item.ProductionYear.HasValue && query.Years.Contains(item.ProductionYear.Value))) + if (item.ProductionYear is null || !query.Years.Contains(item.ProductionYear.Value)) { return false; } diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index b0f51aec71..bc184d82fb 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -277,7 +277,7 @@ namespace MediaBrowser.LocalMetadata.Savers await writer.WriteElementStringAsync(null, "Rating", null, item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } - if (item.ProductionYear.HasValue && item is not Person) + if (item.ProductionYear is not null && item is not Person) { await writer.WriteElementStringAsync(null, "ProductionYear", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index b6acfdbf3b..5533e55223 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -189,7 +189,7 @@ namespace MediaBrowser.MediaEncoding.Probing } // Guess ProductionYear from PremiereDate if missing - if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue) + if (info.ProductionYear is null && info.PremiereDate is not null) { info.ProductionYear = info.PremiereDate.Value.Year; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index a438a94c40..f09c4c876c 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1114,7 +1114,7 @@ namespace MediaBrowser.Providers.Manager target.PremiereDate = source.PremiereDate; } - if (replaceData || !target.ProductionYear.HasValue) + if (replaceData || target.ProductionYear is null) { target.ProductionYear = source.ProductionYear; } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index f9d8883dff..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,17 +432,19 @@ namespace MediaBrowser.Providers.MediaInfo } } - if (data.ProductionYear.HasValue) + // 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.HasValue || replaceData) + if (video.ProductionYear is null || replaceData) { video.ProductionYear = data.ProductionYear; } } - 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.HasValue && !video.ProductionYear.HasValue) + if (useContainerDates && video.PremiereDate is not null && video.ProductionYear is null) { video.ProductionYear = video.PremiereDate.Value.ToLocalTime().Year; } diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index b5ba2d24f2..af2557df78 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -82,7 +82,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("title", album.Name); } - if (album.ProductionYear.HasValue) + if (album.ProductionYear is not null) { writer.WriteElementString("year", album.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 78907a5e68..5a9bbbef4f 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -544,7 +544,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); } - if (item.ProductionYear.HasValue) + if (item.ProductionYear is not null) { writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); } diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs index 846f9baf71..62a06370da 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs @@ -497,7 +497,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable // trim trailing period from the folder name var folderName = _fileSystem.GetValidFilename(timer.Name).Trim().TrimEnd('.').Trim(); - if (metadata is not null && metadata.ProductionYear.HasValue) + if (metadata is not null && metadata.ProductionYear is not null) { folderName += " (" + metadata.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } @@ -532,7 +532,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable } var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); - if (timer.ProductionYear.HasValue) + if (timer.ProductionYear is not null) { folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } @@ -550,7 +550,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable } var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); - if (timer.ProductionYear.HasValue) + if (timer.ProductionYear is not null) { folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs index 7e68dbb547..e0e5a00fb9 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs @@ -290,7 +290,7 @@ public class RecordingsMetadataManager null, DateTime.UtcNow.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false); - if (item.ProductionYear.HasValue) + if (item.ProductionYear is not null) { await writer.WriteElementStringAsync(null, "year", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs index de109c8d65..258cf326ca 100644 --- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs +++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs @@ -6,6 +6,7 @@ using System.Threading; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaSegments; @@ -366,4 +367,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.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs index b441e49b19..7e708c681d 100644 --- a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs @@ -69,6 +69,11 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("Season 1/series-s09e14-720i.mkv", null)] [InlineData("Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)] [InlineData("Season 1/MOONLIGHTING_s01e01-e04", 4)] + // Hyphenated numbers in the episode title must not be read as an episode range + [InlineData("Season 1/S01E01 The 6-10 to Lubbock [WEBRip-1080p][AV1 Opus].mkv", null)] + [InlineData("Season 5/S05E23 11-59 [HDTV-1080p][x265 AC3].mkv", null)] + [InlineData("Season 5/S05E23 11-59 [HDTV-1080p][HEVC AC3].mkv", null)] + [InlineData("Season 1/S01E01 1-23-45 [Bluray-1080p][AV1 Opus].mkv", null)] public void TestGetEndingEpisodeNumberFromFile(string filename, int? endingEpisodeNumber) { var result = _episodePathParser.Parse(filename, false); 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 + }; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs index 562711337f..07c537aee1 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs @@ -306,6 +306,47 @@ public class FindExtrasTests } [Fact] + public void FindExtras_TrailerWithYearInFilename_SetsProductionYearFromFilename() + { + var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" }; + var paths = new List<string> + { + "/movies/Up/Up.mkv", + "/movies/Up/trailers" + }; + + _fileSystemMock.Setup(f => f.GetFiles( + "/movies/Up/trailers", + It.IsAny<string[]>(), + false, + false)) + .Returns(new List<FileSystemMetadata> + { + new() + { + FullName = "/movies/Up/trailers/Trailer 1 (2013).mkv", + Name = "Trailer 1 (2013).mkv", + IsDirectory = false + } + }).Verifiable(); + + var files = paths.Select(p => new FileSystemMetadata + { + FullName = p, + Name = Path.GetFileName(p), + IsDirectory = !Path.HasExtension(p) + }).ToList(); + + var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).ToList(); + + _fileSystemMock.Verify(); + var trailer = Assert.Single(extras); + Assert.Equal(ExtraType.Trailer, trailer.ExtraType); + Assert.Equal(typeof(Trailer), trailer.GetType()); + Assert.Equal(2013, trailer.ProductionYear); + } + + [Fact] public void FindExtras_SeriesWithTrailers_FindsCorrectExtras() { var owner = new Series { Name = "Dexter", Path = "/series/Dexter" }; |
