rewrite of indexer/episode search

This commit is contained in:
kay.one 2013-04-07 00:30:37 -07:00
commit a6a4932b44
114 changed files with 2045 additions and 5577 deletions

View file

@ -1,11 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine
{
public class DownloadDecision
{
public EpisodeParseResult ParseResult { get; private set; }
public IEnumerable<string> Rejections { get; private set; }
public bool Approved
{
get
@ -14,9 +17,23 @@ namespace NzbDrone.Core.DecisionEngine
}
}
public DownloadDecision(params string[] rejections)
public DownloadDecision(EpisodeParseResult parseResult, params string[] rejections)
{
ParseResult = parseResult;
Rejections = rejections.ToList();
}
public static EpisodeParseResult PickBestReport(IEnumerable<DownloadDecision> downloadDecisions)
{
var reports = downloadDecisions
.Where(c => c.Approved)
.Select(c => c.ParseResult)
.OrderByDescending(c => c.Quality)
.ThenBy(c => c.EpisodeNumbers.MinOrDefault())
.ThenBy(c => c.Age);
return reports.SingleOrDefault();
}
}
}

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.DecisionEngine.Specifications.Search;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine
{
public interface IMakeDownloadDecision
{
IEnumerable<DownloadDecision> GetRssDecision(IEnumerable<EpisodeParseResult> episodeParseResults);
IEnumerable<DownloadDecision> GetSearchDecision(IEnumerable<EpisodeParseResult> episodeParseResult, SearchDefinitionBase searchDefinitionBase);
}
public class DownloadDecisionMaker : IMakeDownloadDecision
{
private readonly IEnumerable<IRejectWithReason> _specifications;
public DownloadDecisionMaker(IEnumerable<IRejectWithReason> specifications)
{
_specifications = specifications;
}
public IEnumerable<DownloadDecision> GetRssDecision(IEnumerable<EpisodeParseResult> episodeParseResults)
{
foreach (var parseResult in episodeParseResults)
{
parseResult.Decision = new DownloadDecision(parseResult, GetGeneralRejectionReasons(parseResult).ToArray());
yield return parseResult.Decision;
}
}
public IEnumerable<DownloadDecision> GetSearchDecision(IEnumerable<EpisodeParseResult> episodeParseResults, SearchDefinitionBase searchDefinitionBase)
{
foreach (var parseResult in episodeParseResults)
{
var generalReasons = GetGeneralRejectionReasons(parseResult);
var searchReasons = GetSearchRejectionReasons(parseResult, searchDefinitionBase);
parseResult.Decision = new DownloadDecision(parseResult, generalReasons.Union(searchReasons).ToArray());
yield return parseResult.Decision;
}
}
private IEnumerable<string> GetGeneralRejectionReasons(EpisodeParseResult episodeParseResult)
{
return _specifications
.OfType<IDecisionEngineSpecification>()
.Where(spec => !spec.IsSatisfiedBy(episodeParseResult))
.Select(spec => spec.RejectionReason);
}
private IEnumerable<string> GetSearchRejectionReasons(EpisodeParseResult episodeParseResult, SearchDefinitionBase searchDefinitionBase)
{
return _specifications
.OfType<IDecisionEngineSearchSpecification>()
.Where(spec => !spec.IsSatisfiedBy(episodeParseResult, searchDefinitionBase))
.Select(spec => spec.RejectionReason);
}
}
}

View file

@ -1,32 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine
{
public interface IDownloadDirector
{
DownloadDecision GetDownloadDecision(EpisodeParseResult episodeParseResult);
}
public class DownloadDirector : IDownloadDirector
{
private readonly IEnumerable<IFetchableSpecification> _specifications;
public DownloadDirector(IEnumerable<IFetchableSpecification> specifications)
{
_specifications = specifications;
}
public DownloadDecision GetDownloadDecision(EpisodeParseResult episodeParseResult)
{
var rejections = _specifications
.Where(spec => !spec.IsSatisfiedBy(episodeParseResult))
.Select(spec => spec.RejectionReason).ToArray();
episodeParseResult.Decision = new DownloadDecision(rejections);
return episodeParseResult.Decision;
}
}
}

View file

@ -2,9 +2,8 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine
{
public interface IFetchableSpecification
public interface IDecisionEngineSpecification : IRejectWithReason
{
string RejectionReason { get; }
bool IsSatisfiedBy(EpisodeParseResult subject);
}
}

View file

@ -0,0 +1,7 @@
namespace NzbDrone.Core.DecisionEngine
{
public interface IRejectWithReason
{
string RejectionReason { get; }
}
}

View file

@ -5,7 +5,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class AcceptableSizeSpecification : IFetchableSpecification
public class AcceptableSizeSpecification : IDecisionEngineSpecification
{
private readonly IQualitySizeService _qualityTypeProvider;
private readonly IEpisodeService _episodeService;

View file

@ -5,7 +5,7 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class AllowedReleaseGroupSpecification : IFetchableSpecification
public class AllowedReleaseGroupSpecification : IDecisionEngineSpecification
{
private readonly IConfigService _configService;
private readonly Logger _logger;

View file

@ -4,7 +4,7 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class CustomStartDateSpecification : IFetchableSpecification
public class CustomStartDateSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;

View file

@ -3,7 +3,7 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class LanguageSpecification : IFetchableSpecification
public class LanguageSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;

View file

@ -5,7 +5,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class MonitoredEpisodeSpecification : IFetchableSpecification
public class MonitoredEpisodeSpecification : IDecisionEngineSpecification
{
private readonly IEpisodeService _episodeService;
private readonly ISeriesRepository _seriesRepository;

View file

@ -4,7 +4,7 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class NotInQueueSpecification : IFetchableSpecification
public class NotInQueueSpecification : IDecisionEngineSpecification
{
private readonly IProvideDownloadClient _downloadClientProvider;

View file

@ -3,7 +3,7 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class QualityAllowedByProfileSpecification : IFetchableSpecification
public class QualityAllowedByProfileSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;

View file

@ -4,7 +4,7 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class RetentionSpecification : IFetchableSpecification
public class RetentionSpecification : IDecisionEngineSpecification
{
private readonly IConfigService _configService;
private readonly Logger _logger;

View file

@ -0,0 +1,43 @@
using NLog;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
{
public class DailyEpisodeMatchSpecification : IDecisionEngineSearchSpecification
{
private readonly Logger _logger;
private readonly IEpisodeService _episodeService;
public DailyEpisodeMatchSpecification(Logger logger, IEpisodeService episodeService)
{
_logger = logger;
_episodeService = episodeService;
}
public string RejectionReason
{
get
{
return "Episode doesn't match";
}
}
public bool IsSatisfiedBy(EpisodeParseResult episodeParseResult, SearchDefinitionBase searchDefinitionBase)
{
var dailySearchSpec = searchDefinitionBase as DailyEpisodeSearchDefinition;
if (dailySearchSpec == null) return true;
var episode = _episodeService.GetEpisode(dailySearchSpec.SeriesId, dailySearchSpec.Airtime);
if (!episodeParseResult.AirDate.HasValue || episodeParseResult.AirDate.Value != episode.AirDate.Value)
{
_logger.Trace("Episode AirDate does not match searched episode number, skipping.");
return false;
}
return true;
}
}
}

View file

@ -0,0 +1,11 @@
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
{
public interface IDecisionEngineSearchSpecification : IRejectWithReason
{
bool IsSatisfiedBy(EpisodeParseResult subject, SearchDefinitionBase searchDefinitionBase);
}
}

View file

@ -0,0 +1,38 @@
using NLog;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
{
public class SeasonMatchSpecification : IDecisionEngineSearchSpecification
{
private readonly Logger _logger;
public SeasonMatchSpecification(Logger logger)
{
_logger = logger;
}
public string RejectionReason
{
get
{
return "Episode doesn't match";
}
}
public bool IsSatisfiedBy(EpisodeParseResult episodeParseResult, SearchDefinitionBase searchDefinitionBase)
{
var singleEpisodeSpec = searchDefinitionBase as SeasonSearchDefinition;
if (singleEpisodeSpec == null) return true;
if (singleEpisodeSpec.SeasonNumber != episodeParseResult.SeasonNumber)
{
_logger.Trace("Season number does not match searched season number, skipping.");
return false;
}
return true;
}
}
}

View file

@ -0,0 +1,44 @@
using NLog;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
{
public class SingleEpisodeMatchSpecification : IDecisionEngineSearchSpecification
{
private readonly Logger _logger;
public SingleEpisodeMatchSpecification(Logger logger)
{
_logger = logger;
}
public string RejectionReason
{
get
{
return "Episode doesn't match";
}
}
public bool IsSatisfiedBy(EpisodeParseResult episodeParseResult, SearchDefinitionBase searchDefinitionBase)
{
var singleEpisodeSpec = searchDefinitionBase as SingleEpisodeSearchDefinition;
if (singleEpisodeSpec == null) return true;
if (singleEpisodeSpec.SeasonNumber != episodeParseResult.SeasonNumber)
{
_logger.Trace("Season number does not match searched season number, skipping.");
return false;
}
if (!episodeParseResult.EpisodeNumbers.Contains(singleEpisodeSpec.EpisodeNumber))
{
_logger.Trace("Episode number does not match searched episode number, skipping.");
return false;
}
return true;
}
}
}

View file

@ -6,7 +6,7 @@ using NzbDrone.Core.Tv;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class UpgradeDiskSpecification : IFetchableSpecification
public class UpgradeDiskSpecification : IDecisionEngineSpecification
{
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;
private readonly Logger _logger;

View file

@ -4,7 +4,7 @@ using NzbDrone.Core.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class UpgradeHistorySpecification : IFetchableSpecification
public class UpgradeHistorySpecification : IDecisionEngineSpecification
{
private readonly IHistoryService _historyService;
private readonly QualityUpgradableSpecification _qualityUpgradableSpecification;