Merge branch 'search-log'

pull/4/head
Mark McDowall 13 years ago
commit b52710859c

@ -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<SearchHistoryItem>.CreateListOfSize(5)
.All()
.With(e => e.SearchError = ReportRejectionType.None)
.With(e => e.Success = true)
.Build();
var episodes = Builder<Episode>.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<SearchProvider>()
.Setup(c => c.PartialSeasonSearch(notification, 1, 1))
.Returns(new List<int>{1});
.Returns(new List<int>());
//Act
Mocker.Resolve<SeasonSearchJob>().Start(notification, 1, 1);

@ -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);
}
}
}

@ -77,14 +77,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
{
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()))
.Returns(true);
.Returns(ReportRejectionType.None);
}
private void WithQualityNotNeeded()
{
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()))
.Returns(false);
.Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter);
}
[Test]
@ -103,13 +103,13 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.Is<EpisodeParseResult>(d => d.Quality.QualityType == QualityTypes.Bluray1080p)))
.Returns(true);
.Returns(ReportRejectionType.None);
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeTrue();
result.Should().Contain(n => n.Success);
Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Once());
@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeFalse();
result.Should().NotContain(n => n.Success);
Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Exactly(5));
@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeFalse();
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeFalse();
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
@ -198,7 +198,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeTrue();
result.Should().Contain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Once());
@ -230,7 +230,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeTrue();
result.Should().Contain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Exactly(2));
@ -250,7 +250,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeFalse();
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
@ -270,7 +270,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().BeFalse();
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());

@ -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<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()))
.Returns(true);
.Returns(ReportRejectionType.None);
}
private void WithQualityNotNeeded()
{
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()))
.Returns(false);
.Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter);
}
[Test]
@ -102,14 +103,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.Is<EpisodeParseResult>(d => d.Quality.QualityType == QualityTypes.Bluray1080p)))
.Returns(true);
.Returns(ReportRejectionType.None);
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1);
var result = Mocker.Resolve<SearchProvider>().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<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Once());
@ -135,13 +136,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
WithSuccessfulDownload();
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>())).Returns(true);
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>())).Returns(ReportRejectionType.None);
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, 1, 1);
var result = Mocker.Resolve<SearchProvider>().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<DownloadProvider>().Verify(c => c.DownloadReport(It.Is<EpisodeParseResult>(d => d.Age != 100)), Times.Never());
@ -162,10 +164,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
WithQualityNotNeeded();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1);
var result = Mocker.Resolve<SearchProvider>().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<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Exactly(5));
@ -185,10 +188,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
WithNullSeries();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1);
var result = Mocker.Resolve<SearchProvider>().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<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
@ -206,10 +210,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
WithMisMatchedSeries();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1);
var result = Mocker.Resolve<SearchProvider>().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<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
@ -227,10 +232,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
WithMatchingSeries();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1);
var result = Mocker.Resolve<SearchProvider>().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<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
@ -248,10 +254,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
WithMatchingSeries();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1);
var result = Mocker.Resolve<SearchProvider>().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<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
@ -274,10 +281,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
WithSuccessfulDownload();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1);
var result = Mocker.Resolve<SearchProvider>().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<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Once());
@ -307,10 +315,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.Returns(true);
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1);
var result = Mocker.Resolve<SearchProvider>().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<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Exactly(2));

@ -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),
});
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);

@ -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)

@ -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,
}
}

