From 35cad3d27e054733164eb4c052589cf2a97f6e03 Mon Sep 17 00:00:00 2001
From: Mark McDowall <markus.mcd5@gmail.com>
Date: Wed, 31 Aug 2011 23:58:54 -0700
Subject: [PATCH] Added partial season searching when a full season NZB is not
 available.

---
 NzbDrone.Core.Test/NzbDrone.Core.Test.csproj  |   2 +
 .../SearchProviderTest_PartialSeason.cs       | 210 ++++++++++++++++++
 .../SearchProviderTest_Season.cs              |  23 +-
 NzbDrone.Core.Test/SeasonSearchJobTest.cs     | 114 ++++++++++
 NzbDrone.Core/Model/Search/SearchModel.cs     |   1 +
 NzbDrone.Core/Model/Search/SearchType.cs      |   3 +-
 .../Providers/Indexer/IndexerBase.cs          |  27 +++
 NzbDrone.Core/Providers/Indexer/Newzbin.cs    |  17 +-
 NzbDrone.Core/Providers/Indexer/NzbMatrix.cs  |   8 +-
 NzbDrone.Core/Providers/Indexer/NzbsOrg.cs    |   8 +-
 .../Providers/Jobs/SeasonSearchJob.cs         |  16 +-
 NzbDrone.Core/Providers/SearchProvider.cs     | 101 +++++++++
 12 files changed, 510 insertions(+), 20 deletions(-)
 create mode 100644 NzbDrone.Core.Test/SearchProviderTest_PartialSeason.cs
 create mode 100644 NzbDrone.Core.Test/SeasonSearchJobTest.cs

diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
index 97f14ff4e..678f753ca 100644
--- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
+++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
@@ -89,6 +89,8 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="BacklogSearchJobTest.cs" />
+    <Compile Include="SeasonSearchJobTest.cs" />
+    <Compile Include="SearchProviderTest_PartialSeason.cs" />
     <Compile Include="SearchJobTest.cs" />
     <Compile Include="SeriesSearchJobTest.cs" />
     <Compile Include="SearchProviderTest_Season.cs" />
