diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 8daf0e9f0..a79a703a3 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -144,6 +144,32 @@ namespace NzbDrone.Core.Test.ParserTests { ParseAndVerifyQuality(title, desc, bitrate, Quality.ALAC); } + + [TestCase("Stevie Ray Vaughan Discography (1981-1987) [APE]", null, 0)] + [TestCase("Brain Ape - Rig it [2014][ape]", null, 0)] + [TestCase("", "Monkey's Audio", 0)] + public void should_parse_ape_quality(string title, string desc, int bitrate) + { + ParseAndVerifyQuality(title, desc, bitrate, Quality.APE); + } + + [TestCase("Max Roach - Drums Unlimited (1966) [WavPack]", null, 0)] + [TestCase("Roxette - Charm School(2011) (2CD) [WV]", null, 0)] + [TestCase("", "WavPack", 0)] + public void should_parse_wavpack_quality(string title, string desc, int bitrate) + { + ParseAndVerifyQuality(title, desc, bitrate, Quality.WAVPACK); + } + + [TestCase("Arctic Monkeys - AM {2013-Album}", null, 0)] + [TestCase("Audio Adrinaline - Audio Adrinaline", null, 0)] + [TestCase("Brain Ape - Rig it [2014][flac]", null, 0)] + [TestCase("Coil - The Ape Of Naples(2005) (FLAC)", null, 0)] + public void should_not_parse_ape_quality(string title, string desc, int bitrate) + { + var result = QualityParser.ParseQuality(title, desc, bitrate); + result.Quality.Should().NotBe(Quality.APE); + } [TestCase("Milky Chance - Sadnecessary [256 Kbps] [M4A]", null, 0)] [TestCase("Little Mix - Salute [Deluxe Edition] [2013] [M4A-256]-V3nom [GLT", null, 0)] diff --git a/src/NzbDrone.Core/Datastore/Migration/019_add_ape_quality_in_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/019_add_ape_quality_in_profiles.cs new file mode 100644 index 000000000..97b4e0938 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/019_add_ape_quality_in_profiles.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using FluentMigrator; +using Newtonsoft.Json; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(19)] + public class add_ape_quality_in_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertProfile); + } + + private void ConvertProfile(IDbConnection conn, IDbTransaction tran) + { + var updater = new ProfileUpdater19(conn, tran); + + updater.SplitQualityAppend(6, 35); // APE after Flac + updater.SplitQualityAppend(6, 36); // WavPack after Flac + + updater.Commit(); + } + } + + public class Profile19 + { + public int Id { get; set; } + public string Name { get; set; } + public int Cutoff { get; set; } + public List Items { get; set; } + } + + public class ProfileItem19 + { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int Id { get; set; } + + public string Name { get; set; } + + public int? Quality { get; set; } + + public bool Allowed { get; set; } + public List Items { get; set; } + } + + public class ProfileUpdater19 + { + private readonly IDbConnection _connection; + private readonly IDbTransaction _transaction; + + private List _profiles; + private HashSet _changedProfiles = new HashSet(); + + public ProfileUpdater19(IDbConnection conn, IDbTransaction tran) + { + _connection = conn; + _transaction = tran; + + _profiles = GetProfiles(); + } + + public void Commit() + { + foreach (var profile in _changedProfiles) + { + using (var updateProfileCmd = _connection.CreateCommand()) + { + updateProfileCmd.Transaction = _transaction; + updateProfileCmd.CommandText = "UPDATE Profiles SET Name = ?, Cutoff = ?, Items = ? WHERE Id = ?"; + updateProfileCmd.AddParameter(profile.Name); + updateProfileCmd.AddParameter(profile.Cutoff); + updateProfileCmd.AddParameter(profile.Items.ToJson()); + updateProfileCmd.AddParameter(profile.Id); + + updateProfileCmd.ExecuteNonQuery(); + } + } + + _changedProfiles.Clear(); + } + + public void SplitQualityAppend(int find, int quality) + { + foreach (var profile in _profiles) + { + if (profile.Items.Any(v => v.Quality == quality)) continue; + + var findIndex = profile.Items.FindIndex(v => + { + return v.Quality == find || (v.Items != null && v.Items.Any(b => b.Quality == find)); + }); + + profile.Items.Insert(findIndex + 1, new ProfileItem19 + { + Quality = quality, + Allowed = false + }); + + _changedProfiles.Add(profile); + } + } + + private List GetProfiles() + { + var profiles = new List(); + + using (var getProfilesCmd = _connection.CreateCommand()) + { + getProfilesCmd.Transaction = _transaction; + getProfilesCmd.CommandText = @"SELECT Id, Name, Cutoff, Items FROM Profiles"; + + using (var profileReader = getProfilesCmd.ExecuteReader()) + { + while (profileReader.Read()) + { + profiles.Add(new Profile19 + { + Id = profileReader.GetInt32(0), + Name = profileReader.GetString(1), + Cutoff = profileReader.GetInt32(2), + Items = Json.Deserialize>(profileReader.GetString(3)) + }); + } + } + } + + return profiles; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs index dd049c6bd..f660ae5c0 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs @@ -18,7 +18,9 @@ namespace NzbDrone.Core.MediaFiles { ".ogg", Quality.Unknown }, { ".wma", Quality.WMA }, { ".wav", Quality.WAV }, - { ".flac", Quality.FLAC } + { ".wv" , Quality.WAVPACK }, + { ".flac", Quality.FLAC }, + { ".ape", Quality.APE } }; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 29b455cc7..e312d692e 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -190,6 +190,7 @@ + diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index b5a1f78b6..7f1801ee2 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex SampleSizeRegex = new Regex(@"\b(?:(?24[ ]bit|24bit|[\[\(].*24bit.*[\]\)]))"); - private static readonly Regex CodecRegex = new Regex(@"\b(?:(?MP3.*VBR|MPEG Version 1 Audio, Layer 3 vbr)|(?MP3|MPEG Version \d+ Audio, Layer 3)|(?flac)|(?alac)|(?WMA\d?)|(?WAV|PCM)|(?M4A|AAC|mp4a)|(?OGG|Vorbis))\b", + private static readonly Regex CodecRegex = new Regex(@"\b(?:(?MP3.*VBR|MPEG Version 1 Audio, Layer 3 vbr)|(?MP3|MPEG Version \d+ Audio, Layer 3)|(?flac)|(?wavpack|wv)|(?alac)|(?WMA\d?)|(?WAV|PCM)|(?M4A|AAC|mp4a)|(?OGG|Vorbis))\b|(?monkey's audio|[\[|\(].*ape.*[\]|\)])", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static QualityModel ParseQuality(string name, string desc, int fileBitrate, int fileSampleSize = 0) @@ -66,14 +66,14 @@ namespace NzbDrone.Core.Parser if (desc.IsNotNullOrWhiteSpace()) { - var descCodec = ParseCodec(desc); + var descCodec = ParseCodec(desc, ""); result.Quality = FindQuality(descCodec, fileBitrate, fileSampleSize); if (result.Quality != Quality.Unknown) { return result; } } - var codec = ParseCodec(normalizedName); + var codec = ParseCodec(normalizedName,name); var bitrate = ParseBitRate(normalizedName); var sampleSize = ParseSampleSize(normalizedName); @@ -100,6 +100,12 @@ namespace NzbDrone.Core.Parser 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; @@ -154,7 +160,7 @@ namespace NzbDrone.Core.Parser return result; } - private static Codec ParseCodec(string name) + private static Codec ParseCodec(string name, string origName) { var match = CodecRegex.Match(name); @@ -167,6 +173,8 @@ namespace NzbDrone.Core.Parser if (match.Groups["OGG"].Success) { return Codec.OGG; } 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; } @@ -232,6 +240,10 @@ namespace NzbDrone.Core.Parser 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: @@ -289,12 +301,14 @@ namespace NzbDrone.Core.Parser MP3VBR, FLAC, ALAC, + APE, + WAVPACK, WMA, AAC, AACVBR, OGG, WAV, - Unknown, + Unknown } public enum BitRate diff --git a/src/NzbDrone.Core/Qualities/Quality.cs b/src/NzbDrone.Core/Qualities/Quality.cs index 361dee26a..5aae603f6 100644 --- a/src/NzbDrone.Core/Qualities/Quality.cs +++ b/src/NzbDrone.Core/Qualities/Quality.cs @@ -90,6 +90,8 @@ namespace NzbDrone.Core.Qualities public static Quality MP3_008 => new Quality(32, "MP3-8"); // For Current Files Only public static Quality MP3_112 => new Quality(33, "MP3-112"); // For Current Files Only public static Quality MP3_224 => new Quality(34, "MP3-224"); // For Current Files Only + public static Quality APE => new Quality(35, "APE"); + public static Quality WAVPACK => new Quality(36, "WavPack"); static Quality() { @@ -128,6 +130,8 @@ namespace NzbDrone.Core.Qualities VORBIS_Q5, ALAC, FLAC, + APE, + WAVPACK, FLAC_24, WAV }; @@ -173,6 +177,8 @@ namespace NzbDrone.Core.Qualities new QualityDefinition(Quality.VORBIS_Q10) { Weight = 21, MinSize = 0, MaxSize = 550, GroupName = "High Quality Lossy", GroupWeight = 6 }, new QualityDefinition(Quality.ALAC) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, new QualityDefinition(Quality.FLAC) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, + new QualityDefinition(Quality.APE) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, + new QualityDefinition(Quality.WAVPACK) { Weight = 22, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, new QualityDefinition(Quality.FLAC_24) { Weight = 23, MinSize = 0, MaxSize = null, GroupName = "Lossless", GroupWeight = 7 }, new QualityDefinition(Quality.WAV) { Weight = 24, MinSize = 0, MaxSize = null, GroupWeight = 8} };