diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj
index 7c2b17564..42139a6f8 100644
--- a/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/NzbDrone.Core/NzbDrone.Core.csproj
@@ -328,6 +328,7 @@
+
diff --git a/NzbDrone.Core/Providers/Search/EpisodeSearch.cs b/NzbDrone.Core/Providers/Search/EpisodeSearch.cs
new file mode 100644
index 000000000..e1e856b84
--- /dev/null
+++ b/NzbDrone.Core/Providers/Search/EpisodeSearch.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NLog;
+using NzbDrone.Core.Model;
+using NzbDrone.Core.Providers.DecisionEngine;
+using NzbDrone.Core.Repository;
+using NzbDrone.Core.Repository.Search;
+
+namespace NzbDrone.Core.Providers.Search
+{
+ public class EpisodeSearch : SearchBase
+ {
+ private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+ public EpisodeSearch(EpisodeProvider episodeProvider, DownloadProvider downloadProvider,
+ SeriesProvider seriesProvider, IndexerProvider indexerProvider,
+ SceneMappingProvider sceneMappingProvider, UpgradePossibleSpecification upgradePossibleSpecification,
+ AllowedDownloadSpecification allowedDownloadSpecification, SearchHistoryProvider searchHistoryProvider)
+ : base(episodeProvider, downloadProvider, seriesProvider, indexerProvider, sceneMappingProvider,
+ upgradePossibleSpecification, allowedDownloadSpecification, searchHistoryProvider)
+ {
+ }
+
+ protected override List Search(Series series, dynamic options)
+ {
+ if (options == null)
+ throw new ArgumentNullException(options);
+
+ if (options.SeasonNumber < 0)
+ throw new ArgumentException("SeasonNumber is invalid");
+
+ if (options.EpisodeNumber < 0)
+ throw new ArgumentException("EpisodeNumber is invalid");
+
+ var reports = new List();
+ var title = GetSeriesTitle(series);
+
+ Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer =>
+ {
+ try
+ {
+ reports.AddRange(indexer.FetchEpisode(title, options.SeasonNumber, options.EpisodeNumber));
+ }
+
+ catch (Exception e)
+ {
+ logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00}E{2:00} from: {3}",
+ series.Title, options.SeasonNumber, options.EpisodeNumber, indexer.Name), e);
+ }
+ });
+
+ return reports;
+ }
+
+ protected override SearchHistoryItem CheckEpisode(Series series, List episodes, EpisodeParseResult episodeParseResult,
+ SearchHistoryItem item)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/NzbDrone.Core/Providers/Search/SearchBase.cs b/NzbDrone.Core/Providers/Search/SearchBase.cs
new file mode 100644
index 000000000..243f7fc36
--- /dev/null
+++ b/NzbDrone.Core/Providers/Search/SearchBase.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NLog;
+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.Search
+{
+ public abstract class SearchBase
+ {
+ protected readonly EpisodeProvider _episodeProvider;
+ protected readonly DownloadProvider _downloadProvider;
+ protected readonly SeriesProvider _seriesProvider;
+ protected readonly IndexerProvider _indexerProvider;
+ protected readonly SceneMappingProvider _sceneMappingProvider;
+ protected readonly UpgradePossibleSpecification _upgradePossibleSpecification;
+ protected readonly AllowedDownloadSpecification _allowedDownloadSpecification;
+ protected readonly SearchHistoryProvider _searchHistoryProvider;
+
+ private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+ protected SearchBase(EpisodeProvider episodeProvider, DownloadProvider downloadProvider,SeriesProvider seriesProvider,
+ IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider,
+ UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification,
+ SearchHistoryProvider searchHistoryProvider)
+ {
+ _episodeProvider = episodeProvider;
+ _downloadProvider = downloadProvider;
+ _seriesProvider = seriesProvider;
+ _indexerProvider = indexerProvider;
+ _sceneMappingProvider = sceneMappingProvider;
+ _upgradePossibleSpecification = upgradePossibleSpecification;
+ _allowedDownloadSpecification = allowedDownloadSpecification;
+ _searchHistoryProvider = searchHistoryProvider;
+ }
+
+ protected SearchBase()
+ {
+ }
+
+ protected abstract List Search(Series series, dynamic options);
+ protected abstract SearchHistoryItem CheckEpisode(Series series, List episodes, EpisodeParseResult episodeParseResult,
+ SearchHistoryItem item);
+
+ protected virtual SearchHistoryItem ProcessReport(EpisodeParseResult episodeParseResult, Series series, List episodes)
+ {
+ try
+ {
+ var item = new SearchHistoryItem
+ {
+ ReportTitle = episodeParseResult.OriginalString,
+ NzbUrl = episodeParseResult.NzbUrl,
+ Indexer = episodeParseResult.Indexer,
+ Quality = episodeParseResult.Quality.Quality,
+ Proper = episodeParseResult.Quality.Proper,
+ Size = episodeParseResult.Size,
+ Age = episodeParseResult.Age,
+ Language = episodeParseResult.Language
+ };
+
+ logger.Trace("Analysing report " + episodeParseResult);
+
+ //Get the matching series
+ episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle);
+
+ //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;
+ return item;
+ }
+
+ //If parse result doesn't have an air date or it doesn't match passed in airdate, skip the report.
+ if (CheckEpisode(series, episodes, item).SearchError != ReportRejectionType.None)
+ {
+ return item;
+ }
+
+ episodeParseResult.Episodes = _episodeProvider.GetEpisodesByParseResult(episodeParseResult);
+
+ item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult);
+ return item;
+ }
+ catch (Exception e)
+ {
+ logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e);
+ }
+
+ return null;
+ }
+
+ protected virtual SearchHistoryItem DownloadReport(ProgressNotification notification, EpisodeParseResult episodeParseResult, SearchHistoryItem item)
+ {
+ //Todo: Customize download message per search type? (override)
+
+ logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult);
+ try
+ {
+ if (_downloadProvider.DownloadReport(episodeParseResult))
+ {
+ notification.CurrentMessage =
+ String.Format("{0} - {1} {2} Added to download queue",
+ episodeParseResult.Series.Title, episodeParseResult.AirDate.Value.ToShortDateString(), episodeParseResult.Quality);
+
+ 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;
+ }
+
+ return item;
+ }
+
+ protected virtual string GetSeriesTitle(Series series, int seasonNumber = -1)
+ {
+ //Todo: Add support for per season lookup (used for anime)
+ var title = _sceneMappingProvider.GetSceneName(series.SeriesId);
+
+ if (String.IsNullOrWhiteSpace(title))
+ {
+ title = series.Title;
+ title = title.Replace("&", "and");
+ }
+
+ return title;
+ }
+ }
+}