using System; using System.IO; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Parser { public class QualityParser { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(QualityParser)); private static readonly Regex ProperRegex = new Regex(@"\b(?proper)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex RepackRegex = new Regex(@"\b(?repack|rerip)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex VersionRegex = new Regex(@"\d[-._ ]?v(?\d)[-._ ]|\[v(?\d)\]", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex RealRegex = new Regex(@"\b(?REAL)\b", RegexOptions.Compiled); private static readonly Regex BitRateRegex = new Regex(@"\b(?:(?96[ ]?kbps|96|[\[\(].*96.*[\]\)])| (?128[ ]?kbps|128|[\[\(].*128.*[\]\)])| (?160[ ]?kbps|160|[\[\(].*160.*[\]\)]|q5)| (?192[ ]?kbps|192|[\[\(].*192.*[\]\)]|q6)| (?224[ ]?kbps|224|[\[\(].*224.*[\]\)]|q7)| (?256[ ]?kbps|256|itunes\splus|[\[\(].*256.*[\]\)]|q8)| (?320[ ]?kbps|320|[\[\(].*320.*[\]\)]|q9)| (?500[ ]?kbps|500|[\[\(].*500.*[\]\)]|q10)| (?V0[ ]?kbps|V0|[\[\(].*V0.*[\]\)])| (?V2[ ]?kbps|V2|[\[\(].*V2.*[\]\)]))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); private static readonly Regex SampleSizeRegex = new Regex(@"\b(?:(?24[ ]bit|24bit|[\[\(].*24bit.*[\]\)]))"); 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)|(?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.*[\]|\)])", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex WebRegex = new Regex(@"\b(?WEB)(?:\b|$|[ .])", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static QualityModel ParseQuality(string name, string desc, int fileBitrate, int fileSampleSize = 0) { Logger.Debug("Trying to parse quality for {0}", name); var normalizedName = name.Replace('_', ' ').Trim().ToLower(); var result = ParseQualityModifiers(name, normalizedName); if (desc.IsNotNullOrWhiteSpace()) { var descCodec = ParseCodec(desc, ""); Logger.Trace($"Got codec {descCodec}"); result.Quality = FindQuality(descCodec, fileBitrate, fileSampleSize); if (result.Quality != Quality.Unknown) { result.QualityDetectionSource = QualityDetectionSource.TagLib; return result; } } var codec = ParseCodec(normalizedName, name); var bitrate = ParseBitRate(normalizedName); var sampleSize = ParseSampleSize(normalizedName); switch (codec) { case Codec.MP1: case Codec.MP2: result.Quality = Quality.Unknown; break; case Codec.MP3VBR: if (bitrate == BitRate.VBRV0) { result.Quality = Quality.MP3_VBR; } else if (bitrate == BitRate.VBRV2) { result.Quality = Quality.MP3_VBR_V2; } else { result.Quality = Quality.Unknown; } break; case Codec.MP3CBR: if (bitrate == BitRate.B096) { result.Quality = Quality.MP3_096; } else if (bitrate == BitRate.B128) { result.Quality = Quality.MP3_128; } else if (bitrate == BitRate.B160) { result.Quality = Quality.MP3_160; } else if (bitrate == BitRate.B192) { result.Quality = Quality.MP3_192; } else if (bitrate == BitRate.B256) { result.Quality = Quality.MP3_256; } else if (bitrate == BitRate.B320) { result.Quality = Quality.MP3_320; } else { result.Quality = Quality.Unknown; } break; case Codec.FLAC: if (sampleSize == SampleSize.S24) { result.Quality = Quality.FLAC_24; } else { result.Quality = Quality.FLAC; } break; case Codec.ALAC: result.Quality = Quality.ALAC; break; case Codec.WAVPACK: result.Quality = Quality.WAVPACK; break; case Codec.APE: result.Quality = Quality.APE; break; case Codec.WMA: result.Quality = Quality.WMA; break; case Codec.WAV: result.Quality = Quality.WAV; break; case Codec.AAC: if (bitrate == BitRate.B192) { result.Quality = Quality.AAC_192; } else if (bitrate == BitRate.B256) { result.Quality = Quality.AAC_256; } else if (bitrate == BitRate.B320) { result.Quality = Quality.AAC_320; } else { result.Quality = Quality.AAC_VBR; } break; case Codec.AACVBR: result.Quality = Quality.AAC_VBR; break; case Codec.OGG: case Codec.OPUS: if (bitrate == BitRate.B160) { result.Quality = Quality.VORBIS_Q5; } else if (bitrate == BitRate.B192) { result.Quality = Quality.VORBIS_Q6; } else if (bitrate == BitRate.B224) { result.Quality = Quality.VORBIS_Q7; } else if (bitrate == BitRate.B256) { result.Quality = Quality.VORBIS_Q8; } else if (bitrate == BitRate.B320) { result.Quality = Quality.VORBIS_Q9; } else if (bitrate == BitRate.B500) { result.Quality = Quality.VORBIS_Q10; } else { result.Quality = Quality.Unknown; } break; case Codec.Unknown: if (bitrate == BitRate.B192) { result.Quality = Quality.MP3_192; } else if (bitrate == BitRate.B256) { result.Quality = Quality.MP3_256; } else if (bitrate == BitRate.B320) { result.Quality = Quality.MP3_320; } else if (bitrate == BitRate.VBR) { result.Quality = Quality.MP3_VBR_V2; } else { if (WebRegex.IsMatch(normalizedName)) { result.Quality = Quality.MP3_320; } else { result.Quality = Quality.Unknown; } } break; default: result.Quality = Quality.Unknown; break; } // Based on extension if (result.Quality == Quality.Unknown && !name.ContainsInvalidPathChars()) { try { result.Quality = MediaFileExtensions.GetQualityForExtension(name.GetPathExtension()); result.QualityDetectionSource = QualityDetectionSource.Extension; } catch (ArgumentException) { // Swallow exception for cases where string contains illegal // path characters. } } return result; } public static Codec ParseCodec(string name, string origName) { if (name.IsNullOrWhiteSpace()) { return Codec.Unknown; } var match = CodecRegex.Match(name); if (!match.Success) { return Codec.Unknown; } if (match.Groups["FLAC"].Success) { return Codec.FLAC; } if (match.Groups["ALAC"].Success) { return Codec.ALAC; } if (match.Groups["WMA"].Success) { return Codec.WMA; } if (match.Groups["WAV"].Success) { return Codec.WAV; } if (match.Groups["AAC"].Success) { return Codec.AAC; } if (match.Groups["OGG"].Success) { return Codec.OGG; } if (match.Groups["OPUS"].Success) { return Codec.OPUS; } if (match.Groups["MP1"].Success) { return Codec.MP1; } if (match.Groups["MP2"].Success) { return Codec.MP2; } if (match.Groups["MP3VBR"].Success) { return Codec.MP3VBR; } if (match.Groups["MP3CBR"].Success) { return Codec.MP3CBR; } if (match.Groups["WAVPACK"].Success) { return Codec.WAVPACK; } if (match.Groups["APE"].Success) { return Codec.APE; } return Codec.Unknown; } private static BitRate ParseBitRate(string name) { // var nameWithNoSpaces = Regex.Replace(name, @"\s+", ""); var match = BitRateRegex.Match(name); if (!match.Success) { return BitRate.Unknown; } if (match.Groups["B096"].Success) { return BitRate.B096; } if (match.Groups["B128"].Success) { return BitRate.B128; } if (match.Groups["B160"].Success) { return BitRate.B160; } if (match.Groups["B192"].Success) { return BitRate.B192; } if (match.Groups["B224"].Success) { return BitRate.B224; } if (match.Groups["B256"].Success) { return BitRate.B256; } if (match.Groups["B320"].Success) { return BitRate.B320; } if (match.Groups["B500"].Success) { return BitRate.B500; } if (match.Groups["VBR"].Success) { return BitRate.VBR; } if (match.Groups["VBRV0"].Success) { return BitRate.VBRV0; } if (match.Groups["VBRV2"].Success) { return BitRate.VBRV2; } return BitRate.Unknown; } private static SampleSize ParseSampleSize(string name) { var match = SampleSizeRegex.Match(name); if (!match.Success) { return SampleSize.Unknown; } if (match.Groups["S24"].Success) { return SampleSize.S24; } return SampleSize.Unknown; } private static Quality FindQuality(Codec codec, int bitrate, int sampleSize = 0) { switch (codec) { case Codec.MP1: case Codec.MP2: return Quality.Unknown; case Codec.MP3VBR: return Quality.MP3_VBR; case Codec.MP3CBR: if (bitrate == 8) { return Quality.MP3_008; } if (bitrate == 16) { return Quality.MP3_016; } if (bitrate == 24) { return Quality.MP3_024; } if (bitrate == 32) { return Quality.MP3_032; } if (bitrate == 40) { return Quality.MP3_040; } if (bitrate == 48) { return Quality.MP3_048; } if (bitrate == 56) { return Quality.MP3_056; } if (bitrate == 64) { return Quality.MP3_064; } if (bitrate == 80) { return Quality.MP3_080; } if (bitrate == 96) { return Quality.MP3_096; } if (bitrate == 112) { return Quality.MP3_112; } if (bitrate == 128) { return Quality.MP3_128; } if (bitrate == 160) { return Quality.MP3_160; } if (bitrate == 192) { return Quality.MP3_192; } if (bitrate == 224) { return Quality.MP3_224; } if (bitrate == 256) { return Quality.MP3_256; } if (bitrate == 320) { return Quality.MP3_320; } return Quality.Unknown; case Codec.FLAC: if (sampleSize == 24) { return Quality.FLAC_24; } return Quality.FLAC; case Codec.ALAC: return Quality.ALAC; case Codec.WAVPACK: return Quality.WAVPACK; case Codec.APE: return Quality.APE; case Codec.WMA: return Quality.WMA; case Codec.WAV: return Quality.WAV; case Codec.AAC: if (bitrate == 192) { return Quality.AAC_192; } if (bitrate == 256) { return Quality.AAC_256; } if (bitrate == 320) { return Quality.AAC_320; } return Quality.AAC_VBR; case Codec.OGG: if (bitrate == 160) { return Quality.VORBIS_Q5; } if (bitrate == 192) { return Quality.VORBIS_Q6; } if (bitrate == 224) { return Quality.VORBIS_Q7; } if (bitrate == 256) { return Quality.VORBIS_Q8; } if (bitrate == 320) { return Quality.VORBIS_Q9; } if (bitrate == 500) { return Quality.VORBIS_Q10; } return Quality.Unknown; case Codec.OPUS: if (bitrate < 130) { return Quality.Unknown; } if (bitrate < 180) { return Quality.VORBIS_Q5; } if (bitrate < 205) { return Quality.VORBIS_Q6; } if (bitrate < 240) { return Quality.VORBIS_Q7; } if (bitrate < 290) { return Quality.VORBIS_Q8; } if (bitrate < 410) { return Quality.VORBIS_Q9; } return Quality.VORBIS_Q10; default: return Quality.Unknown; } } private static QualityModel ParseQualityModifiers(string name, string normalizedName) { var result = new QualityModel { Quality = Quality.Unknown }; if (ProperRegex.IsMatch(normalizedName)) { result.Revision.Version = 2; } if (RepackRegex.IsMatch(normalizedName)) { result.Revision.Version = 2; result.Revision.IsRepack = true; } Match versionRegexResult = VersionRegex.Match(normalizedName); if (versionRegexResult.Success) { result.Revision.Version = Convert.ToInt32(versionRegexResult.Groups["version"].Value); } // TODO: re-enable this when we have a reliable way to determine real MatchCollection realRegexResult = RealRegex.Matches(name); if (realRegexResult.Count > 0) { result.Revision.Real = realRegexResult.Count; } return result; } } public enum Codec { MP1, MP2, MP3CBR, MP3VBR, FLAC, ALAC, APE, WAVPACK, WMA, AAC, AACVBR, OGG, OPUS, WAV, Unknown } public enum BitRate { B096, B128, B160, B192, B224, B256, B320, B500, VBR, VBRV0, VBRV2, Unknown, } public enum SampleSize { S24, Unknown } }