diff --git a/NzbDrone.Core.Test/SearchProviderTest_PartialSeason.cs b/NzbDrone.Core.Test/SearchProviderTest_PartialSeason.cs
new file mode 100644
index 000000000..dc57802ec
--- /dev/null
+++ b/NzbDrone.Core.Test/SearchProviderTest_PartialSeason.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AutoMoq;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.Model;
+using NzbDrone.Core.Model.Notification;
+using NzbDrone.Core.Providers;
+using NzbDrone.Core.Providers.Indexer;
+using NzbDrone.Core.Providers.Jobs;
+using NzbDrone.Core.Repository;
+using NzbDrone.Core.Repository.Quality;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test
+{
+    [TestFixture]
+    // ReSharper disable InconsistentNaming
+    public class SearchProviderTest_PartialSeason : TestBase
+    {
+        [Test]
+        public void SeasonPartialSearch_season_success()
+        {
+            var series = Builder<Series>.CreateNew()
+                .With(s => s.SeriesId = 1)
+                .With(s => s.Title = "Title1")
+                .Build();
+
+            var episodes = Builder<Episode>.CreateListOfSize(5)
+                .WhereAll()
+                .Have(e => e.Series = series)
+                .Have(e => e.SeriesId = 1)
+                .Have(e => e.SeasonNumber = 1)
+                .Have(e => e.Ignored = false)
+                .Build();
+
+            var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(4)
+                .WhereAll()
+                .Have(e => e.EpisodeNumbers = Builder<int>.CreateListOfSize(2).Build().ToList())
+                .Build();
+
+            var mocker = new AutoMoqer(MockBehavior.Strict);
+
+            var notification = new ProgressNotification("Season Search");
+
+            var indexer1 = new Mock<IndexerBase>();
+            indexer1.Setup(c => c.FetchPartialSeason(episodes[0].Series.Title, episodes[0].SeasonNumber, 0))
+                .Returns(parseResults).Verifiable();
+
+            var indexer2 = new Mock<IndexerBase>();
+            indexer2.Setup(c => c.FetchPartialSeason(episodes[0].Series.Title, episodes[0].SeasonNumber, 0))
+                .Returns(parseResults).Verifiable();
+
+            var indexers = new List<IndexerBase> { indexer1.Object, indexer2.Object };
+
+            mocker.GetMock<IndexerProvider>()
+                .Setup(c => c.GetEnabledIndexers())
+                .Returns(indexers);
+
+            mocker.GetMock<SeriesProvider>()
+                .Setup(c => c.GetSeries(1)).Returns(series);
+
+            mocker.GetMock<EpisodeProvider>()
+                .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes);
+
+            mocker.GetMock<SceneMappingProvider>()
+                .Setup(s => s.GetSceneName(1)).Returns(String.Empty);
+
+            mocker.GetMock<InventoryProvider>()
+                .Setup(s => s.IsQualityNeeded(It.IsAny<EpisodeParseResult>())).Returns(true);
+
+            mocker.GetMock<DownloadProvider>()
+                .Setup(s => s.DownloadReport(It.IsAny<EpisodeParseResult>())).Returns(true);
+
+            //Act
+            var result = mocker.Resolve<SearchProvider>().PartialSeasonSearch(notification, 1, 1);
+
+            //Assert
+            result.Should().HaveCount(16);
+            mocker.VerifyAllMocks();
+            mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Times.Exactly(8));
+        }
+
+        [Test]
+        public void SeasonPartialSearch_season_no_results()
+        {
+            var series = Builder<Series>.CreateNew()
+                .With(s => s.SeriesId = 1)
+                .With(s => s.Title = "Title1")
+                .Build();
+
+            var episodes = Builder<Episode>.CreateListOfSize(5)
+                .WhereAll()
+                .Have(e => e.Series = series)
+                .Have(e => e.SeriesId = 1)
+                .Have(e => e.SeasonNumber = 1)
+                .Have(e => e.Ignored = false)
+                .Build();
+
+            var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(4)
+                .WhereAll()
+                .Have(e => e.EpisodeNumbers = Builder<int>.CreateListOfSize(2).Build().ToList())
+                .Build();
+
+            var mocker = new AutoMoqer(MockBehavior.Strict);
+
+            var notification = new ProgressNotification("Season Search");
+
+            var indexer1 = new Mock<IndexerBase>();
+            indexer1.Setup(c => c.FetchPartialSeason(episodes[0].Series.Title, episodes[0].SeasonNumber, 0))
+                .Returns(new List<EpisodeParseResult>()).Verifiable();
+
+            var indexer2 = new Mock<IndexerBase>();
+            indexer2.Setup(c => c.FetchPartialSeason(episodes[0].Series.Title, episodes[0].SeasonNumber, 0))
+                .Returns(new List<EpisodeParseResult>()).Verifiable();
+
+            var indexers = new List<IndexerBase> { indexer1.Object, indexer2.Object };
+
+            mocker.GetMock<IndexerProvider>()
+                .Setup(c => c.GetEnabledIndexers())
+                .Returns(indexers);
+
+            mocker.GetMock<SeriesProvider>()
+                .Setup(c => c.GetSeries(1)).Returns(series);
+
+            mocker.GetMock<EpisodeProvider>()
+                .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes);
+
+            mocker.GetMock<SceneMappingProvider>()
+                .Setup(s => s.GetSceneName(1)).Returns(String.Empty);
+
+            //Act
+            var result = mocker.Resolve<SearchProvider>().PartialSeasonSearch(notification, 1, 1);
+
+            //Assert
+            result.Should().HaveCount(0);
+            mocker.VerifyAllMocks();
+            mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Times.Never());
+        }
+
+        [Test]
+        public void ProcessPartialSeasonSearchResults_success()
+        {
+            var series = Builder<Series>.CreateNew()
+                .With(s => s.SeriesId = 1)
+                .With(s => s.Title = "Title1")
+                .Build();
+
+            var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(4)
+                .WhereAll()
+                .Have(e => e.EpisodeNumbers = Builder<int>.CreateListOfSize(2).Build().ToList())
+                .Have(e => e.Series = series)
+                .Build();
+
+            var mocker = new AutoMoqer(MockBehavior.Strict);
+
+            var notification = new ProgressNotification("Season Search");
+
+            mocker.GetMock<InventoryProvider>()
+                .Setup(s => s.IsQualityNeeded(It.IsAny<EpisodeParseResult>())).Returns(true);
+
+            mocker.GetMock<DownloadProvider>()
+                .Setup(s => s.DownloadReport(It.IsAny<EpisodeParseResult>())).Returns(true);
+
+            //Act
+            var result = mocker.Resolve<SearchProvider>().ProcessPartialSeasonSearchResults(notification, parseResults);
+
+            //Assert
+            result.Should().HaveCount(8);
+            mocker.VerifyAllMocks();
+            mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Times.Exactly(4));
+
+        }
+
+        [Test]
+        public void ProcessPartialSeasonSearchResults_failure()
+        {
+            var series = Builder<Series>.CreateNew()
+                .With(s => s.SeriesId = 1)
+                .With(s => s.Title = "Title1")
+                .Build();
+
+            var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(4)
+                .WhereTheFirst(1)
+                .Has(p => p.CleanTitle = "title")
+                .Has(p => p.SeasonNumber = 1)
+                .Has(p => p.FullSeason = true)
+                .Has(p => p.EpisodeNumbers = null)
+                .Build();
+
+            var mocker = new AutoMoqer(MockBehavior.Strict);
+
+            var notification = new ProgressNotification("Season Search");
+
+            mocker.GetMock<InventoryProvider>()
+                .Setup(s => s.IsQualityNeeded(It.IsAny<EpisodeParseResult>())).Returns(false);
+
+            //Act
+            var result = mocker.Resolve<SearchProvider>().ProcessPartialSeasonSearchResults(notification, parseResults);
+
+            //Assert
+            result.Should().HaveCount(0);
+            mocker.VerifyAllMocks();
+            mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Times.Never());
+        }
+    }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/SearchProviderTest_Season.cs b/NzbDrone.Core.Test/SearchProviderTest_Season.cs
index 87fc5bff8..5f0f62658 100644
--- a/NzbDrone.Core.Test/SearchProviderTest_Season.cs
+++ b/NzbDrone.Core.Test/SearchProviderTest_Season.cs
@@ -79,12 +79,11 @@ namespace NzbDrone.Core.Test
                 .Setup(s => s.DownloadReport(It.IsAny<EpisodeParseResult>())).Returns(true);
 
             //Act
