From 70c320e98bfbab5b653935dfa4c100654f716727 Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Sat, 23 Feb 2019 05:45:02 +1100 Subject: [PATCH] New: Added {MediaInfo VideoDynamicRange} renaming token to include HDR in the filename --- .../MediaManagement/Naming/NamingModal.js | 3 +- .../FormatVideoDynamicRangeFixture.cs | 30 +++++ .../UpdateMediaInfoServiceFixture.cs | 109 +++++++++++++++- .../NzbDrone.Core.Test.csproj | 1 + .../FileNameBuilderFixture.cs | 118 ++++++++++++++++++ .../MediaInfo/MediaInfoFormatter.cs | 23 +++- .../MediaInfo/UpdateMediaInfoService.cs | 68 ++++++---- .../Organizer/FileNameBuilder.cs | 39 +++++- 8 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index b7579c675..e03406791 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -93,7 +93,8 @@ const mediaInfoTokens = [ { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' }, { token: '{MediaInfo VideoCodec}', example: 'x264' }, { token: '{MediaInfo AudioFormat}', example: 'DTS' }, - { token: '{MediaInfo AudioChannels}', example: '5.1' } + { token: '{MediaInfo AudioChannels}', example: '5.1' }, + { token: '{MediaInfo VideoDynamicRange}', example: 'HDR' } ]; const otherTokens = [ diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs new file mode 100644 index 000000000..15894449f --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatVideoDynamicRangeFixture : TestBase + { + [TestCase(8, "BT.601 NTSC", "BT.709", "")] + [TestCase(10, "BT.2020", "PQ", "HDR")] + [TestCase(8, "BT.2020", "PQ", "")] + [TestCase(10, "BT.601 NTSC", "PQ", "")] + [TestCase(10, "BT.2020", "BT.709", "")] + [TestCase(10, "BT.2020", "HLG", "HDR")] + public void should_format_video_dynamic_range(int bitDepth, string colourPrimaries, string transferCharacteristics, string expectedVideoDynamicRange) + { + var mediaInfo = new MediaInfoModel + { + VideoBitDepth = bitDepth, + VideoColourPrimaries = colourPrimaries, + VideoTransferCharacteristics = transferCharacteristics, + SchemaRevision = 5 + }; + + MediaInfoFormatter.FormatVideoDynamicRange(mediaInfo).Should().Be(expectedVideoDynamicRange); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs index a4e33ac35..e763dc34d 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs @@ -1,5 +1,6 @@ -using System.IO; +using System.IO; using FizzWare.NBuilder; +using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; @@ -180,5 +181,111 @@ public void should_continue_after_failure() Mocker.GetMock() .Verify(v => v.Update(It.IsAny()), Times.Exactly(1)); } + + [Test] + public void should_not_update_files_if_media_info_disabled() + { + var episodeFiles = Builder.CreateListOfSize(2) + .All() + .With(v => v.RelativePath = "media.mkv") + .TheFirst(1) + .With(v => v.RelativePath = "media2.mkv") + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesBySeries(1)) + .Returns(episodeFiles); + + Mocker.GetMock() + .SetupGet(s => s.EnableMediaInfo) + .Returns(false); + + GivenFileExists(); + GivenSuccessfulScan(); + + Subject.Handle(new SeriesScannedEvent(_series)); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(It.IsAny()), Times.Never()); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Never()); + } + + [Test] + public void should_not_update_if_media_info_disabled() + { + var episodeFile = Builder.CreateNew() + .With(v => v.RelativePath = "media.mkv") + .Build(); + + Mocker.GetMock() + .SetupGet(s => s.EnableMediaInfo) + .Returns(false); + + GivenFileExists(); + GivenSuccessfulScan(); + + Subject.Update(episodeFile, _series); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(It.IsAny()), Times.Never()); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Never()); + } + + [Test] + public void should_update_media_info() + { + var episodeFile = Builder.CreateNew() + .With(v => v.RelativePath = "media.mkv") + .With(e => e.MediaInfo = new MediaInfoModel{SchemaRevision = 3}) + .Build(); + + GivenFileExists(); + GivenSuccessfulScan(); + + Subject.Update(episodeFile, _series); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.Update(episodeFile), Times.Once()); + } + + [Test] + public void should_not_update_media_info_if_new_info_is_null() + { + var episodeFile = Builder.CreateNew() + .With(v => v.RelativePath = "media.mkv") + .With(e => e.MediaInfo = new MediaInfoModel{SchemaRevision = 3}) + .Build(); + + GivenFileExists(); + GivenFailedScan(Path.Combine(_series.Path, "media.mkv")); + + Subject.Update(episodeFile, _series); + + episodeFile.MediaInfo.Should().NotBeNull(); + } + + [Test] + public void should_not_save_episode_file_if_new_info_is_null() + { + var episodeFile = Builder.CreateNew() + .With(v => v.RelativePath = "media.mkv") + .With(e => e.MediaInfo = new MediaInfoModel{SchemaRevision = 3}) + .Build(); + + GivenFileExists(); + GivenFailedScan(Path.Combine(_series.Path, "media.mkv")); + + Subject.Update(episodeFile, _series); + + Mocker.GetMock() + .Verify(v => v.Update(episodeFile), Times.Never()); + } } } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index a15c0a369..ea4bab30e 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -328,6 +328,7 @@ + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index fdc1069fe..f6c448e8a 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -3,8 +3,10 @@ using System.Linq; using FizzWare.NBuilder; using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Organizer; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -738,5 +740,121 @@ public void should_use_existing_casing_for_release_group(string releaseGroup) Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be(releaseGroup); } + + [TestCase(8, "BT.601 NTSC", "BT.709", "South.Park.S15E06.City.Sushi")] + [TestCase(10, "BT.2020", "PQ", "South.Park.S15E06.City.Sushi.HDR")] + [TestCase(10, "BT.2020", "HLG", "South.Park.S15E06.City.Sushi.HDR")] + [TestCase(0, null, null, "South.Park.S15E06.City.Sushi")] + public void should_include_hdr_for_mediainfo_videodynamicrange_with_valid_properties(int bitDepth, string colourPrimaries, + string transferCharacteristics, string expectedName) + { + _namingConfig.StandardEpisodeFormat = + "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(videoBitDepth: bitDepth, videoColourPrimaries: colourPrimaries, videoTransferCharacteristics: transferCharacteristics); + + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile) + .Should().Be(expectedName); + } + + [Test] + public void should_update_media_info_if_token_configured_and_revision_is_old() + { + _namingConfig.StandardEpisodeFormat = + "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(schemaRevision: 3); + + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile); + + Mocker.GetMock().Verify(v => v.Update(_episodeFile, _series), Times.Once()); + } + + [Test] + public void should_not_update_media_info_if_token_not_configured_and_revision_is_old() + { + _namingConfig.StandardEpisodeFormat = + "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}"; + + GivenMediaInfoModel(schemaRevision: 3); + + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile); + + Mocker.GetMock().Verify(v => v.Update(_episodeFile, _series), Times.Never()); + } + + [Test] + public void should_not_update_media_info_if_token_configured_and_revision_is_current() + { + _namingConfig.StandardEpisodeFormat = + "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(schemaRevision: 5); + + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile); + + Mocker.GetMock().Verify(v => v.Update(_episodeFile, _series), Times.Never()); + } + + [Test] + public void should_not_update_media_info_if_token_configured_and_revision_is_newer() + { + _namingConfig.StandardEpisodeFormat = + "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(schemaRevision: 8); + + Subject.BuildFileName(new List {_episode1}, _series, _episodeFile); + + Mocker.GetMock().Verify(v => v.Update(_episodeFile, _series), Times.Never()); + } + + [TestCase("{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MediaInfo VideoDynamicRange}")] + [TestCase("{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MediaInfo.VideoDynamicRange}")] + public void should_use_updated_media_info_if_token_configured_and_revision_is_old(string standardEpisodeFormat) + { + _namingConfig.StandardEpisodeFormat = standardEpisodeFormat; + + GivenMediaInfoModel(schemaRevision: 3); + + Mocker.GetMock() + .Setup(u => u.Update(_episodeFile, _series)) + .Callback((EpisodeFile e, Series s) => e.MediaInfo = new MediaInfoModel + { + VideoCodec = "AVC", + AudioFormat = "DTS", + AudioChannels = 6, + AudioLanguages = "English", + Subtitles = "English/Spanish/Italian", + VideoBitDepth = 10, + VideoColourPrimaries = "BT.2020", + VideoTransferCharacteristics = "PQ", + SchemaRevision = 5 + }); + + var result = Subject.BuildFileName(new List {_episode1}, _series, _episodeFile); + + result.Should().EndWith("HDR"); + + } + + private void GivenMediaInfoModel(string videoCodec = "AVC", string audioCodec = "DTS", int audioChannels = 6, int videoBitDepth = 8, + string videoColourPrimaries = "", string videoTransferCharacteristics = "", string audioLanguages = "English", + string subtitles = "English/Spanish/Italian", int schemaRevision = 5) + { + _episodeFile.MediaInfo = new MediaInfoModel + { + VideoCodec = videoCodec, + AudioFormat = audioCodec, + AudioChannels = audioChannels, + AudioLanguages = audioLanguages, + Subtitles = subtitles, + VideoBitDepth = videoBitDepth, + VideoColourPrimaries = videoColourPrimaries, + VideoTransferCharacteristics = videoTransferCharacteristics, + SchemaRevision = schemaRevision + }; + + } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index f3ad44e87..89dd80466 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using System.IO; using System.Linq; using System.Text.RegularExpressions; using NLog; @@ -473,5 +472,27 @@ private static string GetSceneNameMatch(string sceneName, params string[] tokens // Last token is the default. return tokens.Last(); } + + private static readonly string[] ValidHdrTransferFunctions = {"PQ", "HLG"}; + private const string ValidHdrColourPrimaries = "BT.2020"; + + public static string FormatVideoDynamicRange(MediaInfoModel mediaInfo) + { + // assume SDR by default + var videoDynamicRange = ""; + + if (mediaInfo.VideoBitDepth >= 10 && + !string.IsNullOrEmpty(mediaInfo.VideoColourPrimaries) && + !string.IsNullOrEmpty(mediaInfo.VideoTransferCharacteristics)) + { + if (mediaInfo.VideoColourPrimaries.EqualsIgnoreCase(ValidHdrColourPrimaries) && + ValidHdrTransferFunctions.Any(mediaInfo.VideoTransferCharacteristics.Contains)) + { + videoDynamicRange = "HDR"; + } + } + + return videoDynamicRange; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs index 23026f507..5fcf952ca 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -10,7 +10,12 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo { - public class UpdateMediaInfoService : IHandle + public interface IUpdateMediaInfo + { + void Update(EpisodeFile episodeFile, Series series); + } + + public class UpdateMediaInfoService : IHandle, IUpdateMediaInfo { private readonly IDiskProvider _diskProvider; private readonly IMediaFileService _mediaFileService; @@ -31,28 +36,6 @@ public UpdateMediaInfoService(IDiskProvider diskProvider, _logger = logger; } - private void UpdateMediaInfo(Series series, List mediaFiles) - { - foreach (var mediaFile in mediaFiles) - { - var path = Path.Combine(series.Path, mediaFile.RelativePath); - - if (!_diskProvider.FileExists(path)) - { - _logger.Debug("Can't update MediaInfo because '{0}' does not exist", path); - continue; - } - - mediaFile.MediaInfo = _videoFileInfoReader.GetMediaInfo(path); - - if (mediaFile.MediaInfo != null) - { - _mediaFileService.Update(mediaFile); - _logger.Debug("Updated MediaInfo for '{0}'", path); - } - } - } - public void Handle(SeriesScannedEvent message) { if (!_configService.EnableMediaInfo) @@ -62,9 +45,44 @@ public void Handle(SeriesScannedEvent message) } var allMediaFiles = _mediaFileService.GetFilesBySeries(message.Series.Id); - var filteredMediaFiles = allMediaFiles.Where(c => c.MediaInfo == null || c.MediaInfo.SchemaRevision < VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION).ToList(); + var filteredMediaFiles = allMediaFiles.Where(c => + c.MediaInfo == null || + c.MediaInfo.SchemaRevision < VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION).ToList(); - UpdateMediaInfo(message.Series, filteredMediaFiles); + foreach (var mediaFile in filteredMediaFiles) + { + UpdateMediaInfo(mediaFile, message.Series); + } + } + + public void Update(EpisodeFile episodeFile, Series series) + { + if (!_configService.EnableMediaInfo) + { + _logger.Debug("MediaInfo is disabled"); + return; + } + UpdateMediaInfo(episodeFile, series); + } + + private void UpdateMediaInfo(EpisodeFile episodeFile, Series series) + { + var path = Path.Combine(series.Path, episodeFile.RelativePath); + + if (!_diskProvider.FileExists(path)) + { + _logger.Debug("Can't update MediaInfo because '{0}' does not exist", path); + return; + } + + var updatedMediaInfo = _videoFileInfoReader.GetMediaInfo(path); + + if (updatedMediaInfo != null) + { + episodeFile.MediaInfo = updatedMediaInfo; + _mediaFileService.Update(episodeFile); + _logger.Debug("Updated MediaInfo for '{0}'", path); + } } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f4cf43509..30517658b 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -33,6 +33,7 @@ public class FileNameBuilder : IBuildFileNames private readonly INamingConfigService _namingConfigService; private readonly IQualityDefinitionService _qualityDefinitionService; private readonly IPreferredWordService _preferredWordService; + private readonly IUpdateMediaInfo _mediaInfoUpdater; private readonly ICached _episodeFormatCache; private readonly ICached _absoluteEpisodeFormatCache; private readonly ICached _requiresEpisodeTitleCache; @@ -81,11 +82,13 @@ public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, ICacheManager cacheManager, IPreferredWordService preferredWordService, + IUpdateMediaInfo mediaInfoUpdater, Logger logger) { _namingConfigService = namingConfigService; _qualityDefinitionService = qualityDefinitionService; _preferredWordService = preferredWordService; + _mediaInfoUpdater = mediaInfoUpdater; _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); _requiresEpisodeTitleCache = cacheManager.GetCache(GetType(), "requiresEpisodeTitle"); @@ -138,6 +141,8 @@ public string BuildFileName(List episodes, Series series, EpisodeFile e pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); + UpdateMediaInfoIfNeeded(pattern, episodeFile, series); + AddSeriesTokens(tokenHandlers, series); AddIdTokens(tokenHandlers, series); AddEpisodeTokens(tokenHandlers, episodes); @@ -146,6 +151,7 @@ public string BuildFileName(List episodes, Series series, EpisodeFile e AddMediaInfoTokens(tokenHandlers, episodeFile); AddPreferredWords(tokenHandlers, series, episodeFile, preferredWords); + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); @@ -535,6 +541,13 @@ private void AddQualityTokens(Dictionary> token tokenHandlers["{Quality Real}"] = m => qualityReal; } + private const string MediaInfoVideoDynamicRangeToken = "{MediaInfo VideoDynamicRange}"; + private static readonly IDictionary MinimumMediaInfoSchemaRevisions = + new Dictionary(FileNameBuilderTokenEqualityComparer.Instance) + { + {MediaInfoVideoDynamicRangeToken, 5} + }; + private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) { if (episodeFile.MediaInfo == null) @@ -549,8 +562,10 @@ private void AddMediaInfoTokens(Dictionary> tok var videoCodec = MediaInfoFormatter.FormatVideoCodec(episodeFile.MediaInfo, sceneName); var audioCodec = MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo, sceneName); var audioChannels = MediaInfoFormatter.FormatAudioChannels(episodeFile.MediaInfo); + var audioLanguages = episodeFile.MediaInfo.AudioLanguages ?? string.Empty; + var subtitles = episodeFile.MediaInfo.Subtitles ?? string.Empty; - var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); + var mediaInfoAudioLanguages = GetLanguagesToken(audioLanguages); if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) { mediaInfoAudioLanguages = $"[{mediaInfoAudioLanguages}]"; @@ -561,7 +576,7 @@ private void AddMediaInfoTokens(Dictionary> tok mediaInfoAudioLanguages = string.Empty; } - var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); + var mediaInfoSubtitleLanguages = GetLanguagesToken(subtitles); if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) { mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]"; @@ -586,6 +601,9 @@ private void AddMediaInfoTokens(Dictionary> tok tokenHandlers["{MediaInfo Simple}"] = m => $"{videoCodec} {audioCodec}"; tokenHandlers["{MediaInfo Full}"] = m => $"{videoCodec} {audioCodec}{mediaInfoAudioLanguages} {mediaInfoSubtitleLanguages}"; + + tokenHandlers[MediaInfoVideoDynamicRangeToken] = + m => MediaInfoFormatter.FormatVideoDynamicRange(episodeFile.MediaInfo); } private void AddIdTokens(Dictionary> tokenHandlers, Series series) @@ -614,7 +632,7 @@ private string GetLanguagesToken(string mediaInfoLanguages) tokens.Add(item.Trim()); } - var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); + var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); for (int i = 0; i < tokens.Count; i++) { try @@ -632,6 +650,21 @@ private string GetLanguagesToken(string mediaInfoLanguages) return string.Join("+", tokens.Distinct()); } + private void UpdateMediaInfoIfNeeded(string pattern, EpisodeFile episodeFile, Series series) + { + var schemaRevision = episodeFile.MediaInfo != null ? episodeFile.MediaInfo.SchemaRevision : 0; + var matches = TitleRegex.Matches(pattern); + + var shouldUpdateMediaInfo = matches.Cast() + .Select(m => MinimumMediaInfoSchemaRevisions.GetValueOrDefault(m.Value, -1)) + .Any(r => schemaRevision < r); + + if (shouldUpdateMediaInfo) + { + _mediaInfoUpdater.Update(episodeFile, series); + } + } + private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) { return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig));