diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index 6ab1074ab..25c2010d0 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -25,6 +25,12 @@ const allowFingerprintingOptions = [ { key: 'never', value: 'Never' } ]; +const downloadPropersAndRepacksOptions = [ + { key: 'preferAndUpgrade', value: 'Prefer and Upgrade' }, + { key: 'doNotUpgrade', value: 'Do not Upgrade Automatically' }, + { key: 'doNotPrefer', value: 'Do not Prefer' } +]; + const fileDateOptions = [ { key: 'none', value: 'None' }, { key: 'albumReleaseDate', value: 'Album Release Date' } @@ -209,14 +215,23 @@ class MediaManagement extends Component { isAdvanced={true} size={sizes.MEDIUM} > - Download Propers + Propers and Repacks diff --git a/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs b/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs index 072f0f2bc..7be39b3f1 100644 --- a/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs +++ b/src/Lidarr.Api.V1/Config/MediaManagementConfigResource.cs @@ -1,6 +1,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; using Lidarr.Http.REST; +using NzbDrone.Core.Qualities; namespace Lidarr.Api.V1.Config { @@ -8,7 +9,7 @@ namespace Lidarr.Api.V1.Config { public bool AutoUnmonitorPreviouslyDownloadedTracks { get; set; } public string RecycleBin { get; set; } - public bool AutoDownloadPropers { get; set; } + public ProperDownloadTypes DownloadPropersAndRepacks { get; set; } public bool CreateEmptyArtistFolders { get; set; } public bool DeleteEmptyFolders { get; set; } public FileDateType FileDate { get; set; } @@ -35,7 +36,7 @@ namespace Lidarr.Api.V1.Config { AutoUnmonitorPreviouslyDownloadedTracks = model.AutoUnmonitorPreviouslyDownloadedTracks, RecycleBin = model.RecycleBin, - AutoDownloadPropers = model.AutoDownloadPropers, + DownloadPropersAndRepacks = model.DownloadPropersAndRepacks, CreateEmptyArtistFolders = model.CreateEmptyArtistFolders, DeleteEmptyFolders = model.DeleteEmptyFolders, FileDate = model.FileDate, diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index bcaaabc41..39d2561c0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -17,6 +17,7 @@ using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Test.Languages; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -411,7 +412,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [Test] - public void should_put_higher_quality_before_lower_allways() + public void should_put_higher_quality_before_lower_always() { var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.French); var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.German); @@ -423,5 +424,88 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var qualifiedReports = Subject.PrioritizeDecisions(decisions); qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.MP3_320); } + + + [Test] + public void should_prefer_higher_score_over_lower_score() + { + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC), Language.English); + + remoteAlbum1.PreferredWordScore = 10; + remoteAlbum2.PreferredWordScore = 0; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.PreferredWordScore.Should().Be(10); + } + + [Test] + public void should_prefer_proper_over_score_when_download_propers_is_prefer_and_upgrade() + { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.PreferAndUpgrade); + + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1)), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2)), Language.English); + + remoteAlbum1.PreferredWordScore = 10; + remoteAlbum2.PreferredWordScore = 0; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.Should().Be(2); + } + + [Test] + public void should_prefer_proper_over_score_when_download_propers_is_do_not_upgrade() + { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotUpgrade); + + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1)), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2)), Language.English); + + remoteAlbum1.PreferredWordScore = 10; + remoteAlbum2.PreferredWordScore = 0; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.Should().Be(2); + } + + [Test] + public void should_prefer_score_over_proper_when_download_propers_is_do_not_prefer() + { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotPrefer); + + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1)), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2)), Language.English); + + remoteAlbum1.PreferredWordScore = 10; + remoteAlbum2.PreferredWordScore = 0; + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.FLAC); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.Should().Be(1); + qualifiedReports.First().RemoteAlbum.PreferredWordScore.Should().Be(10); + } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs new file mode 100644 index 000000000..c8b5e8335 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RepackSpecificationFixture.cs @@ -0,0 +1,203 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Music; +using Moq; + +namespace NzbDrone.Core.Test.DecisionEngineTests +{ + [TestFixture] + public class RepackSpecificationFixture : CoreTest + { + private ParsedAlbumInfo _parsedAlbumInfo; + private List _albums; + private List _trackFiles; + + [SetUp] + public void Setup() + { + Mocker.Resolve(); + + _parsedAlbumInfo = Builder.CreateNew() + .With(p => p.Quality = new QualityModel(Quality.FLAC, + new Revision(2, 0, false))) + .With(p => p.ReleaseGroup = "Lidarr") + .Build(); + + _albums = Builder.CreateListOfSize(1) + .All() + .BuildList(); + + _trackFiles = Builder.CreateListOfSize(3) + .All() + .With(t => t.AlbumId = _albums.First().Id) + .BuildList(); + + Mocker.GetMock() + .Setup(c => c.GetFilesByAlbum(It.IsAny())) + .Returns(_trackFiles); + } + + [Test] + public void should_return_true_if_it_is_not_a_repack() + { + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeTrue(); + } + + [Test] + public void should_return_true_if_there_are_is_no_track_files() + { + Mocker.GetMock() + .Setup(c => c.GetFilesByAlbum(It.IsAny())) + .Returns(new List()); + + _parsedAlbumInfo.Quality.Revision.IsRepack = true; + + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeTrue(); + } + + [Test] + public void should_return_true_if_is_a_repack_for_a_different_quality() + { + _parsedAlbumInfo.Quality.Revision.IsRepack = true; + + _trackFiles.Select(c => { c.ReleaseGroup = "Lidarr"; return c; }).ToList(); + _trackFiles.Select(c => { c.Quality = new QualityModel(Quality.MP3_256); return c; }).ToList(); + + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeTrue(); + } + + [Test] + public void should_return_true_if_is_a_repack_for_all_existing_files() + { + _parsedAlbumInfo.Quality.Revision.IsRepack = true; + + _trackFiles.Select(c => { c.ReleaseGroup = "Lidarr"; return c; }).ToList(); + _trackFiles.Select(c => { c.Quality = new QualityModel(Quality.FLAC); return c; }).ToList(); + + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeTrue(); + } + + [Test] + public void should_return_false_if_is_a_repack_for_some_but_not_all_trackfiles() + { + _parsedAlbumInfo.Quality.Revision.IsRepack = true; + + _trackFiles.Select(c => { c.ReleaseGroup = "Lidarr"; return c; }).ToList(); + _trackFiles.Select(c => { c.Quality = new QualityModel(Quality.FLAC); return c; }).ToList(); + + _trackFiles.First().ReleaseGroup = "NotLidarr"; + + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeFalse(); + } + + + [Test] + public void should_return_false_if_is_a_repack_for_different_group() + { + _parsedAlbumInfo.Quality.Revision.IsRepack = true; + + _trackFiles.Select(c => { c.ReleaseGroup = "NotLidarr"; return c; }).ToList(); + _trackFiles.Select(c => { c.Quality = new QualityModel(Quality.FLAC); return c; }).ToList(); + + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeFalse(); + } + + + [Test] + public void should_return_false_if_release_group_for_existing_file_is_unknown() + { + _parsedAlbumInfo.Quality.Revision.IsRepack = true; + + _trackFiles.Select(c => { c.ReleaseGroup = ""; return c; }).ToList(); + _trackFiles.Select(c => { c.Quality = new QualityModel(Quality.FLAC); return c; }).ToList(); + + + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeFalse(); + } + + [Test] + public void should_return_false_if_release_group_for_release_is_unknown() + { + _parsedAlbumInfo.Quality.Revision.IsRepack = true; + _parsedAlbumInfo.ReleaseGroup = null; + + _trackFiles.Select(c => { c.ReleaseGroup = "Lidarr"; return c; }).ToList(); + _trackFiles.Select(c => { c.Quality = new QualityModel(Quality.FLAC); return c; }).ToList(); + + + var remoteAlbum = Builder.CreateNew() + .With(e => e.ParsedAlbumInfo = _parsedAlbumInfo) + .With(e => e.Albums = _albums) + .Build(); + + Subject.IsSatisfiedBy(remoteAlbum, null) + .Accepted + .Should() + .BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index 5e6b3ae57..38768fbd5 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -35,8 +35,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; - var singleEpisodeList = new List { new Album {}, new Album {} }; - var doubleEpisodeList = new List { new Album {}, new Album {}, new Album {} }; + var singleAlbumList = new List { new Album {}, new Album {} }; + var doubleAlbumList = new List { new Album {}, new Album {}, new Album {} }; var fakeArtist = Builder.CreateNew() @@ -51,14 +51,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync { Artist = fakeArtist, ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, - Albums = doubleEpisodeList + Albums = doubleAlbumList }; _parseResultSingle = new RemoteAlbum { Artist = fakeArtist, ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, - Albums = singleEpisodeList + Albums = singleAlbumList }; } @@ -67,13 +67,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _firstFile.Quality = new QualityModel(Quality.MP3_192); } - private void GivenAutoDownloadPropers() - { - Mocker.GetMock() - .Setup(s => s.AutoDownloadPropers) - .Returns(true); - } - [Test] public void should_return_false_when_trackFile_was_added_more_than_7_days_ago() { @@ -124,6 +117,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_return_false_when_proper_but_auto_download_propers_is_false() { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotUpgrade); + _firstFile.Quality.Quality = Quality.MP3_256; _firstFile.DateAdded = DateTime.Today; @@ -133,7 +130,22 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [Test] public void should_return_true_when_trackFile_was_added_today() { - GivenAutoDownloadPropers(); + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.PreferAndUpgrade); + + _firstFile.Quality.Quality = Quality.MP3_256; + + _firstFile.DateAdded = DateTime.Today; + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_propers_are_not_preferred() + { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotPrefer); _firstFile.Quality.Quality = Quality.MP3_256; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs index 9b0141a22..a560b2bec 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -38,17 +38,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private static readonly int NoPreferredWordScore = 0; - private void GivenAutoDownloadPropers(bool autoDownloadPropers) + private void GivenAutoDownloadPropers(ProperDownloadTypes type) { Mocker.GetMock() - .SetupGet(s => s.AutoDownloadPropers) - .Returns(autoDownloadPropers); + .SetupGet(s => s.DownloadPropersAndRepacks) + .Returns(type); } [Test, TestCaseSource(nameof(IsUpgradeTestCases))] public void IsUpgradeTest(Quality current, int currentVersion, Quality newQuality, int newVersion, Quality cutoff, bool expected) { - GivenAutoDownloadPropers(true); + GivenAutoDownloadPropers(ProperDownloadTypes.PreferAndUpgrade); var profile = new QualityProfile { @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test, TestCaseSource(nameof(IsUpgradeTestCasesLanguages))] public void IsUpgradeTestLanguage(Quality current, int currentVersion, Language currentLanguage, Quality newQuality, int newVersion, Language newLanguage, Quality cutoff, Language languageCutoff, bool expected) { - GivenAutoDownloadPropers(true); + GivenAutoDownloadPropers(ProperDownloadTypes.PreferAndUpgrade); var profile = new QualityProfile { @@ -107,9 +107,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [Test] - public void should_return_false_if_proper_and_autoDownloadPropers_is_false() + public void should_return_true_if_proper_and_download_propers_is_do_not_download() { - GivenAutoDownloadPropers(false); + GivenAutoDownloadPropers(ProperDownloadTypes.DoNotUpgrade); var profile = new QualityProfile { @@ -126,13 +126,41 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsUpgradable( profile, langProfile, - new List { new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + new List { new QualityModel(Quality.MP3_256, new Revision(version: 1)) }, new List { Language.English }, NoPreferredWordScore, - new QualityModel(Quality.MP3_256, new Revision(version: 1)), + new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language.English, NoPreferredWordScore) - .Should().BeFalse(); + .Should().BeTrue(); + } + + [Test] + public void should_return_false_if_proper_and_autoDownloadPropers_is_do_not_prefer() + { + GivenAutoDownloadPropers(ProperDownloadTypes.DoNotPrefer); + + var profile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + var langProfile = new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.English + }; + + Subject.IsUpgradable( + profile, + langProfile, + new List { new QualityModel(Quality.MP3_256, new Revision(version: 1)) }, + new List { Language.English }, + NoPreferredWordScore, + new QualityModel(Quality.MP3_256, new Revision(version: 2)), + Language.English, + NoPreferredWordScore) + .Should().BeFalse(); } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs index b1429b900..7488e91de 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications { @@ -19,6 +20,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications public class UpgradeSpecificationFixture : CoreTest { private Artist _artist; + private Album _album; private LocalTrack _localTrack; [SetUp] @@ -35,12 +37,15 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications Cutoff = Language.Spanish, }).Build(); + _album = Builder.CreateNew().Build(); + _localTrack = new LocalTrack { Path = @"C:\Test\Imagine Dragons\Imagine.Dragons.Song.1.mp3", Quality = new QualityModel(Quality.MP3_256, new Revision(version: 1)), Language = Language.Spanish, - Artist = _artist + Artist = _artist, + Album = _album }; } @@ -215,5 +220,71 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeFalse(); } + + + [Test] + public void should_return_false_if_not_a_revision_upgrade_and_prefers_propers() + { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.PreferAndUpgrade); + + _localTrack.Tracks = Builder.CreateListOfSize(1) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_if_not_a_revision_upgrade_and_does_not_prefer_propers() + { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotPrefer); + + _localTrack.Tracks = Builder.CreateListOfSize(1) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_comparing_to_a_lower_quality_proper() + { + Mocker.GetMock() + .Setup(s => s.DownloadPropersAndRepacks) + .Returns(ProperDownloadTypes.DoNotPrefer); + + _localTrack.Quality = new QualityModel(Quality.FLAC); + + _localTrack.Tracks = Builder.CreateListOfSize(1) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)) + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 25758daba..a4aec11e4 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -103,6 +103,7 @@ + @@ -644,4 +645,4 @@ - + \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 4a915a7f3..747f78b25 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -298,6 +298,16 @@ namespace NzbDrone.Core.Test.ParserTests QualityParser.ParseCodec(null, null).Should().Be(Codec.Unknown); } + [TestCase("Artist Title - Album Title 2017 REPACK FLAC aAF", true)] + [TestCase("Artist Title - Album Title 2017 RERIP FLAC aAF", true)] + [TestCase("Artist Title - Album Title 2017 PROPER FLAC aAF", false)] + public void should_be_able_to_parse_repack(string title, bool isRepack) + { + var result = QualityParser.ParseQuality(title, null, 0); + result.Revision.Version.Should().Be(2); + result.Revision.IsRepack.Should().Be(isRepack); + } + private void ParseAndVerifyQuality(string name, string desc, int bitrate, Quality quality, int sampleSize = 0) { var result = QualityParser.ParseQuality(name, desc, bitrate, sampleSize); diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index bd907f7b7..153f76beb 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Messaging.Events; using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Configuration { @@ -112,11 +113,11 @@ namespace NzbDrone.Core.Configuration set { SetValue("MinimumAge", value); } } - public bool AutoDownloadPropers + public ProperDownloadTypes DownloadPropersAndRepacks { - get { return GetValueBoolean("AutoDownloadPropers", true); } + get { return GetValueEnum("DownloadPropersAndRepacks", ProperDownloadTypes.PreferAndUpgrade); } - set { SetValue("AutoDownloadPropers", value); } + set { SetValue("DownloadPropersAndRepacks", value); } } public bool EnableCompletedDownloadHandling diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 5d32059ea..8232db566 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.MediaFiles; using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Configuration { @@ -24,7 +25,7 @@ namespace NzbDrone.Core.Configuration //Media Management bool AutoUnmonitorPreviouslyDownloadedTracks { get; set; } string RecycleBin { get; set; } - bool AutoDownloadPropers { get; set; } + ProperDownloadTypes DownloadPropersAndRepacks { get; set; } bool CreateEmptyArtistFolders { get; set; } bool DeleteEmptyFolders { get; set; } FileDateType FileDate { get; set; } diff --git a/src/NzbDrone.Core/Datastore/Migration/033_download_propers_config.cs b/src/NzbDrone.Core/Datastore/Migration/033_download_propers_config.cs new file mode 100644 index 000000000..a6acd76f1 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/033_download_propers_config.cs @@ -0,0 +1,43 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(033)] + public class download_propers_config : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(SetConfigValue); + Execute.Sql("DELETE FROM Config WHERE Key = 'autodownloadpropers'"); + } + + private void SetConfigValue(IDbConnection conn, IDbTransaction tran) + { + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT Value FROM Config WHERE Key = 'autodownloadpropers'"; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var value = reader.GetString(0); + var newValue = bool.Parse(value) ? "PreferAndUpgrade" : "DoNotUpgrade"; + + using (var updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "INSERT INTO Config (key, value) VALUES ('downloadpropersandrepacks', ?)"; + updateCmd.AddParameter(newValue); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index df0ddb403..01b26bf95 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -1,20 +1,25 @@ using System; using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine { public class DownloadDecisionComparer : IComparer { + private readonly IConfigService _configService; private readonly IDelayProfileService _delayProfileService; + public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y); public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y); - public DownloadDecisionComparer(IDelayProfileService delayProfileService) + public DownloadDecisionComparer(IConfigService configService, IDelayProfileService delayProfileService) { + _configService = configService; _delayProfileService = delayProfileService; } @@ -57,6 +62,12 @@ namespace NzbDrone.Core.DecisionEngine private int CompareQuality(DownloadDecision x, DownloadDecision y) { + if (_configService.DownloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer) + { + return CompareAll(CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.Artist.QualityProfile.Value.GetIndex(remoteAlbum.ParsedAlbumInfo.Quality.Quality)), + CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.ParsedAlbumInfo.Quality.Revision.Real)); + } + return CompareAll(CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.Artist.QualityProfile.Value.GetIndex(remoteAlbum.ParsedAlbumInfo.Quality.Quality)), CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.ParsedAlbumInfo.Quality.Revision.Real), CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.ParsedAlbumInfo.Quality.Revision.Version)); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index dccb46d94..a8e683e77 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -1,7 +1,7 @@ using System.Linq; using System.Collections.Generic; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Profiles.Delay; -using NzbDrone.Core.Languages; namespace NzbDrone.Core.DecisionEngine { @@ -12,10 +12,12 @@ namespace NzbDrone.Core.DecisionEngine public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision { + private readonly IConfigService _configService; private readonly IDelayProfileService _delayProfileService; - public DownloadDecisionPriorizationService(IDelayProfileService delayProfileService) + public DownloadDecisionPriorizationService(IConfigService configService, IDelayProfileService delayProfileService) { + _configService = configService; _delayProfileService = delayProfileService; } @@ -24,7 +26,7 @@ namespace NzbDrone.Core.DecisionEngine return decisions.Where(c => c.RemoteAlbum.DownloadAllowed) .GroupBy(c => c.RemoteAlbum.Artist.Id, (artistId, downloadDecisions) => { - return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_delayProfileService)); + return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_configService, _delayProfileService)); }) .SelectMany(c => c) .Union(decisions.Where(c => !c.RemoteAlbum.DownloadAllowed)) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RepackSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RepackSpecification.cs new file mode 100644 index 000000000..a0ab0e9f8 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RepackSpecification.cs @@ -0,0 +1,67 @@ +using System; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class RepackSpecification : IDecisionEngineSpecification + { + private readonly IMediaFileService _mediaFileService; + private readonly UpgradableSpecification _upgradableSpecification; + private readonly Logger _logger; + + public RepackSpecification(IMediaFileService mediaFileService, UpgradableSpecification upgradableSpecification, Logger logger) + { + _mediaFileService = mediaFileService; + _upgradableSpecification = upgradableSpecification; + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Database; + public RejectionType Type => RejectionType.Permanent; + + public Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) + { + if (!subject.ParsedAlbumInfo.Quality.Revision.IsRepack) + { + return Decision.Accept(); + } + + foreach (var album in subject.Albums) + { + var releaseGroup = subject.ParsedAlbumInfo.ReleaseGroup; + var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id); + + foreach (var file in trackFiles) + { + if (_upgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedAlbumInfo.Quality)) + { + var fileReleaseGroup = file.ReleaseGroup; + + if (fileReleaseGroup.IsNullOrWhiteSpace()) + { + return Decision.Reject("Unable to determine release group for the existing file"); + } + + if (releaseGroup.IsNullOrWhiteSpace()) + { + return Decision.Reject("Unable to determine release group for this release"); + } + + if (!fileReleaseGroup.Equals(releaseGroup, StringComparison.InvariantCultureIgnoreCase)) + { + _logger.Debug("Release is a repack for a different release group. Release Group: {0}. File release group: {0}", releaseGroup, fileReleaseGroup); + return Decision.Reject("Release is a repack for a different release group. Release Group: {0}. File release group: {0}", releaseGroup, fileReleaseGroup); + } + } + } + + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index ef3cefcd8..b52615a51 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { @@ -33,32 +34,34 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return Decision.Accept(); } + var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; + + if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer) + { + _logger.Debug("Propers are not preferred, skipping check"); + return Decision.Accept(); + } + foreach (var album in subject.Albums) { var trackFiles = _mediaFileService.GetFilesByAlbum(album.Id); - if (trackFiles.Any()) + foreach (var file in trackFiles) { - var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First(); - var dateAdded = trackFiles[0].DateAdded; - - _logger.Debug("Comparing file quality with report. Existing file is {0}", lowestQuality); - - if (_qualityUpgradableSpecification.IsRevisionUpgrade(lowestQuality, subject.ParsedAlbumInfo.Quality)) + if (_qualityUpgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedAlbumInfo.Quality)) { - if (dateAdded < DateTime.Today.AddDays(-7)) + if (downloadPropersAndRepacks == ProperDownloadTypes.DoNotUpgrade) { - _logger.Debug("Proper for old file, rejecting: {0}", subject); - return Decision.Reject("Proper for old file"); + _logger.Debug("Auto downloading of propers is disabled"); + return Decision.Reject("Proper downloading is disabled"); } - if (!_configService.AutoDownloadPropers) + if (file.DateAdded < DateTime.Today.AddDays(-7)) { - _logger.Debug("Auto downloading of propers is disabled"); - return Decision.Reject("Proper downloading is disabled"); + _logger.Debug("Proper for old file, rejecting: {0}", subject); + return Decision.Reject("Proper for old file"); } } - } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index 6867486e2..c5cc7b7bf 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -1,9 +1,11 @@ using NLog; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using System.Collections.Generic; +using System.Linq; namespace NzbDrone.Core.DecisionEngine.Specifications { @@ -19,10 +21,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public class UpgradableSpecification : IUpgradableSpecification { + private readonly IConfigService _configService; private readonly Logger _logger; - public UpgradableSpecification(Logger logger) + public UpgradableSpecification(IConfigService configService, Logger logger) { + _configService = configService; _logger = logger; } @@ -77,6 +81,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (totalCompare == 0) { return ProfileComparisonResult.Equal; } + + // Quality Treated as Equal if Propers are not Prefered + if (_configService.DownloadPropersAndRepacks == ProperDownloadTypes.DoNotPrefer && + newQuality.Revision.CompareTo(currentQualities.Min(q => q.Revision)) > 0) + { + return ProfileComparisonResult.Equal; + } } return ProfileComparisonResult.Upgrade; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs index f17fac034..f8b2d39d7 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs @@ -5,35 +5,50 @@ using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Languages; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications { public class UpgradeSpecification : IImportDecisionEngineSpecification { + private readonly IConfigService _configService; private readonly Logger _logger; - public UpgradeSpecification(Logger logger) + public UpgradeSpecification(IConfigService configService, Logger logger) { + _configService = configService; _logger = logger; } public Decision IsSatisfiedBy(LocalTrack localTrack) { + var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks; var qualityComparer = new QualityModelComparer(localTrack.Artist.QualityProfile); var languageComparer = new LanguageComparer(localTrack.Artist.LanguageProfile); - if (localTrack.Tracks.Any(e => e.TrackFileId != 0 && qualityComparer.Compare(e.TrackFile.Value.Quality, localTrack.Quality) > 0)) + foreach (var track in localTrack.Tracks.Where(e => e.TrackFileId > 0)) { - _logger.Debug("This file isn't a quality upgrade for all tracks. Skipping {0}", localTrack.Path); - return Decision.Reject("Not an upgrade for existing track file(s)"); - } + var trackFile = track.TrackFile.Value; + var qualityCompare = qualityComparer.Compare(localTrack.Quality.Quality, trackFile.Quality.Quality); - if (localTrack.Tracks.Any(e => e.TrackFileId != 0 && - languageComparer.Compare(e.TrackFile.Value.Language, localTrack.Language) > 0 && - qualityComparer.Compare(e.TrackFile.Value.Quality, localTrack.Quality) == 0)) - { - _logger.Debug("This file isn't a language upgrade for all tracks. Skipping {0}", localTrack.Path); - return Decision.Reject("Not an upgrade for existing track file(s)"); + if (qualityCompare < 0) + { + _logger.Debug("This file isn't a quality upgrade for all tracks. Skipping {0}", localTrack.Path); + return Decision.Reject("Not an upgrade for existing track file(s)"); + } + + if (qualityCompare == 0 && downloadPropersAndRepacks != ProperDownloadTypes.DoNotPrefer && + localTrack.Quality.Revision.CompareTo(trackFile.Quality.Revision) < 0) + { + _logger.Debug("This file isn't a quality upgrade for all tracks. Skipping {0}", localTrack.Path); + return Decision.Reject("Not an upgrade for existing track file(s)"); + } + + if (languageComparer.Compare(localTrack.Language, trackFile.Language) < 0 && qualityCompare == 0) + { + _logger.Debug("This file isn't a language upgrade for all tracks. Skipping {0}", localTrack.Path); + return Decision.Reject("Not an upgrade for existing track file(s)"); + } } return Decision.Accept(); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index be93c90ac..3e5a46386 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -173,6 +173,7 @@ + @@ -200,6 +201,7 @@ + @@ -1034,6 +1036,7 @@ + @@ -1343,4 +1346,4 @@ --> - + \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index c165b472b..19a30710c 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -13,7 +13,10 @@ namespace NzbDrone.Core.Parser { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(QualityParser)); - private static readonly Regex ProperRegex = new Regex(@"\b(?proper|repack|rerip)\b", + 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(@"\dv(?\d)\b|\[v(?\d)\]", @@ -286,6 +289,12 @@ namespace NzbDrone.Core.Parser result.Revision.Version = 2; } + if (RepackRegex.IsMatch(normalizedName)) + { + result.Revision.Version = 2; + result.Revision.IsRepack = true; + } + Match versionRegexResult = VersionRegex.Match(normalizedName); if (versionRegexResult.Success) @@ -294,7 +303,6 @@ namespace NzbDrone.Core.Parser } //TODO: re-enable this when we have a reliable way to determine real - //TODO: Only treat it as a real if it comes AFTER the season/epsiode number MatchCollection realRegexResult = RealRegex.Matches(name); if (realRegexResult.Count > 0) diff --git a/src/NzbDrone.Core/Qualities/ProperDownloadTypes.cs b/src/NzbDrone.Core/Qualities/ProperDownloadTypes.cs new file mode 100644 index 000000000..d61101e5a --- /dev/null +++ b/src/NzbDrone.Core/Qualities/ProperDownloadTypes.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Qualities +{ + public enum ProperDownloadTypes + { + PreferAndUpgrade, + DoNotUpgrade, + DoNotPrefer + } +} diff --git a/src/NzbDrone.Core/Qualities/Revision.cs b/src/NzbDrone.Core/Qualities/Revision.cs index 7ec095cda..c0477eb45 100644 --- a/src/NzbDrone.Core/Qualities/Revision.cs +++ b/src/NzbDrone.Core/Qualities/Revision.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; namespace NzbDrone.Core.Qualities @@ -9,14 +9,16 @@ namespace NzbDrone.Core.Qualities { } - public Revision(int version = 1, int real = 0) + public Revision(int version = 1, int real = 0, bool isRepack = false) { Version = version; Real = real; + IsRepack = isRepack; } public int Version { get; set; } public int Real { get; set; } + public bool IsRepack { get; set; } public bool Equals(Revision other) {