diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index d2a0b1858..cc4db4e50 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -304,7 +304,7 @@ namespace NzbDrone.Core.Test.ParserTests [Test] public void should_parse_null_quality_description_as_unknown() { - QualityParser.ParseCodec(null, null).Should().Be(Codec.Unknown); + QualityParser.ParseCodec(null).Should().Be(Codec.Unknown); } [TestCase("Artist Title - Album Title 2017 REPACK FLAC aAF", true, 2)] diff --git a/src/NzbDrone.Core/MediaFiles/AudioTag.cs b/src/NzbDrone.Core/MediaFiles/AudioTag.cs index 500a94bed..e1e581089 100644 --- a/src/NzbDrone.Core/MediaFiles/AudioTag.cs +++ b/src/NzbDrone.Core/MediaFiles/AudioTag.cs @@ -177,7 +177,7 @@ namespace NzbDrone.Core.MediaFiles Logger.Debug("Audio Properties: " + acodec.Description + ", Bitrate: " + bitrate + ", Sample Size: " + file.Properties.BitsPerSample + ", SampleRate: " + acodec.AudioSampleRate + ", Channels: " + acodec.AudioChannels); - Quality = QualityParser.ParseQuality(file.Name, acodec.Description, bitrate, file.Properties.BitsPerSample); + Quality = QualityParser.ParseQuality(file.Name, acodec); Logger.Debug($"Quality parsed: {Quality}, Source: {Quality.QualityDetectionSource}"); MediaInfo = new MediaInfoModel diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs index b87fcc619..eef0523de 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles { ".m4a", Quality.Unknown }, { ".m4b", Quality.Unknown }, { ".m4p", Quality.Unknown }, + { ".mka", Quality.Unknown }, { ".ogg", Quality.Unknown }, { ".oga", Quality.Unknown }, { ".opus", Quality.Unknown }, diff --git a/src/NzbDrone.Core/MediaFiles/MediaInfoFormatter.cs b/src/NzbDrone.Core/MediaFiles/MediaInfoFormatter.cs index 4909cfcc8..e1e5246bb 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaInfoFormatter.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaInfoFormatter.cs @@ -56,7 +56,7 @@ namespace NzbDrone.Core.MediaFiles public static string FormatAudioCodec(MediaInfoModel mediaInfo) { - var codec = QualityParser.ParseCodec(mediaInfo.AudioFormat, null); + var codec = QualityParser.ParseCodec(mediaInfo.AudioFormat); if (CodecNames.ContainsKey(codec)) { diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 09baede29..3a781d55b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -308,7 +308,7 @@ namespace NzbDrone.Core.Parser if (result != null) { - result.Quality = QualityParser.ParseQuality(title, null, 0); + result.Quality = QualityParser.ParseQuality(title, null); Logger.Debug("Quality parsed: {0}", result.Quality); return result; @@ -374,7 +374,7 @@ namespace NzbDrone.Core.Parser if (result != null) { - result.Quality = QualityParser.ParseQuality(title, null, 0); + result.Quality = QualityParser.ParseQuality(title, null); Logger.Debug("Quality parsed: {0}", result.Quality); result.ReleaseGroup = ParseReleaseGroup(releaseTitle); @@ -469,7 +469,7 @@ namespace NzbDrone.Core.Parser if (result != null) { - result.Quality = QualityParser.ParseQuality(title, null, 0); + result.Quality = QualityParser.ParseQuality(title, null); Logger.Debug("Quality parsed: {0}", result.Quality); result.ReleaseGroup = ParseReleaseGroup(releaseTitle); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 9b3c2d6c9..f01ec9048 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -1,10 +1,13 @@ using System; +using System.Reflection; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; +using TagLib; +using TagLib.Matroska; namespace NzbDrone.Core.Parser { @@ -38,12 +41,61 @@ namespace NzbDrone.Core.Parser private static readonly Regex SampleSizeRegex = new (@"\b(?:(?24[ ]?bit|tr24|24-(?:44|48|96|192)|[\[\(].*24bit.*[\]\)]))\b", RegexOptions.Compiled); - private static readonly Regex CodecRegex = new (@"\b(?:(?MPEG Version \d(.5)? Audio, Layer 1|MP1)|(?MPEG Version \d(.5)? Audio, Layer 2|MP2)|(?MP3.*VBR|MPEG Version \d(.5)? Audio, Layer 3 vbr)|(?MP3|MPEG Version \d(.5)? Audio, Layer 3)|(?(web)?flac|TR24)|(?wavpack|wv)|(?alac)|(?WMA\d?)|(?WAV|PCM)|(?M4A|M4P|M4B|AAC|mp4a|MPEG-4 Audio(?!.*alac))|(?OGG|OGA|Vorbis))\b|(?monkey's audio|[\[|\(].*\bape\b.*[\]|\)])|(?Opus Version \d(.5)? Audio|[\[|\(].*\bopus\b.*[\]|\)])", + + private static readonly Regex CodecRegex = new Regex(@"\b(?:(?MPEG Version \d(.5)? Audio, Layer 1|MP1)|(?MPEG Version \d(.5)? Audio, Layer 2|MP2)|(?MP3.*VBR|MPEG Version \d(.5)? Audio, Layer 3 vbr)|(?MP3|MPEG Version \d(.5)? Audio, Layer 3)|(?(web)?flac|TR24)|(?wavpack|wv)|(?alac)|(?WMA\d?)|(?WAV|PCM)|(?MKA|M4A|M4P|M4B|AAC|mp4a|MPEG-4 Audio(?!.*alac))|(?MKA|OGG|OGA|Vorbis))\b|(?monkey's audio|[\[|\(].*\bape\b.*[\]|\)])|(?Opus Version \d(.5)? Audio|[\[|\(].*\bopus\b.*[\]|\)])", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex WebRegex = new (@"\b(?WEB)(?:\b|$|[ .])", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static QualityModel ParseQuality(string name, IAudioCodec audioCodec) + { + var normalizedName = name.Replace('_', ' ').Trim().ToLower(); + var result = ParseQualityModifiers(name, normalizedName); + + var codec = Codec.Unknown; + var bitrate = ParseBitRate(normalizedName); + var sampleSize = ParseSampleSize(normalizedName); + + if (audioCodec != null) + { + if (audioCodec is AudioTrack matroskaCodec) + { + codec = ParseCodec(StealCodecFromMatroskaTrack(matroskaCodec)); + } + + if (codec == Codec.Unknown && audioCodec!.Description.IsNotNullOrWhiteSpace()) + { + var descCodec = ParseCodec(audioCodec.Description); + Logger.Trace($"Got codec {descCodec}"); + + result.Quality = FindQuality(descCodec, audioCodec.AudioBitrate); + + if (result.Quality != Quality.Unknown) + { + result.QualityDetectionSource = QualityDetectionSource.TagLib; + return result; + } + } + } + + return ParseQuality(result, codec, name, normalizedName, bitrate, sampleSize); + } + + private static string StealCodecFromMatroskaTrack(AudioTrack matroskaCodec) + { + var field = typeof(Track) + .GetField("track_codec_id", BindingFlags.NonPublic | BindingFlags.Instance); + + if (field == null) + { + return null; + } + + var val = ((string)field.GetValue(matroskaCodec) ?? string.Empty).Replace("A_", string.Empty); + return val; + } + public static QualityModel ParseQuality(string name, string desc, int fileBitrate, int fileSampleSize = 0) { Logger.Debug("Trying to parse quality for '{0}'", name); @@ -58,7 +110,7 @@ namespace NzbDrone.Core.Parser if (desc.IsNotNullOrWhiteSpace()) { - var descCodec = ParseCodec(desc, ""); + var descCodec = ParseCodec(desc); Logger.Trace($"Got codec {descCodec}"); result.Quality = FindQuality(descCodec, fileBitrate, fileSampleSize); @@ -70,10 +122,21 @@ namespace NzbDrone.Core.Parser } } - var codec = ParseCodec(normalizedName, name); + var codec = ParseCodec(normalizedName); var bitrate = ParseBitRate(normalizedName); var sampleSize = ParseSampleSize(normalizedName); + return ParseQuality(result, codec, name, normalizedName, bitrate, sampleSize); + } + + public static QualityModel ParseQuality( + QualityModel result, + Codec codec, + string name, + string normalizedName, + BitRate bitrate, + SampleSize sampleSize) + { switch (codec) { case Codec.MP1: @@ -251,7 +314,7 @@ namespace NzbDrone.Core.Parser return result; } - public static Codec ParseCodec(string name, string origName) + public static Codec ParseCodec(string name) { if (name.IsNullOrWhiteSpace()) {