-            mocker.Resolve<SearchProvider>().SeasonSearch(notification, 1, 1);
+            var result = mocker.Resolve<SearchProvider>().SeasonSearch(notification, 1, 1);
 
             //Assert
+            result.Should().BeTrue();
             mocker.VerifyAllMocks();
-            mocker.GetMock<EpisodeSearchJob>().Verify(c => c.Start(notification, It.IsAny<int>(), 0),
-                                                       Times.Never());
         }
 
         [Test]
@@ -134,14 +133,12 @@ namespace NzbDrone.Core.Test
                 .Setup(s => s.GetSceneName(1)).Returns(String.Empty);
 
             //Act
-            mocker.Resolve<SearchProvider>().SeasonSearch(notification, 1, 1);
+            var result = mocker.Resolve<SearchProvider>().SeasonSearch(notification, 1, 1);
 
             //Assert
             ExceptionVerification.ExcpectedWarns(1);
+            result.Should().BeFalse();
             mocker.VerifyAllMocks();
-            mocker.GetMock<EpisodeSearchJob>().Verify(c => c.Start(notification, It.IsAny<int>(), 0),
-                                                       Times.Never());
-            
         }
 
         [Test]
@@ -171,13 +168,11 @@ namespace NzbDrone.Core.Test
                 .Setup(s => s.DownloadReport(It.IsAny<EpisodeParseResult>())).Returns(true);
 
             //Act
-            mocker.Resolve<SearchProvider>().ProcessSeasonSearchResults(notification, series, 1, parseResults);
+            var result = mocker.Resolve<SearchProvider>().ProcessSeasonSearchResults(notification, series, 1, parseResults);
 
             //Assert
