diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx index f80a2e4ba1..5126da7a54 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx @@ -143,6 +143,7 @@ const mediaInfoTokens = [ { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNotes: '1' }, { token: '{MediaInfo AudioCodec}', example: 'DTS' }, + { token: '{MediaInfo BestAudioCodec}', example: 'DTS-HD MA' }, { token: '{MediaInfo AudioChannels}', example: '5.1' }, { token: '{MediaInfo AudioLanguages}', diff --git a/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatBestAudioCodecFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatBestAudioCodecFixture.cs new file mode 100644 index 0000000000..38fd91e940 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/MediaInfo/MediaInfoFormatterTests/FormatBestAudioCodecFixture.cs @@ -0,0 +1,106 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests +{ + [TestFixture] + public class FormatBestAudioCodecFixture : TestBase + { + private static string sceneName = "My.Movie.2020-Group"; + + [Test] + public void should_pick_truehd_atmos_over_ac3() + { + var mediaInfo = new MediaInfoModel + { + BestAudioFormat = "truehd", + BestAudioCodecID = "thd+", + BestAudioProfile = string.Empty + }; + + MediaInfoFormatter.FormatAudioCodec( + new MediaInfoModel + { + AudioFormat = mediaInfo.BestAudioFormat, + AudioCodecID = mediaInfo.BestAudioCodecID, + AudioProfile = mediaInfo.BestAudioProfile + }, + sceneName).Should().Be("TrueHD Atmos"); + } + + [Test] + public void should_pick_dts_hd_ma_over_ac3() + { + var mediaInfo = new MediaInfoModel + { + BestAudioFormat = "dts", + BestAudioCodecID = string.Empty, + BestAudioProfile = "DTS-HD MA" + }; + + MediaInfoFormatter.FormatAudioCodec( + new MediaInfoModel + { + AudioFormat = mediaInfo.BestAudioFormat, + AudioCodecID = mediaInfo.BestAudioCodecID, + AudioProfile = mediaInfo.BestAudioProfile + }, + sceneName).Should().Be("DTS-HD MA"); + } + + [Test] + public void should_pick_dtsx_over_dts_hd_ma() + { + var mediaInfo = new MediaInfoModel + { + BestAudioFormat = "dts", + BestAudioCodecID = string.Empty, + BestAudioProfile = "DTS:X" + }; + + MediaInfoFormatter.FormatAudioCodec( + new MediaInfoModel + { + AudioFormat = mediaInfo.BestAudioFormat, + AudioCodecID = mediaInfo.BestAudioCodecID, + AudioProfile = mediaInfo.BestAudioProfile + }, + sceneName).Should().Be("DTS-X"); + } + + [Test] + public void should_return_single_stream_codec() + { + var mediaInfo = new MediaInfoModel + { + BestAudioFormat = "ac3", + BestAudioCodecID = string.Empty, + BestAudioProfile = string.Empty + }; + + MediaInfoFormatter.FormatAudioCodec( + new MediaInfoModel + { + AudioFormat = mediaInfo.BestAudioFormat, + AudioCodecID = mediaInfo.BestAudioCodecID, + AudioProfile = mediaInfo.BestAudioProfile + }, + sceneName).Should().Be("AC3"); + } + + [Test] + public void should_return_null_when_best_audio_format_is_null() + { + var mediaInfo = new MediaInfoModel + { + AudioFormat = null, + AudioCodecID = null, + AudioProfile = null + }; + + MediaInfoFormatter.FormatAudioCodec(mediaInfo, sceneName).Should().BeNull(); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 7cf7ce4d66..e700e2b208 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -775,6 +775,30 @@ public void should_not_update_media_info_if_token_configured_and_revision_is_new Mocker.GetMock().Verify(v => v.Update(_movieFile, _movie), Times.Never()); } + [Test] + public void should_format_best_audio_codec() + { + _movieFile.ReleaseGroup = null; + + _movieFile.MediaInfo = new MediaInfoModel + { + VideoFormat = "h264", + AudioFormat = "ac3", + AudioChannels = 6, + AudioLanguages = new List { "eng" }, + Subtitles = new List { "eng" }, + BestAudioFormat = "dts", + BestAudioCodecID = string.Empty, + BestAudioProfile = "DTS-HD MA", + SchemaRevision = 15 + }; + + _namingConfig.StandardMovieFormat = "{MediaInfo BestAudioCodec}"; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be("DTS-HD MA"); + } + private void GivenMediaInfoModel(string videoCodec = "h264", string audioCodec = "dts", int audioChannels = 6, diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/AudioCodec.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/AudioCodec.cs new file mode 100644 index 0000000000..7656bd8b94 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/AudioCodec.cs @@ -0,0 +1,92 @@ +namespace NzbDrone.Core.MediaFiles.MediaInfo +{ + public enum AudioCodec + { + Unknown = 0, + MP2 = 1, + MP3 = 2, + PCM = 3, + Vorbis = 4, + WMA = 5, + Opus = 6, + AAC = 7, + HE_AAC = 8, + AC3 = 9, + FLAC = 10, + EAC3 = 11, + DTS = 12, + DTS_Express = 13, + DTS_9624 = 14, + DTS_ES = 15, + DTS_HD_HRA = 16, + EAC3Atmos = 17, + DTS_HD_MA = 18, + DTS_X = 19, + TrueHD = 20, + TrueHDAtmos = 21 + } + + public static class AudioCodecHelper + { + public static AudioCodec Resolve(string format, string codecID, string profile) + { + format ??= string.Empty; + codecID ??= string.Empty; + profile ??= string.Empty; + + return (codecID, format, profile) switch + { + ("thd+", _, _) => AudioCodec.TrueHDAtmos, + ("ec+3", _, _) => AudioCodec.EAC3Atmos, + (_, "truehd", _) => AudioCodec.TrueHD, + (_, "flac", _) => AudioCodec.FLAC, + (_, "dts", "DTS:X") => AudioCodec.DTS_X, + (_, "dts", "DTS-HD MA") => AudioCodec.DTS_HD_MA, + (_, "dts", "DTS-HD HRA") => AudioCodec.DTS_HD_HRA, + (_, "dts", "DTS-ES") => AudioCodec.DTS_ES, + (_, "dts", "DTS Express") => AudioCodec.DTS_Express, + (_, "dts", "DTS 96/24") => AudioCodec.DTS_9624, + (_, "dts", _) => AudioCodec.DTS, + (_, "eac3", _) => AudioCodec.EAC3, + (_, "ac3", _) => AudioCodec.AC3, + ("A_AAC/MPEG4/LC/SBR", "aac", _) => AudioCodec.HE_AAC, + (_, "aac", _) => AudioCodec.AAC, + (_, "mp3", _) => AudioCodec.MP3, + (_, "mp2", _) => AudioCodec.MP2, + (_, "opus", _) => AudioCodec.Opus, + (_, "vorbis", _) => AudioCodec.Vorbis, + (_, "wmav1", _) => AudioCodec.WMA, + (_, "wmav2", _) => AudioCodec.WMA, + (_, "wmapro", _) => AudioCodec.WMA, + _ when format.StartsWith("pcm_") || format.StartsWith("adpcm_") => AudioCodec.PCM, + _ => AudioCodec.Unknown + }; + } + + public static string GetDisplayName(AudioCodec codec) => codec switch + { + AudioCodec.TrueHDAtmos => "TrueHD Atmos", + AudioCodec.TrueHD => "TrueHD", + AudioCodec.DTS_X => "DTS-X", + AudioCodec.DTS_HD_MA => "DTS-HD MA", + AudioCodec.DTS_HD_HRA => "DTS-HD HRA", + AudioCodec.DTS_ES => "DTS-ES", + AudioCodec.DTS_Express => "DTS Express", + AudioCodec.DTS_9624 => "DTS 96/24", + AudioCodec.DTS => "DTS", + AudioCodec.EAC3Atmos => "EAC3 Atmos", + AudioCodec.EAC3 => "EAC3", + AudioCodec.FLAC => "FLAC", + AudioCodec.AC3 => "AC3", + AudioCodec.HE_AAC => "HE-AAC", + AudioCodec.AAC => "AAC", + AudioCodec.MP3 => "MP3", + AudioCodec.MP2 => "MP2", + AudioCodec.Opus => "Opus", + AudioCodec.PCM => "PCM", + AudioCodec.Vorbis => "Vorbis", + AudioCodec.WMA => "WMA", + _ => string.Empty + }; + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs index 6914519fd4..5bdad3deeb 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoFormatter.cs @@ -45,111 +45,11 @@ public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName } // see definitions here https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c - if (audioCodecID == "thd+") + var codec = AudioCodecHelper.Resolve(audioFormat, audioCodecID, audioProfile); + + if (codec != AudioCodec.Unknown) { - return "TrueHD Atmos"; - } - - if (audioFormat == "truehd") - { - return "TrueHD"; - } - - if (audioFormat == "flac") - { - return "FLAC"; - } - - if (audioFormat == "dts") - { - if (audioProfile == "DTS:X") - { - return "DTS-X"; - } - - if (audioProfile == "DTS-HD MA") - { - return "DTS-HD MA"; - } - - if (audioProfile == "DTS-ES") - { - return "DTS-ES"; - } - - 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 (audioCodecID == "ec+3") - { - return "EAC3 Atmos"; - } - - if (audioFormat == "eac3") - { - return "EAC3"; - } - - if (audioFormat == "ac3") - { - return "AC3"; - } - - if (audioFormat == "aac") - { - if (audioCodecID == "A_AAC/MPEG4/LC/SBR") - { - return "HE-AAC"; - } - - return "AAC"; - } - - if (audioFormat == "mp3") - { - return "MP3"; - } - - if (audioFormat == "mp2") - { - return "MP2"; - } - - if (audioFormat == "opus") - { - return "Opus"; - } - - if (audioFormat.StartsWith("pcm_") || audioFormat.StartsWith("adpcm_")) - { - return "PCM"; - } - - if (audioFormat == "vorbis") - { - return "Vorbis"; - } - - if (audioFormat == "wmav1" || - audioFormat == "wmav2" || - audioFormat == "wmapro") - { - return "WMA"; + return AudioCodecHelper.GetDisplayName(codec); } Logger.ForDebugEvent() diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs index 39647e2396..25d2548d6a 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/MediaInfoModel.cs @@ -43,6 +43,12 @@ public class MediaInfoModel : IEmbeddedDocument public string AudioProfile { get; set; } + public string BestAudioFormat { get; set; } + + public string BestAudioCodecID { get; set; } + + public string BestAudioProfile { get; set; } + public long AudioBitrate { get; set; } public TimeSpan RunTime { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs index cbfb17e888..aa4ee66bcb 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfo/VideoFileInfoReader.cs @@ -22,7 +22,7 @@ public class VideoFileInfoReader : IVideoFileInfoReader private readonly List _pixelFormats; public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 14; - public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 14; + public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 15; private static readonly string[] ValidHdrColourPrimaries = { "bt2020" }; private static readonly string[] HlgTransferFunctions = { "arib-std-b67" }; @@ -92,6 +92,11 @@ public MediaInfoModel GetMediaInfo(string filename) mediaInfoModel.AudioCodecID = analysis.PrimaryAudioStream?.CodecTagString; mediaInfoModel.AudioProfile = analysis.PrimaryAudioStream?.Profile; mediaInfoModel.AudioBitrate = GetBitrate(analysis.PrimaryAudioStream); + + var bestAudioStream = GetBestAudioStream(analysis.AudioStreams); + mediaInfoModel.BestAudioFormat = bestAudioStream?.CodecName; + mediaInfoModel.BestAudioCodecID = bestAudioStream?.CodecTagString; + mediaInfoModel.BestAudioProfile = bestAudioStream?.Profile; mediaInfoModel.RunTime = GetBestRuntime(analysis.PrimaryAudioStream?.Duration, primaryVideoStream?.Duration, analysis.Format.Duration); mediaInfoModel.AudioStreamCount = analysis.AudioStreams.Count; mediaInfoModel.AudioChannels = analysis.PrimaryAudioStream?.Channels ?? 0; @@ -194,6 +199,11 @@ private FFProbePixelFormat GetPixelFormat(string format) return _pixelFormats.Find(x => x.Name == format); } + private static AudioStream GetBestAudioStream(List audioStreams) + { + return audioStreams?.MaxBy(stream => AudioCodecHelper.Resolve(stream.CodecName, stream.CodecTagString, stream.Profile)); + } + public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List sideData) { if (bitDepth < 10) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 24d69c7cb4..4765134195 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -30,6 +30,7 @@ public class FileNameBuilder : IBuildFileNames { private const string MediaInfoVideoDynamicRangeToken = "{MediaInfo VideoDynamicRange}"; private const string MediaInfoVideoDynamicRangeTypeToken = "{MediaInfo VideoDynamicRangeType}"; + private const string MediaInfoBestAudioCodecToken = "{MediaInfo BestAudioCodec}"; private readonly INamingConfigService _namingConfigService; private readonly IQualityDefinitionService _qualityDefinitionService; @@ -366,7 +367,8 @@ private void AddQualityTokens(Dictionary> token new Dictionary(FileNameBuilderTokenEqualityComparer.Instance) { { MediaInfoVideoDynamicRangeToken, 5 }, - { MediaInfoVideoDynamicRangeTypeToken, 13 } + { MediaInfoVideoDynamicRangeTypeToken, 13 }, + { MediaInfoBestAudioCodecToken, 15 } }; private void AddMediaInfoTokens(Dictionary> tokenHandlers, MovieFile movieFile) @@ -415,6 +417,15 @@ private void AddMediaInfoTokens(Dictionary> tok m => MediaInfoFormatter.FormatVideoDynamicRange(movieFile.MediaInfo); tokenHandlers[MediaInfoVideoDynamicRangeTypeToken] = m => MediaInfoFormatter.FormatVideoDynamicRangeType(movieFile.MediaInfo); + + var bestAudioMediaInfo = new MediaInfoModel + { + AudioFormat = movieFile.MediaInfo.BestAudioFormat, + AudioCodecID = movieFile.MediaInfo.BestAudioCodecID, + AudioProfile = movieFile.MediaInfo.BestAudioProfile + }; + var bestAudioCodec = MediaInfoFormatter.FormatAudioCodec(bestAudioMediaInfo, sceneName) ?? string.Empty; + tokenHandlers[MediaInfoBestAudioCodecToken] = m => bestAudioCodec; } private void AddCustomFormats(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile, List customFormats = null) diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index c04d036056..cd80dbbfdb 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -35,6 +35,9 @@ public FileNameSampleService(IBuildFileNames buildFileNames) AudioFormat = "DTS", AudioChannels = 6, AudioChannelPositions = "5.1", + BestAudioFormat = "dts", + BestAudioCodecID = string.Empty, + BestAudioProfile = "DTS-HD MA", AudioLanguages = new List { "ger" }, Subtitles = new List { "eng", "ger" } };