mirror of
https://github.com/Radarr/Radarr
synced 2026-05-07 14:01:25 +02:00
New: Add {MediaInfo BestAudioCodec} naming token
Adds a new naming token that resolves to the highest quality audio codec across all streams in a file, rather than just the primary stream. This addresses the common issue where release groups mux the lowest quality codec first (e.g. AC3), causing Radarr to rename files with the wrong codec and triggering unnecessary upgrade loops. Centralizes audio codec identification into an AudioCodec enum and AudioCodecHelper class (following the HdrFormat pattern), eliminating the duplicated matching logic between MediaInfoFormatter and VideoFileInfoReader. Also adds the token to the naming modal so users can discover it. Fixes #6488
This commit is contained in:
parent
89110c2cc8
commit
6b72400fc0
9 changed files with 372 additions and 106 deletions
|
|
@ -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}',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
176
src/NzbDrone.Core/MediaFiles/MediaInfo/AudioCodec.cs
Normal file
176
src/NzbDrone.Core/MediaFiles/MediaInfo/AudioCodec.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
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 = format ?? string.Empty;
|
||||
codecID = codecID ?? string.Empty;
|
||||
profile = profile ?? string.Empty;
|
||||
|
||||
if (codecID == "thd+")
|
||||
{
|
||||
return AudioCodec.TrueHDAtmos;
|
||||
}
|
||||
|
||||
if (format == "truehd")
|
||||
{
|
||||
return AudioCodec.TrueHD;
|
||||
}
|
||||
|
||||
if (format == "flac")
|
||||
{
|
||||
return AudioCodec.FLAC;
|
||||
}
|
||||
|
||||
if (format == "dts")
|
||||
{
|
||||
if (profile == "DTS:X")
|
||||
{
|
||||
return AudioCodec.DTS_X;
|
||||
}
|
||||
|
||||
if (profile == "DTS-HD MA")
|
||||
{
|
||||
return AudioCodec.DTS_HD_MA;
|
||||
}
|
||||
|
||||
if (profile == "DTS-ES")
|
||||
{
|
||||
return AudioCodec.DTS_ES;
|
||||
}
|
||||
|
||||
if (profile == "DTS-HD HRA")
|
||||
{
|
||||
return AudioCodec.DTS_HD_HRA;
|
||||
}
|
||||
|
||||
if (profile == "DTS Express")
|
||||
{
|
||||
return AudioCodec.DTS_Express;
|
||||
}
|
||||
|
||||
if (profile == "DTS 96/24")
|
||||
{
|
||||
return AudioCodec.DTS_9624;
|
||||
}
|
||||
|
||||
return AudioCodec.DTS;
|
||||
}
|
||||
|
||||
if (codecID == "ec+3")
|
||||
{
|
||||
return AudioCodec.EAC3Atmos;
|
||||
}
|
||||
|
||||
if (format == "eac3")
|
||||
{
|
||||
return AudioCodec.EAC3;
|
||||
}
|
||||
|
||||
if (format == "ac3")
|
||||
{
|
||||
return AudioCodec.AC3;
|
||||
}
|
||||
|
||||
if (format == "aac")
|
||||
{
|
||||
if (codecID == "A_AAC/MPEG4/LC/SBR")
|
||||
{
|
||||
return AudioCodec.HE_AAC;
|
||||
}
|
||||
|
||||
return AudioCodec.AAC;
|
||||
}
|
||||
|
||||
if (format == "mp3")
|
||||
{
|
||||
return AudioCodec.MP3;
|
||||
}
|
||||
|
||||
if (format == "mp2")
|
||||
{
|
||||
return AudioCodec.MP2;
|
||||
}
|
||||
|
||||
if (format == "opus")
|
||||
{
|
||||
return AudioCodec.Opus;
|
||||
}
|
||||
|
||||
if (format.StartsWith("pcm_") || format.StartsWith("adpcm_"))
|
||||
{
|
||||
return AudioCodec.PCM;
|
||||
}
|
||||
|
||||
if (format == "vorbis")
|
||||
{
|
||||
return AudioCodec.Vorbis;
|
||||
}
|
||||
|
||||
if (format == "wmav1" ||
|
||||
format == "wmav2" ||
|
||||
format == "wmapro")
|
||||
{
|
||||
return AudioCodec.WMA;
|
||||
}
|
||||
|
||||
return AudioCodec.Unknown;
|
||||
}
|
||||
|
||||
public static string GetDisplayName(AudioCodec codec)
|
||||
{
|
||||
return 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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,40 @@ private FFProbePixelFormat GetPixelFormat(string format)
|
|||
return _pixelFormats.Find(x => x.Name == format);
|
||||
}
|
||||
|
||||
private static AudioStream GetBestAudioStream(List<AudioStream> audioStreams)
|
||||
{
|
||||
if (audioStreams == null || audioStreams.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (audioStreams.Count == 1)
|
||||
{
|
||||
return audioStreams[0];
|
||||
}
|
||||
|
||||
AudioStream best = null;
|
||||
var bestRank = -1;
|
||||
|
||||
foreach (var stream in audioStreams)
|
||||
{
|
||||
var rank = GetAudioCodecRank(stream.CodecName, stream.CodecTagString, stream.Profile);
|
||||
|
||||
if (rank > bestRank)
|
||||
{
|
||||
bestRank = rank;
|
||||
best = stream;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static int GetAudioCodecRank(string format, string codecID, string profile)
|
||||
{
|
||||
return (int)AudioCodecHelper.Resolve(format, codecID, profile);
|
||||
}
|
||||
|
||||
public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List<SideData> sideData)
|
||||
{
|
||||
if (bitDepth < 10)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue