From d727840fbf2fe2a771d4ae245a221a919d24fb4f Mon Sep 17 00:00:00 2001
From: Icer Addis <iceraddis@gmail.com>
Date: Tue, 7 Jan 2014 00:21:05 -0800
Subject: [PATCH] Indexer searching for special episodes using query string

Added SpecialEpisodeSearchCriteria criteria to handle special episode search queries
Added method NzbSearchService.SearchSpecial() for season0 episodes
Added IIndexer GetSearchUrls() for doing text based queries
---
 .../Definitions/SearchCriteriaBase.cs         |  2 +-
 .../SpecialEpisodeSearchCriteria.cs           | 28 +++++++++++++++++++
 .../IndexerSearch/NzbSearchService.cs         | 23 +++++++++++++++
 src/NzbDrone.Core/Indexers/Eztv/Eztv.cs       |  5 ++++
 src/NzbDrone.Core/Indexers/IIndexer.cs        |  1 +
 src/NzbDrone.Core/Indexers/IndexerBase.cs     |  1 +
 .../Indexers/IndexerFetchService.cs           | 18 ++++++++++--
 src/NzbDrone.Core/Indexers/Newznab/Newznab.cs |  9 ++++++
 .../Indexers/Omgwtfnzbs/Omgwtfnzbs.cs         |  6 ++++
 src/NzbDrone.Core/Indexers/Wombles/Wombles.cs |  5 ++++
 src/NzbDrone.Core/NzbDrone.Core.csproj        |  1 +
 11 files changed, 96 insertions(+), 3 deletions(-)
 create mode 100644 src/NzbDrone.Core/IndexerSearch/Definitions/SpecialEpisodeSearchCriteria.cs

diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs
index c86256099..689d36ab3 100644
--- a/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs
+++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SearchCriteriaBase.cs
@@ -23,7 +23,7 @@ public string QueryTitle
             }
         }
 
-        private static string GetQueryTitle(string title)
+        public static string GetQueryTitle(string title)
         {
             Ensure.That(title,() => title).IsNotNullOrWhiteSpace();
 
diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/SpecialEpisodeSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/SpecialEpisodeSearchCriteria.cs
new file mode 100644
index 000000000..d4f034e44
--- /dev/null
+++ b/src/NzbDrone.Core/IndexerSearch/Definitions/SpecialEpisodeSearchCriteria.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NzbDrone.Core.IndexerSearch.Definitions
+{
+    public class SpecialEpisodeSearchCriteria : SearchCriteriaBase
+    {
+        public string[] EpisodeQueryTitles { get; set; }
+
+        public override string ToString()
+        {
+            var sb = new StringBuilder();
+            bool delimiter = false;
+            foreach (var title in EpisodeQueryTitles)
+            {
+                if (delimiter)
+                {
+                    sb.Append(',');
+                }
+                sb.Append(title);
+                delimiter = true;
+            }
+            return string.Format("[{0} : {1}]", SceneTitle, sb.ToString());
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
index 0981c5eb9..cafc2cd32 100644
--- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
+++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
@@ -64,6 +64,12 @@ public List<DownloadDecision> EpisodeSearch(int episodeId)
                 return SearchDaily(series, episode);
             }
 
+            if (episode.SeasonNumber == 0)
+            {
+                // search for special episodes in season 0 
+                return SearchSpecial(series, new List<Episode>{episode});
+            }
+
             return SearchSingle(series, episode);
         }
 
@@ -103,11 +109,28 @@ private List<DownloadDecision> SearchDaily(Series series, Episode episode)
             return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
         }
 
+        private List<DownloadDecision> SearchSpecial(Series series, List<Episode> episodes)
+        {
+            var searchSpec = Get<SpecialEpisodeSearchCriteria>(series, episodes);
+            // build list of queries for each episode in the form: "<series> <episode-title>"
+            searchSpec.EpisodeQueryTitles = episodes.Where(e => !String.IsNullOrWhiteSpace(e.Title))
+                                                    .Select(e => searchSpec.QueryTitle + "+" + SearchCriteriaBase.GetQueryTitle(e.Title))
+                                                    .ToArray();
+
+            return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
+        }
+
         public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber)
         {
             var series = _seriesService.GetSeries(seriesId);
             var episodes = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber);
 
+            if (seasonNumber == 0)
+            {
+                // search for special episodes in season 0 
+                return SearchSpecial(series, episodes);
+            }
+
             var searchSpec = Get<SeasonSearchCriteria>(series, episodes);
             searchSpec.SeasonNumber = seasonNumber;
 
diff --git a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs
index 52c55df60..315178c54 100644
--- a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs
+++ b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs
@@ -46,5 +46,10 @@ public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle
             //EZTV doesn't support searching based on actual episode airdate. they only support release date.
             return new string[0];
         }
+
+        public override IEnumerable<string> GetSearchUrls(string query, int offset)
+        {
+            return new List<string>();
+        }
     }
 }
\ No newline at end of file
diff --git a/src/NzbDrone.Core/Indexers/IIndexer.cs b/src/NzbDrone.Core/Indexers/IIndexer.cs
index 34daa6a26..4ec97efb2 100644
--- a/src/NzbDrone.Core/Indexers/IIndexer.cs
+++ b/src/NzbDrone.Core/Indexers/IIndexer.cs
@@ -13,5 +13,6 @@ public interface IIndexer : IProvider
         IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);
         IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date);
         IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset);
+        IEnumerable<string> GetSearchUrls(string query, int offset = 0);
     }
 }
\ No newline at end of file
diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs
index 7d42847cd..b89b0538d 100644
--- a/src/NzbDrone.Core/Indexers/IndexerBase.cs
+++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs
@@ -48,6 +48,7 @@ protected TSettings Settings
         public abstract IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);
         public abstract IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date);
         public abstract IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset);
+        public abstract IEnumerable<string> GetSearchUrls(string query, int offset);
 
         public override string ToString()
         {
diff --git a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs
index 2de0c51b0..924b6689a 100644
--- a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs
+++ b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs
@@ -17,6 +17,7 @@ public interface IFetchFeedFromIndexers
         IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria);
         IList<ReleaseInfo> Fetch(IIndexer indexer, SingleEpisodeSearchCriteria searchCriteria);
         IList<ReleaseInfo> Fetch(IIndexer indexer, DailyEpisodeSearchCriteria searchCriteria);
+        IList<ReleaseInfo> Fetch(IIndexer indexer, SpecialEpisodeSearchCriteria searchCriteria);
     }
 
     public class FetchFeedService : IFetchFeedFromIndexers
@@ -77,9 +78,8 @@ public IList<ReleaseInfo> Fetch(IIndexer indexer, SingleEpisodeSearchCriteria se
 
             var searchUrls = indexer.GetEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber);
             var result = Fetch(indexer, searchUrls);
-
-
             _logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count);
+
             return result;
         }
 
@@ -94,6 +94,20 @@ public IList<ReleaseInfo> Fetch(IIndexer indexer, DailyEpisodeSearchCriteria sea
             return result;
         }
 
+        public IList<ReleaseInfo> Fetch(IIndexer indexer, SpecialEpisodeSearchCriteria searchCriteria)
+        {
+            var queryUrls = new List<String>();
+            foreach (var episodeQueryTitle in searchCriteria.EpisodeQueryTitles)
+            {
+                _logger.Debug("Performing query of {0} for {1}", indexer, episodeQueryTitle);
+                queryUrls.AddRange(indexer.GetSearchUrls(episodeQueryTitle));
+            }
+
+            var result = Fetch(indexer, queryUrls);
+            _logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count);
+            return result;
+        }
+
         private List<ReleaseInfo> Fetch(IIndexer indexer, IEnumerable<string> urls)
         {
             var result = new List<ReleaseInfo>();
diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs
index 8a1be6f66..d14df1540 100644
--- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs
+++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs
@@ -104,6 +104,15 @@ public override IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int
             return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&ep={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, episodeNumber));
         }
 
+        public override IEnumerable<string> GetSearchUrls(string query, int offset)
+        {
+            // encode query (replace the + with spaces first)
+            query = query.Replace("+", " ");
+            query = System.Web.HttpUtility.UrlEncode(query);
+            return RecentFeed.Select(url => String.Format("{0}&offset={1}&limit=100&q={2}", url.Replace("t=tvsearch", "t=search"), offset, query));
+        }
+
+
         public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date)
         {
             if (tvRageId > 0)
diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs
index c4daeab66..20f0953af 100644
--- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs
+++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs
@@ -66,5 +66,11 @@ public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int
 
             return searchUrls;
         }
+
+        public override IEnumerable<string> GetSearchUrls(string query, int offset)
+        {
+            return new List<string>();
+        }
+
     }
 }
diff --git a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs
index 5355d853d..f111db688 100644
--- a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs
+++ b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs
@@ -41,5 +41,10 @@ public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle
         {
             return new List<string>();
         }
+
+        public override IEnumerable<string> GetSearchUrls(string query, int offset)
+        {
+            return new List<string>();
+        }
     }
 }
\ No newline at end of file
diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj
index 599e2cdb1..2e2f74d42 100644
--- a/src/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/src/NzbDrone.Core/NzbDrone.Core.csproj
@@ -259,6 +259,7 @@
     <Compile Include="Housekeeping\HousekeepingCommand.cs" />
     <Compile Include="Housekeeping\HousekeepingService.cs" />
     <Compile Include="Housekeeping\IHousekeepingTask.cs" />
+    <Compile Include="IndexerSearch\Definitions\SpecialEpisodeSearchCriteria.cs" />
     <Compile Include="IndexerSearch\SeriesSearchService.cs" />
     <Compile Include="IndexerSearch\SeriesSearchCommand.cs" />
     <Compile Include="IndexerSearch\EpisodeSearchService.cs" />