Skip to content

Commit 5061f7e

Browse files
author
Meyn
committed
Improved AudioConverter lossless and lossy rules
1 parent 7959d3a commit 5061f7e

4 files changed

Lines changed: 128 additions & 8 deletions

File tree

Tubifarry/Core/Model/AudioMetadataHandler.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,45 @@ public bool TryEmbedMetadata(AlbumInfo albumInfo, AlbumSongInfo trackInfo, Relea
381381
}
382382
}
383383

384+
/// <summary>
385+
/// Checks if the specified audio format is supported for encoding by FFmpeg.
386+
/// </summary>
387+
/// <param name="format">The audio format to check</param>
388+
/// <returns>True if the format can be used as a conversion target, false otherwise</returns>
389+
public static bool IsTargetFormatSupportedForEncoding(AudioFormat format) => BaseConversionParameters.ContainsKey(format);
390+
391+
392+
/// <summary>
393+
/// Gets the actual audio codec from a file using FFmpeg and returns the corresponding AudioFormat.
394+
/// </summary>
395+
/// <param name="filePath">Path to the audio file</param>
396+
/// <returns>AudioFormat enum value or AudioFormat.Unknown if codec is not supported or detection fails</returns>
397+
public static async Task<AudioFormat> GetSupportedCodecAsync(string filePath)
398+
{
399+
try
400+
{
401+
IMediaInfo mediaInfo = await FFmpeg.GetMediaInfo(filePath);
402+
IAudioStream? audioStream = mediaInfo.AudioStreams.FirstOrDefault();
403+
404+
if (audioStream == null)
405+
{
406+
NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Debug("No audio stream found in file: {0}", filePath);
407+
return AudioFormat.Unknown;
408+
}
409+
410+
string codec = audioStream.Codec.ToLower();
411+
AudioFormat format = AudioFormatHelper.GetAudioFormatFromCodec(codec);
412+
413+
NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Trace("Detected codec '{0}' as format '{1}' for file: {2}", codec, format, filePath);
414+
return format;
415+
}
416+
catch (Exception ex)
417+
{
418+
NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Error(ex, "Failed to detect codec for file: {0}", filePath);
419+
return AudioFormat.Unknown;
420+
}
421+
}
422+
384423
public static bool CheckFFmpegInstalled()
385424
{
386425
if (_isFFmpegInstalled.HasValue)

Tubifarry/Core/Utilities/AudioFormat.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public enum AudioFormat
1717
MIDI,
1818
AMR,
1919
WMA,
20-
ALAC
20+
ALAC,
21+
APE
2122
}
2223

2324
internal static class AudioFormatHelper
@@ -92,6 +93,7 @@ internal static class AudioFormatHelper
9293
"ac3" => ".ac3",
9394
"alac" => ".m4a",
9495
"vorbis" => ".ogg",
96+
"ape" => ".ape",
9597
"pcm_s16le" or "pcm_s24le" or "pcm_s32le" => ".wav",
9698
_ => ".aac" // Default to AAC if the codec is unknown
9799
};
@@ -113,6 +115,7 @@ internal static class AudioFormatHelper
113115
"amr" => AudioFormat.AMR,
114116
"wma" => AudioFormat.WMA,
115117
"alac" => AudioFormat.ALAC,
118+
"ape" => AudioFormat.APE,
116119
_ => AudioFormat.Unknown // Default for unknown formats
117120
};
118121

@@ -134,6 +137,7 @@ internal static class AudioFormatHelper
134137
AudioFormat.MP4 => ".mp4",
135138
AudioFormat.OGG => ".ogg",
136139
AudioFormat.ALAC => ".m4a",
140+
AudioFormat.APE => ".ape",
137141
_ => ".aac" // Default to AAC if the format is unknown
138142
};
139143

@@ -171,6 +175,7 @@ internal static class AudioFormatHelper
171175
"amr" => AudioFormat.AMR,
172176
"wma" => AudioFormat.WMA,
173177
"alac" => AudioFormat.ALAC,
178+
"ape" => AudioFormat.APE,
174179
_ => AudioFormat.Unknown
175180
};
176181

Tubifarry/Metadata/Converter/AudioConverter.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public override MetadataFileResult TrackMetadata(Artist artist, TrackFile trackF
4141

4242
private async Task ConvertTrack(TrackFile trackFile)
4343
{
44-
AudioFormat trackFormat = GetTrackAudioFormat(trackFile.Path);
44+
AudioFormat trackFormat = await GetTrackAudioFormatAsync(trackFile.Path);
4545
if (trackFormat == AudioFormat.Unknown)
4646
return;
4747

@@ -134,7 +134,7 @@ private async Task<bool> ShouldConvertTrack(TrackFile trackFile)
134134
return false;
135135
}
136136

137-
AudioFormat trackFormat = GetTrackAudioFormat(trackFile.Path);
137+
AudioFormat trackFormat = await GetTrackAudioFormatAsync(trackFile.Path);
138138
if (trackFormat == AudioFormat.Unknown)
139139
return false;
140140

@@ -169,7 +169,7 @@ private bool MatchesAnyCustomRule(AudioFormat trackFormat, int? currentBitrate)
169169

170170
private bool IsRuleMatching(ConversionRule rule, AudioFormat trackFormat, int? currentBitrate)
171171
{
172-
bool formatMatches = rule.IsGlobalRule || rule.SourceFormat == trackFormat;
172+
bool formatMatches = rule.MatchesFormat(trackFormat);
173173
bool bitrateMatches = rule.MatchesBitrate(currentBitrate);
174174
if (formatMatches && bitrateMatches)
175175
{
@@ -179,9 +179,25 @@ private bool IsRuleMatching(ConversionRule rule, AudioFormat trackFormat, int? c
179179
return false;
180180
}
181181

182-
private AudioFormat GetTrackAudioFormat(string trackPath)
182+
private async Task<AudioFormat> GetTrackAudioFormatAsync(string trackPath)
183183
{
184-
AudioFormat trackFormat = AudioFormatHelper.GetAudioCodecFromExtension(Path.GetExtension(trackPath));
184+
string extension = Path.GetExtension(trackPath);
185+
186+
// For .m4a files, use codec detection since they can contain AAC or ALAC
187+
if (string.Equals(extension, ".m4a", StringComparison.OrdinalIgnoreCase))
188+
{
189+
AudioFormat detectedFormat = await AudioMetadataHandler.GetSupportedCodecAsync(trackPath);
190+
if (detectedFormat != AudioFormat.Unknown)
191+
{
192+
_logger.Trace($"Detected codec-based format {detectedFormat} for .m4a file: {trackPath}");
193+
return detectedFormat;
194+
}
195+
196+
_logger.Warn($"Failed to detect codec for .m4a file, falling back to extension-based detection: {trackPath}");
197+
}
198+
199+
// For all other extensions, use extension-based detection
200+
AudioFormat trackFormat = AudioFormatHelper.GetAudioCodecFromExtension(extension);
185201
if (trackFormat == AudioFormat.Unknown)
186202
_logger.Warn($"Unknown audio format for track: {trackPath}");
187203
return trackFormat;
@@ -205,6 +221,7 @@ private static string FormatDescriptionWithBitrate(AudioFormat format, int? bitr
205221
AudioFormat.FLAC => Settings.ConvertFLAC,
206222
AudioFormat.WAV => Settings.ConvertWAV,
207223
AudioFormat.Opus => Settings.ConvertOpus,
224+
AudioFormat.APE => Settings.ConvertOther,
208225
AudioFormat.Vorbis => Settings.ConvertOther,
209226
AudioFormat.OGG => Settings.ConvertOther,
210227
AudioFormat.WMA => Settings.ConvertOther,

Tubifarry/Metadata/Converter/BitrateRules.cs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using NLog;
22
using NzbDrone.Common.Instrumentation;
33
using System.Text.RegularExpressions;
4+
using Tubifarry.Core.Model;
45
using Tubifarry.Core.Utilities;
56

67
namespace Tubifarry.Metadata.Converter
@@ -14,7 +15,12 @@ public class ConversionRule
1415
public int? TargetBitrate { get; set; }
1516
public bool IsArtistRule { get; set; }
1617

17-
public bool IsGlobalRule => SourceFormat.ToString().Equals(RuleParser.GlobalRuleIdentifier, StringComparison.OrdinalIgnoreCase);
18+
// Track the type of category rule
19+
public bool IsGlobalRule { get; set; }
20+
public bool IsLossyRule { get; set; }
21+
public bool IsLosslessRule { get; set; }
22+
23+
public bool IsCategoryRule => IsGlobalRule || IsLossyRule || IsLosslessRule;
1824

1925
public bool MatchesBitrate(int? currentBitrate)
2026
{
@@ -26,6 +32,20 @@ public bool MatchesBitrate(int? currentBitrate)
2632
return EvaluateBitrateCondition(currentBitrate.Value);
2733
}
2834

35+
public bool MatchesFormat(AudioFormat trackFormat)
36+
{
37+
if (IsGlobalRule)
38+
return true;
39+
40+
if (IsLossyRule)
41+
return AudioFormatHelper.IsLossyFormat(trackFormat);
42+
43+
if (IsLosslessRule)
44+
return !AudioFormatHelper.IsLossyFormat(trackFormat);
45+
46+
return SourceFormat == trackFormat;
47+
}
48+
2949
private bool HasBitrateConstraints() => SourceBitrateOperator.HasValue && SourceBitrateValue.HasValue;
3050

3151
private bool EvaluateBitrateCondition(int currentBitrate)
@@ -51,7 +71,16 @@ private bool EvaluateBitrateCondition(int currentBitrate)
5171

5272
private string FormatSourcePart()
5373
{
54-
string source = SourceFormat.ToString();
74+
string source;
75+
if (IsGlobalRule)
76+
source = RuleParser.GlobalRuleIdentifier;
77+
else if (IsLossyRule)
78+
source = RuleParser.LossyRuleIdentifier;
79+
else if (IsLosslessRule)
80+
source = RuleParser.LosslessRuleIdentifier;
81+
else
82+
source = SourceFormat.ToString();
83+
5584
if (HasBitrateConstraints())
5685
source += GetOperatorSymbol() + SourceBitrateValue!.Value;
5786
return source;
@@ -117,6 +146,8 @@ public enum ComparisonOperator
117146
public static class RuleParser
118147
{
119148
public const string GlobalRuleIdentifier = "all";
149+
public const string LossyRuleIdentifier = "lossy";
150+
public const string LosslessRuleIdentifier = "lossless";
120151
public const string NoConversionTag = "no-conversion";
121152
private static readonly Regex SourceFormatPattern = new(@"^([a-zA-Z0-9]+)(?:([!<>=]{1,2})(\d+))?$", RegexOptions.Compiled);
122153
private static readonly Regex TargetFormatPattern = new(@"^([a-zA-Z0-9]+)(?::(\d+)k?)?$", RegexOptions.Compiled);
@@ -193,6 +224,13 @@ private static bool ParseSourcePart(string sourceKey, ConversionRule rule)
193224

194225
if (sourceMatch.Groups[2].Success && sourceMatch.Groups[3].Success)
195226
{
227+
// Category rules (all, lossy, lossless) cannot have bitrate constraints
228+
if (rule.IsCategoryRule)
229+
{
230+
_logger.Warn("Invalid: Bitrate constraints not applicable to category rules (all, lossy, lossless)");
231+
return false;
232+
}
233+
196234
if (!AudioFormatHelper.IsLossyFormat(rule.SourceFormat))
197235
{
198236
_logger.Warn("Invalid: Bitrate constraints not applicable to lossless format");
@@ -211,6 +249,21 @@ private static bool ParseSourceFormat(string formatName, ConversionRule rule)
211249
if (string.Equals(formatName, GlobalRuleIdentifier, StringComparison.OrdinalIgnoreCase))
212250
{
213251
rule.SourceFormat = AudioFormat.Unknown;
252+
rule.IsGlobalRule = true;
253+
return true;
254+
}
255+
256+
if (string.Equals(formatName, LossyRuleIdentifier, StringComparison.OrdinalIgnoreCase))
257+
{
258+
rule.SourceFormat = AudioFormat.Unknown;
259+
rule.IsLossyRule = true;
260+
return true;
261+
}
262+
263+
if (string.Equals(formatName, LosslessRuleIdentifier, StringComparison.OrdinalIgnoreCase))
264+
{
265+
rule.SourceFormat = AudioFormat.Unknown;
266+
rule.IsLosslessRule = true;
214267
return true;
215268
}
216269

@@ -264,6 +317,12 @@ private static bool ParseTargetFormat(string formatName, ConversionRule rule)
264317
return false;
265318
}
266319

320+
if (!AudioMetadataHandler.IsTargetFormatSupportedForEncoding(targetFormat))
321+
{
322+
_logger.Warn("Target format {0} is not supported for encoding by FFmpeg", targetFormat);
323+
return false;
324+
}
325+
267326
rule.TargetFormat = targetFormat;
268327
return true;
269328
}

0 commit comments

Comments
 (0)