This commit is contained in:
Mate Herber 2026-04-19 01:36:50 -07:00 committed by GitHub
commit f532aaf749
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 259 additions and 106 deletions

View file

@ -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}',

View file

@ -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();
}
}
}

View file

@ -775,6 +775,30 @@ public void should_not_update_media_info_if_token_configured_and_revision_is_new
Mocker.GetMock<IUpdateMediaInfo>().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<string> { "eng" },
Subtitles = new List<string> { "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,

View file

@ -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
};
}
}

View file

@ -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()

View file

@ -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; }

View file

@ -22,7 +22,7 @@ public class VideoFileInfoReader : IVideoFileInfoReader
private readonly List<FFProbePixelFormat> _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<AudioStream> 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> sideData)
{
if (bitDepth < 10)

View file

@ -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<string, Func<TokenMatch, string>> token
new Dictionary<string, int>(FileNameBuilderTokenEqualityComparer.Instance)
{
{ MediaInfoVideoDynamicRangeToken, 5 },
{ MediaInfoVideoDynamicRangeTypeToken, 13 }
{ MediaInfoVideoDynamicRangeTypeToken, 13 },
{ MediaInfoBestAudioCodecToken, 15 }
};
private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, MovieFile movieFile)
@ -415,6 +417,15 @@ private void AddMediaInfoTokens(Dictionary<string, Func<TokenMatch, string>> 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<string, Func<TokenMatch, string>> tokenHandlers, Movie movie, MovieFile movieFile, List<CustomFormat> customFormats = null)

View file

@ -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<string> { "ger" },
Subtitles = new List<string> { "eng", "ger" }
};