+            result.Should().BeTrue();
             mocker.VerifyAllMocks();
-            mocker.GetMock<EpisodeSearchJob>().Verify(c => c.Start(notification, It.IsAny<int>(), 0),
-                                                       Times.Never());
-
         }
 
         [Test]
@@ -204,14 +199,12 @@ namespace NzbDrone.Core.Test
                 .Setup(s => s.IsQualityNeeded(It.IsAny<EpisodeParseResult>())).Returns(false);
 
             //Act
-            mocker.Resolve<SearchProvider>().ProcessSeasonSearchResults(notification, series, 1, parseResults);
+            var result = mocker.Resolve<SearchProvider>().ProcessSeasonSearchResults(notification, series, 1, parseResults);
 
             //Assert
+            result.Should().BeFalse();
             ExceptionVerification.ExcpectedWarns(1);
             mocker.VerifyAllMocks();
-            mocker.GetMock<EpisodeSearchJob>().Verify(c => c.Start(notification, It.IsAny<int>(), 0),
-                                                       Times.Never());
-
         }
     }
 }
\ No newline at end of file
diff --git a/NzbDrone.Core.Test/SeasonSearchJobTest.cs b/NzbDrone.Core.Test/SeasonSearchJobTest.cs
new file mode 100644
index 000000000..3dab3660e
--- /dev/null
+++ b/NzbDrone.Core.Test/SeasonSearchJobTest.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AutoMoq;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.Model;
+using NzbDrone.Core.Model.Notification;
+using NzbDrone.Core.Providers;
+using NzbDrone.Core.Providers.Indexer;
+using NzbDrone.Core.Providers.Jobs;
+using NzbDrone.Core.Repository;
+using NzbDrone.Core.Repository.Quality;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test
+{
+    [TestFixture]
+    // ReSharper disable InconsistentNaming
+    public class SeasonSearchJobTest : TestBase
+    {
+        [Test]
+        public void SeasonSearch_full_season_success()
+        {
+            var mocker = new AutoMoqer(MockBehavior.Strict);
+
+            var notification = new ProgressNotification("Season Search");
+
+            mocker.GetMock<SearchProvider>()
+                .Setup(c => c.SeasonSearch(notification, 1, 1)).Returns(true);
+
+            //Act
+            mocker.Resolve<SeasonSearchJob>().Start(notification, 1, 1);
+
+            //Assert
+            mocker.VerifyAllMocks();
+            mocker.GetMock<SearchProvider>().Verify(c => c.SeasonSearch(notification, 1, 1), Times.Once());
+            mocker.GetMock<SearchProvider>().Verify(c => c.PartialSeasonSearch(notification, 1, 1), Times.Never());
+            mocker.GetMock<EpisodeSearchJob>().Verify(c => c.Start(notification, It.IsAny<int>(), 0), Times.Never());
+        }
+
+        [Test]
+        public void SeasonSearch_partial_season_success()
+        {
+            var episodes = Builder<Episode>.CreateListOfSize(5)
+                .WhereAll()
+                .Have(e => e.SeriesId = 1)
+                .Have(e => e.SeasonNumber = 1)
+                .Build();
+
+            var mocker = new AutoMoqer(MockBehavior.Strict);
+
+            var notification = new ProgressNotification("Season Search");
+
+            mocker.GetMock<SearchProvider>()
+                .Setup(c => c.SeasonSearch(notification, 1, 1)).Returns(false);
+
+            mocker.GetMock<EpisodeProvider>()
+                .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes);
+
+            mocker.GetMock<SearchProvider>()
+                .Setup(c => c.PartialSeasonSearch(notification, 1, 1))
+                .Returns(episodes.Select(e => e.EpisodeNumber).ToList());
+
+            //Act
+            mocker.Resolve<SeasonSearchJob>().Start(notification, 1, 1);
+
+            //Assert
+            mocker.VerifyAllMocks();
+            mocker.GetMock<SearchProvider>().Verify(c => c.SeasonSearch(notification, 1, 1), Times.Once());
+            mocker.GetMock<SearchProvider>().Verify(c => c.PartialSeasonSearch(notification, 1, 1), Times.Once());
+            mocker.GetMock<EpisodeSearchJob>().Verify(c => c.Start(notification, It.IsAny<int>(), 0), Times.Never());
+        }
+
+        [Test]
+        public void SeasonSearch_partial_season_failure()
+        {
+            var episodes = Builder<Episode>.CreateListOfSize(5)
+                .WhereAll()
+                .Have(e => e.SeriesId = 1)
+                .Have(e => e.SeasonNumber = 1)
+                .Have(e => e.Ignored = false)
+                .Build();
+
+            var mocker = new AutoMoqer(MockBehavior.Strict);
+
+            var notification = new ProgressNotification("Season Search");
+
+            mocker.GetMock<SearchProvider>()
+                .Setup(c => c.SeasonSearch(notification, 1, 1)).Returns(false);
+
+            mocker.GetMock<EpisodeProvider>()
+                .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes);
+
+            mocker.GetMock<SearchProvider>()
+                .Setup(c => c.PartialSeasonSearch(notification, 1, 1))
+                .Returns(new List<int>{1});
+
+            mocker.GetMock<EpisodeSearchJob>()
+                .Setup(c => c.Start(notification, It.IsAny<int>(), 0)).Verifiable();
+
+            //Act
+            mocker.Resolve<SeasonSearchJob>().Start(notification, 1, 1);
+
+            //Assert
+            mocker.VerifyAllMocks();
+            mocker.GetMock<SearchProvider>().Verify(c => c.SeasonSearch(notification, 1, 1), Times.Once());
+            mocker.GetMock<SearchProvider>().Verify(c => c.PartialSeasonSearch(notification, 1, 1), Times.Once());
+            mocker.GetMock<EpisodeSearchJob>().Verify(c => c.Start(notification, It.IsAny<int>(), 0), Times.Exactly(4));
+        }
+    }
+}
\ No newline at end of file
diff --git a/NzbDrone.Core/Model/Search/SearchModel.cs b/NzbDrone.Core/Model/Search/SearchModel.cs
index 70ef93960..85520b106 100644
--- a/NzbDrone.Core/Model/Search/SearchModel.cs
+++ b/NzbDrone.Core/Model/Search/SearchModel.cs
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Model.Search
         public string SeriesTitle { get; set; }
         public int EpisodeNumber { get; set; }
         public int SeasonNumber { get; set; }
