diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index f95e3171f..d473c9eaa 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -9,6 +9,8 @@ using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Movies; +using NzbDrone.Core.MediaFiles.MediaInfo; +using Moq; namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { @@ -305,26 +307,6 @@ public void should_format_mediainfo_3d_properly() .Should().Be("South.Park.3D.h264.DTS"); } - [Test] - public void should_format_mediainfo_hdr_properly() - { - _namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.HDR}.{MediaInfo.Simple}"; - - _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() - { - VideoFormat = "AVC", - VideoBitDepth = 10, - VideoColourPrimaries = "BT.2020", - VideoTransferCharacteristics = "PQ", - AudioFormat = "DTS", - AudioLanguages = "English", - Subtitles = "English/Spanish/Italian" - }; - - Subject.BuildFileName(_movie, _movieFile) - .Should().Be("South.Park.HDR.h264.DTS"); - } - [Test] public void should_remove_duplicate_non_word_characters() { @@ -506,6 +488,138 @@ public void should_use_existing_casing_for_release_group(string releaseGroup) .Should().Be(releaseGroup); } + [TestCase("English", "")] + [TestCase("English/German", "[EN+DE]")] + public void should_format_audio_languages(string audioLanguages, string expected) + { + _movieFile.ReleaseGroup = null; + + GivenMediaInfoModel(audioLanguages: audioLanguages); + + + _namingConfig.StandardMovieFormat = "{MediaInfo AudioLanguages}"; + + + Subject.BuildFileName( _movie , _movieFile) + .Should().Be(expected); + } + + [TestCase("English", "[EN]")] + [TestCase("English/German", "[EN+DE]")] + public void should_format_audio_languages_all(string audioLanguages, string expected) + { + _movieFile.ReleaseGroup = null; + + GivenMediaInfoModel(audioLanguages: audioLanguages); + + + _namingConfig.StandardMovieFormat = "{MediaInfo AudioLanguagesAll}"; + + + Subject.BuildFileName( _movie , _movieFile) + .Should().Be(expected); + } + + [TestCase(8, "BT.601 NTSC", "BT.709", "South.Park")] + [TestCase(10, "BT.2020", "PQ", "South.Park.HDR")] + [TestCase(10, "BT.2020", "HLG", "South.Park.HDR")] + [TestCase(0, null, null, "South.Park")] + public void should_include_hdr_for_mediainfo_videodynamicrange_with_valid_properties(int bitDepth, string colourPrimaries, + string transferCharacteristics, string expectedName) + { + _namingConfig.StandardMovieFormat = + "{Movie.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(videoBitDepth: bitDepth, videoColourPrimaries: colourPrimaries, videoTransferCharacteristics: transferCharacteristics); + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be(expectedName); + } + + [Test] + public void should_update_media_info_if_token_configured_and_revision_is_old() + { + _namingConfig.StandardMovieFormat = + "{Movie.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(schemaRevision: 3); + + Subject.BuildFileName( _movie, _movieFile); + + Mocker.GetMock().Verify(v => v.Update(_movieFile, _movie), Times.Once()); + } + + [Test] + public void should_not_update_media_info_if_no_movie_path_available() + { + _namingConfig.StandardMovieFormat = + "{Movie.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(schemaRevision: 3); + _movie.Path = null; + + Subject.BuildFileName( _movie, _movieFile); + + Mocker.GetMock().Verify(v => v.Update(_movieFile, _movie), Times.Never()); + } + + [Test] + public void should_not_update_media_info_if_token_not_configured_and_revision_is_old() + { + _namingConfig.StandardMovieFormat = + "{Movie.Title}"; + + GivenMediaInfoModel(schemaRevision: 3); + + Subject.BuildFileName( _movie, _movieFile); + + Mocker.GetMock().Verify(v => v.Update(_movieFile, _movie), Times.Never()); + } + + [Test] + public void should_not_update_media_info_if_token_configured_and_revision_is_current() + { + _namingConfig.StandardMovieFormat = + "{Movie.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(schemaRevision: 5); + + Subject.BuildFileName( _movie, _movieFile); + + Mocker.GetMock().Verify(v => v.Update(_movieFile, _movie), Times.Never()); + } + + [Test] + public void should_not_update_media_info_if_token_configured_and_revision_is_newer() + { + _namingConfig.StandardMovieFormat = + "{Movie.Title}.{MediaInfo VideoDynamicRange}"; + + GivenMediaInfoModel(schemaRevision: 8); + + Subject.BuildFileName(_movie, _movieFile); + + Mocker.GetMock().Verify(v => v.Update(_movieFile, _movie), Times.Never()); + } + + 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) + { + _movieFile.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/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs index 0c10a33c8..4cc1325a1 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(MovieFile movieFile, Movie movie); + } + + 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(Movie movie, List mediaFiles) - { - foreach (var mediaFile in mediaFiles) - { - var path = Path.Combine(movie.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(MovieScannedEvent message) { if (!_configService.EnableMediaInfo) @@ -62,9 +45,45 @@ public void Handle(MovieScannedEvent message) } var allMediaFiles = _mediaFileService.GetFilesByMovie(message.Movie.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.Movie, filteredMediaFiles); + foreach (var mediaFile in filteredMediaFiles) + { + UpdateMediaInfo(mediaFile, message.Movie); + } + } + + public void Update(MovieFile movieFile, Movie movie) + { + if (!_configService.EnableMediaInfo) + { + _logger.Debug("MediaInfo is disabled"); + return; + } + + UpdateMediaInfo(movieFile, movie); + } + + private void UpdateMediaInfo(MovieFile movieFile, Movie movie) + { + var path = Path.Combine(movie.Path, movieFile.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) + { + movieFile.MediaInfo = updatedMediaInfo; + _mediaFileService.Update(movieFile); + _logger.Debug("Updated MediaInfo for '{0}'", path); + } } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 99a8a10ac..d0c16c029 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -28,8 +28,7 @@ public class FileNameBuilder : IBuildFileNames { private readonly INamingConfigService _namingConfigService; private readonly IQualityDefinitionService _qualityDefinitionService; - private readonly ICached _episodeFormatCache; - private readonly ICached _absoluteEpisodeFormatCache; + private readonly IUpdateMediaInfo _mediaInfoUpdater; private readonly Logger _logger; private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", @@ -65,14 +64,12 @@ public class FileNameBuilder : IBuildFileNames public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, - ICacheManager cacheManager, + IUpdateMediaInfo mediaInfoUpdater, Logger logger) { _namingConfigService = namingConfigService; _qualityDefinitionService = qualityDefinitionService; - //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); - _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); - _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); + _mediaInfoUpdater = mediaInfoUpdater; _logger = logger; } @@ -91,6 +88,8 @@ public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namin var pattern = namingConfig.StandardMovieFormat; var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + UpdateMediaInfoIfNeeded(pattern, movieFile, movie); + AddMovieTokens(tokenHandlers, movie); AddReleaseDateTokens(tokenHandlers, movie.Year); AddImdbIdTokens(tokenHandlers, movie.ImdbId); @@ -312,34 +311,44 @@ 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, MovieFile movieFile) { - if (movieFile.MediaInfo == null) return; + if (movieFile.MediaInfo == null) + { + _logger.Trace("Media info is unavailable for {0}", movieFile); + + return; + } var sceneName = movieFile.GetSceneOrFileName(); + var videoCodec = MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName); var audioCodec = MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName); var audioChannels = MediaInfoFormatter.FormatAudioChannels(movieFile.MediaInfo); + var audioLanguages = movieFile.MediaInfo.AudioLanguages ?? string.Empty; + var subtitles = movieFile.MediaInfo.Subtitles ?? string.Empty; - // Workaround until https://github.com/MediaArea/MediaInfo/issues/299 is fixed and release - if (audioCodec.EqualsIgnoreCase("DTS-X")) - { - audioChannels = audioChannels - 1 + 0.1m; - } - - var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); + var mediaInfoAudioLanguages = GetLanguagesToken(audioLanguages); if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) { mediaInfoAudioLanguages = $"[{mediaInfoAudioLanguages}]"; } + var mediaInfoAudioLanguagesAll = mediaInfoAudioLanguages; if (mediaInfoAudioLanguages == "[EN]") { mediaInfoAudioLanguages = string.Empty; } - var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); + var mediaInfoSubtitleLanguages = GetLanguagesToken(subtitles); if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) { mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]"; @@ -347,25 +356,11 @@ private void AddMediaInfoTokens(Dictionary> tok var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; var audioChannelsFormatted = audioChannels > 0 ? - audioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; + audioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; var mediaInfo3D = movieFile.MediaInfo.VideoMultiViewCount > 1 ? "3D" : string.Empty; - var videoColourPrimaries = movieFile.MediaInfo.VideoColourPrimaries ?? string.Empty; - var videoTransferCharacteristics = movieFile.MediaInfo.VideoTransferCharacteristics ?? string.Empty; - var mediaInfoHDR = string.Empty; - - if (movieFile.MediaInfo.VideoBitDepth >= 10 && !videoColourPrimaries.IsNullOrWhiteSpace() && !videoTransferCharacteristics.IsNullOrWhiteSpace()) - { - string[] validTransferFunctions = new string[] { "PQ", "HLG" }; - - if (videoColourPrimaries.EqualsIgnoreCase("BT.2020") && validTransferFunctions.Any(videoTransferCharacteristics.Contains)) - { - mediaInfoHDR = "HDR"; - } - } - tokenHandlers["{MediaInfo Video}"] = m => videoCodec; tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; @@ -377,12 +372,15 @@ private void AddMediaInfoTokens(Dictionary> tok tokenHandlers["{MediaInfo AudioLanguagesAll}"] = m => mediaInfoAudioLanguagesAll; tokenHandlers["{MediaInfo SubtitleLanguages}"] = m => mediaInfoSubtitleLanguages; + tokenHandlers["{MediaInfo SubtitleLanguagesAll}"] = m => mediaInfoSubtitleLanguages; tokenHandlers["{MediaInfo 3D}"] = m => mediaInfo3D; - tokenHandlers["{MediaInfo HDR}"] = m => mediaInfoHDR; tokenHandlers["{MediaInfo Simple}"] = m => $"{videoCodec} {audioCodec}"; tokenHandlers["{MediaInfo Full}"] = m => $"{videoCodec} {audioCodec}{mediaInfoAudioLanguages} {mediaInfoSubtitleLanguages}"; + + tokenHandlers[MediaInfoVideoDynamicRangeToken] = + m => MediaInfoFormatter.FormatVideoDynamicRange(movieFile.MediaInfo); } private string GetLanguagesToken(string mediaInfoLanguages) @@ -394,7 +392,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 @@ -412,6 +410,26 @@ private string GetLanguagesToken(string mediaInfoLanguages) return string.Join("+", tokens.Distinct()); } + private void UpdateMediaInfoIfNeeded(string pattern, MovieFile movieFile, Movie movie) + { + if (movie.Path.IsNullOrWhiteSpace()) + { + return; + } + + var schemaRevision = movieFile.MediaInfo != null ? movieFile.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(movieFile, movie); + } + } + private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) { return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig));