diff --git a/src/Libraries/MediaInfo/MediaInfo.dll b/src/Libraries/MediaInfo/MediaInfo.dll index 24e6cb986..3de8a750f 100644 Binary files a/src/Libraries/MediaInfo/MediaInfo.dll and b/src/Libraries/MediaInfo/MediaInfo.dll differ diff --git a/src/Libraries/MediaInfo/libmediainfo.0.dylib b/src/Libraries/MediaInfo/libmediainfo.0.dylib index 5e5383ded..73ff0ba4f 100644 Binary files a/src/Libraries/MediaInfo/libmediainfo.0.dylib and b/src/Libraries/MediaInfo/libmediainfo.0.dylib differ diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 247274e29..687a7e6de 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -78,6 +78,21 @@ namespace NzbDrone.Common.Extensions return !string.IsNullOrWhiteSpace(text); } + public static bool StartsWithIgnoreCase(this string text, string startsWith) + { + return text.StartsWith(startsWith, StringComparison.InvariantCultureIgnoreCase); + } + + public static bool EndsWithIgnoreCase(this string text, string startsWith) + { + return text.EndsWith(startsWith, StringComparison.InvariantCultureIgnoreCase); + } + + public static bool EqualsIgnoreCase(this string text, string equals) + { + return text.Equals(equals, StringComparison.InvariantCultureIgnoreCase); + } + public static bool ContainsIgnoreCase(this string text, string contains) { return text.IndexOf(contains, StringComparison.InvariantCultureIgnoreCase) > -1; @@ -118,4 +133,4 @@ namespace NzbDrone.Common.Extensions return Encoding.ASCII.GetString(new [] { byteResult }); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs new file mode 100644 index 000000000..7a80c9af4 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs @@ -0,0 +1,177 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatAudioChannelsFixture : TestBase + { + [Test] + public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 6, + AudioChannelPositions = null, + AudioChannelPositionsText = "Front: L C R, Side: L R, LFE" + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = "Front: L R" + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = null, + SchemaRevision = 2 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0); + } + + [Test] + public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = null, + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "2/0/0", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_AudioChannelPositions_including_decimal() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "3/2/0.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_cleanup_extraneous_text_from_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "Object Based / 3/2/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); + } + + [Test] + public void should_skip_empty_groups_in_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = " / 2/0/0.0", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); + } + + [Test] + public void should_sum_first_series_of_numbers_from_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "3/2/2.1 / 3/2/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); + } + + [Test] + public void should_sum_dual_mono_representation_AudioChannelPositions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "1+1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m); + } + + [Test] + public void should_use_AudioChannelPositionText_when_AudioChannelChannelPosition_is_invalid() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 6, + AudioChannelPositions = "15 objects", + AudioChannelPositionsText = "15 objects / Front: L C R, Side: L R, LFE", + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); + } + + [Test] + public void should_remove_atmos_objects_from_AudioChannelPostions() + { + var mediaInfoModel = new MediaInfoModel + { + AudioChannels = 2, + AudioChannelPositions = "15 objects / 3/2.1", + AudioChannelPositionsText = null, + SchemaRevision = 3 + }; + + MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.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..cfdbb02da --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs @@ -0,0 +1,73 @@ +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 + { + private static string sceneName = "My.Series.S01E01-Sonarr"; + + [TestCase("AC-3", "AC3")] + [TestCase("E-AC-3", "EAC3")] + [TestCase("MPEG Audio", "MPEG Audio")] + [TestCase("DTS", "DTS")] + public void should_format_audio_format_legacy(string audioFormat, string expectedFormat) + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = audioFormat + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [TestCase("MPEG Audio, A_MPEG/L2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")] + [TestCase("Vorbis, A_VORBIS, , Xiph.Org libVorbis I 20101101 (Schaufenugget)", "DB Super HDTV", "Vorbis")] + [TestCase("PCM, 1, , ", "DW DVDRip XviD-idTV", "PCM")] // Dubbed most likely + [TestCase("TrueHD, A_TRUEHD, , ", "", "TrueHD")] + [TestCase("WMA, 161, , ", "Droned.wmv", "WMA")] + [TestCase("WMA, 162, Pro, ", "B.N.S04E18.720p.WEB-DL", "WMA")] + [TestCase("Opus, A_OPUS, , ", "Roadkill Ep3x11 - YouTube.webm", "Opus")] + [TestCase("mp3 , 0, , ", "climbing.mp4", "MP3")] + public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat) + { + var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = split[0], + AudioCodecID = split[1], + AudioProfile = split[2], + AudioCodecLibrary = split[3] + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).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, sceneName).Should().Be("MP3"); + } + + [Test] + public void should_return_AudioFormat_by_default() + { + var mediaInfoModel = new MediaInfoModel + { + AudioFormat = "Other Audio Format", + AudioCodecID = "Other Audio Codec" + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(mediaInfoModel.AudioFormat); + } + } +} 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..52aeabff8 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs @@ -0,0 +1,92 @@ +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, "h264")] + [TestCase("AVC", "source.title.x264.720p-Sonarr", "x264")] + [TestCase("AVC", "source.title.h264.720p-Sonarr", "h264")] + [TestCase("V_MPEGH/ISO/HEVC", null, "h265")] + [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_legacy(string videoCodec, string sceneName, string expectedFormat) + { + var mediaInfoModel = new MediaInfoModel + { + VideoFormat = videoCodec + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [TestCase("MPEG Video, 2, Main@High, ", "Droned.S01E02.1080i.HDTV.DD5.1.MPEG2-NTb", "MPEG2")] + [TestCase("MPEG Video, V_MPEG2, Main@High, ", "", "MPEG2")] + [TestCase("MPEG Video, , , ", "The.Simpsons.S13E04.INTERNAL-ANiVCD.mpg", "MPEG")] + [TestCase("VC-1, WVC1, Advanced@L4, ", "B.N.S04E18.720p.WEB-DL", "VC1")] + [TestCase("VC-1, V_MS/VFW/FOURCC / WVC1, Advanced@L3, ", "", "VC1")] + [TestCase("VC-1, WMV3, MP@LL, ", "It's Always Sunny S07E13 The Gang's RevengeHDTV.XviD-2HD.avi", "VC1")] + [TestCase("V.MPEG4/ISO/AVC, V.MPEG4/ISO/AVC, , ", "pd.2015.S03E08.720p.iP.WEBRip.AAC2.0.H264-BTW", "h264")] + [TestCase("WMV2, WMV2, , ", "Droned.wmv", "WMV")] + [TestCase("xvid, xvid, , ", "", "XviD")] + [TestCase("div3, div3, , ", "spsm.dvdrip.divx.avi'.", "DivX")] + [TestCase("VP6, 4, , ", "Top Gear - S12E01 - Lorries - SD TV.flv", "VP6")] + [TestCase("VP7, VP70, General, ", "Sweet Seymour.avi", "VP7")] + [TestCase("VP8, V_VP8, , ", "Dick.mkv", "VP8")] + [TestCase("VP9, V_VP9, , ", "Roadkill Ep3x11 - YouTube.webm", "VP9")] + [TestCase("x264, x264, , ", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")] + [TestCase("V_MPEGH/ISO/HEVC, V_MPEGH/ISO/HEVC, , ", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")] + [TestCase("MPEG-4 Visual, 20, Simple@L1, Lavc52.29.0", "Will.And.Grace.S08E14.WS.DVDrip.XviD.I.Love.L.Gay-Obfuscated", "XviD")] + [TestCase("MPEG-4 Visual, 20, Advanced Simple@L5, XviD0046", "", "XviD")] + [TestCase("mp4v, mp4v, , ", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")] + public void should_format_video_format(string videoFormatPack, string sceneName, string expectedFormat) + { + var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); + var mediaInfoModel = new MediaInfoModel + { + VideoFormat = split[0], + VideoCodecID = split[1], + VideoProfile = split[2], + VideoCodecLibrary = split[3] + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [TestCase("AVC, AVC, , x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag + [TestCase("HEVC, HEVC, , x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag + [TestCase("AVC, AVC, , ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown + [TestCase("HEVC, HEVC, , ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown + [TestCase("AVC, AVC, , ", "Some.Video.S01E01", "h264")] // Default value + [TestCase("HEVC, HEVC, , ", "Some.Video.S01E01", "h265")] // Default value + public void should_format_video_format_fallbacks(string videoFormatPack, string sceneName, string expectedFormat) + { + var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); + var mediaInfoModel = new MediaInfoModel + { + VideoFormat = split[0], + VideoCodecID = split[1], + VideoProfile = split[2], + VideoCodecLibrary = split[3] + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); + } + + [Test] + public void should_return_VideoFormat_by_default() + { + var mediaInfoModel = new MediaInfoModel + { + VideoFormat = "VideoCodec" + }; + + MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, null).Should().Be(mediaInfoModel.VideoFormat); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs index 10f78375f..c41d09ba4 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/UpdateMediaInfoServiceFixture.cs @@ -16,15 +16,15 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo [TestFixture] public class UpdateMediaInfoServiceFixture : CoreTest { - private Movie _series; + private Movie _movie; [SetUp] public void Setup() { - _series = new Movie + _movie = new Movie { Id = 1, - Path = @"C:\series".AsOsAgnostic() + Path = @"C:\movie".AsOsAgnostic() }; Mocker.GetMock() @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo .All() .With(v => v.RelativePath = "media.mkv") .TheFirst(1) - .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = 3 }) + .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.CURRENT_MEDIA_INFO_SCHEMA_REVISION }) .BuildList(); Mocker.GetMock() @@ -70,10 +70,36 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() - .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(2)); + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void should_skip_not_yet_date_media_info() + { + var episodeFiles = Builder.CreateListOfSize(3) + .All() + .With(v => v.RelativePath = "media.mkv") + .TheFirst(1) + .With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION }) + .BuildList(); + + Mocker.GetMock() + .Setup(v => v.GetFilesByMovie(1)) + .Returns(episodeFiles); + + GivenFileExists(); + GivenSuccessfulScan(); + + Subject.Handle(new MovieScannedEvent(_movie)); + + Mocker.GetMock() + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(2)); Mocker.GetMock() .Verify(v => v.Update(It.IsAny()), Times.Exactly(2)); @@ -96,10 +122,10 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenFileExists(); GivenSuccessfulScan(); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() - .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(3)); + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(3)); Mocker.GetMock() .Verify(v => v.Update(It.IsAny()), Times.Exactly(3)); @@ -119,7 +145,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenSuccessfulScan(); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() .Verify(v => v.GetMediaInfo("media.mkv"), Times.Never()); @@ -144,12 +170,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo GivenFileExists(); GivenSuccessfulScan(); - GivenFailedScan(Path.Combine(_series.Path, "media2.mkv")); + GivenFailedScan(Path.Combine(_movie.Path, "media2.mkv")); - Subject.Handle(new MovieScannedEvent(_series)); + Subject.Handle(new MovieScannedEvent(_movie)); Mocker.GetMock() - .Verify(v => v.GetMediaInfo(Path.Combine(_series.Path, "media.mkv")), Times.Exactly(1)); + .Verify(v => v.GetMediaInfo(Path.Combine(_movie.Path, "media.mkv")), Times.Exactly(1)); Mocker.GetMock() .Verify(v => v.Update(It.IsAny()), Times.Exactly(1)); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index 617a4e41c..42ad53e72 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -31,10 +31,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4"); Subject.GetRunTime(path).Seconds.Should().Be(10); - } - [Test] public void get_info() { @@ -42,21 +40,27 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo var info = Subject.GetMediaInfo(path); - + info.VideoFormat.Should().Be("AVC"); + info.VideoCodecID.Should().Be("avc1"); + info.VideoProfile.Should().Be("Baseline@L2.1"); + info.VideoCodecLibrary.Should().Be(""); + info.VideoMultiViewCount.Should().Be(0); + info.VideoColourPrimaries.Should().Be("BT.601 NTSC"); + info.VideoTransferCharacteristics.Should().Be("BT.709"); + info.AudioFormat.Should().Be("AAC"); + info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2"); + info.AudioCodecLibrary.Should().Be(""); info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); - info.AudioFormat.Should().Be("AAC"); info.AudioLanguages.Should().Be("English"); - info.AudioProfile.Should().Be("LC"); + info.AudioAdditionalFeatures.Should().Be(""); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); info.Subtitles.Should().Be(""); info.VideoBitrate.Should().Be(193329); - info.VideoCodec.Should().Be("AVC"); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); - } [Test] @@ -73,20 +77,27 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo var info = Subject.GetMediaInfo(path); + info.VideoFormat.Should().Be("AVC"); + info.VideoCodecID.Should().Be("avc1"); + info.VideoProfile.Should().Be("Baseline@L2.1"); + info.VideoCodecLibrary.Should().Be(""); + info.VideoMultiViewCount.Should().Be(0); + info.VideoColourPrimaries.Should().Be("BT.601 NTSC"); + info.VideoTransferCharacteristics.Should().Be("BT.709"); + info.AudioFormat.Should().Be("AAC"); + info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2"); + info.AudioCodecLibrary.Should().Be(""); info.AudioBitrate.Should().Be(128000); info.AudioChannels.Should().Be(2); - info.AudioFormat.Should().Be("AAC"); info.AudioLanguages.Should().Be("English"); - info.AudioProfile.Should().Be("LC"); + info.AudioAdditionalFeatures.Should().Be(""); info.Height.Should().Be(320); info.RunTime.Seconds.Should().Be(10); info.ScanType.Should().Be("Progressive"); info.Subtitles.Should().Be(""); info.VideoBitrate.Should().Be(193329); - info.VideoCodec.Should().Be("AVC"); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); - } [Test] @@ -100,23 +111,5 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo stream.Close(); } - - [Test] - [TestCase("/ Front: L R", 2.0)] - public void should_correctly_read_audio_channels(string ChannelPositions, decimal formattedChannels) - { - var info = new MediaInfoModel() - { - VideoCodec = "AVC", - AudioFormat = "DTS", - AudioLanguages = "English", - Subtitles = "English", - AudioChannels = 2, - AudioChannelPositions = ChannelPositions, - SchemaRevision = 3, - }; - - info.FormattedAudioChannels.Should().Be(formattedChannels); - } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index bfd83ac26..a7172a0a7 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -314,6 +314,9 @@ + + + @@ -580,4 +583,4 @@ --> - \ No newline at end of file + diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 38d0e5aa7..f95e3171f 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -260,14 +260,14 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() { - VideoCodec = "AVC", + VideoFormat = "AVC", AudioFormat = "DTS", AudioLanguages = "English/Spanish", Subtitles = "English/Spanish/Italian" }; Subject.BuildFileName(_movie, _movieFile) - .Should().Be("South.Park.X264.DTS[EN+ES].[EN+ES+IT]"); + .Should().Be("South.Park.H264.DTS[EN+ES].[EN+ES+IT]"); } [Test] @@ -277,14 +277,52 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() { - VideoCodec = "AVC", + VideoFormat = "AVC", AudioFormat = "DTS", AudioLanguages = "English", Subtitles = "English/Spanish/Italian" }; Subject.BuildFileName(_movie, _movieFile) - .Should().Be("South.Park.X264.DTS.[EN+ES+IT]"); + .Should().Be("South.Park.H264.DTS.[EN+ES+IT]"); + } + + [Test] + public void should_format_mediainfo_3d_properly() + { + _namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.3D}.{MediaInfo.Simple}"; + + _movieFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() + { + VideoFormat = "AVC", + VideoMultiViewCount = 2, + AudioFormat = "DTS", + AudioLanguages = "English", + Subtitles = "English/Spanish/Italian" + }; + + Subject.BuildFileName(_movie, _movieFile) + .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] diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index d600f1041..e0a6a37b5 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -12,6 +12,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.Movies; namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc @@ -32,7 +33,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc _mediaCoverService = mediaCoverService; _diskProvider = diskProvider; _detectNfo = detectNfo; - + } private static readonly Regex MovieImagesRegex = new Regex(@"^(?poster|banner|fanart|clearart|discart|landscape|logo|backdrop|clearlogo)\.(?:png|jpg)", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -161,13 +162,15 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc if (movieFile.MediaInfo != null) { + var sceneName = movieFile.GetSceneOrFileName(); + var fileInfo = new XElement("fileinfo"); var streamDetails = new XElement("streamdetails"); var video = new XElement("video"); video.Add(new XElement("aspect", (float)movieFile.MediaInfo.Width / (float)movieFile.MediaInfo.Height)); video.Add(new XElement("bitrate", movieFile.MediaInfo.VideoBitrate)); - video.Add(new XElement("codec", movieFile.MediaInfo.VideoCodec)); + video.Add(new XElement("codec", MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName))); video.Add(new XElement("framerate", movieFile.MediaInfo.VideoFps)); video.Add(new XElement("height", movieFile.MediaInfo.Height)); video.Add(new XElement("scantype", movieFile.MediaInfo.ScanType)); @@ -184,7 +187,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc var audio = new XElement("audio"); audio.Add(new XElement("bitrate", movieFile.MediaInfo.AudioBitrate)); audio.Add(new XElement("channels", movieFile.MediaInfo.AudioChannels)); - audio.Add(new XElement("codec", GetAudioCodec(movieFile.MediaInfo.AudioFormat))); + audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName))); audio.Add(new XElement("language", movieFile.MediaInfo.AudioLanguages)); streamDetails.Add(audio); @@ -243,16 +246,6 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc return Path.ChangeExtension(movieFilePath, "nfo"); } - private string GetAudioCodec(string audioCodec) - { - if (audioCodec == "AC-3") - { - return "AC3"; - } - - return audioCodec; - } - private bool GetExistingWatchedStatus(Movie movie, string movieFilePath) { var fullPath = Path.Combine(movie.Path, GetMovieMetadataFilename(movieFilePath)); diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs new file mode 100644 index 000000000..53f38e53c --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -0,0 +1,470 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NLog.Fluent; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Instrumentation.Extensions; + +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 audioChannels = FormatAudioChannelsFromAudioChannelPositions(mediaInfo); + + if (audioChannels == null) + { + audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo); + } + + if (audioChannels == null) + { + audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo); + } + + return audioChannels ?? 0; + } + + public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName) + { + if (mediaInfo.AudioCodecID == null) + { + return FormatAudioCodecLegacy(mediaInfo, sceneName); + } + + var audioFormat = mediaInfo.AudioFormat; + var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty; + var audioProfile = mediaInfo.AudioProfile ?? string.Empty; + var audioAdditionalFeatures = mediaInfo.AudioAdditionalFeatures ?? string.Empty; + var audioCodecLibrary = mediaInfo.AudioCodecLibrary ?? string.Empty; + + if (audioFormat.IsNullOrWhiteSpace()) + { + return string.Empty; + } + + if (audioFormat.EqualsIgnoreCase("AC-3")) + { + return "AC3"; + } + + if (audioFormat.EqualsIgnoreCase("E-AC-3")) + { + return "EAC3"; + } + + if (audioFormat.EqualsIgnoreCase("AAC")) + { + if (audioCodecID == "A_AAC/MPEG4/LC/SBR") + { + return "HE-AAC"; + } + + return "AAC"; + } + + if (audioFormat.EqualsIgnoreCase("DTS")) + { + if (audioAdditionalFeatures.StartsWithIgnoreCase("XLL")) + { + if (audioAdditionalFeatures.EndsWithIgnoreCase("X")) + { + return "DTS-X"; + } + + return "DTS-HD MA"; + } + + if (audioAdditionalFeatures.EqualsIgnoreCase("ES")) + { + return "DTS-ES"; + } + + if (audioAdditionalFeatures.EqualsIgnoreCase("XBR")) + { + return "DTS-HD HRA"; + } + + return "DTS"; + } + + if (audioFormat.EqualsIgnoreCase("FLAC")) + { + return "FLAC"; + } + + if (audioFormat.Trim().EqualsIgnoreCase("mp3")) + { + return "MP3"; + } + + if (audioFormat.EqualsIgnoreCase("MPEG Audio")) + { + if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3") + { + return "MP3"; + } + + if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2") + { + return "MP2"; + } + } + + if (audioFormat.EqualsIgnoreCase("Opus")) + { + return "Opus"; + } + + if (audioFormat.EqualsIgnoreCase("PCM")) + { + return "PCM"; + } + + if (audioFormat.EqualsIgnoreCase("MLP FBA")) + { + if (audioAdditionalFeatures == "16-ch") + { + return "TrueHD Atmos"; + } + + return "TrueHD"; + } + + if (audioFormat.EqualsIgnoreCase("Vorbis")) + { + return "Vorbis"; + } + + if (audioFormat == "WMA") + { + return "WMA"; + } + + Logger.Debug("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", audioFormat, audioCodecID, audioProfile, audioAdditionalFeatures, audioCodecLibrary), sceneName); + + return audioFormat; + } + + public static string FormatAudioCodecLegacy(MediaInfoModel mediaInfo, string sceneName) + { + var audioFormat = mediaInfo.AudioFormat; + + if (audioFormat.IsNullOrWhiteSpace()) + { + return audioFormat; + } + + if (audioFormat.EqualsIgnoreCase("AC-3")) + { + return "AC3"; + } + + if (audioFormat.EqualsIgnoreCase("E-AC-3")) + { + return "EAC3"; + } + + if (audioFormat.EqualsIgnoreCase("AAC")) + { + return "AAC"; + } + + if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3") + { + return "MP3"; + } + + if (audioFormat.EqualsIgnoreCase("DTS")) + { + return "DTS"; + } + + if (audioFormat.EqualsIgnoreCase("TrueHD")) + { + return "TrueHD"; + } + + if (audioFormat.EqualsIgnoreCase("FLAC")) + { + return "FLAC"; + } + + if (audioFormat.EqualsIgnoreCase("Vorbis")) + { + return "Vorbis"; + } + + if (audioFormat.EqualsIgnoreCase("Opus")) + { + return "Opus"; + } + + return audioFormat; + } + + public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName) + { + if (mediaInfo.VideoFormat == null) + { + return FormatVideoCodecLegacy(mediaInfo, sceneName); + } + + var videoFormat = mediaInfo.VideoFormat; + var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty; + var videoProfile = mediaInfo.VideoProfile ?? string.Empty; + var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty; + + var result = videoFormat; + + if (videoFormat.IsNullOrWhiteSpace()) + { + return result; + } + + if (videoFormat == "x264") + { + return "x264"; + } + + if (videoFormat == "AVC" || videoFormat == "V.MPEG4/ISO/AVC") + { + if (videoCodecLibrary.StartsWithIgnoreCase("x264")) + { + return "x264"; + } + + return GetSceneNameMatch(sceneName, "AVC", "x264", "h264"); + } + + if (videoFormat == "HEVC" || videoFormat == "V_MPEGH/ISO/HEVC") + { + if (videoCodecLibrary.StartsWithIgnoreCase("x265")) + { + return "x265"; + } + + return GetSceneNameMatch(sceneName, "HEVC", "x265", "h265"); + } + + if (videoFormat == "MPEG Video") + { + if (videoCodecID == "2" || videoCodecID == "V_MPEG2") + { + return "MPEG2"; + } + + if (videoCodecID.IsNullOrWhiteSpace()) + { + return "MPEG"; + } + } + + if (videoFormat == "MPEG-2 Video") + { + return "MPEG2"; + } + + if (videoFormat == "MPEG-4 Visual") + { + if (videoCodecID.ContainsIgnoreCase("XVID") || + videoCodecLibrary.StartsWithIgnoreCase("XviD")) + { + return "XviD"; + } + + if (videoCodecID.ContainsIgnoreCase("DIV3") || + videoCodecID.ContainsIgnoreCase("DIVX") || + videoCodecID.ContainsIgnoreCase("DX50") || + videoCodecLibrary.StartsWithIgnoreCase("DivX")) + { + return "DivX"; + } + } + + if (videoFormat == "MPEG-4 Visual" || videoFormat == "mp4v") + { + result = GetSceneNameMatch(sceneName, "XviD", "DivX", ""); + if (result.IsNotNullOrWhiteSpace()) + { + return result; + } + } + + if (videoFormat == "VC-1") + { + return "VC1"; + } + + if (videoFormat.EqualsIgnoreCase("VP6") || videoFormat.EqualsIgnoreCase("VP7") || + videoFormat.EqualsIgnoreCase("VP8") || videoFormat.EqualsIgnoreCase("VP9")) + { + return videoFormat.ToUpperInvariant(); + } + + if (videoFormat == "WMV2") + { + return "WMV"; + } + + if (videoFormat.EqualsIgnoreCase("DivX") || videoFormat.EqualsIgnoreCase("div3")) + { + return "DivX"; + } + + if (videoFormat.EqualsIgnoreCase("XviD")) + { + return "XviD"; + } + + Logger.Debug("Unknown video format: '{0}' in '{1}'.", string.Join(", ", videoFormat, videoCodecID, videoProfile, videoCodecLibrary), sceneName); + + return result; + } + + public static string FormatVideoCodecLegacy(MediaInfoModel mediaInfo, string sceneName) + { + var videoCodec = mediaInfo.VideoFormat; + + if (videoCodec.IsNullOrWhiteSpace()) + { + return videoCodec; + } + + if (videoCodec == "AVC") + { + return GetSceneNameMatch(sceneName, "AVC", "h264", "x264"); + } + + if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC") + { + return GetSceneNameMatch(sceneName, "HEVC", "h265", "x265"); + } + + if (videoCodec == "MPEG-2 Video") + { + return "MPEG2"; + } + + if (videoCodec == "MPEG-4 Visual") + { + return GetSceneNameMatch(sceneName, "DivX", "XviD"); + } + + if (videoCodec.StartsWithIgnoreCase("XviD")) + { + return "XviD"; + } + + if (videoCodec.StartsWithIgnoreCase("DivX")) + { + return "DivX"; + } + + if (videoCodec.EqualsIgnoreCase("VC-1")) + { + return "VC1"; + } + + return videoCodec; + } + + private static decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfoModel mediaInfo) + { + var audioChannelPositions = mediaInfo.AudioChannelPositions; + + if (audioChannelPositions.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + Logger.Debug("Formatting audio channels using 'AudioChannelPositions', with a value of: '{0}'", audioChannelPositions); + + if (audioChannelPositions.Contains("+")) + { + return audioChannelPositions.Split('+') + .Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture)); + } + + if (audioChannelPositions.Contains("/")) + { + return Regex.Replace(audioChannelPositions, @"^\d+\sobjects", "", RegexOptions.Compiled | RegexOptions.IgnoreCase) + .Replace("Object Based / ", "") + .Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries) + .FirstOrDefault() + ?.Split('/') + .Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); + } + } + catch (Exception e) + { + Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositions'"); + } + + return null; + } + + private static decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfoModel mediaInfo) + { + var audioChannelPositionsText = mediaInfo.AudioChannelPositionsText; + var audioChannels = mediaInfo.AudioChannels; + + if (audioChannelPositionsText.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + Logger.Debug("Formatting audio channels using 'AudioChannelPositionsText', with a value of: '{0}'", audioChannelPositionsText); + + return audioChannelPositionsText.ContainsIgnoreCase("LFE") ? audioChannels - 1 + 0.1m : audioChannels; + } + catch (Exception e) + { + Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText'"); + } + + return null; + } + + private static decimal? FormatAudioChannelsFromAudioChannels(MediaInfoModel mediaInfo) + { + var audioChannels = mediaInfo.AudioChannels; + + if (mediaInfo.SchemaRevision >= 3) + { + Logger.Debug("Formatting audio channels using 'AudioChannels', with a value of: '{0}'", audioChannels); + + return audioChannels; + } + + return null; + } + + private static string GetSceneNameMatch(string sceneName, params string[] tokens) + { + sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty; + + foreach (var token in tokens) + { + if (sceneName.ContainsIgnoreCase(token)) + { + return token; + } + } + + // Last token is the default. + return tokens.Last(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index 8d8b0342e..c8e00352e 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using System.Linq; -using System.Linq.Expressions; using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -10,12 +9,22 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo { public class MediaInfoModel : IEmbeddedDocument { - public string VideoCodec { get; set; } + public string ContainerFormat { get; set; } + public string VideoFormat { get; set; } + public string VideoCodecID { get; set; } + public string VideoProfile { get; set; } + public string VideoCodecLibrary { get; set; } public int VideoBitrate { get; set; } public int VideoBitDepth { get; set; } + public int VideoMultiViewCount { get; set; } + public string VideoColourPrimaries { get; set; } + public string VideoTransferCharacteristics { get; set; } public int Width { get; set; } public int Height { get; set; } public string AudioFormat { get; set; } + public string AudioCodecID { get; set; } + public string AudioCodecLibrary { get; set; } + public string AudioAdditionalFeatures { get; set; } public int AudioBitrate { get; set; } public TimeSpan RunTime { get; set; } public int AudioStreamCount { get; set; } @@ -28,40 +37,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 - { - try - { - return - AudioChannelPositions.Replace("Object Based /", "").Replace(" / ", "$") - .Split('$') - .First() - .Split('/') - .Sum(s => decimal.Parse(s, CultureInfo.InvariantCulture)); - } - catch - { - - if (AudioChannelPositionsText.IsNullOrWhiteSpace()) - { - if (SchemaRevision >= 3) - { - return AudioChannels; - } - - return 0; - } - - return AudioChannelPositionsText.ContainsIgnoreCase("LFE") ? AudioChannels - 1 + 0.1m : AudioChannels; - - - } - - } - } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs index e1ab325d9..0c10a33c8 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/UpdateMediaInfoService.cs @@ -18,8 +18,6 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo private readonly IConfigService _configService; private readonly Logger _logger; - private const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 3; - public UpdateMediaInfoService(IDiskProvider diskProvider, IMediaFileService mediaFileService, IVideoFileInfoReader videoFileInfoReader, @@ -49,7 +47,6 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo if (mediaFile.MediaInfo != null) { - mediaFile.MediaInfo.SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION; _mediaFileService.Update(mediaFile); _logger.Debug("Updated MediaInfo for '{0}'", path); } @@ -65,7 +62,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo } var allMediaFiles = _mediaFileService.GetFilesByMovie(message.Movie.Id); - var filteredMediaFiles = allMediaFiles.Where(c => c.MediaInfo == null || c.MediaInfo.SchemaRevision < CURRENT_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); } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 5f141b188..32edebca5 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -17,6 +17,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo private readonly IDiskProvider _diskProvider; private readonly Logger _logger; + public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 3; + public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 4; public VideoFileInfoReader(IDiskProvider diskProvider, Logger logger) { @@ -90,77 +92,69 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo int audioChannels; int videoBitDepth; decimal videoFrameRate; + int videoMultiViewCount; string subtitles = mediaInfo.Get(StreamKind.General, 0, "Text_Language_List"); string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType"); int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width); int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height); int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate); - if (videoBitRate <= 0) - { - int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate_Nominal"), out videoBitRate); - } decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out videoFrameRate); int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitDepth"), out videoBitDepth); + int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "MultiView_Count"), out videoMultiViewCount); //Runtime int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime); int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime); int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime); - string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate"); - int aBindex = aBitRate.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); - if (aBindex > 0) - { - aBitRate = aBitRate.Remove(aBindex); - } + string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); int.TryParse(aBitRate, out audioBitRate); int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount); - - string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)"); - int aCindex = audioChannelsStr.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); - - if (aCindex > 0) - { - audioChannelsStr = audioChannelsStr.Remove(aCindex); - } + string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2"); var audioChannelPositionsText = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions"); string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List"); - string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile"); - int aPindex = audioProfile.IndexOf(" /", StringComparison.InvariantCultureIgnoreCase); - - if (aPindex > 0) - { - audioProfile = audioProfile.Remove(aPindex); - } + string videoProfile = mediaInfo.Get(StreamKind.Video, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); + string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); int.TryParse(audioChannelsStr, out audioChannels); var mediaInfoModel = new MediaInfoModel - { - VideoCodec = mediaInfo.Get(StreamKind.Video, 0, "Codec/String"), - VideoBitrate = videoBitRate, - VideoBitDepth = videoBitDepth, - Height = height, - Width = width, - AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"), - AudioBitrate = audioBitRate, - RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime), - AudioStreamCount = streamCount, - AudioChannels = audioChannels, - AudioChannelPositions = audioChannelPositions, - AudioChannelPositionsText = audioChannelPositionsText, - AudioProfile = audioProfile.Trim(), - VideoFps = videoFrameRate, - AudioLanguages = audioLanguages, - Subtitles = subtitles, - ScanType = scanType - }; + { + ContainerFormat = mediaInfo.Get(StreamKind.General, 0, "Format"), + VideoFormat = mediaInfo.Get(StreamKind.Video, 0, "Format"), + VideoCodecID = mediaInfo.Get(StreamKind.Video, 0, "CodecID"), + VideoProfile = videoProfile, + VideoCodecLibrary = mediaInfo.Get(StreamKind.Video, 0, "Encoded_Library"), + VideoBitrate = videoBitRate, + VideoBitDepth = videoBitDepth, + VideoMultiViewCount = videoMultiViewCount, + VideoColourPrimaries = mediaInfo.Get(StreamKind.Video, 0, "colour_primaries"), + VideoTransferCharacteristics = mediaInfo.Get(StreamKind.Video, 0, "transfer_characteristics"), + Height = height, + Width = width, + AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"), + AudioCodecID = mediaInfo.Get(StreamKind.Audio, 0, "CodecID"), + AudioProfile = audioProfile, + AudioCodecLibrary = mediaInfo.Get(StreamKind.Audio, 0, "Encoded_Library"), + AudioAdditionalFeatures = mediaInfo.Get(StreamKind.Audio, 0, "Format_AdditionalFeatures"), + AudioBitrate = audioBitRate, + RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime), + AudioStreamCount = streamCount, + AudioChannels = audioChannels, + AudioChannelPositions = audioChannelPositions, + AudioChannelPositionsText = audioChannelPositionsText, + VideoFps = videoFrameRate, + AudioLanguages = audioLanguages, + Subtitles = subtitles, + ScanType = scanType, + SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION + }; return mediaInfoModel; } @@ -175,7 +169,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo } catch (Exception ex) { - _logger.Error(ex, "Unable to parse media info from file: " + filename); + _logger.Error(ex, "Unable to parse media info from file: {0}", filename); } finally { diff --git a/src/NzbDrone.Core/MediaFiles/MovieFile.cs b/src/NzbDrone.Core/MediaFiles/MovieFile.cs index 0414611c8..da51c9f3a 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFile.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFile.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Movies; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.MediaFiles { @@ -27,5 +28,20 @@ namespace NzbDrone.Core.MediaFiles { return string.Format("[{0}] {1}", Id, RelativePath); } + + public string GetSceneOrFileName() + { + if (SceneName.IsNotNullOrWhiteSpace()) + { + return SceneName; + } + + if (RelativePath.IsNotNullOrWhiteSpace()) + { + return System.IO.Path.GetFileName(RelativePath); + } + + return string.Empty; +} } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 866ba4b6a..691004d60 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -847,6 +847,7 @@ Code + @@ -1318,11 +1319,11 @@ - - \ No newline at end of file + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 71b06837d..e31c57371 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -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.Movies; @@ -333,12 +334,12 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; } - private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) + private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile movieFile) { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(movieFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(movieFile); //tokenHandlers["{IMDb Id}"] = m => - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Radarr"); + tokenHandlers["{Release Group}"] = m => movieFile.ReleaseGroup ?? m.DefaultValue("Radarr"); } private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) @@ -366,98 +367,22 @@ namespace NzbDrone.Core.Organizer { if (movieFile.MediaInfo == null) return; - string videoCodec; - switch (movieFile.MediaInfo.VideoCodec) - { - case "AVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; + var sceneName = movieFile.GetSceneOrFileName(); - default: - videoCodec = movieFile.MediaInfo.VideoCodec; - break; - } + var videoCodec = MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName); + var audioCodec = MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName); + var audioChannels = MediaInfoFormatter.FormatAudioChannels(movieFile.MediaInfo); - string audioCodec; - switch (movieFile.MediaInfo.AudioFormat) + // Workaround until https://github.com/MediaArea/MediaInfo/issues/299 is fixed and release + if (audioCodec.EqualsIgnoreCase("DTS-X")) { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "Atmos / TrueHD": - audioCodec = "Atmos TrueHD"; - break; - - case "MPEG Audio": - if (movieFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = movieFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - if (movieFile.MediaInfo.AudioProfile == "ES" || movieFile.MediaInfo.AudioProfile == "ES Discrete" || movieFile.MediaInfo.AudioProfile == "ES Matrix") - { - audioCodec = "DTS-ES"; - } - else if (movieFile.MediaInfo.AudioProfile == "MA") - { - audioCodec = "DTS-HD MA"; - } - else if (movieFile.MediaInfo.AudioProfile == "HRA") - { - audioCodec = "DTS-HD HRA"; - } - else if (movieFile.MediaInfo.AudioProfile == "X") - { - audioCodec = "DTS-X"; - } - else - { - audioCodec = movieFile.MediaInfo.AudioFormat; - } - break; - - default: - audioCodec = movieFile.MediaInfo.AudioFormat; - break; + audioChannels = audioChannels - 1 + 0.1m; } var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + mediaInfoAudioLanguages = $"[{mediaInfoAudioLanguages}]"; } var mediaInfoAudioLanguagesAll = mediaInfoAudioLanguages; if (mediaInfoAudioLanguages == "[EN]") @@ -465,17 +390,32 @@ namespace NzbDrone.Core.Organizer mediaInfoAudioLanguages = string.Empty; } - var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]"; } var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? - movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; + var audioChannelsFormatted = audioChannels > 0 ? + 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; @@ -483,14 +423,17 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannelsFormatted; tokenHandlers["{MediaInfo AudioLanguages}"] = m => mediaInfoAudioLanguages; tokenHandlers["{MediaInfo AudioLanguagesAll}"] = m => mediaInfoAudioLanguagesAll; + tokenHandlers["{MediaInfo SubtitleLanguages}"] = 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}"; } private string GetLanguagesToken(string mediaInfoLanguages) @@ -610,24 +553,24 @@ namespace NzbDrone.Core.Organizer return string.Empty; } - private string GetOriginalTitle(MovieFile episodeFile) + private string GetOriginalTitle(MovieFile movieFile) { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) + if (movieFile.SceneName.IsNullOrWhiteSpace()) { - return GetOriginalFileName(episodeFile); + return GetOriginalFileName(movieFile); } - return episodeFile.SceneName; + return movieFile.SceneName; } - private string GetOriginalFileName(MovieFile episodeFile) + private string GetOriginalFileName(MovieFile movieFile) { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + if (movieFile.RelativePath.IsNullOrWhiteSpace()) { - return Path.GetFileNameWithoutExtension(episodeFile.Path); + return Path.GetFileNameWithoutExtension(movieFile.Path); } - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + return Path.GetFileNameWithoutExtension(movieFile.RelativePath); } } diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 58d785b98..28cb60982 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -26,8 +26,11 @@ namespace NzbDrone.Core.Organizer var mediaInfo = new MediaInfoModel() { - VideoCodec = "AVC", - VideoBitDepth = 8, + VideoFormat = "AVC", + VideoBitDepth = 10, + VideoMultiViewCount = 2, + VideoColourPrimaries = "BT.2020", + VideoTransferCharacteristics = "PQ", AudioFormat = "DTS", AudioChannels = 6, AudioChannelPositions = "3/2/0.1", @@ -37,8 +40,11 @@ namespace NzbDrone.Core.Organizer var mediaInfoAnime = new MediaInfoModel() { - VideoCodec = "AVC", - VideoBitDepth = 8, + VideoFormat = "AVC", + VideoBitDepth = 10, + VideoMultiViewCount = 2, + VideoColourPrimaries = "BT.2020", + VideoTransferCharacteristics = "PQ", AudioFormat = "DTS", AudioChannels = 6, AudioChannelPositions = "3/2/0.1", diff --git a/src/UI/Cells/MediaInfoCell.js b/src/UI/Cells/MediaInfoCell.js index ed42380a3..448416317 100644 --- a/src/UI/Cells/MediaInfoCell.js +++ b/src/UI/Cells/MediaInfoCell.js @@ -12,7 +12,7 @@ module.exports = NzbDroneCell.extend({ if (runtime) { runtime = runtime.split(".")[0]; } - var video = "{0} ({1}x{2}) ({3})".format(info.videoCodec, info.width, info.height, runtime); + var video = "{0} ({1}x{2}) ({3})".format(info.videoFormat, info.width, info.height, runtime); var audio = "{0} ({1})".format(info.audioFormat, info.audioLanguages); this.$el.html(video + " " + audio); }