+        public int EpisodePrefix { get; set; }
         public DateTime AirDate { get; set; }
         public SearchType SearchType { get; set; }
     }
diff --git a/NzbDrone.Core/Model/Search/SearchType.cs b/NzbDrone.Core/Model/Search/SearchType.cs
index deac8a229..6a9666da7 100644
--- a/NzbDrone.Core/Model/Search/SearchType.cs
+++ b/NzbDrone.Core/Model/Search/SearchType.cs
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.Model.Search
     {
         EpisodeSearch = 0,
         DailySearch = 1,
-        SeasonSearch = 2
+        PartialSeasonSearch = 2,
+        SeasonSearch = 3
     }
 }
diff --git a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs
index 333ba0ac3..39c3bd041 100644
--- a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs
+++ b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs
@@ -124,6 +124,33 @@ namespace NzbDrone.Core.Providers.Indexer
             return result;
         }
 
+        public virtual IList<EpisodeParseResult> FetchPartialSeason(string seriesTitle, int seasonNumber, int episodePrefix)
+        {
+            _logger.Debug("Searching {0} for {1}-Season {2}, Prefix: {3}", Name, seriesTitle, seasonNumber, episodePrefix);
+
+            var result = new List<EpisodeParseResult>();
+
+            var searchModel = new SearchModel
+            {
+                SeriesTitle = GetQueryTitle(seriesTitle),
+                SeasonNumber = seasonNumber,
+                EpisodePrefix = episodePrefix,
+                SearchType = SearchType.PartialSeasonSearch
+            };
+
+            var searchUrls = GetSearchUrls(searchModel);
+
+            foreach (var url in searchUrls)
+            {
+                result.AddRange(Fetch(url));
+            }
+
+            result = result.Where(e => e.CleanTitle == Parser.NormalizeTitle(seriesTitle)).ToList();
+
+            _logger.Info("Finished searching {0} for {1}-S{2}, Found {3}", Name, seriesTitle, seasonNumber, result.Count);
+            return result;
+        }
+
         public virtual IList<EpisodeParseResult> FetchEpisode(string seriesTitle, int seasonNumber, int episodeNumber)
         {
             _logger.Debug("Searching {0} for {1}-S{2:00}E{3:00}", Name, seriesTitle, seasonNumber, episodeNumber);
diff --git a/NzbDrone.Core/Providers/Indexer/Newzbin.cs b/NzbDrone.Core/Providers/Indexer/Newzbin.cs
index 3edb4e6b6..ae99761d4 100644
--- a/NzbDrone.Core/Providers/Indexer/Newzbin.cs
+++ b/NzbDrone.Core/Providers/Indexer/Newzbin.cs
@@ -52,12 +52,27 @@ namespace NzbDrone.Core.Providers.Indexer
                            };
             }
 
