diff --git a/NzbDrone.Core.Test/EpisodeProviderTest.cs b/NzbDrone.Core.Test/EpisodeProviderTest.cs index e48aa22d7..3d3112305 100644 --- a/NzbDrone.Core.Test/EpisodeProviderTest.cs +++ b/NzbDrone.Core.Test/EpisodeProviderTest.cs @@ -7,6 +7,7 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Model; using NzbDrone.Core.Providers; using NzbDrone.Core.Providers.Core; using NzbDrone.Core.Repository; @@ -1464,5 +1465,42 @@ namespace NzbDrone.Core.Test //Assert result.Should().BeFalse(); } + + [TestCase("The Office (US) - S01E05 - Episode Title", PostDownloadStatusType.Unpacking, 1)] + [TestCase("The Office (US) - S01E05 - Episode Title", PostDownloadStatusType.Failed, 1)] + [TestCase("The Office (US) - S01E05E06 - Episode Title", PostDownloadStatusType.Unpacking, 2)] + [TestCase("The Office (US) - S01E05E06 - Episode Title", PostDownloadStatusType.Failed, 2)] + [TestCase("The Office (US) - Season 01 - Episode Title", PostDownloadStatusType.Unpacking, 10)] + [TestCase("The Office (US) - Season 01 - Episode Title", PostDownloadStatusType.Failed, 10)] + public void SetPostDownloadStatus(string folderName, PostDownloadStatusType postDownloadStatus, int episodeCount) + { + var db = MockLib.GetEmptyDatabase(); + var mocker = new AutoMoqer(); + mocker.SetConstant(db); + + var fakeSeries = Builder.CreateNew() + .With(s => s.SeriesId = 12345) + .With(s => s.CleanTitle = "officeus") + .Build(); + + var fakeEpisodes = Builder.CreateListOfSize(10) + .WhereAll() + .Have(c => c.SeriesId = 12345) + .Have(c => c.SeasonNumber = 1) + .Have(c => c.PostDownloadStatus = PostDownloadStatusType.Unknown) + .Build(); + + db.Insert(fakeSeries); + db.InsertMany(fakeEpisodes); + + mocker.GetMock().Setup(s => s.FindSeries("officeus")).Returns(fakeSeries); + + //Act + mocker.Resolve().SetPostDownloadStatus(folderName, postDownloadStatus); + + //Assert + var result = db.Fetch(); + result.Where(e => e.PostDownloadStatus == postDownloadStatus).Count().Should().Be(episodeCount); + } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/EpisodeProviderTest_GetEpisodesByParseResult.cs b/NzbDrone.Core.Test/EpisodeProviderTest_GetEpisodesByParseResult.cs index 70c61f288..1b7e5648f 100644 --- a/NzbDrone.Core.Test/EpisodeProviderTest_GetEpisodesByParseResult.cs +++ b/NzbDrone.Core.Test/EpisodeProviderTest_GetEpisodesByParseResult.cs @@ -101,7 +101,6 @@ namespace NzbDrone.Core.Test db.Fetch().Should().HaveCount(1); } - [Test] public void Multi_GetSeason_Episode_Exists() { @@ -235,5 +234,69 @@ namespace NzbDrone.Core.Test db.Fetch().Should().HaveCount(2); ep.First().Ignored.Should().BeFalse(); } + + [Test] + public void Full_Season_return_all_episodes_for_season() + { + var mocker = new AutoMoqer(); + var db = MockLib.GetEmptyDatabase(); + mocker.SetConstant(db); + + var fakeSeries = Builder.CreateNew().Build(); + + var fakeEpisodes = Builder.CreateListOfSize(10) + .WhereAll() + .Have(e => e.SeriesId = fakeSeries.SeriesId) + .Have(e => e.SeasonNumber = 2) + .Build(); + + db.Insert(fakeSeries); + db.InsertMany(fakeEpisodes); + + var parseResult = new EpisodeParseResult + { + Series = fakeSeries, + SeasonNumber = 2, + EpisodeNumbers = new List(), + FullSeason = true + }; + + var ep = mocker.Resolve().GetEpisodesByParseResult(parseResult); + + ep.Should().HaveCount(10); + db.Fetch().Should().HaveCount(10); + } + + [Test] + public void No_Episodes_Not_a_proper_full_season_release() + { + var mocker = new AutoMoqer(); + var db = MockLib.GetEmptyDatabase(); + mocker.SetConstant(db); + + var fakeSeries = Builder.CreateNew().Build(); + + var fakeEpisodes = Builder.CreateListOfSize(10) + .WhereAll() + .Have(e => e.SeriesId = fakeSeries.SeriesId) + .Have(e => e.SeasonNumber = 2) + .Build(); + + db.Insert(fakeSeries); + db.InsertMany(fakeEpisodes); + + var parseResult = new EpisodeParseResult + { + Series = fakeSeries, + SeasonNumber = 2, + EpisodeNumbers = new List(), + FullSeason = false + }; + + var ep = mocker.Resolve().GetEpisodesByParseResult(parseResult); + + ep.Should().HaveCount(0); + db.Fetch().Should().HaveCount(10); + } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/EpisodeStatusTest.cs b/NzbDrone.Core.Test/EpisodeStatusTest.cs index 8d498a1ac..c0a6adfac 100644 --- a/NzbDrone.Core.Test/EpisodeStatusTest.cs +++ b/NzbDrone.Core.Test/EpisodeStatusTest.cs @@ -108,5 +108,27 @@ namespace NzbDrone.Core.Test episode.Status.Should().Be(EpisodeStatusType.NotAired); } + + [TestCase(false, false, EpisodeStatusType.Failed, PostDownloadStatusType.Failed)] + [TestCase(false, false, EpisodeStatusType.Unpacking, PostDownloadStatusType.Unpacking)] + [TestCase(true, false, EpisodeStatusType.Ready, PostDownloadStatusType.Failed)] + [TestCase(true, true, EpisodeStatusType.Ready, PostDownloadStatusType.Unpacking)] + public void episode_downloaded_post_download_status_is_used(bool hasEpisodes, bool ignored, + EpisodeStatusType status, PostDownloadStatusType postDownloadStatus) + { + Episode episode = Builder.CreateNew() + .With(e => e.Ignored = ignored) + .With(e => e.EpisodeFileId = 0) + .With(e => e.GrabDate = DateTime.Now.AddHours(22)) + .With(e => e.PostDownloadStatus = postDownloadStatus) + .Build(); + + if (hasEpisodes) + { + episode.EpisodeFileId = 12; + } + + Assert.AreEqual(status, episode.Status); + } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index dd19dde3b..dc45e4208 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -21,7 +21,7 @@ DEBUG;TRACE prompt 4 - AnyCPU + x86 pdbonly diff --git a/NzbDrone.Core/Datastore/Migrations/Migration20111011.cs b/NzbDrone.Core/Datastore/Migrations/Migration20111011.cs new file mode 100644 index 000000000..0e8d97a8c --- /dev/null +++ b/NzbDrone.Core/Datastore/Migrations/Migration20111011.cs @@ -0,0 +1,21 @@ +using System; +using System.Data; +using Migrator.Framework; + +namespace NzbDrone.Core.Datastore.Migrations +{ + + [Migration(20111011)] + public class Migration20111011 : Migration + { + public override void Up() + { + Database.AddColumn("Episodes", "PostDownloadStatus", DbType.Int32, ColumnProperty.Null); + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Model/EpisodeStatusType.cs b/NzbDrone.Core/Model/EpisodeStatusType.cs index 345706da6..db98c54bb 100644 --- a/NzbDrone.Core/Model/EpisodeStatusType.cs +++ b/NzbDrone.Core/Model/EpisodeStatusType.cs @@ -5,7 +5,7 @@ /// /// Episode has not aired yet /// - NotAired , + NotAired, /// /// Episode is ignored @@ -16,12 +16,22 @@ /// Episode has aired but no episode /// files have avilable /// - Missing , + Missing, /// /// Episode is being downloaded /// - Downloading , + Downloading, + + /// + /// Episode has been downloaded and is unpacking (_UNPACK_) + /// + Unpacking, + + /// + /// Episode has failed to download properly (_FAILED_) + /// + Failed, /// /// Episode is present in disk diff --git a/NzbDrone.Core/Model/PostDownloadStatusType.cs b/NzbDrone.Core/Model/PostDownloadStatusType.cs new file mode 100644 index 000000000..a330c8954 --- /dev/null +++ b/NzbDrone.Core/Model/PostDownloadStatusType.cs @@ -0,0 +1,25 @@ +namespace NzbDrone.Core.Model +{ + public enum PostDownloadStatusType + { + /// + /// Unknown (Default) + /// + Unknown = 0, + + /// + /// Unpacking + /// + Unpacking = 1, + + /// + /// Failed + /// + Failed = 2, + + /// + /// Processed + /// + Processed = 3 + } +} \ No newline at end of file diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 5a958d2e4..a935d6dfb 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -176,6 +176,7 @@ + @@ -192,6 +193,7 @@ + diff --git a/NzbDrone.Core/Providers/DiskScanProvider.cs b/NzbDrone.Core/Providers/DiskScanProvider.cs index 3348910f3..070d2ad9a 100644 --- a/NzbDrone.Core/Providers/DiskScanProvider.cs +++ b/NzbDrone.Core/Providers/DiskScanProvider.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Ninject; using NLog; +using NzbDrone.Core.Model; using NzbDrone.Core.Providers.Core; using NzbDrone.Core.Repository; using PetaPoco; @@ -146,6 +147,7 @@ namespace NzbDrone.Core.Providers foreach (var ep in episodes) { ep.EpisodeFileId = fileId; + ep.PostDownloadStatus = PostDownloadStatusType.Processed; _episodeProvider.UpdateEpisode(ep); Logger.Debug("Linking [{0}] > [{1}]", filePath, ep); } diff --git a/NzbDrone.Core/Providers/EpisodeProvider.cs b/NzbDrone.Core/Providers/EpisodeProvider.cs index eac5ef7fe..676f1c144 100644 --- a/NzbDrone.Core/Providers/EpisodeProvider.cs +++ b/NzbDrone.Core/Providers/EpisodeProvider.cs @@ -125,6 +125,14 @@ namespace NzbDrone.Core.Providers { var result = new List(); + if (parseResult.EpisodeNumbers.Count == 0 && parseResult.FullSeason) + { + result.AddRange(GetEpisodesBySeason(parseResult.Series.SeriesId, parseResult.SeasonNumber)); + + //Return now as no further processing is required + return result; + } + foreach (var episodeNumber in parseResult.EpisodeNumbers) { var episodeInfo = GetEpisode(parseResult.Series.SeriesId, parseResult.SeasonNumber, episodeNumber); @@ -396,5 +404,20 @@ namespace NzbDrone.Core.Providers Logger.Trace("Finished deleting invalid episodes for {0}", series.SeriesId); } + + public virtual void SetPostDownloadStatus(string folderName, PostDownloadStatusType postDownloadStatus) + { + var parseResult = Parser.ParseTitle(folderName); + parseResult.Series = _seriesProvider.FindSeries(parseResult.CleanTitle); + + var episodeIds = GetEpisodesByParseResult(parseResult).Select(e => e.EpisodeId); + var episodeIdString = String.Join(", ", episodeIds); + + var episodeIdQuery = String.Format(@"UPDATE Episodes SET PostDownloadStatus = {0} + WHERE EpisodeId IN ({1})", (int)postDownloadStatus, episodeIdString); + + Logger.Trace("Updating PostDownloadStatus for all episodeIds in {0}", episodeIdString); + _database.Execute(episodeIdQuery); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Providers/Jobs/PostDownloadScanJob.cs b/NzbDrone.Core/Providers/Jobs/PostDownloadScanJob.cs index 97e5feb6d..3d393058f 100644 --- a/NzbDrone.Core/Providers/Jobs/PostDownloadScanJob.cs +++ b/NzbDrone.Core/Providers/Jobs/PostDownloadScanJob.cs @@ -2,6 +2,7 @@ using System.IO; using Ninject; using NLog; +using NzbDrone.Core.Model; using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers.Core; @@ -13,16 +14,19 @@ namespace NzbDrone.Core.Providers.Jobs private readonly DiskProvider _diskProvider; private readonly DiskScanProvider _diskScanProvider; private readonly SeriesProvider _seriesProvider; + private readonly EpisodeProvider _episodeProvider; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); [Inject] public PostDownloadScanJob(ConfigProvider configProvider, DiskProvider diskProvider, - DiskScanProvider diskScanProvider, SeriesProvider seriesProvider) + DiskScanProvider diskScanProvider, SeriesProvider seriesProvider, + EpisodeProvider episodeProvider) { _configProvider = configProvider; _diskProvider = diskProvider; _diskScanProvider = diskScanProvider; _seriesProvider = seriesProvider; + _episodeProvider = episodeProvider; } public PostDownloadScanJob() @@ -63,12 +67,14 @@ namespace NzbDrone.Core.Providers.Jobs if (subfolderInfo.Name.StartsWith("_UNPACK_", StringComparison.CurrentCultureIgnoreCase)) { + _episodeProvider.SetPostDownloadStatus(subfolderInfo.Name.Substring(8), PostDownloadStatusType.Unpacking); Logger.Debug("Folder [{0}] is still being unpacked. skipping.", subfolder); continue; } if (subfolderInfo.Name.StartsWith("_FAILED_", StringComparison.CurrentCultureIgnoreCase)) { + _episodeProvider.SetPostDownloadStatus(subfolderInfo.Name.Substring(8), PostDownloadStatusType.Failed); Logger.Debug("Folder [{0}] is marked as failed. skipping.", subfolder); continue; } diff --git a/NzbDrone.Core/Repository/Episode.cs b/NzbDrone.Core/Repository/Episode.cs index 12dc398d3..2ad2562ca 100644 --- a/NzbDrone.Core/Repository/Episode.cs +++ b/NzbDrone.Core/Repository/Episode.cs @@ -23,6 +23,8 @@ namespace NzbDrone.Core.Repository public Boolean Ignored { get; set; } + public PostDownloadStatusType PostDownloadStatus { get; set; } + /// /// Gets or sets the grab date. /// @@ -39,15 +41,23 @@ namespace NzbDrone.Core.Repository { if (EpisodeFileId != 0) return EpisodeStatusType.Ready; - if (GrabDate != null && GrabDate.Value.AddDays(1) >= DateTime.Now) + if (GrabDate != null) { - return EpisodeStatusType.Downloading; + if (PostDownloadStatus == PostDownloadStatusType.Unpacking) + return EpisodeStatusType.Unpacking; + + if (PostDownloadStatus == PostDownloadStatusType.Failed) + return EpisodeStatusType.Failed; + + if (GrabDate.Value.AddDays(1) >= DateTime.Now) + return EpisodeStatusType.Downloading; } + if (GrabDate != null && GrabDate.Value.AddDays(1) >= DateTime.Now) + return EpisodeStatusType.Downloading; + if (AirDate != null && AirDate.Value.Date < DateTime.Now) - { return EpisodeStatusType.Missing; - } return EpisodeStatusType.NotAired; } diff --git a/NzbDrone.Web/Content/Images/Failed.png b/NzbDrone.Web/Content/Images/Failed.png new file mode 100644 index 000000000..0da860271 Binary files /dev/null and b/NzbDrone.Web/Content/Images/Failed.png differ diff --git a/NzbDrone.Web/Content/Images/Unpacking.png b/NzbDrone.Web/Content/Images/Unpacking.png new file mode 100644 index 000000000..7ed2b5905 Binary files /dev/null and b/NzbDrone.Web/Content/Images/Unpacking.png differ diff --git a/NzbDrone.Web/NzbDrone.Web.csproj b/NzbDrone.Web/NzbDrone.Web.csproj index 39eb8d0f4..1068453a3 100644 --- a/NzbDrone.Web/NzbDrone.Web.csproj +++ b/NzbDrone.Web/NzbDrone.Web.csproj @@ -333,6 +333,7 @@ + @@ -340,6 +341,7 @@ +