diff --git a/src/NuGet.Config b/src/NuGet.Config index 43cd33c3b..3733bf1b9 100644 --- a/src/NuGet.Config +++ b/src/NuGet.Config @@ -7,5 +7,6 @@ + \ No newline at end of file diff --git a/src/NzbDrone.Common/Composition/AssemblyLoader.cs b/src/NzbDrone.Common/Composition/AssemblyLoader.cs index 2d01967a0..67faadea1 100644 --- a/src/NzbDrone.Common/Composition/AssemblyLoader.cs +++ b/src/NzbDrone.Common/Composition/AssemblyLoader.cs @@ -75,10 +75,6 @@ namespace NzbDrone.Common.Composition { mappedName = "libsqlite3.so.0"; } - else if (libraryName == "mediainfo") - { - mappedName = "libmediainfo.so.0"; - } } return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs deleted file mode 100644 index 3cd71c247..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioChannelsFixture.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System.Globalization; -using System.Threading; -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 - { - AudioChannelsContainer = 6, - AudioChannelPositions = null, - AudioChannelPositionsTextContainer = "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 - { - AudioChannelsContainer = 2, - AudioChannelPositions = null, - AudioChannelPositionsTextContainer = "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 - { - AudioChannelsContainer = 2, - AudioChannelPositions = null, - AudioChannelPositionsTextContainer = 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 - { - AudioChannelsContainer = 2, - AudioChannelPositions = null, - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); - } - - [Test] - public void should_use_AudioChannels_if_schema_revision_is_3_and_AudioChannelPositions_is_0() - { - var mediaInfoModel = new MediaInfoModel - { - AudioFormat = "FLAC", - AudioChannelsContainer = 6, - AudioChannelPositions = "0/0/0", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); - } - - [Test] - public void should_sum_AudioChannelPositions() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "2/0/0", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); - } - - [Test] - public void should_sum_AudioChannelPositions_including_decimal() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "3/2/0.1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); - } - - [Test] - public void should_format_8_channel_object_based_as_71_if_dtsx() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 8, - AudioChannelsStream = 0, - AudioFormat = "DTS", - AudioAdditionalFeatures = "XLL X", - AudioChannelPositions = "Object Based", - AudioChannelPositionsTextContainer = "Object Based", - AudioChannelPositionsTextStream = "Object Based", - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); - } - - [Test] - public void should_format_8_channel_blank_as_71_if_dtsx() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 8, - AudioChannelsStream = 0, - AudioFormat = "DTS", - AudioAdditionalFeatures = "XLL X", - AudioChannelPositions = "", - AudioChannelPositionsTextContainer = "", - AudioChannelPositionsTextStream = "Object Based", - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); - } - - [Test] - public void should_format_6_channel_zero_as_51_if_flac() - { - var mediaInfoModel = new MediaInfoModel - { - AudioFormat = "FLAC", - AudioChannelsContainer = 6, - AudioChannelPositions = "0/0/0", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); - } - - [Test] - public void should_ignore_culture_on_channel_summary() - { - Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR"); - - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "3/2/0.1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); - } - - [Test] - public void should_handle_AudioChannelPositions_three_digits() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "3/2/0.2.1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); - } - - [Test] - public void should_cleanup_extraneous_text_from_AudioChannelPositions() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "Object Based / 3/2/2.1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); - } - - [Test] - public void should_skip_empty_groups_in_AudioChannelPositions() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = " / 2/0/0.0", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2); - } - - [Test] - public void should_sum_first_series_of_numbers_from_AudioChannelPositions() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "3/2/2.1 / 3/2/2.1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); - } - - [Test] - public void should_sum_first_series_of_numbers_from_AudioChannelPositions_with_three_digits() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "3/2/0.2.1 / 3/2/0.1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); - } - - [Test] - public void should_sum_dual_mono_representation_AudioChannelPositions() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 2, - AudioChannelPositions = "1+1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m); - } - - [Test] - public void should_use_AudioChannelPositionText_when_AudioChannelChannelPosition_is_invalid() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 6, - AudioChannelPositions = "15 objects", - AudioChannelPositionsTextContainer = "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 - { - AudioChannelsContainer = 2, - AudioChannelPositions = "15 objects / 3/2.1", - AudioChannelPositionsTextContainer = null, - SchemaRevision = 3 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m); - } - - [Test] - public void should_use_audio_stream_text_when_exists() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 6, - AudioChannelsStream = 8, - AudioChannelPositions = null, - AudioChannelPositionsTextContainer = null, - AudioChannelPositionsTextStream = "Front: L C R, Side: L R, Back: L R, LFE", - SchemaRevision = 6 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m); - } - - [Test] - public void should_use_audio_stream_channels_when_exists() - { - var mediaInfoModel = new MediaInfoModel - { - AudioChannelsContainer = 6, - AudioChannelsStream = 8, - AudioChannelPositions = null, - AudioChannelPositionsTextContainer = null, - AudioChannelPositionsTextStream = null, - SchemaRevision = 6 - }; - - MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(8m); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs index 7015bf192..ee70b202b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatAudioCodecFixture.cs @@ -10,45 +10,30 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests { 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("MLP FBA, A_TRUEHD, , , ", "TrueHD", "TrueHD")] - [TestCase("MLP FBA, A_TRUEHD, , , 16-ch", "Atmos", "TrueHD Atmos")] - [TestCase("Atmos / TrueHD, A_TRUEHD, , , ", "TrueHD.Atmos.7.1", "TrueHD Atmos")] - [TestCase("Atmos / TrueHD / AC-3, 131, , , ", "", "TrueHD Atmos")] - [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")] - [TestCase("DTS, A_DTS, , , XLL", "DTS-HD.MA", "DTS-HD MA")] - [TestCase("DTS, A_DTS, , , XLL X", "DTS-X", "DTS-X")] - [TestCase("DTS, A_DTS, , , ES XLL", "DTS-HD.MA", "DTS-HD MA")] - [TestCase("DTS, A_DTS, , , ES", "DTS-ES", "DTS-ES")] - [TestCase("DTS, A_DTS, , , ES XXCH", "DTS", "DTS-ES")] - [TestCase("DTS, A_DTS, , , XBR", "DTSHD-HRA", "DTS-HD HRA")] - [TestCase("DTS, A_DTS, , , DTS", "DTS", "DTS")] - [TestCase("E-AC-3, A_EAC3, , , JOC", "Carbon.Copy.S02E01.The.Phreak.Lady.1080p.NF.Webrip.x265.10bit.EAC3.5.1.Atmos-RGP", "EAC3 Atmos")] - [TestCase("E-AC-3, A_EAC3, , , ", "DD5.1", "EAC3")] - [TestCase("AC-3, A_AC3, , , ", "DD5.1", "AC3")] - [TestCase("A_QUICKTIME, A_QUICKTIME, , , ", "", "")] - [TestCase("ADPCM, 2, , , ", "Custom?", "PCM")] - [TestCase("ADPCM, ima4, , , ", "Custom", "PCM")] + [TestCase("mp2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")] + [TestCase("vorbis, , ", "DB Super HDTV", "Vorbis")] + [TestCase("pcm_s16le, , ", "DW DVDRip XviD-idTV, ", "PCM")] + [TestCase("truehd, , ", "", "TrueHD")] + [TestCase("truehd, , ", "TrueHD", "TrueHD")] + [TestCase("truehd, thd+, ", "Atmos", "TrueHD Atmos")] + [TestCase("truehd, thd+, ", "TrueHD.Atmos.7.1", "TrueHD Atmos")] + [TestCase("truehd, thd+, ", "", "TrueHD Atmos")] + [TestCase("wmav1, , ", "Droned.wmv", "WMA")] + [TestCase("wmav2, , ", "B.N.S04E18.720p.WEB-DL", "WMA")] + [TestCase("opus, , ", "Roadkill Ep3x11 - YouTube.webm", "Opus")] + [TestCase("mp3, , ", "climbing.mp4", "MP3")] + [TestCase("dts, , DTS-HD MA", "DTS-HD.MA", "DTS-HD MA")] + [TestCase("dts, , DTS:X", "DTS-X", "DTS-X")] + [TestCase("dts, , DTS-HD MA", "DTS-HD.MA", "DTS-HD MA")] + [TestCase("dts, , DTS-ES", "DTS-ES", "DTS-ES")] + [TestCase("dts, , DTS-ES", "DTS", "DTS-ES")] + [TestCase("dts, , DTS-HD HRA", "DTSHD-HRA", "DTS-HD HRA")] + [TestCase("dts, , DTS", "DTS", "DTS")] + [TestCase("eac3, ec+3, ", "EAC3.Atmos", "EAC3 Atmos")] + [TestCase("eac3, , ", "DDP5.1", "EAC3")] + [TestCase("ac3, , ", "DD5.1", "AC3")] + [TestCase("adpcm_ima_qt, , ", "Custom?", "PCM")] + [TestCase("adpcm_ms, , ", "Custom", "PCM")] public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat) { var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None); @@ -56,26 +41,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests { AudioFormat = split[0], AudioCodecID = split[1], - AudioProfile = split[2], - AudioCodecLibrary = split[3], - AudioAdditionalFeatures = split[4] + AudioProfile = split[2] }; 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() { diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs index 05d7ebf90..5bab3401e 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoCodecFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Test.Common; @@ -8,94 +8,60 @@ 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_legacy(string videoCodec, string sceneName, string expectedFormat) - { - var mediaInfoModel = new MediaInfoModel - { - VideoCodec = 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("AVC / AVC, V_MPEG4/ISO/AVC, High@L4, ", "Resistance.2019.S01E03.1080p.RTE.WEB-DL.AAC2.0.x264-RTN", "x264")] - [TestCase("WMV1, WMV1, , ", "Droned.wmv", "WMV")] - [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("MPEG-4 Visual, 20, , ", "", "")] - [TestCase("MPEG-4 Visual, mp4v-20, Simple@L1, Lavc57.48.101", "", "")] - [TestCase("mp4v, mp4v, , ", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")] - [TestCase("V_QUICKTIME, V_QUICKTIME, , ", "Custom", "")] - [TestCase("MPEG-4 Visual, FMP4, , ", "", "")] - [TestCase("MPEG-4 Visual, MP42, , ", "", "")] - [TestCase("mp43, V_MS/VFW/FOURCC / mp43, , ", "Bubble.Guppies.S01E13.480p.WEB-DL.H.264-BTN-Custom", "")] + [TestCase("mpeg2video, ", "Droned.S01E02.1080i.HDTV.DD5.1.MPEG2-NTb", "MPEG2")] + [TestCase("mpeg2video, ", "", "MPEG2")] + [TestCase("mpeg1video, ", "The.Simpsons.S13E04.INTERNAL-ANiVCD.mpg", "MPEG")] + [TestCase("vc1, WVC1", "B.N.S04E18.720p.WEB-DL", "VC1")] + [TestCase("vc1, V_MS/VFW/FOURCC/WVC1", "", "VC1")] + [TestCase("vc1, WMV3", "It's Always Sunny S07E13 The Gang's RevengeHDTV.XviD-2HD.avi", "VC1")] + [TestCase("h264, V.MPEG4/ISO/AVC", "pd.2015.S03E08.720p.iP.WEBRip.AAC2.0.H264-BTW", "h264")] + [TestCase("h264, V_MPEG4/ISO/AVC", "Resistance.2019.S01E03.1080p.RTE.WEB-DL.AAC2.0.x264-RTN", "x264")] + [TestCase("wmv1, WMV1", "Droned.wmv", "WMV")] + [TestCase("wmv2, WMV2", "Droned.wmv", "WMV")] + [TestCase("mpeg4, XVID", "", "XviD")] + [TestCase("mpeg4, DIVX", "", "DivX")] + [TestCase("mpeg4, divx", "", "DivX")] + [TestCase("mpeg4, DIV3", "spsm.dvdrip.divx.avi'.", "DivX")] + [TestCase("msmpeg4, DIV3", "Exit the Dragon, Enter the Tiger (1976) 360p MPEG Audio.avi", "DivX")] + [TestCase("msmpeg4v2, DIV3", "Exit the Dragon, Enter the Tiger (1976) 360p MPEG Audio.avi", "DivX")] + [TestCase("msmpeg4v3, DIV3", "Exit the Dragon, Enter the Tiger (1976) 360p MPEG Audio.avi", "DivX")] + [TestCase("vp6, 4", "Top Gear - S12E01 - Lorries - SD TV.flv", "VP6")] + [TestCase("vp7, VP70", "Sweet Seymour.avi", "VP7")] + [TestCase("vp8, V_VP8", "Dick.mkv", "VP8")] + [TestCase("vp9, V_VP9", "Roadkill Ep3x11 - YouTube.webm", "VP9")] + [TestCase("h264, x264", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")] + [TestCase("hevc, V_MPEGH/ISO/HEVC", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")] + [TestCase("mpeg4, mp4v-20", "", "")] + [TestCase("mpeg4, XVID", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")] + [TestCase("rpza, V_QUICKTIME", "Custom", "")] + [TestCase("mpeg4, FMP4", "", "")] + [TestCase("mpeg4, MP42", "", "")] + [TestCase("mpeg4, mp43", "Bubble.Guppies.S01E13.480p.WEB-DL.H.264-BTN-Custom", "")] 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] + VideoCodecID = split[1] }; 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 + [TestCase("h264, x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag + [TestCase("hevc, x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag + [TestCase("h264, ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown + [TestCase("hevc, ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown + [TestCase("h264, ", "Some.Video.S01E01", "h264")] // Default value + [TestCase("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); - } - - [TestCase("MPEG-4 Visual, 20, , Intel(R) MPEG-4 encoder based on Intel(R) IPP 6.1 build 137.20[6.1.137.763]", "", "")] - public void should_warn_on_unknown_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] + VideoCodecID = split[1] }; MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat); diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs index e5000554c..ab3b620bf 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeFixture.cs @@ -1,4 +1,4 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Test.Common; @@ -8,63 +8,21 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests [TestFixture] public class FormatVideoDynamicRangeFixture : TestBase { - [TestCase(8, "", "", "", "", "")] - [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")] - [TestCase(10, "", "", "Dolby Vision", "", "HDR")] - [TestCase(10, "", "", "SMPTE ST 2086", "HDR10", "HDR")] - [TestCase(8, "", "", "Dolby Vision", "", "")] - [TestCase(8, "", "", "SMPTE ST 2086", "HDR10", "")] - [TestCase(10, "BT.2020", "PQ", "Dolby Vision / SMPTE ST 2086", "Blu-ray / HDR10", "HDR")] - public void should_format_video_dynamic_range(int bitDepth, string colourPrimaries, string transferCharacteristics, string hdrFormat, string hdrFormatCompatibility, string expectedVideoDynamicRange) + [TestCase(HdrFormat.None, "")] + [TestCase(HdrFormat.Hlg10, "HDR")] + [TestCase(HdrFormat.Pq10, "HDR")] + [TestCase(HdrFormat.Hdr10, "HDR")] + [TestCase(HdrFormat.Hdr10Plus, "HDR")] + [TestCase(HdrFormat.DolbyVision, "HDR")] + public void should_format_video_dynamic_range(HdrFormat format, string expectedVideoDynamicRange) { var mediaInfo = new MediaInfoModel { - VideoBitDepth = bitDepth, - VideoColourPrimaries = colourPrimaries, - VideoTransferCharacteristics = transferCharacteristics, - VideoHdrFormat = hdrFormat, - VideoHdrFormatCompatibility = hdrFormatCompatibility, - SchemaRevision = 7 + VideoHdrFormat = format, + SchemaRevision = 8 }; MediaInfoFormatter.FormatVideoDynamicRange(mediaInfo).Should().Be(expectedVideoDynamicRange); } - - [TestCase(8, "", "", "", "")] - [TestCase(8, "BT.601 NTSC", "BT.709", "", "")] - [TestCase(8, "BT.2020", "PQ", "", "")] - [TestCase(8, "", "", "Dolby Vision", "")] - [TestCase(8, "", "", "SMPTE ST 2086", "")] - [TestCase(10, "BT.601 NTSC", "PQ", "", "")] - [TestCase(10, "BT.2020", "BT.709", "", "")] - [TestCase(10, "BT.2020", "PQ", "", "PQ")] - [TestCase(10, "BT.2020", "HLG", "", "HLG")] - [TestCase(10, "", "", "SMPTE ST 2086", "HDR10")] - [TestCase(10, "", "", "HDR10", "HDR10")] - [TestCase(10, "", "", "SMPTE ST 2094 App 4", "HDR10Plus")] - [TestCase(10, "", "", "Dolby Vision", "DV")] - [TestCase(10, "BT.2020", "PQ", "Dolby Vision / SMPTE ST 2086", "DV HDR10")] - [TestCase(10, "", "", "SL-HDR1", "HDR")] - [TestCase(10, "", "", "SL-HDR2", "HDR")] - [TestCase(10, "", "", "SL-HDR3", "HDR")] - [TestCase(10, "", "", "Technicolor Advanced HDR", "HDR")] - public void should_format_video_dynamic_range_type(int bitDepth, string colourPrimaries, string transferCharacteristics, string hdrFormat, string expectedVideoDynamicRange) - { - var mediaInfo = new MediaInfoModel - { - VideoBitDepth = bitDepth, - VideoColourPrimaries = colourPrimaries, - VideoTransferCharacteristics = transferCharacteristics, - VideoHdrFormat = hdrFormat, - SchemaRevision = 7 - }; - - MediaInfoFormatter.FormatVideoDynamicRangeType(mediaInfo).Should().Be(expectedVideoDynamicRange); - } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeTypeFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeTypeFixture.cs new file mode 100644 index 000000000..3c37d0f34 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatVideoDynamicRangeTypeFixture.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatVideoDynamicRangeTypeFixture : TestBase + { + [TestCase(HdrFormat.None, "")] + [TestCase(HdrFormat.Hlg10, "HLG")] + [TestCase(HdrFormat.Pq10, "PQ")] + [TestCase(HdrFormat.Hdr10, "HDR10")] + [TestCase(HdrFormat.Hdr10Plus, "HDR10Plus")] + [TestCase(HdrFormat.DolbyVision, "DV")] + [TestCase(HdrFormat.DolbyVisionHdr10, "DV HDR10")] + [TestCase(HdrFormat.DolbyVisionHlg, "DV HLG")] + [TestCase(HdrFormat.DolbyVisionSdr, "DV SDR")] + public void should_format_video_dynamic_range_type(HdrFormat format, string expectedVideoDynamicRangeType) + { + var mediaInfo = new MediaInfoModel + { + VideoHdrFormat = format, + SchemaRevision = 9 + }; + + MediaInfoFormatter.FormatVideoDynamicRangeType(mediaInfo).Should().Be(expectedVideoDynamicRangeType); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoModelExtensionsFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoModelExtensionsFixture.cs deleted file mode 100644 index 5beec7b1d..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoModelExtensionsFixture.cs +++ /dev/null @@ -1,43 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.MediaFiles.MediaInfo; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.MediaFiles.MediaInfo -{ - [TestFixture] - public class MediaInfoModelExtensionsFixture : TestBase - { - [TestCase(8, "", "", "", HdrFormat.None)] - [TestCase(8, "BT.601 NTSC", "BT.709", "", HdrFormat.None)] - [TestCase(8, "BT.2020", "PQ", "", HdrFormat.None)] - [TestCase(8, "", "", "Dolby Vision", HdrFormat.None)] - [TestCase(8, "", "", "SMPTE ST 2086", HdrFormat.None)] - [TestCase(10, "BT.601 NTSC", "PQ", "", HdrFormat.None)] - [TestCase(10, "BT.2020", "BT.709", "", HdrFormat.None)] - [TestCase(10, "BT.2020", "PQ", "", HdrFormat.Pq10)] - [TestCase(10, "BT.2020", "HLG", "", HdrFormat.Hlg10)] - [TestCase(10, "", "", "SMPTE ST 2086", HdrFormat.Hdr10)] - [TestCase(10, "", "", "HDR10", HdrFormat.Hdr10)] - [TestCase(10, "", "", "SMPTE ST 2094 App 4", HdrFormat.Hdr10Plus)] - [TestCase(10, "", "", "Dolby Vision", HdrFormat.DolbyVision)] - [TestCase(10, "BT.2020", "PQ", "Dolby Vision / SMPTE ST 2086", HdrFormat.DolbyVisionHdr10)] - [TestCase(10, "", "", "SL-HDR1", HdrFormat.UnknownHdr)] - [TestCase(10, "", "", "SL-HDR2", HdrFormat.UnknownHdr)] - [TestCase(10, "", "", "SL-HDR3", HdrFormat.UnknownHdr)] - [TestCase(10, "", "", "Technicolor Advanced HDR", HdrFormat.UnknownHdr)] - public void should_get_hdr_format(int bitDepth, string colourPrimaries, string transferCharacteristics, string hdrFormat, HdrFormat expectedVideoDynamicRange) - { - var mediaInfo = new MediaInfoModel - { - VideoBitDepth = bitDepth, - VideoColourPrimaries = colourPrimaries, - VideoTransferCharacteristics = transferCharacteristics, - VideoHdrFormat = hdrFormat, - SchemaRevision = 7 - }; - - MediaInfoModelExtensions.GetHdrFormat(mediaInfo).Should().Be(expectedVideoDynamicRange); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs index 946efbf62..f0826d64b 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/VideoFileInfoReaderFixture.cs @@ -1,8 +1,13 @@ +using System; using System.IO; +using System.Linq; +using System.Reflection; +using FFMpegCore; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common.Categories; @@ -40,33 +45,26 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo var info = Subject.GetMediaInfo(path); - info.VideoCodec.Should().BeNull(); - info.VideoFormat.Should().Be("AVC"); + info.VideoFormat.Should().Be("h264"); info.VideoCodecID.Should().Be("avc1"); - info.VideoProfile.Should().Be("Baseline@L2.1"); - info.VideoCodecLibrary.Should().Be(""); - info.AudioFormat.Should().Be("AAC"); - info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2"); - info.AudioProfile.Should().BeOneOf("", "LC"); - info.AudioCodecLibrary.Should().Be(""); - info.AudioBitrate.Should().Be(128000); - info.AudioChannelsContainer.Should().Be(2); - info.AudioChannelsStream.Should().Be(0); - info.AudioChannelPositionsTextContainer.Should().Be("Front: L R"); - info.AudioChannelPositionsTextStream.Should().Be(""); - info.AudioLanguages.Should().Be("English"); + info.VideoProfile.Should().Be("Constrained Baseline"); + info.AudioFormat.Should().Be("aac"); + info.AudioCodecID.Should().Be("mp4a"); + info.AudioProfile.Should().Be("LC"); + info.AudioBitrate.Should().Be(125488); + info.AudioChannels.Should().Be(2); + info.AudioChannelPositions.Should().Be("stereo"); + info.AudioLanguages.Should().BeEquivalentTo("eng"); 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.Subtitles.Should().BeEmpty(); + info.VideoBitrate.Should().Be(193328); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); - info.VideoColourPrimaries.Should().Be("BT.601 NTSC"); - info.VideoTransferCharacteristics.Should().Be("BT.709"); - info.AudioAdditionalFeatures.Should().BeOneOf("", "LC"); - info.VideoHdrFormat.Should().BeEmpty(); - info.VideoHdrFormatCompatibility.Should().BeEmpty(); + info.VideoBitDepth.Should().Be(8); + info.VideoColourPrimaries.Should().Be("smpte170m"); + info.VideoTransferCharacteristics.Should().Be("bt709"); } [Test] @@ -83,45 +81,62 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo var info = Subject.GetMediaInfo(path); - info.VideoCodec.Should().BeNull(); - info.VideoFormat.Should().Be("AVC"); + info.VideoFormat.Should().Be("h264"); info.VideoCodecID.Should().Be("avc1"); - info.VideoProfile.Should().Be("Baseline@L2.1"); - info.VideoCodecLibrary.Should().Be(""); - info.AudioFormat.Should().Be("AAC"); - info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2"); - info.AudioProfile.Should().BeOneOf("", "LC"); - info.AudioCodecLibrary.Should().Be(""); - info.AudioBitrate.Should().Be(128000); - info.AudioChannelsContainer.Should().Be(2); - info.AudioChannelsStream.Should().Be(0); - info.AudioChannelPositionsTextContainer.Should().Be("Front: L R"); - info.AudioChannelPositionsTextStream.Should().Be(""); - info.AudioLanguages.Should().Be("English"); + info.VideoProfile.Should().Be("Constrained Baseline"); + info.AudioFormat.Should().Be("aac"); + info.AudioCodecID.Should().Be("mp4a"); + info.AudioProfile.Should().Be("LC"); + info.AudioBitrate.Should().Be(125488); + info.AudioChannels.Should().Be(2); + info.AudioChannelPositions.Should().Be("stereo"); + info.AudioLanguages.Should().BeEquivalentTo("eng"); 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.Subtitles.Should().BeEmpty(); + info.VideoBitrate.Should().Be(193328); info.VideoFps.Should().Be(24); info.Width.Should().Be(480); - info.VideoColourPrimaries.Should().Be("BT.601 NTSC"); - info.VideoTransferCharacteristics.Should().Be("BT.709"); - info.AudioAdditionalFeatures.Should().BeOneOf("", "LC"); - info.VideoHdrFormat.Should().BeEmpty(); - info.VideoHdrFormatCompatibility.Should().BeEmpty(); + info.VideoColourPrimaries.Should().Be("smpte170m"); + info.VideoTransferCharacteristics.Should().Be("bt709"); } - [Test] - public void should_dispose_file_after_scanning_mediainfo() + [TestCase(8, "", "", "", null, HdrFormat.None)] + [TestCase(10, "", "", "", null, HdrFormat.None)] + [TestCase(10, "bt709", "bt709", "", null, HdrFormat.None)] + [TestCase(8, "bt2020", "smpte2084", "", null, HdrFormat.None)] + [TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.Hlg10)] + [TestCase(10, "bt2020", "arib-std-b67", "", null, HdrFormat.Hlg10)] + [TestCase(10, "bt2020", "smpte2084", "", null, HdrFormat.Pq10)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", null, HdrFormat.Pq10)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.MasteringDisplayMetadata", null, HdrFormat.Hdr10)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.ContentLightLevelMetadata", null, HdrFormat.Hdr10)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", null, HdrFormat.Hdr10Plus)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", null, HdrFormat.DolbyVision)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 1, HdrFormat.DolbyVisionHdr10)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 2, HdrFormat.DolbyVisionSdr)] + [TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", 4, HdrFormat.DolbyVisionHlg)] + public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, int? doviConfigId, HdrFormat expected) { - var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4"); + var assembly = Assembly.GetAssembly(typeof(FFProbe)); + var types = sideDataTypes.Split(",").Select(x => x.Trim()).ToList(); + var sideData = types.Where(x => x.IsNotNullOrWhiteSpace()).Select(x => assembly.CreateInstance(x)).Cast().ToList(); - var info = Subject.GetMediaInfo(path); + if (doviConfigId.HasValue) + { + sideData.ForEach(x => + { + if (x.GetType().Name == "DoviConfigurationRecordSideData") + { + ((DoviConfigurationRecordSideData)x).DvBlSignalCompatibilityId = doviConfigId.Value; + } + }); + } - var stream = new FileStream(path, FileMode.Open, FileAccess.Write); + var result = VideoFileInfoReader.GetHdrFormat(bitDepth, colourPrimaries, transferFunction, sideData); - stream.Close(); + result.Should().Be(expected); } } } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 646db8634..2e504b707 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -17,6 +17,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests { + [Platform(Exclude = "Win")] [TestFixture] public class FileNameBuilderFixture : CoreTest @@ -539,37 +540,44 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _episodeFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() { - VideoCodec = "AVC", - AudioFormat = "DTS", - AudioChannelsContainer = 6, - AudioLanguages = "English/Spanish", - Subtitles = "English/Spanish/Italian", - SchemaRevision = 3 + VideoFormat = "h264", + AudioFormat = "dts", + AudioLanguages = new List { "eng", "spa" }, + Subtitles = new List { "eng", "spa", "ita" } }; Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) - .Should().Be("South.Park.S15E06.City.Sushi.X264.DTS[EN+ES].[EN+ES+IT]"); + .Should().Be("South.Park.S15E06.City.Sushi.H264.DTS[EN+ES].[EN+ES+IT]"); } - [TestCase("Norwegian Bokmal", "NB")] - [TestCase("Swedis", "SV")] - [TestCase("Chinese", "ZH")] + [TestCase("nob", "NB")] + [TestCase("swe", "SV")] + [TestCase("zho", "ZH")] + [TestCase("chi", "ZH")] + [TestCase("fre", "FR")] + [TestCase("rum", "RO")] + [TestCase("per", "FA")] + [TestCase("ger", "DE")] + [TestCase("cze", "CS")] + [TestCase("ice", "IS")] + [TestCase("dut", "NL")] + [TestCase("nor", "NO")] public void should_format_languagecodes_properly(string language, string code) { _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MEDIAINFO.FULL}"; _episodeFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() { - VideoCodec = "AVC", - AudioFormat = "DTS", - AudioChannelsContainer = 6, - AudioLanguages = "English", - Subtitles = language, + VideoFormat = "h264", + AudioFormat = "dts", + AudioChannels = 6, + AudioLanguages = new List { "eng" }, + Subtitles = new List { language }, SchemaRevision = 3 }; Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) - .Should().Be($"South.Park.S15E06.City.Sushi.X264.DTS.[{code}]"); + .Should().Be($"South.Park.S15E06.City.Sushi.H264.DTS.[{code}]"); } [Test] @@ -579,18 +587,17 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _episodeFile.MediaInfo = new Core.MediaFiles.MediaInfo.MediaInfoModel() { - VideoCodec = "AVC", - AudioFormat = "DTS", - AudioChannelsContainer = 6, - AudioLanguages = "English", - Subtitles = "English/Spanish/Italian", - SchemaRevision = 3 + VideoFormat = "h264", + AudioFormat = "dts", + AudioLanguages = new List { "eng" }, + Subtitles = new List { "eng", "spa", "ita" } }; Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) - .Should().Be("South.Park.S15E06.City.Sushi.X264.DTS.[EN+ES+IT]"); + .Should().Be("South.Park.S15E06.City.Sushi.H264.DTS.[EN+ES+IT]"); } + [Ignore("not currently supported")] [Test] public void should_remove_duplicate_non_word_characters() { @@ -810,7 +817,7 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests _episodeFile.ReleaseGroup = null; - GivenMediaInfoModel(audioLanguages: "English/German"); + GivenMediaInfoModel(audioLanguages: "eng/deu"); _namingConfig.StandardEpisodeFormat = "{MediaInfo AudioLanguages}"; @@ -823,8 +830,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests } } - [TestCase("English", "")] - [TestCase("English/German", "[EN+DE]")] + [TestCase("eng", "")] + [TestCase("eng/deu", "[EN+DE]")] public void should_format_audio_languages(string audioLanguages, string expected) { _episodeFile.ReleaseGroup = null; @@ -837,8 +844,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be(expected); } - [TestCase("English", "[EN]")] - [TestCase("English/German", "[EN+DE]")] + [TestCase("eng", "[EN]")] + [TestCase("eng/deu", "[EN+DE]")] public void should_format_audio_languages_all(string audioLanguages, string expected) { _episodeFile.ReleaseGroup = null; @@ -851,15 +858,15 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be(expected); } - [TestCase("English/German", "", "[EN+DE]")] - [TestCase("English/Dutch/German", "", "[EN+NL+DE]")] - [TestCase("English/German", ":DE", "[DE]")] - [TestCase("English/Dutch/German", ":EN+NL", "[EN+NL]")] - [TestCase("English/Dutch/German", ":NL+EN", "[NL+EN]")] - [TestCase("English/Dutch/German", ":-NL", "[EN+DE]")] - [TestCase("English/Dutch/German", ":DE+", "[DE+-]")] - [TestCase("English/Dutch/German", ":DE+NO.", "[DE].")] - [TestCase("English/Dutch/German", ":-EN-", "[NL+DE]-")] + [TestCase("eng/deu", "", "[EN+DE]")] + [TestCase("eng/nld/deu", "", "[EN+NL+DE]")] + [TestCase("eng/deu", ":DE", "[DE]")] + [TestCase("eng/nld/deu", ":EN+NL", "[EN+NL]")] + [TestCase("eng/nld/deu", ":NL+EN", "[NL+EN]")] + [TestCase("eng/nld/deu", ":-NL", "[EN+DE]")] + [TestCase("eng/nld/deu", ":DE+", "[DE+-]")] + [TestCase("eng/nld/deu", ":DE+NO.", "[DE].")] + [TestCase("eng/nld/deu", ":-EN-", "[NL+DE]-")] public void should_format_subtitle_languages_all(string subtitleLanguages, string format, string expected) { _episodeFile.ReleaseGroup = null; @@ -872,19 +879,15 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Should().Be(expected + "End"); } - [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) + [TestCase(HdrFormat.None, "South.Park.S15E06.City.Sushi")] + [TestCase(HdrFormat.Hlg10, "South.Park.S15E06.City.Sushi.HDR")] + [TestCase(HdrFormat.Hdr10, "South.Park.S15E06.City.Sushi.HDR")] + public void should_include_hdr_for_mediainfo_videodynamicrange_with_valid_properties(HdrFormat hdrFormat, string expectedName) { _namingConfig.StandardEpisodeFormat = "{Series.Title}.S{season:00}E{episode:00}.{Episode.Title}.{MediaInfo VideoDynamicRange}"; - GivenMediaInfoModel(videoBitDepth: bitDepth, videoColourPrimaries: colourPrimaries, videoTransferCharacteristics: transferCharacteristics); + GivenMediaInfoModel(hdrFormat: hdrFormat); Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) .Should().Be(expectedName); @@ -968,14 +971,15 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests .Setup(u => u.Update(_episodeFile, _series)) .Callback((EpisodeFile e, Series s) => e.MediaInfo = new MediaInfoModel { - VideoCodec = "AVC", + VideoFormat = "AVC", AudioFormat = "DTS", - AudioChannelsContainer = 6, - AudioLanguages = "English", - Subtitles = "English/Spanish/Italian", + AudioChannels = 6, + AudioLanguages = new List { "eng" }, + Subtitles = new List { "eng", "esp", "ita" }, VideoBitDepth = 10, - VideoColourPrimaries = "BT.2020", + VideoColourPrimaries = "bt2020", VideoTransferCharacteristics = "PQ", + VideoHdrFormat = HdrFormat.Pq10, SchemaRevision = 5 }); @@ -984,26 +988,24 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests result.Should().EndWith("HDR"); } - private void GivenMediaInfoModel(string videoCodec = "AVC", - string audioCodec = "DTS", + private void GivenMediaInfoModel(string videoCodec = "h264", + string audioCodec = "dts", int audioChannels = 6, int videoBitDepth = 8, - string videoColourPrimaries = "", - string videoTransferCharacteristics = "", - string audioLanguages = "English", - string subtitles = "English/Spanish/Italian", + HdrFormat hdrFormat = HdrFormat.None, + string audioLanguages = "eng", + string subtitles = "eng/spa/ita", int schemaRevision = 5) { _episodeFile.MediaInfo = new MediaInfoModel { - VideoCodec = videoCodec, + VideoFormat = videoCodec, AudioFormat = audioCodec, - AudioChannelsContainer = audioChannels, - AudioLanguages = audioLanguages, - Subtitles = subtitles, + AudioChannels = audioChannels, + AudioLanguages = audioLanguages.Split("/").ToList(), + Subtitles = subtitles.Split("/").ToList(), VideoBitDepth = videoBitDepth, - VideoColourPrimaries = videoColourPrimaries, - VideoTransferCharacteristics = videoTransferCharacteristics, + VideoHdrFormat = hdrFormat, SchemaRevision = schemaRevision }; } diff --git a/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs b/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs new file mode 100644 index 000000000..ce6e43b3e --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/163_mediainfo_to_ffmpeg.cs @@ -0,0 +1,903 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Dapper; +using FluentMigrator; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(163)] + public class mediainfo_to_ffmpeg : NzbDroneMigrationBase + { + private readonly JsonSerializerOptions _serializerSettings; + + public mediainfo_to_ffmpeg() + { + var serializerSettings = new JsonSerializerOptions + { + AllowTrailingCommas = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); + serializerSettings.Converters.Add(new STJTimeSpanConverter()); + serializerSettings.Converters.Add(new STJUtcConverter()); + + _serializerSettings = serializerSettings; + } + + protected override void MainDbUpgrade() + { + Execute.WithConnection(MigrateToFfprobe); + } + + private void MigrateToFfprobe(IDbConnection conn, IDbTransaction tran) + { + var existing = conn.Query("SELECT Id, MediaInfo, SceneName FROM EpisodeFiles"); + + var updated = new List(); + + foreach (var row in existing) + { + if (row.MediaInfo.IsNullOrWhiteSpace()) + { + continue; + } + + // basic parse to check schema revision + // in case user already tested ffmpeg branch + var mediaInfoVersion = JsonSerializer.Deserialize(row.MediaInfo, _serializerSettings); + if (mediaInfoVersion.SchemaRevision >= 8) + { + continue; + } + + // parse and migrate + var mediaInfo = JsonSerializer.Deserialize(row.MediaInfo, _serializerSettings); + + var ffprobe = MigrateMediaInfo(mediaInfo, row.SceneName); + + updated.Add(new MediaInfoRaw + { + Id = row.Id, + MediaInfo = JsonSerializer.Serialize(ffprobe, _serializerSettings) + }); + } + + var updateSql = "UPDATE EpisodeFiles SET MediaInfo = @MediaInfo WHERE Id = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + + public MediaInfo163 MigrateMediaInfo(MediaInfo162 old, string sceneName) + { + var m = new MediaInfo163 + { + SchemaRevision = old.SchemaRevision, + ContainerFormat = old.ContainerFormat, + VideoProfile = old.VideoProfile, + VideoBitrate = old.VideoBitrate, + VideoBitDepth = old.VideoBitDepth, + VideoMultiViewCount = old.VideoMultiViewCount, + VideoColourPrimaries = MigratePrimaries(old.VideoColourPrimaries), + VideoTransferCharacteristics = MigrateTransferCharacteristics(old.VideoTransferCharacteristics), + Height = old.Height, + Width = old.Width, + AudioBitrate = old.AudioBitrate, + RunTime = old.RunTime, + AudioStreamCount = old.AudioStreamCount, + VideoFps = old.VideoFps, + ScanType = old.ScanType, + AudioLanguages = MigrateLanguages(old.AudioLanguages), + Subtitles = MigrateLanguages(old.Subtitles) + }; + + m.VideoHdrFormat = MigrateHdrFormat(old); + + MigrateVideoCodec(old, m, sceneName); + MigrateAudioCodec(old, m); + MigrateAudioChannelPositions(old, m); + + m.AudioChannels = old.AudioChannelsStream > 0 ? old.AudioChannelsStream : old.AudioChannelsContainer; + + return m; + } + + private void MigrateVideoCodec(MediaInfo162 mediaInfo, MediaInfo163 m, string sceneName) + { + if (mediaInfo.VideoFormat == null) + { + MigrateVideoCodecLegacy(mediaInfo, m, sceneName); + return; + } + + var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); + var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty; + var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty; + + var result = mediaInfo.VideoFormat.Trim(); + + m.VideoFormat = result; + m.VideoCodecID = null; + + if (videoFormat.ContainsIgnoreCase("x264")) + { + m.VideoFormat = "h264"; + m.VideoCodecID = "x264"; + return; + } + + if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC")) + { + m.VideoFormat = "h264"; + + if (videoCodecLibrary.StartsWithIgnoreCase("x264")) + { + m.VideoCodecID = "x264"; + } + + return; + } + + if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC")) + { + m.VideoFormat = "hevc"; + if (videoCodecLibrary.StartsWithIgnoreCase("x265")) + { + m.VideoCodecID = "x265"; + } + + return; + } + + if (videoFormat.ContainsIgnoreCase("MPEG Video")) + { + if (videoCodecID == "2" || videoCodecID == "V_MPEG2") + { + m.VideoFormat = "mpeg2video"; + } + + if (videoCodecID.IsNullOrWhiteSpace()) + { + m.VideoFormat = "MPEG"; + } + } + + if (videoFormat.ContainsIgnoreCase("MPEG-2 Video")) + { + m.VideoFormat = "mpeg2video"; + } + + if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual")) + { + m.VideoFormat = "mpeg4"; + + if (videoCodecID.ContainsIgnoreCase("XVID") || + videoCodecLibrary.StartsWithIgnoreCase("XviD")) + { + m.VideoCodecID = "XVID"; + } + + if (videoCodecID.ContainsIgnoreCase("DIV3") || + videoCodecID.ContainsIgnoreCase("DIVX") || + videoCodecID.ContainsIgnoreCase("DX50") || + videoCodecLibrary.StartsWithIgnoreCase("DivX")) + { + m.VideoCodecID = "DIVX"; + } + + return; + } + + if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v")) + { + m.VideoFormat = "mpeg4"; + + result = GetSceneNameMatch(sceneName, "XviD", "DivX", ""); + + if (result == "XviD") + { + m.VideoCodecID = "XVID"; + } + + if (result == "DivX") + { + m.VideoCodecID = "DIVX"; + } + + return; + } + + if (videoFormat.ContainsIgnoreCase("VC-1")) + { + m.VideoFormat = "vc1"; + return; + } + + if (videoFormat.ContainsIgnoreCase("AV1")) + { + m.VideoFormat = "av1"; + return; + } + + if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") || + videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9")) + { + m.VideoFormat = videoFormat.First().ToLowerInvariant(); + return; + } + + if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2")) + { + m.VideoFormat = "WMV"; + return; + } + + if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3")) + { + m.VideoFormat = "mpeg4"; + m.VideoCodecID = "DIVX"; + return; + } + + if (videoFormat.ContainsIgnoreCase("XviD")) + { + m.VideoFormat = "mpeg4"; + m.VideoCodecID = "XVID"; + return; + } + + if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") || + videoFormat.ContainsIgnoreCase("RealVideo 4")) + { + m.VideoFormat = "qtrle"; + return; + } + + if (videoFormat.ContainsIgnoreCase("mp42") || + videoFormat.ContainsIgnoreCase("mp43")) + { + m.VideoFormat = "mpeg4"; + return; + } + } + + private void MigrateVideoCodecLegacy(MediaInfo162 mediaInfo, MediaInfo163 m, string sceneName) + { + var videoCodec = mediaInfo.VideoCodec; + + m.VideoFormat = videoCodec; + m.VideoCodecID = null; + + if (videoCodec.IsNullOrWhiteSpace()) + { + m.VideoFormat = null; + return; + } + + if (videoCodec == "AVC") + { + m.VideoFormat = "h264"; + } + + if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC") + { + m.VideoFormat = "hevc"; + } + + if (videoCodec == "MPEG-2 Video") + { + m.VideoFormat = "mpeg2video"; + } + + if (videoCodec == "MPEG-4 Visual") + { + var result = GetSceneNameMatch(sceneName, "DivX", "XviD"); + if (result == "DivX") + { + m.VideoFormat = "mpeg4"; + m.VideoCodecID = "DIVX"; + } + + m.VideoFormat = "mpeg4"; + m.VideoCodecID = "XVID"; + } + + if (videoCodec.StartsWithIgnoreCase("XviD")) + { + m.VideoFormat = "mpeg4"; + m.VideoCodecID = "XVID"; + } + + if (videoCodec.StartsWithIgnoreCase("DivX")) + { + m.VideoFormat = "mpeg4"; + m.VideoCodecID = "DIVX"; + } + + if (videoCodec.EqualsIgnoreCase("VC-1")) + { + m.VideoFormat = "vc1"; + } + } + + private HdrFormat MigrateHdrFormat(MediaInfo162 mediaInfo) + { + if (mediaInfo.VideoHdrFormatCompatibility.IsNotNullOrWhiteSpace()) + { + if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("HLG")) + { + return HdrFormat.Hlg10; + } + + if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby")) + { + return HdrFormat.DolbyVision; + } + + if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby")) + { + return HdrFormat.DolbyVision; + } + + if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10+")) + { + return HdrFormat.Hdr10Plus; + } + + if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10")) + { + return HdrFormat.Hdr10; + } + } + + return VideoFileInfoReader.GetHdrFormat(mediaInfo.VideoBitDepth, mediaInfo.VideoColourPrimaries, mediaInfo.VideoTransferCharacteristics, new ()); + } + + private void MigrateAudioCodec(MediaInfo162 mediaInfo, MediaInfo163 m) + { + if (mediaInfo.AudioCodecID == null) + { + MigrateAudioCodecLegacy(mediaInfo, m); + return; + } + + var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); + var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty; + var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + m.AudioFormat = ""; + + if (audioFormat.Empty()) + { + return; + } + + if (audioFormat.ContainsIgnoreCase("Atmos")) + { + m.AudioFormat = "truehd"; + m.AudioCodecID = "thd+"; + return; + } + + if (audioFormat.ContainsIgnoreCase("MLP FBA")) + { + m.AudioFormat = "truehd"; + + if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch")) + { + m.AudioCodecID = "thd+"; + return; + } + + return; + } + + if (audioFormat.ContainsIgnoreCase("TrueHD")) + { + m.AudioFormat = "truehd"; + return; + } + + if (audioFormat.ContainsIgnoreCase("FLAC")) + { + m.AudioFormat = "flac"; + return; + } + + if (audioFormat.ContainsIgnoreCase("DTS")) + { + m.AudioFormat = "dts"; + + if (splitAdditionalFeatures.ContainsIgnoreCase("XLL")) + { + if (splitAdditionalFeatures.ContainsIgnoreCase("X")) + { + m.AudioProfile = "DTS:X"; + return; + } + + m.AudioProfile = "DTS-HD MA"; + return; + } + + if (splitAdditionalFeatures.ContainsIgnoreCase("ES")) + { + m.AudioProfile = "DTS-ES"; + return; + } + + if (splitAdditionalFeatures.ContainsIgnoreCase("XBR")) + { + m.AudioProfile = "DTS-HD HRA"; + return; + } + + return; + } + + if (audioFormat.ContainsIgnoreCase("E-AC-3")) + { + m.AudioFormat = "eac3"; + + if (splitAdditionalFeatures.ContainsIgnoreCase("JOC")) + { + m.AudioCodecID = "ec+3"; + } + + return; + } + + if (audioFormat.ContainsIgnoreCase("AC-3")) + { + m.AudioFormat = "ac3"; + return; + } + + if (audioFormat.ContainsIgnoreCase("AAC")) + { + m.AudioFormat = "aac"; + + if (audioCodecID == "A_AAC/MPEG4/LC/SBR") + { + m.AudioCodecID = audioCodecID; + } + + return; + } + + if (audioFormat.ContainsIgnoreCase("mp3")) + { + m.AudioFormat = "mp3"; + return; + } + + if (audioFormat.ContainsIgnoreCase("MPEG Audio")) + { + if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3") + { + m.AudioFormat = "mp3"; + return; + } + + if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2") + { + m.AudioFormat = "mp2"; + } + } + + if (audioFormat.ContainsIgnoreCase("Opus")) + { + m.AudioFormat = "opus"; + return; + } + + if (audioFormat.ContainsIgnoreCase("PCM")) + { + m.AudioFormat = "pcm_s16le"; + return; + } + + if (audioFormat.ContainsIgnoreCase("ADPCM")) + { + m.AudioFormat = "pcm_s16le"; + return; + } + + if (audioFormat.ContainsIgnoreCase("Vorbis")) + { + m.AudioFormat = "vorbis"; + return; + } + + if (audioFormat.ContainsIgnoreCase("WMA")) + { + m.AudioFormat = "wmav1"; + return; + } + } + + private void MigrateAudioCodecLegacy(MediaInfo162 mediaInfo, MediaInfo163 m) + { + var audioFormat = mediaInfo.AudioFormat; + + m.AudioFormat = audioFormat; + + if (audioFormat.IsNullOrWhiteSpace()) + { + m.AudioFormat = null; + return; + } + + if (audioFormat.EqualsIgnoreCase("AC-3")) + { + m.AudioFormat = "ac3"; + return; + } + + if (audioFormat.EqualsIgnoreCase("E-AC-3")) + { + m.AudioFormat = "eac3"; + return; + } + + if (audioFormat.EqualsIgnoreCase("AAC")) + { + m.AudioFormat = "aac"; + return; + } + + if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3") + { + m.AudioFormat = "mp3"; + return; + } + + if (audioFormat.EqualsIgnoreCase("DTS")) + { + m.AudioFormat = "DTS"; + return; + } + + if (audioFormat.EqualsIgnoreCase("TrueHD")) + { + m.AudioFormat = "truehd"; + return; + } + + if (audioFormat.EqualsIgnoreCase("FLAC")) + { + m.AudioFormat = "flac"; + return; + } + + if (audioFormat.EqualsIgnoreCase("Vorbis")) + { + m.AudioFormat = "vorbis"; + return; + } + + if (audioFormat.EqualsIgnoreCase("Opus")) + { + m.AudioFormat = "opus"; + return; + } + } + + private void MigrateAudioChannelPositions(MediaInfo162 mediaInfo, MediaInfo163 m) + { + var audioChannels = FormatAudioChannelsFromAudioChannelPositions(mediaInfo); + + if (audioChannels == null || audioChannels == 0.0m) + { + audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo); + } + + if (audioChannels == null || audioChannels == 0.0m) + { + audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo); + } + + audioChannels ??= 0; + + m.AudioChannelPositions = audioChannels.ToString(); + } + + private decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfo162 mediaInfo) + { + var audioChannelPositions = mediaInfo.AudioChannelPositions; + + if (audioChannelPositions.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + if (audioChannelPositions.Contains("+")) + { + return audioChannelPositions.Split('+') + .Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture)); + } + + if (audioChannelPositions.Contains("/")) + { + var channelStringList = Regex.Replace(audioChannelPositions, + @"^\d+\sobjects", + "", + RegexOptions.Compiled | RegexOptions.IgnoreCase) + .Replace("Object Based / ", "") + .Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries) + .FirstOrDefault() + ?.Split('/'); + + var positions = default(decimal); + + if (channelStringList == null) + { + return 0; + } + + foreach (var channel in channelStringList) + { + var channelSplit = channel.Split(new string[] { "." }, StringSplitOptions.None); + + if (channelSplit.Length == 3) + { + positions += decimal.Parse(string.Format("{0}.{1}", channelSplit[1], channelSplit[2]), CultureInfo.InvariantCulture); + } + else + { + positions += decimal.Parse(channel, CultureInfo.InvariantCulture); + } + } + + return positions; + } + } + catch + { + } + + return null; + } + + private decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfo162 mediaInfo) + { + var audioChannelPositionsTextContainer = mediaInfo.AudioChannelPositionsTextContainer; + var audioChannelPositionsTextStream = mediaInfo.AudioChannelPositionsTextStream; + var audioChannelsContainer = mediaInfo.AudioChannelsContainer; + var audioChannelsStream = mediaInfo.AudioChannelsStream; + + //Skip if the positions texts give us nothing + if ((audioChannelPositionsTextContainer.IsNullOrWhiteSpace() || audioChannelPositionsTextContainer == "Object Based") && + (audioChannelPositionsTextStream.IsNullOrWhiteSpace() || audioChannelPositionsTextStream == "Object Based")) + { + return null; + } + + try + { + if (audioChannelsStream > 0) + { + return audioChannelPositionsTextStream.ContainsIgnoreCase("LFE") ? audioChannelsStream - 1 + 0.1m : audioChannelsStream; + } + + return audioChannelPositionsTextContainer.ContainsIgnoreCase("LFE") ? audioChannelsContainer - 1 + 0.1m : audioChannelsContainer; + } + catch + { + } + + return null; + } + + private decimal? FormatAudioChannelsFromAudioChannels(MediaInfo162 mediaInfo) + { + var audioChannelsContainer = mediaInfo.AudioChannelsContainer; + var audioChannelsStream = mediaInfo.AudioChannelsStream; + + var audioFormat = (mediaInfo.AudioFormat ?? string.Empty).Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); + var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + // Workaround https://github.com/MediaArea/MediaInfo/issues/299 for DTS-X Audio + if (audioFormat.ContainsIgnoreCase("DTS") && + splitAdditionalFeatures.ContainsIgnoreCase("XLL") && + splitAdditionalFeatures.ContainsIgnoreCase("X") && + audioChannelsContainer > 0) + { + return audioChannelsContainer - 1 + 0.1m; + } + + // FLAC 6 channels is likely 5.1 + if (audioFormat.ContainsIgnoreCase("FLAC") && audioChannelsContainer == 6) + { + return 5.1m; + } + + if (mediaInfo.SchemaRevision > 5) + { + return audioChannelsStream > 0 ? audioChannelsStream : audioChannelsContainer; + } + + if (mediaInfo.SchemaRevision >= 3) + { + return audioChannelsContainer; + } + + return null; + } + + private List MigrateLanguages(string mediaInfoLanguages) + { + var languages = new List(); + + var tokens = mediaInfoLanguages.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); + + var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); + for (int i = 0; i < tokens.Count; i++) + { + if (tokens[i] == "Swedis") + { + // Probably typo in mediainfo (should be 'Swedish') + languages.Add("swe"); + continue; + } + + if (tokens[i] == "Chinese" && OsInfo.IsNotWindows) + { + // Mono only has 'Chinese (Simplified)' & 'Chinese (Traditional)' + languages.Add("zho"); + continue; + } + + if (tokens[i] == "Norwegian") + { + languages.Add("nor"); + continue; + } + + try + { + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]); + + if (cultureInfo != null) + { + languages.Add(cultureInfo.ThreeLetterISOLanguageName.ToLowerInvariant()); + } + } + catch + { + } + } + + return languages; + } + + private string MigratePrimaries(string primary) + { + return primary.IsNotNullOrWhiteSpace() ? primary.Replace("BT.", "bt") : primary; + } + + private string MigrateTransferCharacteristics(string transferCharacteristics) + { + if (transferCharacteristics == "PQ") + { + return "smpte2084"; + } + + if (transferCharacteristics == "HLG") + { + return "arib-std-b67"; + } + + return "bt709"; + } + + 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(); + } + + public class MediaInfoRaw : ModelBase + { + public string MediaInfo { get; set; } + public string SceneName { get; set; } + } + + public class MediaInfoBase + { + public int SchemaRevision { get; set; } + } + + public class MediaInfo162 : MediaInfoBase + { + public string ContainerFormat { get; set; } + + // Deprecated according to MediaInfo + public string VideoCodec { 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 string VideoHdrFormat { get; set; } + public string VideoHdrFormatCompatibility { 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; } + public int AudioChannelsContainer { get; set; } + public int AudioChannelsStream { get; set; } + public string AudioChannelPositions { get; set; } + public string AudioChannelPositionsTextContainer { get; set; } + public string AudioChannelPositionsTextStream { get; set; } + public string AudioProfile { get; set; } + public decimal VideoFps { get; set; } + public string AudioLanguages { get; set; } + public string Subtitles { get; set; } + public string ScanType { get; set; } + } + + public class MediaInfo163 : MediaInfoBase + { + public string ContainerFormat { get; set; } + public string VideoFormat { get; set; } + public string VideoCodecID { get; set; } + public string VideoProfile { 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 HdrFormat VideoHdrFormat { get; set; } + public int Height { get; set; } + public int Width { get; set; } + public string AudioFormat { get; set; } + public string AudioCodecID { get; set; } + public string AudioProfile { get; set; } + public int AudioBitrate { get; set; } + public TimeSpan RunTime { get; set; } + public int AudioStreamCount { get; set; } + public int AudioChannels { get; set; } + public string AudioChannelPositions { get; set; } + public decimal VideoFps { get; set; } + public List AudioLanguages { get; set; } + public List Subtitles { get; set; } + public string ScanType { get; set; } + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index d050f171f..76e114fb3 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -319,14 +319,14 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc streamDetails.Add(video); var audio = new XElement("audio"); - var audioChannelCount = episodeFile.MediaInfo.AudioChannelsStream > 0 ? episodeFile.MediaInfo.AudioChannelsStream : episodeFile.MediaInfo.AudioChannelsContainer; + var audioChannelCount = episodeFile.MediaInfo.AudioChannels; audio.Add(new XElement("bitrate", episodeFile.MediaInfo.AudioBitrate)); audio.Add(new XElement("channels", audioChannelCount)); audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo, sceneName))); audio.Add(new XElement("language", episodeFile.MediaInfo.AudioLanguages)); streamDetails.Add(audio); - if (episodeFile.MediaInfo.Subtitles.IsNotNullOrWhiteSpace()) + if (episodeFile.MediaInfo.Subtitles != null && episodeFile.MediaInfo.Subtitles.Count > 0) { var subtitle = new XElement("subtitle"); subtitle.Add(new XElement("language", episodeFile.MediaInfo.Subtitles)); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MediaInfoDllCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MediaInfoDllCheck.cs deleted file mode 100644 index b8ca565c5..000000000 --- a/src/NzbDrone.Core/HealthCheck/Checks/MediaInfoDllCheck.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using NzbDrone.Core.MediaFiles.MediaInfo; - -namespace NzbDrone.Core.HealthCheck.Checks -{ - public class MediaInfoDllCheck : HealthCheckBase - { - [MethodImpl(MethodImplOptions.NoOptimization)] - public override HealthCheck Check() - { - try - { - var mediaInfo = new MediaInfo(); - } - catch (Exception e) - { - return new HealthCheck(GetType(), HealthCheckResult.Warning, $"MediaInfo Library could not be loaded {e.Message}"); - } - - return new HealthCheck(GetType()); - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs index b3e45c183..34e13383d 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/DetectSample.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (!runTime.HasValue) { - _logger.Error("Failed to get runtime from the file, make sure mediainfo is available"); + _logger.Error("Failed to get runtime from the file, make sure ffprobe is available"); return DetectSampleResult.Indeterminate; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/HdrFormat.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/HdrFormat.cs index aa67cdb26..ffe98300a 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/HdrFormat.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/HdrFormat.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace NzbDrone.Core.MediaFiles.MediaInfo { public enum HdrFormat @@ -15,6 +9,8 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo Hdr10Plus, Hlg10, DolbyVision, - DolbyVisionHdr10 + DolbyVisionHdr10, + DolbyVisionSdr, + DolbyVisionHlg } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 8494c9772..42b941344 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -1,4 +1,3 @@ -using System; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -12,9 +11,10 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo { public static class MediaInfoFormatter { - private const string ValidHdrColourPrimaries = "BT.2020"; private const string VideoDynamicRangeHdr = "HDR"; + private static readonly Regex PositionRegex = new Regex(@"(?^\d\.\d)", RegexOptions.Compiled); + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfoFormatter)); public static decimal FormatAudioChannels(MediaInfoModel mediaInfo) @@ -23,101 +23,95 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo if (audioChannels == null || audioChannels == 0.0m) { - audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo); + audioChannels = mediaInfo.AudioChannels; } - if (audioChannels == null || audioChannels == 0.0m) - { - audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo); - } - - return audioChannels ?? 0; + return audioChannels.Value; } public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName) { - if (mediaInfo.AudioCodecID == null) + if (mediaInfo.AudioFormat == null) { - return FormatAudioCodecLegacy(mediaInfo, sceneName); + return null; } - var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); + var audioFormat = mediaInfo.AudioFormat; var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty; var audioProfile = mediaInfo.AudioProfile ?? string.Empty; - var audioCodecLibrary = mediaInfo.AudioCodecLibrary ?? string.Empty; - var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (audioFormat.Empty()) { return string.Empty; } - if (audioFormat.ContainsIgnoreCase("Atmos")) + // see definitions here https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c + if (audioCodecID == "thd+") { return "TrueHD Atmos"; } - if (audioFormat.ContainsIgnoreCase("MLP FBA")) - { - if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch")) - { - return "TrueHD Atmos"; - } - - return "TrueHD"; - } - - if (audioFormat.ContainsIgnoreCase("TrueHD")) + if (audioFormat == "truehd") { return "TrueHD"; } - if (audioFormat.ContainsIgnoreCase("FLAC")) + if (audioFormat == "flac") { return "FLAC"; } - if (audioFormat.ContainsIgnoreCase("DTS")) + if (audioFormat == "dts") { - if (splitAdditionalFeatures.ContainsIgnoreCase("XLL")) + if (audioProfile == "DTS:X") { - if (splitAdditionalFeatures.ContainsIgnoreCase("X")) - { - return "DTS-X"; - } + return "DTS-X"; + } + if (audioProfile == "DTS-HD MA") + { return "DTS-HD MA"; } - if (splitAdditionalFeatures.ContainsIgnoreCase("ES")) + if (audioProfile == "DTS-ES") { return "DTS-ES"; } - if (splitAdditionalFeatures.ContainsIgnoreCase("XBR")) + if (audioProfile == "DTS-HD HRA") { return "DTS-HD HRA"; } + if (audioProfile == "DTS Express") + { + return "DTS Express"; + } + + if (audioProfile == "DTS 96/24") + { + return "DTS 96/24"; + } + return "DTS"; } - if (audioFormat.ContainsIgnoreCase("E-AC-3")) + if (audioCodecID == "ec+3") { - if (splitAdditionalFeatures.ContainsIgnoreCase("JOC")) - { - return "EAC3 Atmos"; - } + return "EAC3 Atmos"; + } + if (audioFormat == "eac3") + { return "EAC3"; } - if (audioFormat.ContainsIgnoreCase("AC-3")) + if (audioFormat == "ac3") { return "AC3"; } - if (audioFormat.ContainsIgnoreCase("AAC")) + if (audioFormat == "aac") { if (audioCodecID == "A_AAC/MPEG4/LC/SBR") { @@ -127,450 +121,169 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return "AAC"; } - if (audioFormat.ContainsIgnoreCase("mp3")) + if (audioFormat == "mp3") { return "MP3"; } - if (audioFormat.ContainsIgnoreCase("MPEG Audio")) + if (audioFormat == "mp2") { - 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"; - } + return "MP2"; } - if (audioFormat.ContainsIgnoreCase("Opus")) + if (audioFormat == "opus") { return "Opus"; } - if (audioFormat.ContainsIgnoreCase("PCM")) + if (audioFormat.StartsWith("pcm_") || audioFormat.StartsWith("adpcm_")) { return "PCM"; } - if (audioFormat.ContainsIgnoreCase("ADPCM")) - { - return "PCM"; - } - - if (audioFormat.ContainsIgnoreCase("Vorbis")) + if (audioFormat == "vorbis") { return "Vorbis"; } - if (audioFormat.ContainsIgnoreCase("WMA")) + if (audioFormat == "wmav1" || + audioFormat == "wmav2" || + audioFormat == "wmapro") { return "WMA"; } - if (audioFormat.ContainsIgnoreCase("A_QUICKTIME")) - { - return ""; - } - Logger.Debug() - .Message("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", mediaInfo.AudioFormat, audioCodecID, audioProfile, audioCodecLibrary, mediaInfo.AudioAdditionalFeatures), sceneName) - .WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID) + .Message("Unknown audio format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName) + .WriteSentryWarn("UnknownAudioFormatFFProbe", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID) .Write(); return mediaInfo.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); + return null; } - var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); + var videoFormat = mediaInfo.VideoFormat; var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty; - var videoProfile = mediaInfo.VideoProfile ?? string.Empty; - var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty; - var result = mediaInfo.VideoFormat.Trim(); + var result = videoFormat.Trim(); if (videoFormat.Empty()) { return result; } - if (videoFormat.ContainsIgnoreCase("x264")) + // see definitions here: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c + if (videoCodecID == "x264") { return "x264"; } - if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC")) + if (videoFormat == "h264") { - if (videoCodecLibrary.StartsWithIgnoreCase("x264")) - { - return "x264"; - } - return GetSceneNameMatch(sceneName, "AVC", "x264", "h264"); } - if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC")) + if (videoCodecID == "x265") { - if (videoCodecLibrary.StartsWithIgnoreCase("x265")) - { - return "x265"; - } + return "x265"; + } + if (videoFormat == "hevc") + { return GetSceneNameMatch(sceneName, "HEVC", "x265", "h265"); } - if (videoFormat.ContainsIgnoreCase("MPEG Video")) - { - if (videoCodecID == "2" || videoCodecID == "V_MPEG2") - { - return "MPEG2"; - } - - if (videoCodecID.IsNullOrWhiteSpace()) - { - return "MPEG"; - } - } - - if (videoFormat.ContainsIgnoreCase("MPEG-2 Video")) + if (videoFormat == "mpeg2video") { return "MPEG2"; } - if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual")) + if (videoFormat == "mpeg1video") { - if (videoCodecID.ContainsIgnoreCase("XVID") || - videoCodecLibrary.StartsWithIgnoreCase("XviD")) + return "MPEG"; + } + + if (videoFormat == "mpeg4" || videoFormat.Contains("msmpeg4")) + { + if (videoCodecID == "XVID") { return "XviD"; } - if (videoCodecID.ContainsIgnoreCase("DIV3") || - videoCodecID.ContainsIgnoreCase("DIVX") || - videoCodecID.ContainsIgnoreCase("DX50") || - videoCodecLibrary.StartsWithIgnoreCase("DivX")) + if (videoCodecID == "DIV3" || + videoCodecID == "DX50" || + videoCodecID.ToUpperInvariant() == "DIVX") { return "DivX"; } + + return ""; } - if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v")) - { - result = GetSceneNameMatch(sceneName, "XviD", "DivX", ""); - if (result.IsNotNullOrWhiteSpace()) - { - return result; - } - - if (videoCodecLibrary.Contains("Lavc")) - { - return ""; // libavcodec mpeg-4 - } - - if (videoCodecLibrary.Contains("em4v")) - { - return ""; // NeroDigital - } - - if (videoCodecLibrary.Contains("Intel(R) IPP")) - { - return ""; // Intel(R) IPP - } - - if (videoCodecLibrary == "") - { - return ""; // Unknown mp4v - } - } - - if (videoFormat.ContainsIgnoreCase("VC-1")) + if (videoFormat == "vc1") { return "VC1"; } - if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") || - videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9")) + if (videoFormat == "av1") { - return videoFormat.First().ToUpperInvariant(); + return "AV1"; } - if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2")) + if (videoFormat == "vp6" || + videoFormat == "vp7" || + videoFormat == "vp8" || + videoFormat == "vp9") + { + return videoFormat.ToUpperInvariant(); + } + + if (videoFormat == "wmv1" || + videoFormat == "wmv2" || + videoFormat == "wmv3") { return "WMV"; } - if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3")) + if (videoFormat == "qtrle" || + videoFormat == "rpza" || + videoFormat == "rv10" || + videoFormat == "rv20" || + videoFormat == "rv30" || + videoFormat == "rv40" || + videoFormat == "cinepak") { - return "DivX"; - } - - if (videoFormat.ContainsIgnoreCase("XviD")) - { - return "XviD"; - } - - if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") || - videoFormat.ContainsIgnoreCase("RealVideo 4")) - { - return ""; - } - - if (videoFormat.ContainsIgnoreCase("mp42") || - videoFormat.ContainsIgnoreCase("mp43")) - { - // MS old DivX competitor return ""; } Logger.Debug() - .Message("Unknown video format: '{0}' in '{1}'.", string.Join(", ", videoFormat, videoCodecID, videoProfile, videoCodecLibrary), sceneName) - .WriteSentryWarn("UnknownVideoFormat", mediaInfo.ContainerFormat, mediaInfo.VideoFormat, videoCodecID) + .Message("Unknown video format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName) + .WriteSentryWarn("UnknownVideoFormatFFProbe", mediaInfo.ContainerFormat, videoFormat, videoCodecID) .Write(); return result; } - public static string FormatVideoCodecLegacy(MediaInfoModel mediaInfo, string sceneName) - { - var videoCodec = mediaInfo.VideoCodec; - - 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; - var audioFormat = mediaInfo.AudioFormat; - - if (audioChannelPositions.IsNullOrWhiteSpace()) + if (mediaInfo.AudioChannelPositions == null) { - return null; + return 0; } - try + var match = PositionRegex.Match(mediaInfo.AudioChannelPositions); + if (match.Success) { - if (audioChannelPositions.Contains("+")) - { - return audioChannelPositions.Split('+') - .Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture)); - } - - if (audioChannelPositions.Contains("/")) - { - var channelStringList = Regex.Replace(audioChannelPositions, - @"^\d+\sobjects", - "", - RegexOptions.Compiled | RegexOptions.IgnoreCase) - .Replace("Object Based / ", "") - .Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries) - .FirstOrDefault() - ?.Split('/'); - - var positions = default(decimal); - - if (channelStringList == null) - { - return 0; - } - - foreach (var channel in channelStringList) - { - var channelSplit = channel.Split(new string[] { "." }, StringSplitOptions.None); - - if (channelSplit.Count() == 3) - { - positions += decimal.Parse(string.Format("{0}.{1}", channelSplit[1], channelSplit[2]), CultureInfo.InvariantCulture); - } - else - { - positions += decimal.Parse(channel, CultureInfo.InvariantCulture); - } - } - - return positions; - } - } - catch (Exception e) - { - Logger.Warn() - .Message("Unable to format audio channels using 'AudioChannelPositions', with a value of: '{0}' and '{1}'. Error {2}", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer, e.Message) - .WriteSentryWarn("UnknownAudioChannelFormat", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer) - .Write(); + return decimal.Parse(match.Groups["position"].Value, NumberStyles.Number, CultureInfo.InvariantCulture); } - return null; - } - - private static decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfoModel mediaInfo) - { - var audioChannelPositionsTextContainer = mediaInfo.AudioChannelPositionsTextContainer; - var audioChannelPositionsTextStream = mediaInfo.AudioChannelPositionsTextStream; - var audioChannelsContainer = mediaInfo.AudioChannelsContainer; - var audioChannelsStream = mediaInfo.AudioChannelsStream; - - // Skip if the positions texts give us nothing - if ((audioChannelPositionsTextContainer.IsNullOrWhiteSpace() || audioChannelPositionsTextContainer == "Object Based") && - (audioChannelPositionsTextStream.IsNullOrWhiteSpace() || audioChannelPositionsTextStream == "Object Based")) - { - return null; - } - - try - { - if (audioChannelsStream > 0) - { - return audioChannelPositionsTextStream.ContainsIgnoreCase("LFE") ? audioChannelsStream - 1 + 0.1m : audioChannelsStream; - } - - return audioChannelPositionsTextContainer.ContainsIgnoreCase("LFE") ? audioChannelsContainer - 1 + 0.1m : audioChannelsContainer; - } - catch (Exception e) - { - Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText' or 'AudioChannelPositionsTextStream', with value of: '{0}' and '{1}", audioChannelPositionsTextContainer, audioChannelPositionsTextStream); - } - - return null; - } - - private static decimal? FormatAudioChannelsFromAudioChannels(MediaInfoModel mediaInfo) - { - var audioChannelsContainer = mediaInfo.AudioChannelsContainer; - var audioChannelsStream = mediaInfo.AudioChannelsStream; - - var audioFormat = (mediaInfo.AudioFormat ?? string.Empty).Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); - var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - // Workaround https://github.com/MediaArea/MediaInfo/issues/299 for DTS-X Audio - if (audioFormat.ContainsIgnoreCase("DTS") && - splitAdditionalFeatures.ContainsIgnoreCase("XLL") && - splitAdditionalFeatures.ContainsIgnoreCase("X") && - audioChannelsContainer > 0) - { - return audioChannelsContainer - 1 + 0.1m; - } - - // FLAC 6 channels is likely 5.1 - if (audioFormat.ContainsIgnoreCase("FLAC") && audioChannelsContainer == 6) - { - return 5.1m; - } - - if (mediaInfo.SchemaRevision > 5) - { - return audioChannelsStream > 0 ? audioChannelsStream : audioChannelsContainer; - } - - if (mediaInfo.SchemaRevision >= 3) - { - return audioChannelsContainer; - } - - return null; + return 0; } private static string GetSceneNameMatch(string sceneName, params string[] tokens) @@ -591,17 +304,21 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo public static string FormatVideoDynamicRange(MediaInfoModel mediaInfo) { - return mediaInfo.GetHdrFormat() == HdrFormat.None ? "" : "HDR"; + return mediaInfo.VideoHdrFormat != HdrFormat.None ? VideoDynamicRangeHdr : ""; } public static string FormatVideoDynamicRangeType(MediaInfoModel mediaInfo) { - switch (mediaInfo.GetHdrFormat()) + switch (mediaInfo.VideoHdrFormat) { case HdrFormat.DolbyVision: return "DV"; case HdrFormat.DolbyVisionHdr10: return "DV HDR10"; + case HdrFormat.DolbyVisionHlg: + return "DV HLG"; + case HdrFormat.DolbyVisionSdr: + return "DV SDR"; case HdrFormat.Hdr10: return "HDR10"; case HdrFormat.Hdr10Plus: diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoLib.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoLib.cs deleted file mode 100644 index 0be357863..000000000 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoLib.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using NLog; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Instrumentation; - -namespace NzbDrone.Core.MediaFiles.MediaInfo -{ - [Flags] - public enum BufferStatus - { - Accepted = 1, - Filled = 2, - Updated = 4, - Finalized = 8 - } - - public enum StreamKind - { - General, - Video, - Audio, - Text, - Other, - Image, - Menu - } - - public enum InfoKind - { - Name, - Text, - Measure, - Options, - NameText, - MeasureText, - Info, - HowTo - } - - public enum InfoOptions - { - ShowInInform, - Support, - ShowInSupported, - TypeOfValue - } - - public enum InfoFileOptions - { - FileOption_Nothing = 0x00, - FileOption_NoRecursive = 0x01, - FileOption_CloseAll = 0x02, - FileOption_Max = 0x04 - } - - public class MediaInfo : IDisposable - { - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfo)); - private IntPtr _handle; - - public bool MustUseAnsi { get; set; } - public Encoding Encoding { get; set; } - - public MediaInfo() - { - _handle = MediaInfo_New(); - - InitializeEncoding(); - } - - ~MediaInfo() - { - if (_handle != IntPtr.Zero) - { - MediaInfo_Delete(_handle); - } - } - - public void Dispose() - { - if (_handle != IntPtr.Zero) - { - MediaInfo_Delete(_handle); - } - - GC.SuppressFinalize(this); - } - - private void InitializeEncoding() - { - if (Environment.OSVersion.ToString().IndexOf("Windows") != -1) - { - // Windows guaranteed UCS-2 - MustUseAnsi = false; - Encoding = Encoding.Unicode; - } - else - { - var responses = new List(); - - // Linux normally UCS-4. As fallback we try UCS-2 and plain Ansi. - MustUseAnsi = false; - Encoding = Encoding.UTF32; - - var version = Option("Info_Version", ""); - responses.Add(version); - if (version.StartsWith("MediaInfoLib")) - { - return; - } - - Encoding = Encoding.Unicode; - - version = Option("Info_Version", ""); - responses.Add(version); - if (version.StartsWith("MediaInfoLib")) - { - return; - } - - MustUseAnsi = true; - Encoding = Encoding.Default; - - version = Option("Info_Version", ""); - responses.Add(version); - if (version.StartsWith("MediaInfoLib")) - { - return; - } - - throw new NotSupportedException("Unsupported MediaInfoLib encoding, version check responses (may be gibberish, show it to the Sonarr devs): " + responses.Join(", ")); - } - } - - private IntPtr MakeStringParameter(string value) - { - var buffer = Encoding.GetBytes(value); - - Array.Resize(ref buffer, buffer.Length + 4); - - var buf = Marshal.AllocHGlobal(buffer.Length); - Marshal.Copy(buffer, 0, buf, buffer.Length); - - return buf; - } - - private string MakeStringResult(IntPtr value) - { - if (Encoding == Encoding.Unicode) - { - return Marshal.PtrToStringUni(value); - } - else if (Encoding == Encoding.UTF32) - { - int i = 0; - for (; i < 1024; i += 4) - { - var data = Marshal.ReadInt32(value, i); - if (data == 0) - { - break; - } - } - - var buffer = new byte[i]; - Marshal.Copy(value, buffer, 0, i); - - return Encoding.GetString(buffer, 0, i); - } - else - { - return Marshal.PtrToStringAnsi(value); - } - } - - public int Open(string fileName) - { - var pFileName = MakeStringParameter(fileName); - try - { - if (MustUseAnsi) - { - return (int)MediaInfoA_Open(_handle, pFileName); - } - else - { - return (int)MediaInfo_Open(_handle, pFileName); - } - } - finally - { - Marshal.FreeHGlobal(pFileName); - } - } - - public int Open(Stream stream) - { - if (stream.Length < 1024) - { - return 0; - } - - var isValid = (int)MediaInfo_Open_Buffer_Init(_handle, stream.Length, 0); - if (isValid == 1) - { - var buffer = new byte[16 * 1024]; - long seekStart = 0; - long totalRead = 0; - int bufferRead; - - do - { - bufferRead = stream.Read(buffer, 0, buffer.Length); - totalRead += bufferRead; - - var status = (BufferStatus)MediaInfo_Open_Buffer_Continue(_handle, buffer, (IntPtr)bufferRead); - - if (status.HasFlag(BufferStatus.Finalized) || status <= 0 || bufferRead == 0) - { - Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart); - break; - } - - var seekPos = MediaInfo_Open_Buffer_Continue_GoTo_Get(_handle); - if (seekPos != -1) - { - Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart); - seekPos = stream.Seek(seekPos, SeekOrigin.Begin); - seekStart = seekPos; - MediaInfo_Open_Buffer_Init(_handle, stream.Length, seekPos); - } - } - while (bufferRead > 0); - - MediaInfo_Open_Buffer_Finalize(_handle); - - Logger.Trace("Read a total of {0} bytes ({1:0.0}%)", totalRead, totalRead * 100.0 / stream.Length); - } - - return isValid; - } - - public void Close() - { - MediaInfo_Close(_handle); - } - - public string Get(StreamKind streamKind, int streamNumber, string parameter, InfoKind infoKind = InfoKind.Text, InfoKind searchKind = InfoKind.Name) - { - var pParameter = MakeStringParameter(parameter); - try - { - if (MustUseAnsi) - { - return MakeStringResult(MediaInfoA_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind)); - } - else - { - return MakeStringResult(MediaInfo_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind)); - } - } - finally - { - Marshal.FreeHGlobal(pParameter); - } - } - - public string Get(StreamKind streamKind, int streamNumber, int parameter, InfoKind infoKind) - { - if (MustUseAnsi) - { - return MakeStringResult(MediaInfoA_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind)); - } - else - { - return MakeStringResult(MediaInfo_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind)); - } - } - - public string Option(string option, string value) - { - var pOption = MakeStringParameter(option.ToLowerInvariant()); - var pValue = MakeStringParameter(value); - try - { - if (MustUseAnsi) - { - return MakeStringResult(MediaInfoA_Option(_handle, pOption, pValue)); - } - else - { - return MakeStringResult(MediaInfo_Option(_handle, pOption, pValue)); - } - } - finally - { - Marshal.FreeHGlobal(pOption); - Marshal.FreeHGlobal(pValue); - } - } - - public int State_Get() - { - return (int)MediaInfo_State_Get(_handle); - } - - public int Count_Get(StreamKind streamKind, int streamNumber = -1) - { - return (int)MediaInfo_Count_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber); - } - - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_New(); - [DllImport("mediainfo")] - private static extern void MediaInfo_Delete(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_Open(IntPtr handle, IntPtr fileName); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize); - [DllImport("mediainfo")] - private static extern long MediaInfo_Open_Buffer_Continue_GoTo_Get(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_Open_Buffer_Finalize(IntPtr handle); - [DllImport("mediainfo")] - private static extern void MediaInfo_Close(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_Option(IntPtr handle, IntPtr option, IntPtr value); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_State_Get(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfo_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber); - - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_New(); - [DllImport("mediainfo")] - private static extern void MediaInfoA_Delete(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_Open(IntPtr handle, IntPtr fileName); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize); - [DllImport("mediainfo")] - private static extern long MediaInfoA_Open_Buffer_Continue_GoTo_Get(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_Open_Buffer_Finalize(IntPtr handle); - [DllImport("mediainfo")] - private static extern void MediaInfoA_Close(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_Option(IntPtr handle, IntPtr option, IntPtr value); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_State_Get(IntPtr handle); - [DllImport("mediainfo")] - private static extern IntPtr MediaInfoA_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber); - } -} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index a329af173..1d58d1f4e 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -1,48 +1,64 @@ -using System; -using System.Globalization; -using System.Linq; -using Newtonsoft.Json; -using NzbDrone.Common.Extensions; +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using FFMpegCore; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaFiles.MediaInfo { public class MediaInfoModel : IEmbeddedDocument { - public string ContainerFormat { get; set; } - - // Deprecated according to MediaInfo - public string VideoCodec { 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 string VideoHdrFormat { get; set; } - public string VideoHdrFormatCompatibility { 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; } - public int AudioChannelsContainer { get; set; } - public int AudioChannelsStream { get; set; } - public string AudioChannelPositions { get; set; } - public string AudioChannelPositionsTextContainer { get; set; } - public string AudioChannelPositionsTextStream { get; set; } - public string AudioProfile { get; set; } - public decimal VideoFps { get; set; } - public string AudioLanguages { get; set; } - public string Subtitles { get; set; } - public string ScanType { get; set; } + public string RawStreamData { get; set; } + public string RawFrameData { get; set; } public int SchemaRevision { get; set; } + + public string ContainerFormat { get; set; } + public string VideoFormat { get; set; } + + public string VideoCodecID { get; set; } + + public string VideoProfile { get; set; } + + public long VideoBitrate { get; set; } + + public int VideoBitDepth { get; set; } + + public int VideoMultiViewCount { get; set; } + + public string VideoColourPrimaries { get; set; } + + public string VideoTransferCharacteristics { get; set; } + + public DoviConfigurationRecordSideData DoviConfigurationRecord { get; set; } + + public HdrFormat VideoHdrFormat { get; set; } + + public int Height { get; set; } + + public int Width { get; set; } + + public string AudioFormat { get; set; } + + public string AudioCodecID { get; set; } + + public string AudioProfile { get; set; } + + public long AudioBitrate { get; set; } + + public TimeSpan RunTime { get; set; } + + public int AudioStreamCount { get; set; } + + public int AudioChannels { get; set; } + + public string AudioChannelPositions { get; set; } + + public decimal VideoFps { get; set; } + + public List AudioLanguages { get; set; } + + public List Subtitles { get; set; } + + public string ScanType { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModelExtensions.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModelExtensions.cs deleted file mode 100644 index b9038f83a..000000000 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModelExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Linq; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Core.MediaFiles.MediaInfo -{ - public static class MediaInfoModelExtensions - { - private const string HlgTransferFunction = "HLG"; - private const string PgTransferFunction = "PQ"; - private const string ValidHdrColourPrimaries = "BT.2020"; - - private static readonly string[] Hdr10Formats = { "SMPTE ST 2086", "HDR10" }; - private static readonly string[] Hdr10PlusFormats = { "SMPTE ST 2094 App 4" }; - private static readonly string[] DolbyVisionFormats = { "Dolby Vision" }; - - public static HdrFormat GetHdrFormat(this MediaInfoModel mediaInfo) - { - if (mediaInfo.VideoBitDepth < 10) - { - return HdrFormat.None; - } - - if (mediaInfo.VideoHdrFormat.IsNotNullOrWhiteSpace()) - { - if (DolbyVisionFormats.Any(mediaInfo.VideoHdrFormat.ContainsIgnoreCase)) - { - if (Hdr10Formats.Any(mediaInfo.VideoHdrFormat.ContainsIgnoreCase)) - { - return HdrFormat.DolbyVisionHdr10; - } - else - { - return HdrFormat.DolbyVision; - } - } - else if (Hdr10Formats.Any(mediaInfo.VideoHdrFormat.ContainsIgnoreCase)) - { - return HdrFormat.Hdr10; - } - else if (Hdr10PlusFormats.Any(mediaInfo.VideoHdrFormat.ContainsIgnoreCase)) - { - return HdrFormat.Hdr10Plus; - } - } - - // We didn't match straight from the format from MediaInfo, so try and match in ColourPrimaries and TransferCharacteristics - if (mediaInfo.VideoColourPrimaries.IsNotNullOrWhiteSpace() && mediaInfo.VideoTransferCharacteristics.IsNotNullOrWhiteSpace()) - { - if (mediaInfo.VideoColourPrimaries.EqualsIgnoreCase(ValidHdrColourPrimaries)) - { - if (mediaInfo.VideoTransferCharacteristics.EqualsIgnoreCase(PgTransferFunction)) - { - return HdrFormat.Pq10; - } - else if (mediaInfo.VideoTransferCharacteristics.EqualsIgnoreCase(HlgTransferFunction)) - { - return HdrFormat.Hlg10; - } - } - } - - if (mediaInfo.VideoHdrFormat.IsNotNullOrWhiteSpace()) - { - return HdrFormat.UnknownHdr; // MediaInfo reported Hdr information, but we're unsure of what type. - } - - return HdrFormat.None; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index 65e95a09f..70089544f 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -1,6 +1,8 @@ using System; -using System.Globalization; +using System.Collections.Generic; using System.IO; +using System.Linq; +using FFMpegCore; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; @@ -17,14 +19,33 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo { private readonly IDiskProvider _diskProvider; private readonly Logger _logger; + private readonly List _pixelFormats; - public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 3; - public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 7; + public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 8; + public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 8; + + private static readonly string[] ValidHdrColourPrimaries = { "bt2020" }; + private static readonly string[] HlgTransferFunctions = { "bt2020-10", "arib-std-b67" }; + private static readonly string[] PqTransferFunctions = { "smpte2084" }; + private static readonly string[] ValidHdrTransferFunctions = HlgTransferFunctions.Concat(PqTransferFunctions).ToArray(); public VideoFileInfoReader(IDiskProvider diskProvider, Logger logger) { _diskProvider = diskProvider; _logger = logger; + + // We bundle ffprobe for all platforms + GlobalFFOptions.Configure(options => options.BinaryFolder = AppDomain.CurrentDomain.BaseDirectory); + + try + { + _pixelFormats = FFProbe.GetPixelFormats(); + } + catch (Exception e) + { + _logger.Error(e, "Failed to get supported pixel formats from ffprobe"); + _pixelFormats = new List(); + } } public MediaInfoModel GetMediaInfo(string filename) @@ -34,182 +55,76 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo throw new FileNotFoundException("Media file does not exist: " + filename); } - MediaInfo mediaInfo = null; - // TODO: Cache media info by path, mtime and length so we don't need to read files multiple times try { - mediaInfo = new MediaInfo(); _logger.Debug("Getting media info from {0}", filename); + var ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 50000000" }); - if (filename.ToLower().EndsWith(".ts")) + var analysis = FFProbe.AnalyseStreamJson(ffprobeOutput); + + if (analysis.PrimaryAudioStream?.ChannelLayout.IsNullOrWhiteSpace() ?? true) { - // For .ts files we often have to scan more of the file to get all the info we need - mediaInfo.Option("ParseSpeed", "0.3"); - } - else - { - mediaInfo.Option("ParseSpeed", "0.0"); + ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 150000000 -analyzeduration 150000000" }); + analysis = FFProbe.AnalyseStreamJson(ffprobeOutput); } - int open; + var mediaInfoModel = new MediaInfoModel(); + mediaInfoModel.ContainerFormat = analysis.Format.FormatName; + mediaInfoModel.VideoFormat = analysis.PrimaryVideoStream?.CodecName; + mediaInfoModel.VideoCodecID = analysis.PrimaryVideoStream?.CodecTagString; + mediaInfoModel.VideoProfile = analysis.PrimaryVideoStream?.Profile; + mediaInfoModel.VideoBitrate = analysis.PrimaryVideoStream?.BitRate ?? 0; + mediaInfoModel.VideoMultiViewCount = 1; + mediaInfoModel.VideoBitDepth = GetPixelFormat(analysis.PrimaryVideoStream?.PixelFormat)?.Components.Min(x => x.BitDepth) ?? 8; + mediaInfoModel.VideoColourPrimaries = analysis.PrimaryVideoStream?.ColorPrimaries; + mediaInfoModel.VideoTransferCharacteristics = analysis.PrimaryVideoStream?.ColorTransfer; + mediaInfoModel.DoviConfigurationRecord = analysis.PrimaryVideoStream?.SideDataList?.Find(x => x.GetType().Name == nameof(DoviConfigurationRecordSideData)) as DoviConfigurationRecordSideData; + mediaInfoModel.Height = analysis.PrimaryVideoStream?.Height ?? 0; + mediaInfoModel.Width = analysis.PrimaryVideoStream?.Width ?? 0; + mediaInfoModel.AudioFormat = analysis.PrimaryAudioStream?.CodecName; + mediaInfoModel.AudioCodecID = analysis.PrimaryAudioStream?.CodecTagString; + mediaInfoModel.AudioProfile = analysis.PrimaryAudioStream?.Profile; + mediaInfoModel.AudioBitrate = analysis.PrimaryAudioStream?.BitRate ?? 0; + mediaInfoModel.RunTime = GetBestRuntime(analysis.PrimaryAudioStream?.Duration, analysis.PrimaryVideoStream?.Duration, analysis.Format.Duration); + mediaInfoModel.AudioStreamCount = analysis.AudioStreams.Count; + mediaInfoModel.AudioChannels = analysis.PrimaryAudioStream?.Channels ?? 0; + mediaInfoModel.AudioChannelPositions = analysis.PrimaryAudioStream?.ChannelLayout; + mediaInfoModel.VideoFps = analysis.PrimaryVideoStream?.FrameRate ?? 0; + mediaInfoModel.AudioLanguages = analysis.AudioStreams?.Select(x => x.Language) + .Where(l => l.IsNotNullOrWhiteSpace()) + .ToList(); + mediaInfoModel.Subtitles = analysis.SubtitleStreams?.Select(x => x.Language) + .Where(l => l.IsNotNullOrWhiteSpace()) + .ToList(); + mediaInfoModel.ScanType = "Progressive"; + mediaInfoModel.RawStreamData = ffprobeOutput; + mediaInfoModel.SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION; - using (var stream = _diskProvider.OpenReadStream(filename)) + FFProbeFrames frames = null; + + // if it looks like PQ10 or similar HDR, do a frame analysis to figure out which type it is + if (PqTransferFunctions.Contains(mediaInfoModel.VideoTransferCharacteristics)) { - open = mediaInfo.Open(stream); + var frameOutput = FFProbe.GetFrameJson(filename, ffOptions: new () { ExtraArguments = "-read_intervals \"%+#1\" -select_streams v" }); + mediaInfoModel.RawFrameData = frameOutput; + + frames = FFProbe.AnalyseFrameJson(frameOutput); } - if (open != 0) - { - int audioRuntime; - int videoRuntime; - int generalRuntime; + var streamSideData = analysis.PrimaryVideoStream?.SideDataList ?? new (); + var framesSideData = frames?.Frames?.Count > 0 ? frames?.Frames[0]?.SideDataList ?? new () : new (); - // 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); + var sideData = streamSideData.Concat(framesSideData).ToList(); + mediaInfoModel.VideoHdrFormat = GetHdrFormat(mediaInfoModel.VideoBitDepth, mediaInfoModel.VideoColourPrimaries, mediaInfoModel.VideoTransferCharacteristics, sideData); - // Audio Channels - var audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); - var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2"); - int.TryParse(audioChannelsStr, out var audioChannels); - - if (audioRuntime == 0 && videoRuntime == 0 && generalRuntime == 0) - { - // No runtime, ask mediainfo to scan the whole file - _logger.Trace("No runtime value found, rescanning at 1.0 scan depth"); - mediaInfo.Option("ParseSpeed", "1.0"); - - using (var stream = _diskProvider.OpenReadStream(filename)) - { - open = mediaInfo.Open(stream); - } - } - else if (audioChannels > 2 && audioChannelPositions.IsNullOrWhiteSpace()) - { - // Some files with DTS don't have ChannelPositions unless more of the file is scanned - _logger.Trace("DTS audio without expected channel information, rescanning at 0.3 scan depth"); - mediaInfo.Option("ParseSpeed", "0.3"); - - using (var stream = _diskProvider.OpenReadStream(filename)) - { - open = mediaInfo.Open(stream); - } - } - } - - if (open != 0) - { - int width; - int height; - int videoBitRate; - int audioBitRate; - int audioRuntime; - int videoRuntime; - int generalRuntime; - int streamCount; - int audioChannels; - int audioChannelsOrig; - 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").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); - - int.TryParse(aBitRate, out audioBitRate); - int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount); - - var audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); - int.TryParse(audioChannelsStr, out audioChannels); - - var audioChannelsStrOrig = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)_Original").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim(); - int.TryParse(audioChannelsStrOrig, out audioChannelsOrig); - - var audioChannelPositionsText = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions"); - var audioChannelPositionsTextOrig = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions_Original"); - var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2"); - - string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List"); - - 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(); - - var mediaInfoModel = new MediaInfoModel - { - 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"), - VideoHdrFormat = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format"), - VideoHdrFormatCompatibility = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format_Compatibility"), - 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, - AudioChannelsContainer = audioChannels, - AudioChannelsStream = audioChannelsOrig, - AudioChannelPositions = audioChannelPositions, - AudioChannelPositionsTextContainer = audioChannelPositionsText, - AudioChannelPositionsTextStream = audioChannelPositionsTextOrig, - VideoFps = videoFrameRate, - AudioLanguages = audioLanguages, - Subtitles = subtitles, - ScanType = scanType, - SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION - }; - - return mediaInfoModel; - } - else - { - _logger.Warn("Unable to open media info from file: " + filename); - } - } - catch (DllNotFoundException ex) - { - _logger.Error(ex, "mediainfo is required but was not found"); + return mediaInfoModel; } catch (Exception ex) { _logger.Error(ex, "Unable to parse media info from file: {0}", filename); } - finally - { - mediaInfo?.Close(); - } return null; } @@ -221,19 +136,80 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo return info?.RunTime; } - private TimeSpan GetBestRuntime(int audio, int video, int general) + private static TimeSpan GetBestRuntime(TimeSpan? audio, TimeSpan? video, TimeSpan general) { - if (video == 0) + if (!video.HasValue || video.Value.TotalMilliseconds == 0) { - if (audio == 0) + if (!audio.HasValue || audio.Value.TotalMilliseconds == 0) { - return TimeSpan.FromMilliseconds(general); + return general; } - return TimeSpan.FromMilliseconds(audio); + return audio.Value; } - return TimeSpan.FromMilliseconds(video); + return video.Value; + } + + private FFProbePixelFormat GetPixelFormat(string format) + { + return _pixelFormats.Find(x => x.Name == format); + } + + public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List sideData) + { + if (bitDepth < 10) + { + return HdrFormat.None; + } + + if (TryGetSideData(sideData, out var dovi)) + { + return dovi.DvBlSignalCompatibilityId switch + { + 1 => HdrFormat.DolbyVisionHdr10, + 2 => HdrFormat.DolbyVisionSdr, + 4 => HdrFormat.DolbyVisionHlg, + 6 => HdrFormat.DolbyVisionHdr10, + _ => HdrFormat.DolbyVision + }; + } + + if (!ValidHdrColourPrimaries.Contains(colorPrimaries) || !ValidHdrTransferFunctions.Contains(transferFunction)) + { + return HdrFormat.None; + } + + if (HlgTransferFunctions.Contains(transferFunction)) + { + return HdrFormat.Hlg10; + } + + if (PqTransferFunctions.Contains(transferFunction)) + { + if (TryGetSideData(sideData, out _)) + { + return HdrFormat.Hdr10Plus; + } + + if (TryGetSideData(sideData, out _) || + TryGetSideData(sideData, out _)) + { + return HdrFormat.Hdr10; + } + + return HdrFormat.Pq10; + } + + return HdrFormat.None; + } + + private static bool TryGetSideData(List list, out T result) + where T : SideData + { + result = (T)list?.FirstOrDefault(x => x.GetType().Name == typeof(T).Name); + + return result != null; } } } diff --git a/src/NzbDrone.Core/Notifications/Discord/Discord.cs b/src/NzbDrone.Core/Notifications/Discord/Discord.cs index 2970bc4df..54725f176 100644 --- a/src/NzbDrone.Core/Notifications/Discord/Discord.cs +++ b/src/NzbDrone.Core/Notifications/Discord/Discord.cs @@ -194,11 +194,11 @@ namespace NzbDrone.Core.Notifications.Discord break; case DiscordImportFieldType.Languages: discordField.Name = "Languages"; - discordField.Value = message.EpisodeFile.MediaInfo.AudioLanguages; + discordField.Value = message.EpisodeFile.MediaInfo.AudioLanguages.ConcatToString("/"); break; case DiscordImportFieldType.Subtitles: discordField.Name = "Subtitles"; - discordField.Value = message.EpisodeFile.MediaInfo.Subtitles; + discordField.Value = message.EpisodeFile.MediaInfo.Subtitles.ConcatToString("/"); break; case DiscordImportFieldType.Release: discordField.Name = "Release"; diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 6c57e5ec9..ce79519d7 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; @@ -86,6 +87,31 @@ namespace NzbDrone.Core.Organizer private static readonly Regex ReservedDeviceNamesRegex = new Regex(@"^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)\.", RegexOptions.Compiled | RegexOptions.IgnoreCase); + // generated from https://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt + public static readonly ImmutableDictionary Iso639BTMap = new Dictionary + { + { "alb", "sqi" }, + { "arm", "hye" }, + { "baq", "eus" }, + { "bur", "mya" }, + { "chi", "zho" }, + { "cze", "ces" }, + { "dut", "nld" }, + { "fre", "fra" }, + { "geo", "kat" }, + { "ger", "deu" }, + { "gre", "ell" }, + { "ice", "isl" }, + { "mac", "mkd" }, + { "mao", "mri" }, + { "may", "msa" }, + { "per", "fas" }, + { "rum", "ron" }, + { "slo", "slk" }, + { "tib", "bod" }, + { "wel", "cym" } + }.ToImmutableDictionary(); + public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, ICacheManager cacheManager, @@ -616,7 +642,7 @@ namespace NzbDrone.Core.Organizer new Dictionary(FileNameBuilderTokenEqualityComparer.Instance) { { MediaInfoVideoDynamicRangeToken, 5 }, - { MediaInfoVideoDynamicRangeTypeToken, 5 } + { MediaInfoVideoDynamicRangeTypeToken, 8 } }; private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) @@ -630,13 +656,13 @@ namespace NzbDrone.Core.Organizer var sceneName = episodeFile.GetSceneOrFileName(); - var videoCodec = MediaInfoFormatter.FormatVideoCodec(episodeFile.MediaInfo, sceneName); - var audioCodec = MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo, sceneName); + 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 audioLanguages = episodeFile.MediaInfo.AudioLanguages ?? new List(); + var subtitles = episodeFile.MediaInfo.Subtitles ?? new List(); - var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : 8.ToString(); var audioChannelsFormatted = audioChannels > 0 ? audioChannels.ToString("F1", CultureInfo.InvariantCulture) : string.Empty; @@ -701,42 +727,29 @@ namespace NzbDrone.Core.Organizer }; } - private string GetLanguagesToken(string mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted) + private string GetLanguagesToken(List mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted) { - List tokens = new List(); - foreach (var item in mediaInfoLanguages.Split('/')) + var tokens = new List(); + foreach (var item in mediaInfoLanguages) { - if (!string.IsNullOrWhiteSpace(item)) + if (!string.IsNullOrWhiteSpace(item) && item != "und") { tokens.Add(item.Trim()); } } - var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); for (int i = 0; i < tokens.Count; i++) { - if (tokens[i] == "Swedis") - { - // Probably typo in mediainfo (should be 'Swedish') - tokens[i] = "SV"; - continue; - } - - if (tokens[i] == "Chinese" && OsInfo.IsNotWindows) - { - // Mono only has 'Chinese (Simplified)' & 'Chinese (Traditional)' - tokens[i] = "ZH"; - continue; - } - try { - var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]); - - if (cultureInfo != null) + var token = tokens[i].ToLowerInvariant(); + if (Iso639BTMap.TryGetValue(token, out var mapped)) { - tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); + token = mapped; } + + var cultureInfo = new CultureInfo(token); + tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); } catch { diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index e5e706361..12160cc61 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -101,28 +101,29 @@ namespace NzbDrone.Core.Organizer var mediaInfo = new MediaInfoModel() { - VideoCodec = "AVC", + VideoFormat = "AVC", VideoBitDepth = 10, - VideoColourPrimaries = "BT.2020", + VideoMultiViewCount = 2, + VideoColourPrimaries = "bt2020", VideoTransferCharacteristics = "HLG", AudioFormat = "DTS", - AudioChannelsContainer = 6, - AudioChannelPositions = "3/2/0.1", - AudioLanguages = "English", - Subtitles = "English/German" + AudioChannels = 6, + AudioChannelPositions = "5.1", + AudioLanguages = new List { "ger" }, + Subtitles = new List { "eng", "ger" } }; var mediaInfoAnime = new MediaInfoModel() { - VideoCodec = "AVC", + VideoFormat = "AVC", VideoBitDepth = 10, VideoColourPrimaries = "BT.2020", VideoTransferCharacteristics = "HLG", AudioFormat = "DTS", - AudioChannelsContainer = 6, - AudioChannelPositions = "3/2/0.1", - AudioLanguages = "Japanese", - Subtitles = "Japanese/English" + AudioChannels = 6, + AudioChannelPositions = "5.1", + AudioLanguages = new List { "jpn" }, + Subtitles = new List { "jpn", "eng" } }; _singleEpisodeFile = new EpisodeFile diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs index f5d6ef5b2..3da24f3fc 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguages.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using NzbDrone.Core.Languages; +using NzbDrone.Core.Organizer; namespace NzbDrone.Core.Parser { @@ -52,6 +54,11 @@ namespace NzbDrone.Core.Parser else if (isoCode.Length == 3) { //Lookup ISO639-2T code + if (FileNameBuilder.Iso639BTMap.TryGetValue(isoCode, out var mapped)) + { + isoCode = mapped; + } + return All.SingleOrDefault(l => l.ThreeLetterCode == isoCode); } diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index 239a3f42b..a763944ee 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -5,6 +5,8 @@ + + diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index c3287f3fe..8dc52c2bc 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -125,10 +125,11 @@ namespace NzbDrone.Update.UpdateEngine _logger.Info("Copying new files to target folder"); _diskTransferService.MirrorFolder(_appFolderInfo.GetUpdatePackageFolder(), installationFolder); - // Set executable flag on app + // Set executable flag on app and ffprobe if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore)) { _diskProvider.SetFilePermissions(Path.Combine(installationFolder, "Sonarr"), "755", null); + _diskProvider.SetFilePermissions(Path.Combine(installationFolder, "ffprobe"), "755", null); } } catch (Exception e) diff --git a/src/Runtimes/linux-x64/Sonarr.Core.dll.config b/src/Runtimes/linux-x64/Sonarr.Core.dll.config deleted file mode 100644 index f40a271ce..000000000 --- a/src/Runtimes/linux-x64/Sonarr.Core.dll.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/Runtimes/osx-x64/libmediainfo.dylib b/src/Runtimes/osx-x64/libmediainfo.dylib deleted file mode 100644 index c9a4fa29a..000000000 Binary files a/src/Runtimes/osx-x64/libmediainfo.dylib and /dev/null differ diff --git a/src/Runtimes/win-x64/MediaInfo.dll b/src/Runtimes/win-x64/MediaInfo.dll deleted file mode 100644 index a88193891..000000000 Binary files a/src/Runtimes/win-x64/MediaInfo.dll and /dev/null differ diff --git a/src/Runtimes/win-x86/MediaInfo.dll b/src/Runtimes/win-x86/MediaInfo.dll deleted file mode 100644 index 7c556d260..000000000 Binary files a/src/Runtimes/win-x86/MediaInfo.dll and /dev/null differ diff --git a/src/Sonarr.Api.V3/EpisodeFiles/MediaInfoResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/MediaInfoResource.cs index ae85ae5ef..fe773adb7 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/MediaInfoResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/MediaInfoResource.cs @@ -1,5 +1,5 @@ using System; -using System.Text; +using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles.MediaInfo; using Sonarr.Http.REST; @@ -7,13 +7,13 @@ namespace Sonarr.Api.V3.EpisodeFiles { public class MediaInfoResource : RestResource { - public int AudioBitrate { get; set; } + public long AudioBitrate { get; set; } public decimal AudioChannels { get; set; } public string AudioCodec { get; set; } public string AudioLanguages { get; set; } public int AudioStreamCount { get; set; } public int VideoBitDepth { get; set; } - public int VideoBitrate { get; set; } + public long VideoBitrate { get; set; } public string VideoCodec { get; set; } public decimal VideoFps { get; set; } public string VideoDynamicRange { get; set; } @@ -34,23 +34,23 @@ namespace Sonarr.Api.V3.EpisodeFiles } return new MediaInfoResource - { - AudioBitrate = model.AudioBitrate, - AudioChannels = MediaInfoFormatter.FormatAudioChannels(model), - AudioLanguages = model.AudioLanguages, - AudioStreamCount = model.AudioStreamCount, - AudioCodec = MediaInfoFormatter.FormatAudioCodec(model, sceneName), - VideoBitDepth = model.VideoBitDepth, - VideoBitrate = model.VideoBitrate, - VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName), - VideoFps = model.VideoFps, - VideoDynamicRange = MediaInfoFormatter.FormatVideoDynamicRange(model), - VideoDynamicRangeType = MediaInfoFormatter.FormatVideoDynamicRangeType(model), - Resolution = $"{model.Width}x{model.Height}", - RunTime = FormatRuntime(model.RunTime), - ScanType = model.ScanType, - Subtitles = model.Subtitles - }; + { + AudioBitrate = model.AudioBitrate, + AudioChannels = MediaInfoFormatter.FormatAudioChannels(model), + AudioLanguages = model.AudioLanguages.ConcatToString("/"), + AudioStreamCount = model.AudioStreamCount, + AudioCodec = MediaInfoFormatter.FormatAudioCodec(model, sceneName), + VideoBitDepth = model.VideoBitDepth, + VideoBitrate = model.VideoBitrate, + VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName), + VideoFps = Math.Round(model.VideoFps, 3), + VideoDynamicRange = MediaInfoFormatter.FormatVideoDynamicRange(model), + VideoDynamicRangeType = MediaInfoFormatter.FormatVideoDynamicRangeType(model), + Resolution = $"{model.Width}x{model.Height}", + RunTime = FormatRuntime(model.RunTime), + ScanType = model.ScanType, + Subtitles = model.Subtitles.ConcatToString("/") + }; } private static string FormatRuntime(TimeSpan runTime)