From 7771899e29a5b91886776c810f705907fec791fb Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 22 Aug 2013 08:34:51 -0700 Subject: [PATCH 1/6] NotUnpacking check added to episode import Checks last write time when in working folder, should help with moving while unpacking --- NzbDrone.Common/DiskProvider.cs | 1 - .../NotUnpackingSpecificationFixture.cs | 77 +++++++++++++++++++ NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 1 + NzbDrone.Core/Configuration/ConfigService.cs | 6 ++ NzbDrone.Core/Configuration/IConfigService.cs | 1 + .../NotUnpackingSpecification.cs | 48 ++++++++++++ NzbDrone.Core/NzbDrone.Core.csproj | 1 + 7 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 NzbDrone.Core.Test/MediaFileTests/EpisodeImportTests/NotUnpackingSpecificationFixture.cs create mode 100644 NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs diff --git a/NzbDrone.Common/DiskProvider.cs b/NzbDrone.Common/DiskProvider.cs index 9ffdb8a77..062cd79d7 100644 --- a/NzbDrone.Common/DiskProvider.cs +++ b/NzbDrone.Common/DiskProvider.cs @@ -93,7 +93,6 @@ namespace NzbDrone.Common { Ensure.That(() => path).IsValidPath(); - if (!FileExists(path)) throw new FileNotFoundException("File doesn't exist: " + path); diff --git a/NzbDrone.Core.Test/MediaFileTests/EpisodeImportTests/NotUnpackingSpecificationFixture.cs b/NzbDrone.Core.Test/MediaFileTests/EpisodeImportTests/NotUnpackingSpecificationFixture.cs new file mode 100644 index 000000000..102f7a4f1 --- /dev/null +++ b/NzbDrone.Core.Test/MediaFileTests/EpisodeImportTests/NotUnpackingSpecificationFixture.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Marr.Data; +using Moq; +using Newtonsoft.Json.Serialization; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests +{ + [TestFixture] + public class NotUnpackingSpecificationFixture : CoreTest + { + private LocalEpisode _localEpisode; + + [SetUp] + public void Setup() + { + Mocker.GetMock() + .SetupGet(s => s.DownloadClientWorkingFolders) + .Returns("_UNPACK_|_FAILED_"); + + _localEpisode = new LocalEpisode + { + Path = @"C:\Test\Unsorted TV\30.rock\30.rock.s01e01.avi".AsOsAgnostic(), + Size = 100, + Series = Builder.CreateNew().Build() + }; + } + + private void GivenInWorkingFolder() + { + _localEpisode.Path = @"C:\Test\Unsorted TV\_UNPACK_30.rock\30.rock.s01e01.avi".AsOsAgnostic(); + } + + private void GivenLastWriteTimeUtc(DateTime time) + { + Mocker.GetMock() + .Setup(s => s.GetLastFileWrite(It.IsAny())) + .Returns(time); + } + + [Test] + public void should_return_true_if_not_in_working_folder() + { + Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + } + + [Test] + public void should_return_true_when_in_old_working_folder() + { + GivenInWorkingFolder(); + GivenLastWriteTimeUtc(DateTime.UtcNow.AddHours(-1)); + + Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + } + + [Test] + public void should_return_false_if_in_working_folder_and_last_write_time_was_recent() + { + GivenInWorkingFolder(); + GivenLastWriteTimeUtc(DateTime.UtcNow); + + Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + } + } +} diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index a65e1eb59..8ed2f855d 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -129,6 +129,7 @@ + diff --git a/NzbDrone.Core/Configuration/ConfigService.cs b/NzbDrone.Core/Configuration/ConfigService.cs index a98e06cb5..0d9189fc6 100644 --- a/NzbDrone.Core/Configuration/ConfigService.cs +++ b/NzbDrone.Core/Configuration/ConfigService.cs @@ -258,6 +258,12 @@ namespace NzbDrone.Core.Configuration set { SetValue("AutoDownloadPropers", value); } } + public string DownloadClientWorkingFolders + { + get { return GetValue("DownloadClientWorkingFolders", "_UNPACK_|_FAILED_"); } + set { SetValue("DownloadClientWorkingFolders", value); } + } + private string GetValue(string key) { return GetValue(key, String.Empty); diff --git a/NzbDrone.Core/Configuration/IConfigService.cs b/NzbDrone.Core/Configuration/IConfigService.cs index 0b5779a37..07e70d25b 100644 --- a/NzbDrone.Core/Configuration/IConfigService.cs +++ b/NzbDrone.Core/Configuration/IConfigService.cs @@ -38,6 +38,7 @@ namespace NzbDrone.Core.Configuration string ReleaseRestrictions { get; set; } Int32 RssSyncInterval { get; set; } Boolean AutoDownloadPropers { get; set; } + String DownloadClientWorkingFolders { get; set; } void SaveValues(Dictionary configValues); } } diff --git a/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs new file mode 100644 index 000000000..0f1e66d3a --- /dev/null +++ b/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class NotUnpackingSpecification : IImportDecisionEngineSpecification + { + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public NotUnpackingSpecification(IDiskProvider diskProvider, IConfigService configService, Logger logger) + { + _diskProvider = diskProvider; + _configService = configService; + _logger = logger; + } + + public string RejectionReason { get { return "File is still being unpacked"; } } + + public bool IsSatisfiedBy(LocalEpisode localEpisode) + { + if (_diskProvider.IsParent(localEpisode.Series.Path, localEpisode.Path)) + { + _logger.Trace("{0} is in series folder, unpacking check", localEpisode.Path); + return true; + } + + foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|')) + { + if (Directory.GetParent(localEpisode.Path).Name.StartsWith(workingFolder)) + { + if (_diskProvider.GetLastFileWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) + { + _logger.Trace("{0} appears to be unpacking still", localEpisode.Path); + return false; + } + } + } + + return true; + } + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index bc4e017d5..57b9acb0a 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -225,6 +225,7 @@ + From eb6d6e74b0d3defae558e730ee63cd58c06eb41a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 22 Aug 2013 18:52:19 -0700 Subject: [PATCH 2/6] Fixed add series referencing spinnerView --- UI/AddSeries/AddSeriesView.js | 6 +++--- UI/Episode/Summary/Layout.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UI/AddSeries/AddSeriesView.js b/UI/AddSeries/AddSeriesView.js index 7a6fd002b..a28433c53 100644 --- a/UI/AddSeries/AddSeriesView.js +++ b/UI/AddSeries/AddSeriesView.js @@ -5,8 +5,8 @@ define( 'marionette', 'AddSeries/Collection', 'AddSeries/SearchResultCollectionView', - 'Shared/SpinnerView' - ], function (App, Marionette, AddSeriesCollection, SearchResultCollectionView, SpinnerView) { + 'Shared/LoadingView' + ], function (App, Marionette, AddSeriesCollection, SearchResultCollectionView, LoadingView) { return Marionette.Layout.extend({ template: 'AddSeries/AddSeriesTemplate', @@ -88,7 +88,7 @@ define( this.searchResult.close(); } else { - this.searchResult.show(new SpinnerView()); + this.searchResult.show(new LoadingView()); this.currentSearchPromise = this.collection.fetch({ data: { term: options.term } }).done(function () { diff --git a/UI/Episode/Summary/Layout.js b/UI/Episode/Summary/Layout.js index 9a92affe2..b263c5682 100644 --- a/UI/Episode/Summary/Layout.js +++ b/UI/Episode/Summary/Layout.js @@ -55,7 +55,7 @@ define( this.activity.show(new Backgrid.Grid({ collection: new Backbone.Collection(episodeFile), columns : this.columns, - className : 'table table-bordered' + className : 'table table-bordered'spinn })); } From c9a0aebbfb0ba5e18955920516bc5561e12898d1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 22 Aug 2013 19:07:13 -0700 Subject: [PATCH 3/6] Fixed automatic search failing because series wasn't part of episode --- UI/Episode/Search/Layout.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/UI/Episode/Search/Layout.js b/UI/Episode/Search/Layout.js index 08acca75d..15b89f79a 100644 --- a/UI/Episode/Search/Layout.js +++ b/UI/Episode/Search/Layout.js @@ -6,11 +6,12 @@ define( 'Episode/Search/ButtonsView', 'Episode/Search/ManualLayout', 'Release/Collection', + 'Series/SeriesCollection', 'Shared/LoadingView', 'Shared/Messenger', 'Commands/CommandController', 'Shared/FormatHelpers' - ], function (App, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, LoadingView, Messenger, CommandController, FormatHelpers) { + ], function (App, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection, LoadingView, Messenger, CommandController, FormatHelpers) { return Marionette.Layout.extend({ template: 'Episode/Search/LayoutTemplate', @@ -40,7 +41,8 @@ define( CommandController.Execute('episodeSearch', { episodeId: this.model.get('id') }); - var seriesTitle = this.model.get('series').get('title'); + var series = SeriesCollection.get(this.model.get('seriesId')); + var seriesTitle = series.get('title'); var season = this.model.get('seasonNumber'); var episode = this.model.get('episodeNumber'); var message = seriesTitle + ' - ' + season + 'x' + FormatHelpers.pad(episode, 2); From dbc4f01d6a5b0205182ba91c53ea64869e9bb74b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 22 Aug 2013 22:30:34 -0700 Subject: [PATCH 4/6] AVCDVD releases will not be detected as DVD --- NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs | 3 ++- NzbDrone.Core/Parser/Parser.cs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index a0ee12d92..7f433c41e 100644 --- a/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -78,7 +78,8 @@ namespace NzbDrone.Core.Test.ParserTests new object[] { "POI S02E11 1080i HDTV DD5.1 MPEG2-TrollHD", Quality.RAWHD, false }, new object[] { "How I Met Your Mother S01E18 Nothing Good Happens After 2 A.M. 720p HDTV DD5.1 MPEG2-TrollHD", Quality.RAWHD, false }, new object[] { "Arrested.Development.S04E01.iNTERNAL.1080p.WEBRip.x264-QRUS", Quality.WEBDL1080p, false }, - new object[] { "Arrested.Development.S04E01.720p.WEBRip.AAC2.0.x264-NFRiP", Quality.WEBDL720p, false } + new object[] { "Arrested.Development.S04E01.720p.WEBRip.AAC2.0.x264-NFRiP", Quality.WEBDL720p, false }, + new object[] { "Sons.Of.Anarchy.S02E13.1080p.BluRay.x264-AVCDVD", Quality.Bluray1080p, false } }; public static object[] SelfQualityParserCases = diff --git a/NzbDrone.Core/Parser/Parser.cs b/NzbDrone.Core/Parser/Parser.cs index 299e17688..c77295ca7 100644 --- a/NzbDrone.Core/Parser/Parser.cs +++ b/NzbDrone.Core/Parser/Parser.cs @@ -249,7 +249,9 @@ namespace NzbDrone.Core.Parser var result = new QualityModel { Quality = Quality.Unknown }; result.Proper = (normalizedName.Contains("proper") || normalizedName.Contains("repack")); - if (normalizedName.Contains("dvd") || normalizedName.Contains("bdrip") || normalizedName.Contains("brrip")) + //if (Regex.Match(normalizedName)) + + if ((normalizedName.Contains("dvd") && !normalizedName.Contains("avcdvd")) || normalizedName.Contains("bdrip") || normalizedName.Contains("brrip")) { result.Quality = Quality.DVD; return result; @@ -325,8 +327,8 @@ namespace NzbDrone.Core.Parser result.Quality = Quality.HDTV720p; return result; } - //Based on extension + //Based on extension if (result.Quality == Quality.Unknown && !name.ContainsInvalidPathChars()) { try From 4fb2c0a1430606967f2178b1d987c50e326ffb3a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 22 Aug 2013 22:37:37 -0700 Subject: [PATCH 5/6] Rename will just refetch episodeFiles instead of the whole page --- UI/Series/Details/SeriesDetailsLayout.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/UI/Series/Details/SeriesDetailsLayout.js b/UI/Series/Details/SeriesDetailsLayout.js index 871428ea5..8f073cc96 100644 --- a/UI/Series/Details/SeriesDetailsLayout.js +++ b/UI/Series/Details/SeriesDetailsLayout.js @@ -156,7 +156,7 @@ define( }, element : this.ui.rename, context : this, - onSuccess : this._showSeasons, + onSuccess : this._refetchEpisodeFiles, failMessage: 'Series search failed' }); }, @@ -209,6 +209,10 @@ define( _showInfo: function () { this.info.show(new InfoView({ model: this.model })); + }, + + _refetchEpisodeFiles: function () { + this.episodeFileCollection.fetch(); } }); }); From 32803f6061ec3e16f91fd4bbd2394a7e394f89c2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 22 Aug 2013 22:43:21 -0700 Subject: [PATCH 6/6] Removed commented line --- NzbDrone.Core/Parser/Parser.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/NzbDrone.Core/Parser/Parser.cs b/NzbDrone.Core/Parser/Parser.cs index c77295ca7..2f88fe81e 100644 --- a/NzbDrone.Core/Parser/Parser.cs +++ b/NzbDrone.Core/Parser/Parser.cs @@ -249,8 +249,6 @@ namespace NzbDrone.Core.Parser var result = new QualityModel { Quality = Quality.Unknown }; result.Proper = (normalizedName.Contains("proper") || normalizedName.Contains("repack")); - //if (Regex.Match(normalizedName)) - if ((normalizedName.Contains("dvd") && !normalizedName.Contains("avcdvd")) || normalizedName.Contains("bdrip") || normalizedName.Contains("brrip")) { result.Quality = Quality.DVD;