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