From 0d782e1cac528c1f9e73b39816931e102f0de0d5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 19 May 2017 11:18:50 -0700 Subject: [PATCH] Consistent formatting for MediaInfo in various locations Fixed: Stream details for MP3 and EAC3 in Kodi metadata Closes #1534 --- .../FormatAudioChannelsFixture.cs} | 21 ++-- .../FormatAudioCodecFixture.cs | 49 ++++++++++ .../FormatVideoCodecFixture.cs | 40 ++++++++ .../NzbDrone.Core.Test.csproj | 4 +- .../Metadata/Consumers/Xbmc/XbmcMetadata.cs | 17 +--- .../MediaInfo/MediaInfoFormatter.cs | 98 +++++++++++++++++++ .../MediaFiles/MediaInfo/MediaInfoModel.cs | 28 ------ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Organizer/FileNameBuilder.cs | 84 +++------------- 9 files changed, 218 insertions(+), 124 deletions(-) rename src/NzbDrone.Core.Test/MediaFiles/MediaInfo/{FormattedAudioChannelsFixture.cs => MediaInfoFormatterTests/FormatAudioChannelsFixture.cs} (78%) create mode 100644 src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs create mode 100644 src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs create mode 100644 src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/FormattedAudioChannelsFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs similarity index 78% rename from src/NzbDrone.Core.Test/MediaFiles/MediaInfo/FormattedAudioChannelsFixture.cs rename to src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs index c344c0906..4c565585f 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/FormattedAudioChannelsFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs @@ -1,11 +1,12 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; -namespace NzbDrone.Core.Test.MediaFiles.MediaInfo +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests { [TestFixture] - public class FormattedAudioChannelsFixture + public class FormatAudioChannelsFixture : TestBase { [Test] public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText() @@ -17,7 +18,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo AudioChannelPositionsText = "Front: L C R, Side: L R, LFE" }; - mediaInfoModel.FormattedAudioChannels.Should().Be(5.1m); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); } [Test] @@ -30,7 +31,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo AudioChannelPositionsText = "Front: L R" }; - mediaInfoModel.FormattedAudioChannels.Should().Be(2); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); } [Test] @@ -44,7 +45,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo SchemaRevision = 2 }; - mediaInfoModel.FormattedAudioChannels.Should().Be(0); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0); } [Test] @@ -58,7 +59,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo SchemaRevision = 3 }; - mediaInfoModel.FormattedAudioChannels.Should().Be(2); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); } [Test] @@ -72,7 +73,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo SchemaRevision = 3 }; - mediaInfoModel.FormattedAudioChannels.Should().Be(2); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); } [Test] @@ -86,7 +87,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo SchemaRevision = 3 }; - mediaInfoModel.FormattedAudioChannels.Should().Be(5.1m); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); } [Test] @@ -100,7 +101,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo SchemaRevision = 3 }; - mediaInfoModel.FormattedAudioChannels.Should().Be(7.1m); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); } [Test] @@ -114,7 +115,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo SchemaRevision = 3 }; - mediaInfoModel.FormattedAudioChannels.Should().Be(7.1m); + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs new file mode 100644 index 000000000..fd4949c60 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatAudioCodecFixture : TestBase + { + [TestCase("AC-3", "AC3")] + [TestCase("E-AC-3", "EAC3")] + [TestCase("MPEG Audio", "MPEG Audio")] + [TestCase("DTS", "DTS")] + public void should_format_audio_format(string audioFormat, string expectedFormat) + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = audioFormat + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(expectedFormat); + } + + [Test] + public void should_return_MP3_for_MPEG_Audio_with_Layer_3_for_the_profile() + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = "MPEG Audio", + AudioProfile = "Layer 3" + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be("MP3"); + } + + [Test] + public void should_return_AudioFormat_by_default() + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = "Other Audio Format" + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(mediaInfoModel.AudioFormat); + ExceptionVerification.ExpectedErrors(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs new file mode 100644 index 000000000..d4345b026 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatVideoCodecFixture : TestBase + { + [TestCase("AVC", null, "x264")] + [TestCase("AVC", "source.title.x264.720p-Sonarr", "x264")] + [TestCase("AVC", "source.title.h264.720p-Sonarr", "h264")] + [TestCase("V_MPEGH/ISO/HEVC", null, "x265")] + [TestCase("V_MPEGH/ISO/HEVC", "source.title.x265.720p-Sonarr", "x265")] + [TestCase("V_MPEGH/ISO/HEVC", "source.title.h265.720p-Sonarr", "h265")] + [TestCase("MPEG-2 Video", null, "MPEG2")] + public void should_format_video_codec_with_source_title(string videoCodec, string sceneName, string expectedFormat) + { + var mediaInfoModel = new MediaInfoModel + { + VideoCodec = videoCodec + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [Test] + public void should_return_VideoCodec_by_default() + { + var mediaInfoModel = new MediaInfoModel + { + VideoCodec = "VideoCodec" + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, null).Should().Be(mediaInfoModel.VideoCodec); + ExceptionVerification.ExpectedErrors(1); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 9b325cfb8..c5254bc2f 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -291,7 +291,9 @@ - + + + diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index 75a80225f..85485bc87 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -11,6 +11,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc @@ -247,7 +248,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc var video = new XElement("video"); video.Add(new XElement("aspect", (float)episodeFile.MediaInfo.Width / (float)episodeFile.MediaInfo.Height)); video.Add(new XElement("bitrate", episodeFile.MediaInfo.VideoBitrate)); - video.Add(new XElement("codec", episodeFile.MediaInfo.VideoCodec)); + video.Add(new XElement("codec", MediaInfoFormatter.FormatVideoCodec(episodeFile.MediaInfo, episodeFile.SceneName))); video.Add(new XElement("framerate", episodeFile.MediaInfo.VideoFps)); video.Add(new XElement("height", episodeFile.MediaInfo.Height)); video.Add(new XElement("scantype", episodeFile.MediaInfo.ScanType)); @@ -264,11 +265,11 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc var audio = new XElement("audio"); audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate)); audio.Add(new XElement("channels", episodeFile.MediaInfo.AudioChannels)); - audio.Add(new XElement("codec", GetAudioCodec(episodeFile.MediaInfo.AudioFormat))); + audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo))); audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages)); streamDetails.Add(audio); - if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Length > 0) + if (episodeFile.MediaInfo.Subtitles.IsNotNullOrWhiteSpace()) { var subtitle = new XElement("subtitle"); subtitle.Add(new XElement("language", episodeFile.MediaInfo.Subtitles)); @@ -379,15 +380,5 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc { return Path.ChangeExtension(episodeFilePath, "").Trim('.') + "-thumb.jpg"; } - - private string GetAudioCodec(string audioCodec) - { - if (audioCodec == "AC-3") - { - return "AC3"; - } - - return audioCodec; - } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs new file mode 100644 index 000000000..f7ebcb0ae --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -0,0 +1,98 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; + +namespace NzbDrone.Core.MediaFiles.MediaInfo +{ + public static class MediaInfoFormatter + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfoFormatter)); + + public static decimal FormatAudioChannels(MediaInfoModel mediaInfo) + { + var audioChannelPositions = mediaInfo.AudioChannelPositions; + var audioChannelPositionsText = mediaInfo.AudioChannelPositionsText; + var audioChannels = mediaInfo.AudioChannels; + + if (audioChannelPositions.IsNullOrWhiteSpace()) + { + if (audioChannelPositionsText.IsNullOrWhiteSpace()) + { + if (mediaInfo.SchemaRevision >= 3) + { + return audioChannels; + } + + return 0; + } + + return mediaInfo.AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? audioChannels - 1 + 0.1m : audioChannels; + } + + return audioChannelPositions.Replace("Object Based / ", "") + .Split(new string[] { " / " }, StringSplitOptions.None) + .First() + .Split('/') + .Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); + } + + public static string FormatAudioCodec(MediaInfoModel mediaInfo) + { + var audioFormat = mediaInfo.AudioFormat; + + if (audioFormat == "AC-3") + { + return "AC3"; + } + + if (audioFormat == "E-AC-3") + { + return "EAC3"; + } + + if (audioFormat == "MPEG Audio") + { + return mediaInfo.AudioProfile == "Layer 3" ? "MP3" : audioFormat; + } + + if (audioFormat == "DTS") + { + return "DTS"; + } + + Logger.Error("Unknown audio format: {0}", audioFormat); + return audioFormat; + } + + public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName) + { + var videoCodec = mediaInfo.VideoCodec; + + if (videoCodec == "AVC") + { + return sceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(sceneName).Contains("h264") + ? "h264" + : "x264"; + } + + if (videoCodec == "V_MPEGH/ISO/HEVC") + { + return sceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(sceneName).Contains("h265") + ? "h265" + : "x265"; + } + + if (videoCodec == "MPEG-2 Video") + { + return "MPEG2"; + } + + Logger.Error("Unknown video codec: {0}", videoCodec); + return videoCodec; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index af02288e8..26a2e7235 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -27,33 +27,5 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo public string Subtitles { get; set; } public string ScanType { get; set; } public int SchemaRevision { get; set; } - - [JsonIgnore] - public decimal FormattedAudioChannels - { - get - { - if (AudioChannelPositions.IsNullOrWhiteSpace()) - { - if (AudioChannelPositionsText.IsNullOrWhiteSpace()) - { - if (SchemaRevision >= 3) - { - return AudioChannels; - } - - return 0; - } - - return AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? AudioChannels - 1 + 0.1m : AudioChannels; - } - - return AudioChannelPositions.Replace("Object Based / ", "") - .Split(new string[] { " / " }, StringSplitOptions.None) - .First() - .Split('/') - .Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); - } - } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 62f7edd61..22106bc73 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -783,6 +783,7 @@ Code + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 31cbd53ef..890c067b1 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -9,6 +9,7 @@ using NzbDrone.Common.Cache; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -445,75 +446,14 @@ namespace NzbDrone.Core.Organizer { if (episodeFile.MediaInfo == null) return; - string videoCodec; - switch (episodeFile.MediaInfo.VideoCodec) - { - case "AVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = episodeFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (episodeFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "MPEG Audio": - if (episodeFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = episodeFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - - default: - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - } + var videoCodec = MediaInfoFormatter.FormatVideoCodec(episodeFile.MediaInfo, episodeFile.SceneName); + var audioCodec = MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo); + var audioChannels = MediaInfoFormatter.FormatAudioChannels(episodeFile.MediaInfo); var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + mediaInfoAudioLanguages = $"[{mediaInfoAudioLanguages}]"; } if (mediaInfoAudioLanguages == "[EN]") @@ -524,12 +464,12 @@ namespace NzbDrone.Core.Organizer var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]"; } var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = episodeFile.MediaInfo.FormattedAudioChannels > 0 ? - episodeFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + var audioChannelsFormatted = audioChannels > 0 ? + audioChannels.ToString("F1", CultureInfo.InvariantCulture) : string.Empty; tokenHandlers["{MediaInfo Video}"] = m => videoCodec; @@ -538,11 +478,11 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannelsFormatted; - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + tokenHandlers["{MediaInfo Simple}"] = m => $"{videoCodec} {audioCodec}"; - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + tokenHandlers["{MediaInfo Full}"] = m => $"{videoCodec} {audioCodec}{mediaInfoAudioLanguages} {mediaInfoSubtitleLanguages}"; } private string GetLanguagesToken(string mediaInfoLanguages)