diff --git a/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs b/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs index d30e91f8d..8ca03f86b 100644 --- a/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs +++ b/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs @@ -6,9 +6,11 @@ using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Core.Jobs; +using NzbDrone.Core.Model; using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers; using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common.AutoMoq; @@ -39,10 +41,16 @@ namespace NzbDrone.Core.Test.JobTests [Test] public void SeasonSearch_partial_season_success() { + var resultItems = Builder.CreateListOfSize(5) + .All() + .With(e => e.SearchError = ReportRejectionType.None) + .With(e => e.Success = true) + .Build(); + var episodes = Builder.CreateListOfSize(5) .All() - .With(e => e.SeriesId = 1) .With(e => e.SeasonNumber = 1) + .With(e => e.SeriesId = 5) .Build(); var notification = new ProgressNotification("Season Search"); @@ -88,7 +96,7 @@ namespace NzbDrone.Core.Test.JobTests Mocker.GetMock() .Setup(c => c.PartialSeasonSearch(notification, 1, 1)) - .Returns(new List{1}); + .Returns(new List()); //Act Mocker.Resolve().Start(notification, 1, 1); diff --git a/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs b/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs index c33f6c89c..17ff465c9 100644 --- a/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs @@ -88,42 +88,42 @@ namespace NzbDrone.Core.Test.ProviderTests.DecisionEngineTests [Test] public void should_be_allowed_if_all_conditions_are_met() { - spec.IsSatisfiedBy(parseResult).Should().BeTrue(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.None); } [Test] public void should_not_be_allowed_if_profile_is_not_allowed() { WithProfileNotAllowed(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.QualityNotWanted); } [Test] public void should_not_be_allowed_if_size_is_not_allowed() { WithNotAcceptableSize(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.Size); } [Test] public void should_not_be_allowed_if_disk_is_not_upgrade() { WithNoDiskUpgrade(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.ExistingQualityIsEqualOrBetter); } [Test] public void should_not_be_allowed_if_episode_is_already_in_queue() { WithEpisodeAlreadyInQueue(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.AlreadyInQueue); } [Test] public void should_not_be_allowed_if_report_is_over_retention() { WithOverRetention(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.Retention); } [Test] @@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DecisionEngineTests WithProfileNotAllowed(); WithOverRetention(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.QualityNotWanted); } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs index 404f8acd0..5c1f6dfb3 100644 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs @@ -77,14 +77,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(true); + .Returns(ReportRejectionType.None); } private void WithQualityNotNeeded() { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(false); + .Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter); } [Test] @@ -103,13 +103,13 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.Is(d => d.Quality.QualityType == QualityTypes.Bluray1080p))) - .Returns(true); + .Returns(ReportRejectionType.None); //Act var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeTrue(); + result.Should().Contain(n => n.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Exactly(5)); @@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -198,7 +198,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeTrue(); + result.Should().Contain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Once()); @@ -230,7 +230,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeTrue(); + result.Should().Contain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Exactly(2)); @@ -250,7 +250,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -270,7 +270,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs index 40d79bce6..145c8301a 100644 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Providers; using NzbDrone.Core.Providers.DecisionEngine; using NzbDrone.Core.Repository; using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Repository.Search; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests @@ -73,14 +74,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(true); + .Returns(ReportRejectionType.None); } private void WithQualityNotNeeded() { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(false); + .Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter); } [Test] @@ -102,14 +103,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.Is(d => d.Quality.QualityType == QualityTypes.Bluray1080p))) - .Returns(true); + .Returns(ReportRejectionType.None); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(1); - result.First().Should().Be(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -135,13 +136,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithSuccessfulDownload(); Mocker.GetMock() - .Setup(s => s.IsSatisfiedBy(It.IsAny())).Returns(true); + .Setup(s => s.IsSatisfiedBy(It.IsAny())).Returns(ReportRejectionType.None); //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, new SearchHistory(), _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.Is(d => d.Age != 100)), Times.Never()); @@ -162,10 +164,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithQualityNotNeeded(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Exactly(5)); @@ -185,10 +188,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithNullSeries(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -206,10 +210,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithMisMatchedSeries(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -227,10 +232,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithMatchingSeries(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -248,10 +254,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithMatchingSeries(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -274,10 +281,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithSuccessfulDownload(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1); //Assert - result.Should().HaveCount(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Once()); @@ -307,10 +315,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .Returns(true); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1); + var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1); //Assert - result.Should().HaveCount(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Exactly(2)); diff --git a/NzbDrone.Core/Datastore/Migrations/Migration20120420.cs b/NzbDrone.Core/Datastore/Migrations/Migration20120420.cs new file mode 100644 index 000000000..839d416f0 --- /dev/null +++ b/NzbDrone.Core/Datastore/Migrations/Migration20120420.cs @@ -0,0 +1,39 @@ +using System.Data; +using Migrator.Framework; + +namespace NzbDrone.Core.Datastore.Migrations +{ + [Migration(20120420)] + public class Migration20120420 : NzbDroneMigration + { + protected override void MainDbUpgrade() + { + Database.AddTable("SearchHistory", new[] + { + new Column("Id", DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), + new Column("SeriesId", DbType.Int32, ColumnProperty.NotNull), + new Column("SeasonNumber", DbType.Int32, ColumnProperty.Null), + new Column("EpisodeId", DbType.Int32, ColumnProperty.Null), + new Column("SearchTime", DbType.DateTime, ColumnProperty.NotNull), + new Column("SuccessfulDownload", DbType.Boolean, ColumnProperty.NotNull) + }); + + Database.AddTable("SearchHistoryItems", new[] + { + new Column("Id", DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), + new Column("SearchHistoryId", DbType.Int32, ColumnProperty.NotNull), + new Column("ReportTitle", DbType.String, ColumnProperty.NotNull), + new Column("Indexer", DbType.String, ColumnProperty.NotNull), + new Column("NzbUrl", DbType.String, ColumnProperty.NotNull), + new Column("NzbInfoUrl", DbType.String, ColumnProperty.Null), + new Column("Success", DbType.Boolean, ColumnProperty.NotNull), + new Column("SearchError", DbType.Int32, ColumnProperty.NotNull), + new Column("Quality", DbType.Int32, ColumnProperty.NotNull), + new Column("Proper", DbType.Boolean, ColumnProperty.NotNull), + new Column("Age", DbType.Int32, ColumnProperty.NotNull), + new Column("Language", DbType.Int32, ColumnProperty.NotNull), + new Column("Size", DbType.Int64, ColumnProperty.NotNull), + }); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Fluent.cs b/NzbDrone.Core/Fluent.cs index f27ec5b76..a66ccbe20 100644 --- a/NzbDrone.Core/Fluent.cs +++ b/NzbDrone.Core/Fluent.cs @@ -87,5 +87,60 @@ namespace NzbDrone.Core return s.Substring(0, i); } + public static string AddSpacesToEnum(this Enum enumValue) + { + var text = enumValue.ToString(); + + if (string.IsNullOrWhiteSpace(text)) + return ""; + var newText = new StringBuilder(text.Length * 2); + newText.Append(text[0]); + for (int i = 1; i < text.Length; i++) + { + if (char.IsUpper(text[i]) && text[i - 1] != ' ') + newText.Append(' '); + newText.Append(text[i]); + } + return newText.ToString(); + } + + private const Decimal ONE_KILOBYTE = 1024M; + private const Decimal ONE_MEGABYTE = ONE_KILOBYTE * 1024M; + private const Decimal ONE_GIGABYTE = ONE_MEGABYTE * 1024M; + + public static string ToBestFileSize(this long bytes, int precision = 0) + { + if (bytes == 0) + return "0B"; + + decimal size = Convert.ToDecimal(bytes); + + string suffix; + + if (size > ONE_GIGABYTE) + { + size /= ONE_GIGABYTE; + suffix = "GB"; + } + + else if (size > ONE_MEGABYTE) + { + size /= ONE_MEGABYTE; + suffix = "MB"; + } + + else if (size > ONE_KILOBYTE) + { + size /= ONE_KILOBYTE; + suffix = "KB"; + } + + else + { + suffix = " B"; + } + + return String.Format("{0:N" + precision + "} {1}", size, suffix); + } } } diff --git a/NzbDrone.Core/Helpers/FileSizeFormatHelper.cs b/NzbDrone.Core/Helpers/FileSizeFormatHelper.cs deleted file mode 100644 index 515543fc8..000000000 --- a/NzbDrone.Core/Helpers/FileSizeFormatHelper.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace NzbDrone.Core.Helpers -{ - public static class FileSizeFormatHelper - { - private const Decimal OneKiloByte = 1024M; - private const Decimal OneMegaByte = OneKiloByte * 1024M; - private const Decimal OneGigaByte = OneMegaByte * 1024M; - - public static string Format(long bytes, int precision = 0) - { - if (bytes == 0) - return "0B"; - - decimal size = Convert.ToDecimal(bytes); - - string suffix; - - if (size > OneGigaByte) - { - size /= OneGigaByte; - suffix = "GB"; - } - - else if (size > OneMegaByte) - { - size /= OneMegaByte; - suffix = "MB"; - } - - else if (size > OneKiloByte) - { - size /= OneKiloByte; - suffix = "KB"; - } - - else - { - suffix = " B"; - } - - return String.Format("{0:N" + precision + "}{1}", size, suffix); - } - } -} diff --git a/NzbDrone.Core/Jobs/RssSyncJob.cs b/NzbDrone.Core/Jobs/RssSyncJob.cs index fc10aafd5..e663dffee 100644 --- a/NzbDrone.Core/Jobs/RssSyncJob.cs +++ b/NzbDrone.Core/Jobs/RssSyncJob.cs @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Jobs try { if (_isMonitoredEpisodeSpecification.IsSatisfiedBy(episodeParseResult) && - _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult) && + _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult) == ReportRejectionType.None && _upgradeHistorySpecification.IsSatisfiedBy(episodeParseResult)) { _downloadProvider.DownloadReport(episodeParseResult); diff --git a/NzbDrone.Core/Jobs/SeasonSearchJob.cs b/NzbDrone.Core/Jobs/SeasonSearchJob.cs index d30a9396e..1ac305bd7 100644 --- a/NzbDrone.Core/Jobs/SeasonSearchJob.cs +++ b/NzbDrone.Core/Jobs/SeasonSearchJob.cs @@ -60,15 +60,15 @@ namespace NzbDrone.Core.Jobs //Perform a Partial Season Search var addedSeries = _searchProvider.PartialSeasonSearch(notification, targetId, secondaryTargetId); - addedSeries.Distinct().ToList().Sort(); - var episodeNumbers = episodes.Where(w => w.AirDate <= DateTime.Today.AddDays(1)).Select(s => s.EpisodeNumber).ToList(); - episodeNumbers.Sort(); + //addedSeries.Distinct().ToList().Sort(); + //var episodeNumbers = episodes.Where(w => w.AirDate <= DateTime.Today.AddDays(1)).Select(s => s.EpisodeNumber).ToList(); + //episodeNumbers.Sort(); - if (addedSeries.SequenceEqual(episodeNumbers)) - return; + //if (addedSeries.SequenceEqual(episodeNumbers)) + // return; - //Get the list of episodes that weren't downloaded - var missingEpisodes = episodeNumbers.Except(addedSeries).ToList(); + ////Get the list of episodes that weren't downloaded + //var missingEpisodes = episodeNumbers.Except(addedSeries).ToList(); //TODO: do one by one check only when max number of feeds have been returned by the indexer //Only process episodes that is in missing episodes (To ensure we double check if the episode is available) diff --git a/NzbDrone.Core/Model/ReportRejectionType.cs b/NzbDrone.Core/Model/ReportRejectionType.cs new file mode 100644 index 000000000..867f37565 --- /dev/null +++ b/NzbDrone.Core/Model/ReportRejectionType.cs @@ -0,0 +1,21 @@ +using System.Linq; + +namespace NzbDrone.Core.Model +{ + public enum ReportRejectionType + { + None = 0, + WrongSeries = 1, + QualityNotWanted = 2, + WrongSeason = 3, + WrongEpisode = 4, + Size = 5, + Retention = 6, + ExistingQualityIsEqualOrBetter = 7, + Cutoff = 8, + AlreadyInQueue = 9, + DownloadClientFailure = 10, + Skipped = 11, + Failure = 12, + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index a4d858652..aa0f193df 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -222,6 +222,7 @@ + @@ -239,7 +240,6 @@ - @@ -277,6 +277,7 @@ + @@ -304,6 +305,9 @@ Code + + + Code diff --git a/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs b/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs index 7321c6a4c..c6e437c5f 100644 --- a/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs +++ b/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs @@ -2,6 +2,7 @@ using NLog; using Ninject; using NzbDrone.Core.Model; +using NzbDrone.Core.Repository.Search; namespace NzbDrone.Core.Providers.DecisionEngine { @@ -30,16 +31,16 @@ namespace NzbDrone.Core.Providers.DecisionEngine { } - public virtual bool IsSatisfiedBy(EpisodeParseResult subject) + public virtual ReportRejectionType IsSatisfiedBy(EpisodeParseResult subject) { - if (!_qualityAllowedByProfileSpecification.IsSatisfiedBy(subject)) return false; - if (!_upgradeDiskSpecification.IsSatisfiedBy(subject)) return false; - if (!_retentionSpecification.IsSatisfiedBy(subject)) return false; - if (!_acceptableSizeSpecification.IsSatisfiedBy(subject)) return false; - if (_alreadyInQueueSpecification.IsSatisfiedBy(subject)) return false; + if (!_qualityAllowedByProfileSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.QualityNotWanted; + if (!_upgradeDiskSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.ExistingQualityIsEqualOrBetter; + if (!_retentionSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.Retention; + if (!_acceptableSizeSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.Size; + if (_alreadyInQueueSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.AlreadyInQueue; logger.Debug("Episode {0} is needed", subject); - return true; + return ReportRejectionType.None; } } } \ No newline at end of file diff --git a/NzbDrone.Core/Providers/DownloadProvider.cs b/NzbDrone.Core/Providers/DownloadProvider.cs index f9473c0bc..521491f4f 100644 --- a/NzbDrone.Core/Providers/DownloadProvider.cs +++ b/NzbDrone.Core/Providers/DownloadProvider.cs @@ -124,7 +124,7 @@ namespace NzbDrone.Core.Providers foreach (var episode in parseResult.EpisodeNumbers) { - episodeString.Add(String.Format("{0}x{1}", parseResult.SeasonNumber, episode)); + episodeString.Add(String.Format("{0}x{1:00}", parseResult.SeasonNumber, episode)); } var epNumberString = String.Join("-", episodeString); diff --git a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs index efda27500..d105e7a3e 100644 --- a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs +++ b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs @@ -192,7 +192,6 @@ namespace NzbDrone.Core.Providers.Indexer { parsedEpisode.NzbUrl = NzbDownloadUrl(item); parsedEpisode.Indexer = Name; - parsedEpisode.OriginalString = item.Title.Text; result.Add(parsedEpisode); } } @@ -237,6 +236,7 @@ namespace NzbDrone.Core.Providers.Indexer var title = TitlePreParser(item); var episodeParseResult = Parser.ParseTitle(title); + episodeParseResult.OriginalString = title; if (episodeParseResult != null) episodeParseResult.Age = DateTime.Now.Date.Subtract(item.PublishDate.Date).Days; _logger.Trace("Parsed: {0} from: {1}", episodeParseResult, item.Title.Text); diff --git a/NzbDrone.Core/Providers/SearchHistoryProvider.cs b/NzbDrone.Core/Providers/SearchHistoryProvider.cs new file mode 100644 index 000000000..3432cf273 --- /dev/null +++ b/NzbDrone.Core/Providers/SearchHistoryProvider.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using Ninject; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; +using PetaPoco; + +namespace NzbDrone.Core.Providers +{ + public class SearchHistoryProvider + { + private readonly IDatabase _database; + private readonly SeriesProvider _seriesProvider; + private readonly DownloadProvider _downloadProvider; + private readonly EpisodeProvider _episodeProvider; + + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + [Inject] + public SearchHistoryProvider(IDatabase database, SeriesProvider seriesProvider, + DownloadProvider downloadProvider, EpisodeProvider episodeProvider) + { + _database = database; + _seriesProvider = seriesProvider; + _downloadProvider = downloadProvider; + _episodeProvider = episodeProvider; + } + + public SearchHistoryProvider() + { + + } + + public virtual void Add(SearchHistory searchResult) + { + logger.Trace("Adding new search result"); + searchResult.SuccessfulDownload = searchResult.SearchHistoryItems.Any(s => s.Success); + var id = Convert.ToInt32(_database.Insert(searchResult)); + + searchResult.SearchHistoryItems.ForEach(s => s.SearchHistoryId = id); + logger.Trace("Adding search result items"); + _database.InsertMany(searchResult.SearchHistoryItems); + } + + public virtual void Delete(int id) + { + logger.Trace("Deleting search result items attached to: {0}", id); + _database.Execute("DELETE FROM SearchHistoryItems WHERE SearchHistoryId = @0", id); + + logger.Trace("Deleting search result: {0}", id); + _database.Delete(id); + } + + public virtual List AllSearchHistory() + { + var sql = @"SELECT SearchHistory.Id, SearchHistory.SeriesId, SearchHistory.SeasonNumber, + SearchHistory.EpisodeId, SearchHistory.SearchTime, + Series.Title as SeriesTitle, Series.IsDaily, + Episodes.EpisodeNumber, Episodes.SeasonNumber, Episodes.Title as EpisodeTitle, + Episodes.AirDate, + Count(SearchHistoryItems.Id) as TotalItems, + SUM(CASE WHEN SearchHistoryItems.Success = 1 THEN 1 ELSE 0 END) as SuccessfulCount + FROM SearchHistory + INNER JOIN Series + ON Series.SeriesId = SearchHistory.SeriesId + LEFT JOIN Episodes + ON Episodes.EpisodeId = SearchHistory.EpisodeId + INNER JOIN SearchHistoryItems + ON SearchHistoryItems.SearchHistoryId = SearchHistory.Id + GROUP BY SearchHistory.Id, SearchHistory.SeriesId, SearchHistory.SeasonNumber, + SearchHistory.EpisodeId, SearchHistory.SearchTime, + Series.Title, Series.IsDaily, + Episodes.EpisodeNumber, Episodes.SeasonNumber, Episodes.Title, + Episodes.AirDate"; + + return _database.Fetch(sql); + } + + public virtual SearchHistory GetSearchHistory(int id) + { + var sql = @"SELECT SearchHistory.Id, SearchHistory.SeriesId, SearchHistory.SeasonNumber, + SearchHistory.EpisodeId, SearchHistory.SearchTime, + Series.Title as SeriesTitle, Series.IsDaily, + Episodes.EpisodeNumber, Episodes.SeasonNumber, Episodes.Title as EpisodeTitle, + Episodes.AirDate + FROM SearchHistory + INNER JOIN Series + ON Series.SeriesId = SearchHistory.SeriesId + LEFT JOIN Episodes + ON Episodes.EpisodeId = SearchHistory.EpisodeId + WHERE SearchHistory.Id = @0"; + + var result = _database.Single(sql, id); + result.SearchHistoryItems = _database.Fetch("WHERE SearchHistoryId = @0", id); + + return result; + } + + public virtual void ForceDownload(int itemId) + { + var item = _database.Single(itemId); + var searchResult = _database.Single(item.SearchHistoryId); + var series = _seriesProvider.GetSeries(searchResult.SeriesId); + + var parseResult = Parser.ParseTitle(item.ReportTitle); + parseResult.NzbUrl = item.NzbUrl; + parseResult.Series = series; + parseResult.Indexer = item.Indexer; + var episodes = _episodeProvider.GetEpisodesByParseResult(parseResult); + + _downloadProvider.DownloadReport(parseResult); + } + } +} diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs index 1e4ea8a5f..036411ecd 100644 --- a/NzbDrone.Core/Providers/SearchProvider.cs +++ b/NzbDrone.Core/Providers/SearchProvider.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Model; using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers.DecisionEngine; using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; namespace NzbDrone.Core.Providers { @@ -21,13 +22,15 @@ namespace NzbDrone.Core.Providers private readonly SceneMappingProvider _sceneMappingProvider; private readonly UpgradePossibleSpecification _upgradePossibleSpecification; private readonly AllowedDownloadSpecification _allowedDownloadSpecification; + private readonly SearchHistoryProvider _searchHistoryProvider; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); [Inject] public SearchProvider(EpisodeProvider episodeProvider, DownloadProvider downloadProvider, SeriesProvider seriesProvider, IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, - UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification) + UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification, + SearchHistoryProvider searchHistoryProvider) { _episodeProvider = episodeProvider; _downloadProvider = downloadProvider; @@ -36,6 +39,7 @@ namespace NzbDrone.Core.Providers _sceneMappingProvider = sceneMappingProvider; _upgradePossibleSpecification = upgradePossibleSpecification; _allowedDownloadSpecification = allowedDownloadSpecification; + _searchHistoryProvider = searchHistoryProvider; } public SearchProvider() @@ -44,13 +48,20 @@ namespace NzbDrone.Core.Providers public virtual bool SeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) { + var searchResult = new SearchHistory + { + SearchTime = DateTime.Now, + SeriesId = seriesId, + SeasonNumber = seasonNumber + }; + var series = _seriesProvider.GetSeries(seriesId); if (series == null) { Logger.Error("Unable to find an series {0} in database", seriesId); return false; - } + } //Return false if the series is a daily series (we only support individual episode searching if (series.IsDaily) @@ -80,19 +91,20 @@ namespace NzbDrone.Core.Providers e => e.EpisodeNumbers = episodeNumbers.ToList() ); - var downloadedEpisodes = ProcessSearchResults(notification, reports, series, seasonNumber); + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber); + _searchHistoryProvider.Add(searchResult); - downloadedEpisodes.Sort(); - episodeNumbers.ToList().Sort(); - - //Returns true if the list of downloaded episodes matches the list of episode numbers - //(either a full season release was grabbed or all individual episodes) - return (downloadedEpisodes.SequenceEqual(episodeNumbers)); + return (searchResult.Successes.Count == episodeNumbers.Count); } public virtual List PartialSeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) { - //This method will search for episodes in a season in groups of 10 episodes S01E0, S01E1, S01E2, etc + var searchResult = new SearchHistory + { + SearchTime = DateTime.Now, + SeriesId = seriesId, + SeasonNumber = seasonNumber + }; var series = _seriesProvider.GetSeries(seriesId); @@ -107,19 +119,18 @@ namespace NzbDrone.Core.Providers return new List(); notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); - var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); - var reports = PerformSearch(notification, series, seasonNumber, episodes); - Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); if (reports.Count == 0) return new List(); notification.CurrentMessage = "Processing search results"; + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber); - return ProcessSearchResults(notification, reports, series, seasonNumber); + _searchHistoryProvider.Add(searchResult); + return searchResult.Successes; } public virtual bool EpisodeSearch(ProgressNotification notification, int episodeId) @@ -136,7 +147,7 @@ namespace NzbDrone.Core.Providers if (!_upgradePossibleSpecification.IsSatisfiedBy(episode)) { Logger.Info("Search for {0} was aborted, file in disk meets or exceeds Profile's Cutoff", episode); - notification.CurrentMessage = String.Format("Skipping search for {0}, file you have is already at cutoff", episode); + notification.CurrentMessage = String.Format("Skipping search for {0}, the file you have is already at cutoff", episode); return false; } @@ -145,19 +156,39 @@ namespace NzbDrone.Core.Providers if (episode.Series.IsDaily && !episode.AirDate.HasValue) { Logger.Warn("AirDate is not Valid for: {0}", episode); + notification.CurrentMessage = String.Format("Search for {0} Failed, AirDate is invalid", episode); return false; } + var searchResult = new SearchHistory + { + SearchTime = DateTime.Now, + SeriesId = episode.Series.SeriesId + }; + var reports = PerformSearch(notification, episode.Series, episode.SeasonNumber, new List { episode }); Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); notification.CurrentMessage = "Processing search results"; - if (!episode.Series.IsDaily && ProcessSearchResults(notification, reports, episode.Series, episode.SeasonNumber, episode.EpisodeNumber).Count == 1) - return true; + if (episode.Series.IsDaily) + { + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, episode.Series, episode.AirDate.Value); + _searchHistoryProvider.Add(searchResult); - if (episode.Series.IsDaily && ProcessSearchResults(notification, reports, episode.Series, episode.AirDate.Value)) + if (searchResult.SearchHistoryItems.Any(r => r.Success)) + return true; + } + + else + { + searchResult.EpisodeId = episodeId; + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, episode.Series, episode.SeasonNumber, episode.EpisodeNumber); + _searchHistoryProvider.Add(searchResult); + + if (searchResult.SearchHistoryItems.Any(r => r.Success)) return true; + } Logger.Warn("Unable to find {0} in any of indexers.", episode); @@ -170,7 +201,6 @@ namespace NzbDrone.Core.Providers notification.CurrentMessage = String.Format("Sorry, couldn't find you {0} in any of indexers.", episode); } - return false; } @@ -227,9 +257,10 @@ namespace NzbDrone.Core.Providers return reports; } - public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, int seasonNumber, int? episodeNumber = null) + public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, SearchHistory searchResult, Series series, int seasonNumber, int? episodeNumber = null) { var successes = new List(); + var items = new List(); foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality).ThenBy(c => c.Age)) { @@ -237,6 +268,20 @@ namespace NzbDrone.Core.Providers { Logger.Trace("Analysing report " + episodeParseResult); + var item = new SearchHistoryItem + { + ReportTitle = episodeParseResult.OriginalString, + NzbUrl = episodeParseResult.NzbUrl, + Indexer = episodeParseResult.Indexer, + Quality = episodeParseResult.Quality.QualityType, + Proper = episodeParseResult.Quality.Proper, + Size = episodeParseResult.Size, + Age = episodeParseResult.Age, + Language = episodeParseResult.Language + }; + + items.Add(item); + //Get the matching series episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); @@ -244,6 +289,7 @@ namespace NzbDrone.Core.Providers if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) { Logger.Trace("Unexpected series for search: {0}. Skipping.", episodeParseResult.CleanTitle); + item.SearchError = ReportRejectionType.WrongSeries; continue; } @@ -251,6 +297,7 @@ namespace NzbDrone.Core.Providers if (episodeParseResult.SeasonNumber != seasonNumber) { Logger.Trace("Season number does not match searched season number, skipping."); + item.SearchError = ReportRejectionType.WrongSeason; continue; } @@ -258,6 +305,7 @@ namespace NzbDrone.Core.Providers if (episodeNumber.HasValue && !episodeParseResult.EpisodeNumbers.Contains(episodeNumber.Value)) { Logger.Trace("Searched episode number is not contained in post, skipping."); + item.SearchError = ReportRejectionType.WrongEpisode; continue; } @@ -265,10 +313,12 @@ namespace NzbDrone.Core.Providers if (successes.Intersect(episodeParseResult.EpisodeNumbers).Any()) { Logger.Trace("Episode has already been downloaded in this search, skipping."); + item.SearchError = ReportRejectionType.Skipped; continue; } - if (_allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult)) + item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); + if (item.SearchError == ReportRejectionType.None) { Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); try @@ -279,12 +329,18 @@ namespace NzbDrone.Core.Providers //Add the list of episode numbers from this release successes.AddRange(episodeParseResult.EpisodeNumbers); + item.Success = true; + } + else + { + item.SearchError = ReportRejectionType.DownloadClientFailure; } } catch (Exception e) { Logger.ErrorException("Unable to add report to download queue." + episodeParseResult, e); notification.CurrentMessage = String.Format("Unable to add report to download queue. {0}", episodeParseResult); + item.SearchError = ReportRejectionType.DownloadClientFailure; } } } @@ -294,15 +350,38 @@ namespace NzbDrone.Core.Providers } } - return successes; + return items; } - public bool ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, DateTime airDate) + public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, DateTime airDate) { + var items = new List(); + var skip = false; + foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) { try { + var item = new SearchHistoryItem + { + ReportTitle = episodeParseResult.OriginalString, + NzbUrl = episodeParseResult.NzbUrl, + Indexer = episodeParseResult.Indexer, + Quality = episodeParseResult.Quality.QualityType, + Proper = episodeParseResult.Quality.Proper, + Size = episodeParseResult.Size, + Age = episodeParseResult.Age, + Language = episodeParseResult.Language + }; + + items.Add(item); + + if (skip) + { + item.SearchError = ReportRejectionType.Skipped; + continue; + } + Logger.Trace("Analysing report " + episodeParseResult); //Get the matching series @@ -310,13 +389,20 @@ namespace NzbDrone.Core.Providers //If series is null or doesn't match the series we're looking for return if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) + { + item.SearchError = ReportRejectionType.WrongSeries; continue; + } //If parse result doesn't have an air date or it doesn't match passed in airdate, skip the report. if (!episodeParseResult.AirDate.HasValue || episodeParseResult.AirDate.Value.Date != airDate.Date) + { + item.SearchError = ReportRejectionType.WrongEpisode; continue; + } - if (_allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult)) + item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); + if (item.SearchError == ReportRejectionType.None) { Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); try @@ -327,13 +413,19 @@ namespace NzbDrone.Core.Providers String.Format("{0} - {1} {2} Added to download queue", episodeParseResult.Series.Title, episodeParseResult.AirDate.Value.ToShortDateString(), episodeParseResult.Quality); - return true; + item.Success = true; + skip = true; + } + else + { + item.SearchError = ReportRejectionType.DownloadClientFailure; } } catch (Exception e) { Logger.ErrorException("Unable to add report to download queue." + episodeParseResult, e); notification.CurrentMessage = String.Format("Unable to add report to download queue. {0}", episodeParseResult); + item.SearchError = ReportRejectionType.DownloadClientFailure; } } } @@ -342,7 +434,8 @@ namespace NzbDrone.Core.Providers Logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); } } - return false; + + return items; } private List GetEpisodeNumberPrefixes(IEnumerable episodeNumbers) diff --git a/NzbDrone.Core/Repository/Search/SearchHistory.cs b/NzbDrone.Core/Repository/Search/SearchHistory.cs new file mode 100644 index 000000000..ce4c831aa --- /dev/null +++ b/NzbDrone.Core/Repository/Search/SearchHistory.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using NzbDrone.Core.Model; +using NzbDrone.Core.Repository.Quality; +using PetaPoco; + +namespace NzbDrone.Core.Repository.Search +{ + [PrimaryKey("Id", autoIncrement = true)] + [TableName("SearchHistory")] + public class SearchHistory + { + public int Id { get; set; } + public int SeriesId { get; set; } + public int? SeasonNumber { get; set; } + public int? EpisodeId { get; set; } + public DateTime SearchTime { get; set; } + public bool SuccessfulDownload { get; set; } + + [ResultColumn] + public List SearchHistoryItems { get; set; } + + [Ignore] + public List Successes { get; set; } + + [ResultColumn] + public string SeriesTitle { get; set; } + + [ResultColumn] + public bool IsDaily { get; set; } + + [ResultColumn] + public int? EpisodeNumber { get; set; } + + [ResultColumn] + public string EpisodeTitle { get; set; } + + [ResultColumn] + public DateTime AirDate { get; set; } + + [ResultColumn] + public int TotalItems { get; set; } + + [ResultColumn] + public int SuccessfulCount { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Repository/Search/SearchHistoryItem.cs b/NzbDrone.Core/Repository/Search/SearchHistoryItem.cs new file mode 100644 index 000000000..41ad06665 --- /dev/null +++ b/NzbDrone.Core/Repository/Search/SearchHistoryItem.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using NzbDrone.Core.Model; +using NzbDrone.Core.Repository.Quality; +using PetaPoco; + +namespace NzbDrone.Core.Repository.Search +{ + [PrimaryKey("Id", autoIncrement = true)] + [TableName("SearchHistoryItems")] + public class SearchHistoryItem + { + public int Id { get; set; } + public int SearchHistoryId { get; set; } + public string ReportTitle { get; set; } + public string Indexer { get; set; } + public string NzbUrl { get; set; } + public string NzbInfoUrl { get; set; } + public bool Success { get; set; } + public ReportRejectionType SearchError { get; set; } + public QualityTypes Quality { get; set; } + public bool Proper { get; set; } + public int Age { get; set; } + public LanguageType Language { get; set; } + public long Size { get; set; } + + public override string ToString() + { + return String.Format("{0} - {1} - {2}", ReportTitle, Quality, SearchError); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Web/Controllers/LogController.cs b/NzbDrone.Web/Controllers/LogController.cs index 60f9668de..6df8bd2ad 100644 --- a/NzbDrone.Web/Controllers/LogController.cs +++ b/NzbDrone.Web/Controllers/LogController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Linq.Dynamic; using System.Text; using System.Web.Mvc; +using DataTables.Mvc.Core; using DataTables.Mvc.Core.Models; using NzbDrone.Common; using NzbDrone.Core.Instrumentation; @@ -17,7 +18,8 @@ namespace NzbDrone.Web.Controllers private readonly EnvironmentProvider _environmentProvider; private readonly DiskProvider _diskProvider; - public LogController(LogProvider logProvider, EnvironmentProvider environmentProvider, DiskProvider diskProvider) + public LogController(LogProvider logProvider, EnvironmentProvider environmentProvider, + DiskProvider diskProvider) { _logProvider = logProvider; _environmentProvider = environmentProvider; diff --git a/NzbDrone.Web/Controllers/SearchHistoryController.cs b/NzbDrone.Web/Controllers/SearchHistoryController.cs new file mode 100644 index 000000000..96c696e33 --- /dev/null +++ b/NzbDrone.Web/Controllers/SearchHistoryController.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using NzbDrone.Core; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Repository.Search; +using NzbDrone.Web.Models; + +namespace NzbDrone.Web.Controllers +{ + public class SearchHistoryController : Controller + { + private readonly SearchHistoryProvider _searchHistoryProvider; + + public SearchHistoryController(SearchHistoryProvider searchHistoryProvider) + { + _searchHistoryProvider = searchHistoryProvider; + } + + public ActionResult Index() + { + var results = _searchHistoryProvider.AllSearchHistory(); + + var model = results.Select(s => new SearchHistoryModel + { + Id = s.Id, + SearchTime = s.SearchTime.ToString(), + DisplayName = GetDisplayName(s), + ReportCount = s.TotalItems, + Successful = s.SuccessfulCount > 0 + }); + + return View(model); + } + + public ActionResult Details(int searchId) + { + var searchResult = _searchHistoryProvider.GetSearchHistory(searchId); + var model = new SearchDetailsModel + { + Id = searchResult.Id, + DisplayName = GetDisplayName(searchResult), + SearchHistoryItems = + searchResult.SearchHistoryItems.Select(s => new SearchItemModel + { + Id = s.Id, + ReportTitle = s.ReportTitle, + Indexer = s.Indexer, + NzbUrl = s.NzbUrl, + NzbInfoUrl = s.NzbInfoUrl, + Success = s.Success, + SearchError = s.SearchError.AddSpacesToEnum().Replace("None", "Grabbed"), + Quality = s.Quality.ToString(), + QualityInt = (int)s.Quality, + Proper = s.Proper, + Age = s.Age, + Size = s.Size.ToBestFileSize(1), + Language = s.Language.ToString() + }).ToList() + }; + + return View(model); + } + + public JsonResult ForceDownload(int id) + { + _searchHistoryProvider.ForceDownload(id); + + return new JsonResult { Data = "ok", JsonRequestBehavior = JsonRequestBehavior.AllowGet }; + } + + public string GetDisplayName(SearchHistory searchResult) + { + if (!searchResult.EpisodeNumber.HasValue) + { + return String.Format("{0} - Season {1}", searchResult.SeriesTitle, searchResult.SeasonNumber); + } + + string episodeString; + + if (searchResult.IsDaily) + episodeString = searchResult.AirDate.ToShortDateString().Replace('/', '-'); + + else + episodeString = String.Format("S{0:00}E{1:00}", searchResult.SeasonNumber, + searchResult.EpisodeNumber); + + return String.Format("{0} - {1} - {2}", searchResult.SeriesTitle, episodeString, searchResult.EpisodeTitle); + } + } +} diff --git a/NzbDrone.Web/Controllers/SystemController.cs b/NzbDrone.Web/Controllers/SystemController.cs index a8c7406da..da688d12e 100644 --- a/NzbDrone.Web/Controllers/SystemController.cs +++ b/NzbDrone.Web/Controllers/SystemController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Web.Mvc; using System.Web.Script.Serialization; using NzbDrone.Common; +using NzbDrone.Core; using NzbDrone.Core.Helpers; using NzbDrone.Core.Jobs; using NzbDrone.Core.Providers; @@ -129,7 +130,7 @@ namespace NzbDrone.Web.Controllers foreach (var fileInfo in files) { fileResult += String.Format("
{0}
{1}
", fileInfo.Name, - FileSizeFormatHelper.Format(fileInfo.Length, 1)); + fileInfo.Length.ToBestFileSize(1)); } model.Files = fileResult; diff --git a/NzbDrone.Web/Models/SearchDetailsModel.cs b/NzbDrone.Web/Models/SearchDetailsModel.cs new file mode 100644 index 000000000..e68a7762b --- /dev/null +++ b/NzbDrone.Web/Models/SearchDetailsModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Repository.Search; + +namespace NzbDrone.Web.Models +{ + public class SearchDetailsModel + { + public int Id { get; set; } + public string DisplayName { get; set; } + public List SearchHistoryItems { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Web/Models/SearchHistoryModel.cs b/NzbDrone.Web/Models/SearchHistoryModel.cs new file mode 100644 index 000000000..d7cd78857 --- /dev/null +++ b/NzbDrone.Web/Models/SearchHistoryModel.cs @@ -0,0 +1,13 @@ +using System; + +namespace NzbDrone.Web.Models +{ + public class SearchHistoryModel + { + public int Id { get; set; } + public string DisplayName { get; set; } + public string SearchTime { get; set; } + public int ReportCount { get; set; } + public bool Successful { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Web/Models/SearchItemModel.cs b/NzbDrone.Web/Models/SearchItemModel.cs new file mode 100644 index 000000000..7e9821a18 --- /dev/null +++ b/NzbDrone.Web/Models/SearchItemModel.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Model; +using NzbDrone.Core.Repository.Quality; + +namespace NzbDrone.Web.Models +{ + public class SearchItemModel + { + public int Id { get; set; } + public string ReportTitle { get; set; } + public string Indexer { get; set; } + public string NzbUrl { get; set; } + public string NzbInfoUrl { get; set; } + public bool Success { get; set; } + public string SearchError { get; set; } + public string Quality { get; set; } + public int QualityInt { get; set; } + public bool Proper { get; set; } + public int Age { get; set; } + public string Language { get; set; } + public string Size { get; set; } + public string Details { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Web/NzbDrone.Web.csproj b/NzbDrone.Web/NzbDrone.Web.csproj index e34f22940..e64985d2d 100644 --- a/NzbDrone.Web/NzbDrone.Web.csproj +++ b/NzbDrone.Web/NzbDrone.Web.csproj @@ -52,7 +52,8 @@ - ..\packages\DataTables.Mvc.0.1.0.54\lib\DataTables.Mvc.Core.dll + ..\packages\DataTables.Mvc.0.1.0.67\lib\DataTables.Mvc.Core.dll + True ..\packages\DynamicQuery.1.0\lib\35\Dynamic.dll @@ -142,6 +143,7 @@ + @@ -235,9 +237,12 @@ + + + @@ -520,6 +525,12 @@ + + + + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/NzbDrone.Web/Scripts/NzbDrone/Notification.js b/NzbDrone.Web/Scripts/NzbDrone/Notification.js index 0eaa53086..7cad8c155 100644 --- a/NzbDrone.Web/Scripts/NzbDrone/Notification.js +++ b/NzbDrone.Web/Scripts/NzbDrone/Notification.js @@ -34,7 +34,7 @@ jqXHR.error(function (xhr, textStatus, thrownError) { //ignore notification errors. - if (this.url.indexOf("/notification/Comet") === 0 || this.url.indexOf("/Health/Index") === 0) + if (this.url.indexOf("/notification/Comet") === 0 || this.url.indexOf("/Health/Index") === 0 || this.url.indexOf("/signalr") === 0) return; alert("Status: " + textStatus + ", Error: " + thrownError); diff --git a/NzbDrone.Web/Scripts/NzbDrone/grid.js b/NzbDrone.Web/Scripts/NzbDrone/grid.js index e2fc1f040..50c4c74fb 100644 --- a/NzbDrone.Web/Scripts/NzbDrone/grid.js +++ b/NzbDrone.Web/Scripts/NzbDrone/grid.js @@ -92,5 +92,5 @@ $(function () { }; // Start the connection - $.connection.hub.start(); + $.connection.hub.start({ transport: 'longPolling' }); }); \ No newline at end of file diff --git a/NzbDrone.Web/Views/History/Index.cshtml b/NzbDrone.Web/Views/History/Index.cshtml index 2b7e46ae6..799a6a325 100644 --- a/NzbDrone.Web/Views/History/Index.cshtml +++ b/NzbDrone.Web/Views/History/Index.cshtml @@ -4,6 +4,7 @@ } diff --git a/NzbDrone.Web/Views/SearchHistory/Details.cshtml b/NzbDrone.Web/Views/SearchHistory/Details.cshtml new file mode 100644 index 000000000..0546caba1 --- /dev/null +++ b/NzbDrone.Web/Views/SearchHistory/Details.cshtml @@ -0,0 +1,45 @@ +@using DataTables.Mvc.Core +@using DataTables.Mvc.Core.Enum +@model NzbDrone.Web.Models.SearchDetailsModel + +@{ + ViewBag.Title = "Search Details"; +} + +

@Model.DisplayName

+ +@Html.GridHtml("searchDetailsGrid") + +@section Scripts +{ + @(Html.GridScriptFor(m => m.SearchHistoryItems, "#searchDetailsGrid") + .PageLength(20) + .ChangePageLength(false) + .AddColumn(new Column().Image("/Content/Images/Indexers/{Indexer}.png", new { alt = "{Indexer}", title = "{Indexer}" }, "{Indexer}").Sortable(false).Title("").Width("20px")) + .AddColumn(new Column().DataProperty("ReportTitle").Title("Report Title")) + .AddColumn(new Column().DataProperty("Success").Title("Successful").Width("120px")) + .AddColumn(new Column().DisplayAndSort("Quality", "QualityInt").Title("Quality").Width("80px")) + .AddColumn(new Column().DataProperty("SearchError").Title("Error")) + .AddColumn(new Column().DataProperty("return actionColumn(source, type, val);", true)) + .AddColumn(new Column().DataProperty("Details").RenderFunction("return getDetails(row, val);").Visible(false)) + .AddSorting(3, SortDirection.Desc)) + + +} diff --git a/NzbDrone.Web/Views/SearchHistory/Index.cshtml b/NzbDrone.Web/Views/SearchHistory/Index.cshtml new file mode 100644 index 000000000..e5b8556e3 --- /dev/null +++ b/NzbDrone.Web/Views/SearchHistory/Index.cshtml @@ -0,0 +1,22 @@ +@using DataTables.Mvc.Core +@model IEnumerable + +@{ + ViewBag.Title = "Search Results"; +} + +@Html.GridHtml("searchResultsGrid", "dataTablesGrid no-details") + +@section Scripts +{ + @( + Html.GridScriptForModel("#searchResultsGrid") + .PageLength(20) + .ChangePageLength(false) + .AddColumn(new Column().DataProperty("DisplayName").Link("SearchHistory/Details?searchId={Id}", "{DisplayName}", null).Title("Name")) + .AddColumn(new Column().DataProperty("SearchTime").Title("Time").Width("170px")) + .AddColumn(new Column().DataProperty("ReportCount").Title("Reports Found").Width("140px")) + .AddColumn(new Column().DataProperty("Successful").Title("Successful").Width("110px")) + .AddSorting(1) + ) +} diff --git a/NzbDrone.Web/packages.config b/NzbDrone.Web/packages.config index 67cad6c8d..f1a80c99a 100644 --- a/NzbDrone.Web/packages.config +++ b/NzbDrone.Web/packages.config @@ -1,6 +1,6 @@  - + diff --git a/packages/DataTables.Mvc.0.1.0.54/DataTables.Mvc.0.1.0.54.nupkg b/packages/DataTables.Mvc.0.1.0.54/DataTables.Mvc.0.1.0.54.nupkg deleted file mode 100644 index 28848589e..000000000 Binary files a/packages/DataTables.Mvc.0.1.0.54/DataTables.Mvc.0.1.0.54.nupkg and /dev/null differ diff --git a/packages/DataTables.Mvc.0.1.0.54/lib/DataTables.Mvc.Core.dll b/packages/DataTables.Mvc.0.1.0.54/lib/DataTables.Mvc.Core.dll deleted file mode 100644 index c6457bd9b..000000000 Binary files a/packages/DataTables.Mvc.0.1.0.54/lib/DataTables.Mvc.Core.dll and /dev/null differ diff --git a/packages/DataTables.Mvc.0.1.0.67/Content/App_Start/DataTablesMvc.cs.pp b/packages/DataTables.Mvc.0.1.0.67/Content/App_Start/DataTablesMvc.cs.pp new file mode 100644 index 000000000..abaaa76f4 --- /dev/null +++ b/packages/DataTables.Mvc.0.1.0.67/Content/App_Start/DataTablesMvc.cs.pp @@ -0,0 +1,17 @@ +using DataTables.Mvc.Core.Helpers; +using DataTables.Mvc.Core.Models; +using System.Web.Mvc; + +[assembly: WebActivator.PreApplicationStartMethod(typeof($rootnamespace$.App_Start.DataTablesModelBinderActivator), "Start")] + +namespace $rootnamespace$.App_Start +{ + public static class DataTablesModelBinderActivator + { + public static void Start() + { + if (!ModelBinders.Binders.ContainsKey(typeof(DataTablesParams))) + ModelBinders.Binders.Add(typeof(DataTablesParams), new DataTablesModelBinder()); + } + } +} \ No newline at end of file diff --git a/packages/DataTables.Mvc.0.1.0.67/DataTables.Mvc.0.1.0.67.nupkg b/packages/DataTables.Mvc.0.1.0.67/DataTables.Mvc.0.1.0.67.nupkg new file mode 100644 index 000000000..a3bc6861c Binary files /dev/null and b/packages/DataTables.Mvc.0.1.0.67/DataTables.Mvc.0.1.0.67.nupkg differ diff --git a/packages/DataTables.Mvc.0.1.0.67/lib/DataTables.Mvc.Core.dll b/packages/DataTables.Mvc.0.1.0.67/lib/DataTables.Mvc.Core.dll new file mode 100644 index 000000000..a18c8bf2c Binary files /dev/null and b/packages/DataTables.Mvc.0.1.0.67/lib/DataTables.Mvc.Core.dll differ diff --git a/packages/DataTables.Mvc.0.1.0.67/tools/install.ps1 b/packages/DataTables.Mvc.0.1.0.67/tools/install.ps1 new file mode 100644 index 000000000..7e1f5357f --- /dev/null +++ b/packages/DataTables.Mvc.0.1.0.67/tools/install.ps1 @@ -0,0 +1,5 @@ +param($installPath, $toolsPath, $package, $project) + +$path = [System.IO.Path] +$appstart = $path::Combine($path::GetDirectoryName($project.FileName), "App_Start\DataTablesMvc.cs") +$DTE.ItemOperations.OpenFile($appstart) \ No newline at end of file