-            return new List<string>
+            if (searchModel.SearchType == SearchType.SeasonSearch)
+            {
+                return new List<string>
                            {
                                String.Format(
                                    @"http://www.newzbin.com/search/query/?q={0}+Season+{1}&fpn=p&searchaction=Go&category=8&{2}",
                                    searchModel.SeriesTitle, searchModel.SeasonNumber, UrlParams)
                            };
+            }
+
+            if (searchModel.SearchType == SearchType.PartialSeasonSearch)
+            {
+                return new List<string>
+                           {
+                               String.Format(
+                                   @"http://www.newzbin.com/search/query/?q={0}+{1}x{2}&fpn=p&searchaction=Go&category=8&{3}",
+                                   searchModel.SeriesTitle, searchModel.SeasonNumber, searchModel.EpisodePrefix, UrlParams)
+                           };
+            }
+
+            return new List<string>();
         }
 
         public override string Name
diff --git a/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs b/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs
index 4366490cb..e9799e6c8 100644
--- a/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs
+++ b/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs
@@ -51,7 +51,13 @@ namespace NzbDrone.Core.Providers.Indexer
                                                  searchModel.SeasonNumber, searchModel.EpisodeNumber));
                 }
 
-                else
+                if (searchModel.SearchType == SearchType.PartialSeasonSearch)
+                {
+                    searchUrls.Add(String.Format("{0}&term={1}+S{2:00}E{3}",
+                        url, searchModel.SeriesTitle, searchModel.SeasonNumber, searchModel.EpisodePrefix));
+                }
+
+                if (searchModel.SearchType == SearchType.SeasonSearch)
                 {
                     searchUrls.Add(String.Format("{0}&term={1}+Season", url, searchModel.SeriesTitle));
                     searchUrls.Add(String.Format("{0}&term={1}+S{2:00}", url, searchModel.SeriesTitle, searchModel.SeasonNumber));
diff --git a/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs b/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs
index 7d3d60576..cb6acef3c 100644
--- a/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs
+++ b/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs
@@ -49,7 +49,13 @@ namespace NzbDrone.Core.Providers.Indexer
                                                  searchModel.SeriesTitle, searchModel.SeasonNumber, searchModel.EpisodeNumber));
                 }
 
-                else
+                if (searchModel.SearchType == SearchType.PartialSeasonSearch)
+                {
+                    searchUrls.Add(String.Format("{0}&action=search&q={1}+S{2:00}E{3}",
+                        url, searchModel.SeriesTitle, searchModel.SeasonNumber, searchModel.EpisodePrefix));
+                }
+
+                if (searchModel.SearchType == SearchType.SeasonSearch)
                 {
                     searchUrls.Add(String.Format("{0}&action=search&q={1}+Season", url, searchModel.SeriesTitle));
                     searchUrls.Add(String.Format("{0}&action=search&q={1}+S{2:00}", url, searchModel.SeriesTitle, searchModel.SeasonNumber));
diff --git a/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs b/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs
index 9f90c6e58..cfc224537 100644
--- a/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs
+++ b/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs
@@ -59,7 +59,21 @@ namespace NzbDrone.Core.Providers.Jobs
                 return;
             }
 