@ -222,6 +222,7 @@
<Compile Include="Datastore\MigrationLogger.cs" />
<Compile Include="Datastore\MigrationsHelper.cs" />
<Compile Include="Datastore\CustomeMapper.cs" />
<Compile Include="Datastore\Migrations\Migration20120420.cs" />
<Compile Include="Datastore\Migrations\Migration20120228.cs" />
<Compile Include="Datastore\Migrations\Migration20120227.cs" />
<Compile Include="Datastore\Migrations\Migration20120220.cs" />
@ -239,7 +240,6 @@
<Compile Include="Datastore\PetaPoco\EpisodeSeasonRelator.cs" />
<Compile Include="Fluent.cs" />
<Compile Include="Helpers\EpisodeSortingHelper.cs" />
<Compile Include="Helpers\FileSizeFormatHelper.cs" />
<Compile Include="Helpers\SortHelper.cs" />
<Compile Include="Helpers\SabnzbdQueueTimeConverter.cs" />
<Compile Include="Jobs\CheckpointJob.cs" />
@ -277,6 +277,7 @@
<Compile Include="Providers\Indexer\NzbIndex.cs" />
<Compile Include="Providers\Indexer\FileSharingTalk.cs" />
<Compile Include="Providers\Indexer\Wombles.cs" />
<Compile Include="Providers\SearchHistoryProvider.cs" />
<Compile Include="Providers\SeasonProvider.cs" />
<Compile Include="Jobs\RecentBacklogSearchJob.cs" />
<Compile Include="Jobs\TrimLogsJob.cs" />
@ -304,6 +305,9 @@
<Compile Include="Providers\AnalyticsProvider.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Repository\Search\SearchHistoryItem.cs" />
<Compile Include="Repository\Search\SearchHistory.cs" />
<Compile Include="Model\ReportRejectionType.cs" />
<Compile Include="Repository\Season.cs" />
<Compile Include="Providers\AutoConfigureProvider.cs">
<SubType>Code</SubType>

@ -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;
}
}
}

@ -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);

@ -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);

@ -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<SearchHistory>(id);
}
public virtual List<SearchHistory> 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<SearchHistory>(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<SearchHistory>(sql, id);
result.SearchHistoryItems = _database.Fetch<SearchHistoryItem>("WHERE SearchHistoryId = @0", id);
return result;
}
public virtual void ForceDownload(int itemId)
{
var item = _database.Single<SearchHistoryItem>(itemId);
var searchResult = _database.Single<SearchHistory>(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);
}
}
}

@ -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<int> 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<int>();
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<int>();
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> { 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<int> ProcessSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports, Series series, int seasonNumber, int? episodeNumber = null)
public List<SearchHistoryItem> ProcessSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports, SearchHistory searchResult, Series series, int seasonNumber, int? episodeNumber = null)
{
var successes = new List<int>();
var items = new List<SearchHistoryItem>();
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<EpisodeParseResult> reports, Series series, DateTime airDate)
public List<SearchHistoryItem> ProcessSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports, Series series, DateTime airDate)
{
var items = new List<SearchHistoryItem>();
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<int> GetEpisodeNumberPrefixes(IEnumerable<int> episodeNumbers)

@ -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<SearchHistoryItem> SearchHistoryItems { get; set; }
[Ignore]
public List<int> 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; }
}
}

@ -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);
}
}
}

@ -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;

@ -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);
}
}
}

@ -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("<div><div style=\"width: 600px; display: inline-block;\">{0}</div><div style=\"display: inline-block;\">{1}</div></div>", fileInfo.Name,
FileSizeFormatHelper.Format(fileInfo.Length, 1));
fileInfo.Length.ToBestFileSize(1));
}
model.Files = fileResult;

@ -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<SearchItemModel> SearchHistoryItems { get; set; }
}
}

@ -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; }
}
}

@ -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; }
}
}