-            foreach (var episode in episodes.Where(e => !e.Ignored))
+            //Perform a Partial Season Search
+            var addedSeries = _searchProvider.PartialSeasonSearch(notification, targetId, secondaryTargetId);
+
+            addedSeries.Distinct().ToList().Sort();
+            var episodeNumbers = episodes.Select(s => s.EpisodeNumber).ToList();
+            episodeNumbers.Sort();
+
+            if (addedSeries.SequenceEqual(episodeNumbers))
+                return;
+            
+            //Get the list of episodes that weren't downloaded
+            var missingEpisodes = episodeNumbers.Except(addedSeries).ToList();
+
+            //Only process episodes that is in missing episodes (To ensure we double check if the episode is available)
+            foreach (var episode in episodes.Where(e => !e.Ignored && missingEpisodes.Contains(e.EpisodeNumber)))
             {
                 _episodeSearchJob.Start(notification, episode.EpisodeId, 0);
             }
diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs
index 01afb165f..5e7f713ac 100644
--- a/NzbDrone.Core/Providers/SearchProvider.cs
+++ b/NzbDrone.Core/Providers/SearchProvider.cs
@@ -229,5 +229,106 @@ namespace NzbDrone.Core.Providers
 
             return false;
         }
+
+        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 series = _seriesProvider.GetSeries(seriesId);
+
+            if (series == null)
+            {
+                Logger.Error("Unable to find an series {0} in database", seriesId);
+                return new List<int>();
+            }
+
+            notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series, seasonNumber);
+
+            var indexers = _indexerProvider.GetEnabledIndexers();
+            var reports = new List<EpisodeParseResult>();
+
+            var title = _sceneMappingProvider.GetSceneName(series.SeriesId);
+
+            if (string.IsNullOrWhiteSpace(title))
+            {
+                title = series.Title;
+            }
+
+            var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber);
+            var episodeCount = episodes.Count;
+            var episodePrefix = 0;
+
+            while(episodeCount >= 0)
+            {
+                //Do the actual search for each indexer
+                foreach (var indexer in indexers)
+                {
+                    try
+                    {
+                        var indexerResults = indexer.FetchPartialSeason(title, seasonNumber, episodePrefix);
+
+                        reports.AddRange(indexerResults);
+                    }
+                    catch (Exception e)
+                    {
+                        Logger.ErrorException("An error has occurred while fetching items from " + indexer.Name, e);
+                    }
+                }
+
+                episodePrefix++;
+                episodeCount -= 10;
+            }
+
+            Logger.Debug("Finished searching all indexers. Total {0}", reports.Count);
+
+            if (reports.Count == 0)
+                return new List<int>();
+
+            notification.CurrentMessage = "Processing search results";
+
+            reports.ForEach(c =>
+            {
+                c.Series = series;
+            });
+
+            return  ProcessPartialSeasonSearchResults(notification, reports);
+        }
+
+        public List<int> ProcessPartialSeasonSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports)
+        {
+            var successes = new List<int>();
+
+            foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality))
+            {
+                try
+                {
+                    Logger.Trace("Analysing report " + episodeParseResult);
+                    if (_inventoryProvider.IsQualityNeeded(episodeParseResult))
+                    {
+                        Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult);
+                        try
+                        {
+                            _downloadProvider.DownloadReport(episodeParseResult);
+                            notification.CurrentMessage = String.Format("{0} - S{1:00}E{2:00} {3}Added to download queue",
+                                episodeParseResult.Series.Title, episodeParseResult.SeasonNumber, episodeParseResult.EpisodeNumbers[0], episodeParseResult.Quality);
+
+                            //Add the list of episode numbers from this release
+                            successes.AddRange(episodeParseResult.EpisodeNumbers);
+                        }
+                        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);
+                        }
+                    }
+                }
+                catch (Exception e)
+                {
+                    Logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e);
+                }
+            }
+
+            return successes;
+        }
     }
 }