@ -52,7 +52,8 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="DataTables.Mvc.Core">
<HintPath>..\packages\DataTables.Mvc.0.1.0.54\lib\DataTables.Mvc.Core.dll</HintPath>
<HintPath>..\packages\DataTables.Mvc.0.1.0.67\lib\DataTables.Mvc.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Dynamic">
<HintPath>..\packages\DynamicQuery.1.0\lib\35\Dynamic.dll</HintPath>
@ -142,6 +143,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="App_Start\DataTablesMvc.cs" />
<Compile Include="Controllers\SearchHistoryController.cs" />
<Compile Include="Helpers\Validation\RequiredIfAnyAttribute.cs" />
<Compile Include="Helpers\Validation\RequiredIfAttribute.cs" />
<Content Include="Content\DataTables-1.9.0\media\css\jquery.dataTables.css" />
@ -235,9 +237,12 @@
<Compile Include="Helpers\DescriptionExtension.cs" />
<Compile Include="Helpers\HtmlPrefixScopeExtensions.cs" />
<Compile Include="Helpers\IsCurrentActionHelper.cs" />
<Compile Include="Models\SearchDetailsModel.cs" />
<Compile Include="Models\JobModel.cs" />
<Compile Include="Models\LogModel.cs" />
<Compile Include="Models\PostUpgradeModel.cs" />
<Compile Include="Models\SearchItemModel.cs" />
<Compile Include="Models\SearchHistoryModel.cs" />
<Compile Include="Models\UpcomingEpisodesModel.cs" />
<Compile Include="Models\SeasonModel.cs" />
<Compile Include="Models\SeriesDetailsModel.cs" />
@ -520,6 +525,12 @@
<Content Include="Views\Shared\NoSeriesBanner.cshtml" />
<Content Include="Views\Update\Post.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\SearchHistory\Index.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\SearchHistory\Details.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

@ -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);

@ -92,5 +92,5 @@ $(function () {
};
// Start the connection
$.connection.hub.start();
$.connection.hub.start({ transport: 'longPolling' });
});

@ -4,6 +4,7 @@
<ul class="sub-menu">
<li>@Ajax.ActionLink("Trim History", "Trim", "History", null, new AjaxOptions{ OnSuccess = "reloadGrid", Confirm = "Delete history items older than 30 days?"}, new { Title = "Delete history items older than 30 days" })</li>
<li>@Ajax.ActionLink("Purge History", "Purge", "History", null, new AjaxOptions{ OnSuccess = "reloadGrid", Confirm = "Purge all history items?" }, new { Title = "Delete all history items" })</li>
<li>@Html.ActionLink("Search Hisotry", "Index", "SearchHistory", null, new { Title = "Review recent searches" })</li>
</ul>
}

@ -0,0 +1,45 @@
@using DataTables.Mvc.Core
@using DataTables.Mvc.Core.Enum
@model NzbDrone.Web.Models.SearchDetailsModel
@{
ViewBag.Title = "Search Details";
}
<h2>@Model.DisplayName</h2>
@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))
<script type="text/javascript">
function getDetails(row, val) {
var result = "<a href=\"" + row.aData["NzbInfoUrl"] + "\">Nzb Info</a><br/>" +
"<b>Proper: </b>" + row.aData["Proper"] + " <br/>" +
"<b>Age: </b>" + row.aData["Age"] + " days<br/>" +
"<b>Size: </b>" + row.aData["Size"] + " <br/>" +
"<b>Language: </b>" + row.aData["Language"] + " <br/>";
return result;
}
function actionColumn(source, type, val) {
if (type === 'display' || type === 'filter') {
return '<a href="/SearchHistory/ForceDownload/' + source["Id"] + '" data-ajax="true" data-ajax-confirm="Are you sure?"><img src="/Content/Images/Plus.png" alt="Force" title="Force" class="gridAction"/></a>';
}
// 'sort' and 'type' both just use the raw data
return '';
}
</script>
}

@ -0,0 +1,22 @@
@using DataTables.Mvc.Core
@model IEnumerable<NzbDrone.Web.Models.SearchHistoryModel>
@{
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)
)
}

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="DataTables.Mvc" version="0.1.0.54" />
<package id="DataTables.Mvc" version="0.1.0.67" />
<package id="DynamicQuery" version="1.0" />
<package id="EntityFramework" version="4.3.0" />
<package id="EntityFramework.SqlServerCompact" version="4.1.8482.2" />

@ -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());
}
}
}

@ -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)
Loading…
Cancel
Save