From 7c5bc94f6eb835898ecc798918b4688630b00800 Mon Sep 17 00:00:00 2001 From: mythjuha Date: Sat, 19 Apr 2014 16:53:08 +0200 Subject: [PATCH 01/16] Fixed binary files. --- .../TvTests/RefreshEpisodeServiceFixture.cs | Bin 7498 -> 6557 bytes .../SeriesServiceTests/AddSeriesFixture.cs | Bin 2001 -> 1060 bytes .../UpdateMultipleSeriesFixture.cs | Bin 2730 -> 1789 bytes .../SeriesServiceTests/UpdateSeriesFixture.cs | Bin 3305 -> 2364 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index cdf3492bd1172834f547df2faefbfc08a7f5d96d..7234166a4bfd613f0b838e28bf54a2d481550fe5 100644 GIT binary patch delta 7 OcmX?QHP?8Fu?N@uWAYAU^qyb+C$@xtp($hZtOad zUpC6|%!se>sLgV7znXtAx&c1b$oJXYi$<+_g!xv!=-vPpVE@iubo^t7@<`Qi(4Z9C ziVD~tJ&3C4h|%&H7zGx8J5@or8V-I82MptyJ~Q>rc>}lcs`c|)RjjIowd`2Rh)y+Z zpV1o$b+|=4iQq&_)U8pSaKK_2yAw-@#~_?4D6VJVF5A?aE>inWD~V9&+tD~uQ?f@- zpBqkg#L^U#tO=hQ!1g&jj>bIXMq}(uFp4K~FCLQ%Z)mW=yi!OBvIKIzvDvyC{LGD_1G+~yoaXPW>s3)dSu7+`M3NH+d@eAZIjeTAI zN@FJsi(caEXMX+wmE-I6>JiYCHlv|l5f+@|v*10BG}rC~a&$3_bsKPnGrRj9{wC+N h&2|AY?FDuO88T=~Zvg9kz{AtKUF;m#Iq-it@Eh38JV*cl diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesServiceTests/UpdateMultipleSeriesFixture.cs index 9522472e904bc1215938a4689d9c648fe145fec8..86cbe732044d38eec0b17cbb37b7c4f33c72ae8b 100644 GIT binary patch delta 7 OcmZ1_`j>aZUp4>^U;}*s literal 2730 zcmeHIO-~y!5bZfq{)3g%Myf2eQZE#uhP0`W2wxlE0M%;Q9f*r#Z*A{}q~e$L(4W-5 z(3xE)*#rnuIaR_3#E!>rX5M>t{`~r#TcK`PIJFMOyr-pv#0jk|{|X9>NIo_jD=GUz z{aD2MVm3RE4e*2AT*ws6a!2N%T-RDKCEQ|TpqIxFPL*)HZ{jgbw7Fg69L%mh8?6BQ zLF{4i)CSm%r-!uP)8nyLh|p{(tYfn{0k&3v@`eSaTOQ)jVDWDu5-3B>#?NMh;kuyD zzPNL_fp1kP%dezJSR!L<*=d%>4hFe&B9l-7`E<6$Xchw4Ua8QjaC`--k{MAT4BHor z+zyLu*;Y9QCDFPF!JTHl5eoQb3N2EGdwCYLD|FhdRiIA~$aa`l&bQU!Jp)*tsvlm5 zVg|v78VVhy#k*40(W@xN&xIQWmhE(y<;Qs}g_|B3eJv#HrC99UV_5Yw1C5qIrqbsU zLx`1fK&GR-a#pOSb<)=)4fCt-czJnc!^&lW z*O}T_)}|f?I=Kadx7^Wx+j~|130AWtz_f!ioT2Hir$Iq-79Mv%P0Uf>vZ25`#Sc{i z$lwcRttN~zR4ST`Kt*PS?nv0k=2<2Mq#GzGV#5?nNU9aRhiC8C<*xNgbohyQUz;y+ zvas+2(LMS@J~y(z4oeQ=VW~3G9J<)J@)FF^U&5r)&iiE}n=sDYKf7sVo>gD1nUc6A zRp2f|Z$j$t&YbQLe|#ha__~!(1{#u{i5+}A_R=<|zDQ$Ikrz56|Z zp>!HXz&bI=be7L>)BY6UgT`mhBPfpgy@!5};ku^JQ+{VM1z+q@n#~g)u_y_(W>;w( z8n6LMGxnAdOmaMEMIKrIGBQO^70=O~Su++a!rL+yR!9c4x<+uvwZT?cCp2tKucu(H z(h_IkYq92%$C$)qsW)h>9Ux@uWM_x0Y*Sgm$UjzqNGGuN27d9+8#ovX|2D9{j?n<> zpuoO_8U==t1gzOPmW^=esAQ7tX>|&rBXY2|59L0|R&!7o>-CUbNYokjD8^5tGGN4{JHocr7>XCP`FZixQBAbAt^zBu7K%ILIf#>%Tz{j#3mM&l!f9gY?O<&6 zeF>@5)U3_O^|h(#i9C|7Q6(crv&eX3Zc1pc(-v3Nt;&i? z@Wj>9OPM8cAfyShVju*NaxG)f0bc~V&}^^_wJ<@n48;w^ZY>@8KFZpTnx@(OD-K}a z9YCIQg{h9VBD?&aScph7jlHMX{e-h|vGH=EHz%%3xB3blZR|aBll$X%$U1Fs4(3uO z)R-ZgaU9S71aeULjgYGb{VVk)?Yfxc!FmZI;1${WKgh0d(4L2i*&a4C#DQ9wtel~; z2onuE9dO!H-F| Date: Tue, 13 May 2014 22:18:02 +0200 Subject: [PATCH 02/16] Removed specials from test data to fix RefreshEpisodeService tests. --- src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs index 7234166a4..b375ed4a2 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshEpisodeServiceFixture.cs @@ -24,6 +24,9 @@ namespace NzbDrone.Core.Test.TvTests public void TestFixture() { _gameOfThrones = Mocker.Resolve().GetSeriesInfo(121361);//Game of thrones + + // Remove specials. + _gameOfThrones.Item2.RemoveAll(v => v.SeasonNumber == 0); } private List GetEpisodes() From 9cf8436dbcd2b67a2b9c09f0b541cb20a3456d7d Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 11 May 2014 00:57:06 +0200 Subject: [PATCH 03/16] Added 'Nordic' as norwegian language. --- src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index 3b4ed3746..5eea42c15 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -40,6 +40,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)] [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)] [TestCase("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA", Language.English)] + [TestCase("Revolution S01E03 No Quarter 2012 WEB-DL 720p Nordic-philipo mkv", Language.Norwegian)] public void should_parse_language(string postTitle, Language language) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 8b7987734..edf3ed9db 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -502,6 +502,9 @@ namespace NzbDrone.Core.Parser if (lowerTitle.Contains("norwegian")) return Language.Norwegian; + if (lowerTitle.Contains("nordic")) + return Language.Norwegian; + if (lowerTitle.Contains("finnish")) return Language.Finnish; From 063b9a177891250297c409b4d413ede48aed9b9a Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 11 May 2014 12:26:32 +0200 Subject: [PATCH 04/16] Fixed: Failed history items now get removed from Nzbget if configured. --- src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index a4467f38f..c1c176e4b 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -85,7 +85,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public void RemoveFromHistory(string id, NzbgetSettings settings) { var history = GetHistory(settings); - var item = history.SingleOrDefault(h => h.Parameters.SingleOrDefault(p => p.Name == "drone") != null); + var item = history.SingleOrDefault(h => h.Parameters.Any(p => p.Name == "drone" && id == (p.Value as string))); if (item == null) { From dcb586b9373e365bd0c151b4fa596bc53cdd1589 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 17 May 2014 18:16:32 +0200 Subject: [PATCH 05/16] Fixed: Better parsing of the delimiters for absolute episode numbering. --- .../ParserTests/AbsoluteEpisodeNumberParserFixture.cs | 5 +++-- src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index d86a4b563..7fb5d1986 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -33,13 +33,14 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[ACX]Hack Sign 01 Role Play [Kosaka] [9C57891E].mkv", "Hack Sign", 1, 0, 0)] [TestCase("[SFW-sage] Bakuman S3 - 12 [720p][D07C91FC]", "Bakuman S3", 12, 0, 0)] [TestCase("ducktales_e66_time_is_money_part_one_marking_time", "DuckTales", 66, 0, 0)] + [TestCase("[Underwater-FFF] No Game No Life - 01 (720p) [27AAA0A0].mkv", "No Game No Life", 1, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); result.Should().NotBeNull(); - result.AbsoluteEpisodeNumbers.First().Should().Be(absoluteEpisodeNumber); + result.AbsoluteEpisodeNumbers.Single().Should().Be(absoluteEpisodeNumber); result.SeasonNumber.Should().Be(seasonNumber); - result.EpisodeNumbers.FirstOrDefault().Should().Be(episodeNumber); + result.EpisodeNumbers.SingleOrDefault().Should().Be(episodeNumber); result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); result.FullSeason.Should().BeFalse(); } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index edf3ed9db..528b6ae27 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Anime - [SubGroup] Title Absolute Episode Number - new Regex(@"^\[(?.+?)\](?:_|-|\s|\.)?(?.+?)(?:(?:\W|_)+(?<absoluteepisode>\d{2,}))+", + new Regex(@"^\[(?<subgroup>.+?)\](?:_|-|\s|\.)?(?<title>.+?)(?:[ ._-]+(?<absoluteepisode>\d{2,}))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Multi-Part episodes without a title (S01E05.S01E06) From 2035fe8578b17245c604d1eaa8922722711e2a0b Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 19 Apr 2014 17:09:22 +0200 Subject: [PATCH 06/16] New: Drone now uses the Download Client API to determine if a download is ready for import. (User configuration is required to replace the drone factory with this feature) --- .../Config/DownloadClientConfigResource.cs | 5 +- .../DownloadClient/DownloadClientResource.cs | 3 +- .../DownloadClientSchemaModule.cs | 20 +- src/NzbDrone.Api/Indexers/IndexerResource.cs | 2 + .../Indexers/IndexerSchemaModule.cs | 2 +- src/NzbDrone.Api/Indexers/ReleaseResource.cs | 2 + src/NzbDrone.Api/ProviderModuleBase.cs | 3 +- .../DiskProviderFixtureBase.cs | 19 +- .../DiskProviderTests/IsParentFixtureBase.cs | 27 +- .../PathExtensionFixture.cs | 51 +++ src/NzbDrone.Common/ConvertBase32.cs | 39 ++ src/NzbDrone.Common/Disk/DiskProviderBase.cs | 64 ++- src/NzbDrone.Common/Disk/IDiskProvider.cs | 2 + src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs | 6 +- src/NzbDrone.Common/Http/HttpProvider.cs | 1 + src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + src/NzbDrone.Common/PathExtensions.cs | 35 +- .../HistorySpecificationFixture.cs | 6 +- .../NotInQueueSpecificationFixture.cs | 26 +- .../CompletedDownloadServiceFixture.cs | 380 ++++++++++++++++++ .../Blackhole/UsenetBlackholeFixture.cs | 118 ++++++ .../BlackholeProviderFixture.cs | 74 ---- .../DownloadClientFixtureBase.cs | 106 +++++ .../NzbgetTests/DownloadNzbFixture.cs | 61 --- .../NzbgetTests/NzbgetFixture.cs | 207 ++++++++++ .../NzbgetTests/QueueFixture.cs | 84 ---- .../PneumaticProviderFixture.cs | 14 +- .../SabnzbdTests/SabnzbdFixture.cs | 227 ++++++++++- .../Download/DownloadServiceFixture.cs | 87 +++- .../Download/FailedDownloadServiceFixture.cs | 82 ++-- .../Checks/DownloadClientCheckFixture.cs | 18 +- .../Checks/DroneFactoryCheckFixture.cs | 12 +- .../Checks/ImportMechanismCheckFixture.cs | 95 +++++ .../IndexerTests/IndexerServiceFixture.cs | 31 -- .../IndexerIntegrationTests.cs | 34 -- .../IndexerTests/SeasonSearchFixture.cs | 2 +- .../DownloadedEpisodesImportServiceFixture.cs | 16 +- .../NotInUseSpecificationFixture.cs | 75 ---- .../ImportApprovedEpisodesFixture.cs | 18 +- .../NzbDrone.Core.Test.csproj | 9 +- src/NzbDrone.Core.Test/app.config | 2 + .../Configuration/ConfigService.cs | 33 +- .../Configuration/IConfigService.cs | 9 +- src/NzbDrone.Core/Datastore/TableMapping.cs | 3 +- .../Specifications/NotInQueueSpecification.cs | 17 +- .../RssSync/HistorySpecification.cs | 5 +- ....cs => CheckForFinishedDownloadCommand.cs} | 2 +- .../Download/Clients/Blackhole/Blackhole.cs | 84 ---- .../Clients/Nzbget/NzbGetQueueItem.cs | 11 +- .../Download/Clients/Nzbget/Nzbget.cs | 96 +++-- .../Clients/Nzbget/NzbgetHistoryItem.cs | 4 +- .../Clients/Nzbget/TestNzbgetCommand.cs | 5 + .../Download/Clients/Pneumatic/Pneumatic.cs | 42 +- .../PneumaticSettings.cs} | 2 +- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 157 +++++--- .../Clients/Sabnzbd/SabnzbdDownloadStatus.cs | 22 + .../Clients/Sabnzbd/SabnzbdHistoryItem.cs | 8 +- .../Download/Clients/Sabnzbd/SabnzbdProxy.cs | 2 +- .../Clients/Sabnzbd/SabnzbdQueueItem.cs | 4 +- .../Clients/Sabnzbd/TestSabnzbdCommand.cs | 4 + .../TestUsenetBlackholeCommand.cs} | 7 +- .../UsenetBlackhole/UsenetBlackhole.cs | 154 +++++++ .../UsenetBlackholeSettings.cs | 35 ++ .../Download/CompletedDownloadService.cs | 148 +++++++ .../Download/DownloadClientBase.cs | 42 +- .../Download/DownloadClientFactory.cs | 15 +- .../Download/DownloadClientItem.cs | 29 ++ .../Download/DownloadClientProvider.cs | 17 +- .../Download/DownloadItemStatus.cs | 16 + src/NzbDrone.Core/Download/DownloadService.cs | 7 +- .../Download/DownloadTrackingService.cs | 209 ++++++++++ .../Download/Events/DownloadFailedEvent.cs | 18 - .../Download/Events/EpisodeGrabbedEvent.cs | 18 - src/NzbDrone.Core/Download/FailedDownload.cs | 11 - .../Download/FailedDownloadService.cs | 216 ++++------ src/NzbDrone.Core/Download/HistoryItem.cs | 22 - src/NzbDrone.Core/Download/IDownloadClient.cs | 11 +- src/NzbDrone.Core/Download/QueueItem.cs | 16 - src/NzbDrone.Core/Download/TrackedDownload.cs | 25 ++ .../HealthCheck/Checks/DownloadClientCheck.cs | 10 +- .../HealthCheck/Checks/DroneFactoryCheck.cs | 2 +- .../Checks/ImportMechanismCheck.cs | 44 ++ .../History/HistoryRepository.cs | 6 + src/NzbDrone.Core/History/HistoryService.cs | 8 + .../Indexers/DownloadProtocol.cs | 13 + .../Indexers/DownloadProtocols.cs | 8 - src/NzbDrone.Core/Indexers/Eztv/Eztv.cs | 63 --- src/NzbDrone.Core/Indexers/IIndexer.cs | 1 + src/NzbDrone.Core/Indexers/IndexerBase.cs | 12 +- .../Indexers/IndexerDefinition.cs | 2 + src/NzbDrone.Core/Indexers/IndexerFactory.cs | 19 +- .../Indexers/IndexerFetchService.cs | 12 +- src/NzbDrone.Core/Indexers/Newznab/Newznab.cs | 19 +- .../Indexers/Omgwtfnzbs/Omgwtfnzbs.cs | 17 +- src/NzbDrone.Core/Indexers/RssParserBase.cs | 8 +- src/NzbDrone.Core/Indexers/Wombles/Wombles.cs | 25 +- .../Indexers/XElementExtensions.cs | 4 +- src/NzbDrone.Core/Jobs/TaskManager.cs | 2 +- .../MediaFiles/DiskScanService.cs | 2 +- .../DownloadedEpisodesImportService.cs | 64 ++- .../MediaFiles/EpisodeFileMovingService.cs | 29 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 27 +- .../Specifications/NotInUseSpecification.cs | 37 -- .../MediaFiles/Events/EpisodeImportedEvent.cs | 11 + .../MediaFileTableCleanupService.cs | 2 +- .../MediaFiles/UpgradeMediaFileService.cs | 13 +- .../Consumers/Roksbox/RoksboxMetadata.cs | 4 +- .../MetaData/Consumers/Wdtv/WdtvMetadata.cs | 4 +- .../MetaData/Consumers/Xbmc/XbmcMetadata.cs | 4 +- src/NzbDrone.Core/MetaData/MetadataService.cs | 12 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 36 +- src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs | 2 + src/NzbDrone.Core/Parser/ParsingService.cs | 3 +- src/NzbDrone.Core/Queue/Queue.cs | 2 + src/NzbDrone.Core/Queue/QueueService.cs | 40 +- .../ThingiProvider/ProviderFactory.cs | 19 +- src/NzbDrone.Core/packages.config | 2 +- .../IntegrationTest.cs | 11 + .../NzbDrone.Integration.Test.csproj | 7 +- src/NzbDrone.Integration.Test/packages.config | 2 +- src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs | 24 +- .../NzbDrone.Test.Common.csproj | 7 +- src/NzbDrone.Test.Common/packages.config | 2 +- src/NzbDrone.sln | 4 +- src/UI/History/Queue/QueueStatusCell.js | 5 + .../Add/DownloadClientAddCollectionView.js | 21 +- .../Add/DownloadClientAddItemView.js | 9 +- .../Add/DownloadClientSchemaModal.js | 36 ++ .../DownloadClient/Add/SchemaModal.js | 20 - .../Delete/DownloadClientDeleteView.js | 38 +- .../DownloadClientCollection.js | 37 +- .../DownloadClientCollectionView.js | 48 ++- .../DownloadClientCollectionViewTemplate.html | 2 +- .../DownloadClient/DownloadClientItemView.js | 39 +- .../DownloadClient/DownloadClientLayout.js | 51 ++- .../DownloadClientLayoutTemplate.html | 5 +- .../DownloadClient/DownloadClientModel.js | 13 +- .../DownloadClientSettingsModel.js | 18 +- .../DownloadHandling/DownloadHandlingView.js | 60 +++ .../DownloadHandlingViewTemplate.html} | 58 ++- .../DroneFactoryView.js} | 2 +- .../DroneFactoryViewTemplate.html} | 4 +- .../Edit/DownloadClientEditView.js | 171 ++++---- .../FailedDownloadHandlingView.js | 37 -- .../Indexers/Add/IndexerAddCollectionView.js | 14 + .../Add/IndexerAddCollectionViewTemplate.html | 16 + .../Indexers/Add/IndexerAddItemView.js | 37 ++ .../Add/IndexerAddItemViewTemplate.html} | 0 .../Indexers/Add/IndexerSchemaModal.js | 36 ++ src/UI/Settings/Indexers/Collection.js | 11 - src/UI/Settings/Indexers/CollectionView.js | 52 --- .../Indexers/Delete/IndexerDeleteView.js | 23 ++ .../IndexerDeleteViewTemplate.html} | 0 src/UI/Settings/Indexers/DeleteView.js | 23 -- .../Settings/Indexers/Edit/IndexerEditView.js | 86 ++++ .../IndexerEditViewTemplate.html} | 18 +- src/UI/Settings/Indexers/EditView.js | 86 ---- src/UI/Settings/Indexers/IndexerCollection.js | 31 ++ .../Indexers/IndexerCollectionView.js | 29 ++ ...tml => IndexerCollectionViewTemplate.html} | 6 +- src/UI/Settings/Indexers/IndexerItemView.js | 26 ++ .../Indexers/IndexerItemViewTemplate.html | 13 + src/UI/Settings/Indexers/IndexerLayout.js | 44 +- .../Indexers/IndexerLayoutTemplate.html | 5 +- src/UI/Settings/Indexers/IndexerModel.js | 9 + .../Settings/Indexers/IndexerSettingsModel.js | 18 +- src/UI/Settings/Indexers/ItemTemplate.html | 37 -- src/UI/Settings/Indexers/ItemView.js | 29 -- src/UI/Settings/Indexers/Model.js | 24 -- src/UI/Settings/Indexers/indexers.less | 42 +- .../Add/NotificationAddCollectionView.js | 13 + ...otificationAddCollectionViewTemplate.html} | 0 .../NotificationAddItemView.js} | 17 +- .../Add/NotificationAddItemViewTemplate.html | 6 + .../Add/NotificationSchemaModal.js | 21 + src/UI/Settings/Notifications/AddView.js | 23 -- src/UI/Settings/Notifications/Collection.js | 11 - .../Notifications/CollectionTemplate.html | 13 - .../Settings/Notifications/CollectionView.js | 28 -- .../Delete/NotificationDeleteView.js | 23 ++ .../NotificationDeleteViewTemplate.html} | 0 src/UI/Settings/Notifications/DeleteView.js | 23 -- .../Edit/NotificationEditView.js | 113 ++++++ .../NotificationEditViewTemplate.html | 6 +- .../Notifications/NotificationCollection.js | 13 + .../NotificationCollectionView.js | 29 ++ .../NotificationCollectionViewTemplate.html | 16 + .../Notifications/NotificationEditView.js | 113 ------ ...onsItemView.js => NotificationItemView.js} | 9 +- .../{Model.js => NotificationModel.js} | 3 +- src/UI/Settings/Notifications/SchemaModal.js | 20 - src/UI/Settings/SettingsLayout.js | 10 +- src/UI/Settings/ThingyAddCollectionView.js | 18 + src/UI/Settings/ThingyHeaderGroupView.js | 23 ++ .../ThingyHeaderGroupViewTemplate.html | 2 + src/UI/Settings/thingy.less | 5 + 196 files changed, 3961 insertions(+), 2223 deletions(-) create mode 100644 src/NzbDrone.Common/ConvertBase32.cs create mode 100644 src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs delete mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs create mode 100644 src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs create mode 100644 src/NzbDrone.Core.Test/app.config rename src/NzbDrone.Core/Download/{CheckForFailedDownloadCommand.cs => CheckForFinishedDownloadCommand.cs} (61%) delete mode 100644 src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs rename src/NzbDrone.Core/Download/Clients/{FolderSettings.cs => Pneumatic/PneumaticSettings.cs} (94%) create mode 100644 src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdDownloadStatus.cs rename src/NzbDrone.Core/Download/Clients/{Blackhole/TestBlackholeCommand.cs => UsenetBlackhole/TestUsenetBlackholeCommand.cs} (51%) create mode 100644 src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs create mode 100644 src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs create mode 100644 src/NzbDrone.Core/Download/CompletedDownloadService.cs create mode 100644 src/NzbDrone.Core/Download/DownloadClientItem.cs create mode 100644 src/NzbDrone.Core/Download/DownloadItemStatus.cs create mode 100644 src/NzbDrone.Core/Download/DownloadTrackingService.cs delete mode 100644 src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs delete mode 100644 src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs delete mode 100644 src/NzbDrone.Core/Download/FailedDownload.cs delete mode 100644 src/NzbDrone.Core/Download/HistoryItem.cs delete mode 100644 src/NzbDrone.Core/Download/QueueItem.cs create mode 100644 src/NzbDrone.Core/Download/TrackedDownload.cs create mode 100644 src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs create mode 100644 src/NzbDrone.Core/Indexers/DownloadProtocol.cs delete mode 100644 src/NzbDrone.Core/Indexers/DownloadProtocols.cs delete mode 100644 src/NzbDrone.Core/Indexers/Eztv/Eztv.cs delete mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs create mode 100644 src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js delete mode 100644 src/UI/Settings/DownloadClient/Add/SchemaModal.js create mode 100644 src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js rename src/UI/Settings/DownloadClient/{FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html => DownloadHandling/DownloadHandlingViewTemplate.html} (62%) rename src/UI/Settings/DownloadClient/{Options/DownloadClientOptionsView.js => DroneFactory/DroneFactoryView.js} (86%) rename src/UI/Settings/DownloadClient/{Options/DownloadClientOptionsViewTemplate.html => DroneFactory/DroneFactoryViewTemplate.html} (86%) delete mode 100644 src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js create mode 100644 src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js create mode 100644 src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.html create mode 100644 src/UI/Settings/Indexers/Add/IndexerAddItemView.js rename src/UI/Settings/{Notifications/AddItemTemplate.html => Indexers/Add/IndexerAddItemViewTemplate.html} (100%) create mode 100644 src/UI/Settings/Indexers/Add/IndexerSchemaModal.js delete mode 100644 src/UI/Settings/Indexers/Collection.js delete mode 100644 src/UI/Settings/Indexers/CollectionView.js create mode 100644 src/UI/Settings/Indexers/Delete/IndexerDeleteView.js rename src/UI/Settings/Indexers/{DeleteViewTemplate.html => Delete/IndexerDeleteViewTemplate.html} (100%) delete mode 100644 src/UI/Settings/Indexers/DeleteView.js create mode 100644 src/UI/Settings/Indexers/Edit/IndexerEditView.js rename src/UI/Settings/Indexers/{EditTemplate.html => Edit/IndexerEditViewTemplate.html} (73%) delete mode 100644 src/UI/Settings/Indexers/EditView.js create mode 100644 src/UI/Settings/Indexers/IndexerCollection.js create mode 100644 src/UI/Settings/Indexers/IndexerCollectionView.js rename src/UI/Settings/Indexers/{CollectionTemplate.html => IndexerCollectionViewTemplate.html} (57%) create mode 100644 src/UI/Settings/Indexers/IndexerItemView.js create mode 100644 src/UI/Settings/Indexers/IndexerItemViewTemplate.html create mode 100644 src/UI/Settings/Indexers/IndexerModel.js delete mode 100644 src/UI/Settings/Indexers/ItemTemplate.html delete mode 100644 src/UI/Settings/Indexers/ItemView.js delete mode 100644 src/UI/Settings/Indexers/Model.js create mode 100644 src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js rename src/UI/Settings/Notifications/{AddTemplate.html => Add/NotificationAddCollectionViewTemplate.html} (100%) rename src/UI/Settings/Notifications/{AddItemView.js => Add/NotificationAddItemView.js} (53%) create mode 100644 src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html create mode 100644 src/UI/Settings/Notifications/Add/NotificationSchemaModal.js delete mode 100644 src/UI/Settings/Notifications/AddView.js delete mode 100644 src/UI/Settings/Notifications/Collection.js delete mode 100644 src/UI/Settings/Notifications/CollectionTemplate.html delete mode 100644 src/UI/Settings/Notifications/CollectionView.js create mode 100644 src/UI/Settings/Notifications/Delete/NotificationDeleteView.js rename src/UI/Settings/Notifications/{DeleteTemplate.html => Delete/NotificationDeleteViewTemplate.html} (100%) delete mode 100644 src/UI/Settings/Notifications/DeleteView.js create mode 100644 src/UI/Settings/Notifications/Edit/NotificationEditView.js rename src/UI/Settings/Notifications/{ => Edit}/NotificationEditViewTemplate.html (94%) create mode 100644 src/UI/Settings/Notifications/NotificationCollection.js create mode 100644 src/UI/Settings/Notifications/NotificationCollectionView.js create mode 100644 src/UI/Settings/Notifications/NotificationCollectionViewTemplate.html delete mode 100644 src/UI/Settings/Notifications/NotificationEditView.js rename src/UI/Settings/Notifications/{NotificationsItemView.js => NotificationItemView.js} (65%) rename src/UI/Settings/Notifications/{Model.js => NotificationModel.js} (74%) delete mode 100644 src/UI/Settings/Notifications/SchemaModal.js create mode 100644 src/UI/Settings/ThingyAddCollectionView.js create mode 100644 src/UI/Settings/ThingyHeaderGroupView.js create mode 100644 src/UI/Settings/ThingyHeaderGroupViewTemplate.html diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs index 14a9eff74..5440099d7 100644 --- a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs +++ b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs @@ -9,9 +9,12 @@ namespace NzbDrone.Api.Config public String DownloadClientWorkingFolders { get; set; } public Int32 DownloadedEpisodesScanInterval { get; set; } + public Boolean EnableCompletedDownloadHandling { get; set; } + public Boolean RemoveCompletedDownloads { get; set; } + + public Boolean EnableFailedDownloadHandling { get; set; } public Boolean AutoRedownloadFailed { get; set; } public Boolean RemoveFailedDownloads { get; set; } - public Boolean EnableFailedDownloadHandling { get; set; } public Int32 BlacklistGracePeriod { get; set; } public Int32 BlacklistRetryInterval { get; set; } public Int32 BlacklistRetryLimit { get; set; } diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs index cb1054168..69cca07fe 100644 --- a/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs +++ b/src/NzbDrone.Api/DownloadClient/DownloadClientResource.cs @@ -1,10 +1,11 @@ using System; +using NzbDrone.Core.Indexers; namespace NzbDrone.Api.DownloadClient { public class DownloadClientResource : ProviderResource { public Boolean Enable { get; set; } - public Int32 Protocol { get; set; } + public DownloadProtocol Protocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs index 58c1a2149..0ea47266b 100644 --- a/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs +++ b/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs @@ -7,28 +7,28 @@ namespace NzbDrone.Api.DownloadClient { public class DownloadClientSchemaModule : NzbDroneRestModule<DownloadClientResource> { - private readonly IDownloadClientFactory _notificationFactory; + private readonly IDownloadClientFactory _downloadClientFactory; - public DownloadClientSchemaModule(IDownloadClientFactory notificationFactory) + public DownloadClientSchemaModule(IDownloadClientFactory downloadClientFactory) : base("downloadclient/schema") { - _notificationFactory = notificationFactory; + _downloadClientFactory = downloadClientFactory; GetResourceAll = GetSchema; } private List<DownloadClientResource> GetSchema() { - var notifications = _notificationFactory.Templates(); + var downloadClients = _downloadClientFactory.Templates(); - var result = new List<DownloadClientResource>(notifications.Count); + var result = new List<DownloadClientResource>(downloadClients.Count); - foreach (var notification in notifications) + foreach (var downloadClient in downloadClients) { - var notificationResource = new DownloadClientResource(); - notificationResource.InjectFrom(notification); - notificationResource.Fields = SchemaBuilder.ToSchema(notification.Settings); + var downloadClientResource = new DownloadClientResource(); + downloadClientResource.InjectFrom(downloadClient); + downloadClientResource.Fields = SchemaBuilder.ToSchema(downloadClient.Settings); - result.Add(notificationResource); + result.Add(downloadClientResource); } return result; diff --git a/src/NzbDrone.Api/Indexers/IndexerResource.cs b/src/NzbDrone.Api/Indexers/IndexerResource.cs index dbb55c3f0..651d57ccf 100644 --- a/src/NzbDrone.Api/Indexers/IndexerResource.cs +++ b/src/NzbDrone.Api/Indexers/IndexerResource.cs @@ -1,9 +1,11 @@ using System; +using NzbDrone.Core.Indexers; namespace NzbDrone.Api.Indexers { public class IndexerResource : ProviderResource { public Boolean Enable { get; set; } + public DownloadProtocol Protocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs b/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs index d433102c8..3de973599 100644 --- a/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs +++ b/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Api.Indexers private List<IndexerResource> GetSchema() { - var indexers = _indexerFactory.Templates().Where(c => c.Implementation =="Newznab"); + var indexers = _indexerFactory.Templates(); var result = new List<IndexerResource>(indexers.Count()); diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index c99982d69..859399588 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Api.REST; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Indexers; namespace NzbDrone.Api.Indexers { @@ -30,5 +31,6 @@ namespace NzbDrone.Api.Indexers public String DownloadUrl { get; set; } public String InfoUrl { get; set; } public Boolean DownloadAllowed { get; set; } + public DownloadProtocol DownloadProtocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index 33f567850..fb0ca9389 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -30,10 +30,11 @@ namespace NzbDrone.Api DeleteResource = DeleteProvider; SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Name).Must((v,c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); - PostValidator.RuleFor(c => c.Fields).NotEmpty(); + PostValidator.RuleFor(c => c.Fields).NotNull(); } private TProviderResource GetProviderById(int id) diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs index 8c62331de..5af9890ba 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs @@ -137,7 +137,7 @@ namespace NzbDrone.Common.Test.DiskProviderTests } [Test] - public void move_read_only_file() + public void should_be_able_to_move_read_only_file() { var source = GetTempFilePath(); var destination = GetTempFilePath(); @@ -151,6 +151,23 @@ namespace NzbDrone.Common.Test.DiskProviderTests Subject.MoveFile(source, destination); } + [Test] + public void should_be_able_to_delete_directory_with_read_only_file() + { + var sourceDir = GetTempFilePath(); + var source = Path.Combine(sourceDir, "test.txt"); + + Directory.CreateDirectory(sourceDir); + + Subject.WriteAllText(source, "SourceFile"); + + File.SetAttributes(source, FileAttributes.ReadOnly); + + Subject.DeleteFolder(sourceDir, true); + + Directory.Exists(sourceDir).Should().BeFalse(); + } + [Test] public void empty_folder_should_return_folder_modified_date() { diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs b/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs index a9bc32930..9de173002 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs @@ -5,32 +5,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Common.Test.DiskProviderTests { - public class IsParentFixture : TestBase + public class IsParentPathFixture : TestBase { - private string _parent = @"C:\Test".AsOsAgnostic(); - - [Test] - public void should_return_false_when_not_a_child() - { - var path = @"C:\Another Folder".AsOsAgnostic(); - - DiskProviderBase.IsParent(_parent, path).Should().BeFalse(); - } - - [Test] - public void should_return_true_when_folder_is_parent_of_another_folder() - { - var path = @"C:\Test\TV".AsOsAgnostic(); - - DiskProviderBase.IsParent(_parent, path).Should().BeTrue(); - } - - [Test] - public void should_return_true_when_folder_is_parent_of_a_file() - { - var path = @"C:\Test\30.Rock.S01E01.Pilot.avi".AsOsAgnostic(); - - DiskProviderBase.IsParent(_parent, path).Should().BeTrue(); - } } } diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index fd59e7eec..2564d676a 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -85,6 +85,57 @@ namespace NzbDrone.Common.Test { first.AsOsAgnostic().PathEquals(second.AsOsAgnostic()).Should().BeFalse(); } + + private string _parent = @"C:\Test".AsOsAgnostic(); + + [Test] + public void should_return_false_when_not_a_child() + { + var path = @"C:\Another Folder".AsOsAgnostic(); + + _parent.IsParentPath(path).Should().BeFalse(); + } + + [Test] + public void should_return_true_when_folder_is_parent_of_another_folder() + { + var path = @"C:\Test\TV".AsOsAgnostic(); + + _parent.IsParentPath(path).Should().BeTrue(); + } + + [Test] + public void should_return_true_when_folder_is_parent_of_a_file() + { + var path = @"C:\Test\30.Rock.S01E01.Pilot.avi".AsOsAgnostic(); + + _parent.IsParentPath(path).Should().BeTrue(); + } + [TestCase(@"C:\Test\", @"C:\Test\mydir")] + [TestCase(@"C:\Test\", @"C:\Test\mydir\")] + [TestCase(@"C:\Test", @"C:\Test\30.Rock.S01E01.Pilot.avi")] + public void path_should_be_parent(string parentPath, string childPath) + { + parentPath.AsOsAgnostic().IsParentPath(childPath.AsOsAgnostic()).Should().BeTrue(); + } + + [TestCase(@"C:\Test2\", @"C:\Test")] + [TestCase(@"C:\Test\Test\", @"C:\Test\")] + [TestCase(@"C:\Test\", @"C:\Test")] + [TestCase(@"C:\Test\", @"C:\Test\")] + public void path_should_not_be_parent(string parentPath, string childPath) + { + parentPath.AsOsAgnostic().IsParentPath(childPath.AsOsAgnostic()).Should().BeFalse(); + } + + [TestCase(@"C:\test\", @"C:\Test\mydir")] + [TestCase(@"C:\test", @"C:\Test\mydir\")] + public void path_should_be_parent_on_windows_only(string parentPath, string childPath) + { + var expectedResult = OsInfo.IsWindows; + + parentPath.IsParentPath(childPath).Should().Be(expectedResult); + } [Test] public void normalize_path_exception_empty() diff --git a/src/NzbDrone.Common/ConvertBase32.cs b/src/NzbDrone.Common/ConvertBase32.cs new file mode 100644 index 000000000..0b69c2d6b --- /dev/null +++ b/src/NzbDrone.Common/ConvertBase32.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common +{ + public static class ConvertBase32 + { + private static string ValidChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + + public static byte[] FromBase32String(string str) + { + int numBytes = str.Length * 5 / 8; + byte[] bytes = new Byte[numBytes]; + + // all UPPERCASE chars + str = str.ToUpper(); + + int bitBuffer = 0; + int bitBufferCount = 0; + int index = 0; + + for (int i = 0; i < str.Length;i++ ) + { + bitBuffer = (bitBuffer << 5) | ValidChars.IndexOf(str[i]); + bitBufferCount += 5; + + if (bitBufferCount >= 8) + { + bitBufferCount -= 8; + bytes[index++] = (byte)(bitBuffer >> bitBufferCount); + } + } + + return bytes; + } + } +} diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 47a14be5c..c9de8d11c 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -25,45 +25,17 @@ namespace NzbDrone.Common.Disk public abstract void SetPermissions(string path, string mask, string user, string group); public abstract long? GetTotalSize(string path); - public static string GetRelativePath(string parentPath, string childPath) + + public DateTime FolderGetCreationTimeUtc(string path) { - if (!IsParent(parentPath, childPath)) - { - throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); - } + CheckFolderExists(path); - return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); - } - - public static bool IsParent(string parentPath, string childPath) - { - parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); - childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); - - var parent = new DirectoryInfo(parentPath); - var child = new DirectoryInfo(childPath); - - while (child.Parent != null) - { - if (child.Parent.FullName == parent.FullName) - { - return true; - } - - child = child.Parent; - } - - return false; + return new DirectoryInfo(path).CreationTimeUtc; } public DateTime FolderGetLastWrite(string path) { - Ensure.That(path, () => path).IsValidPath(); - - if (!FolderExists(path)) - { - throw new DirectoryNotFoundException("Directory doesn't exist. " + path); - } + CheckFolderExists(path); var dirFiles = GetFiles(path, SearchOption.AllDirectories).ToList(); @@ -76,21 +48,38 @@ namespace NzbDrone.Common.Disk .Max(c => c.LastWriteTimeUtc); } + public DateTime FileGetCreationTimeUtc(string path) + { + CheckFileExists(path); + + return new FileInfo(path).CreationTimeUtc; + } + public DateTime FileGetLastWrite(string path) { - PathEnsureFileExists(path); + CheckFileExists(path); return new FileInfo(path).LastWriteTime; } public DateTime FileGetLastWriteUtc(string path) { - PathEnsureFileExists(path); + CheckFileExists(path); return new FileInfo(path).LastWriteTimeUtc; } - private void PathEnsureFileExists(string path) + private void CheckFolderExists(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + if (!FolderExists(path)) + { + throw new DirectoryNotFoundException("Directory doesn't exist. " + path); + } + } + + private void CheckFileExists(string path) { Ensure.That(path, () => path).IsValidPath(); @@ -286,6 +275,9 @@ namespace NzbDrone.Common.Disk { Ensure.That(path, () => path).IsValidPath(); + var files = Directory.GetFiles(path, "*.*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + Array.ForEach(files, RemoveReadOnly); + Directory.Delete(path, recursive); } diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index b57486ff9..afbd7ce60 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -11,7 +11,9 @@ namespace NzbDrone.Common.Disk void InheritFolderPermissions(string filename); void SetPermissions(string path, string mask, string user, string group); long? GetTotalSize(string path); + DateTime FolderGetCreationTimeUtc(string path); DateTime FolderGetLastWrite(string path); + DateTime FileGetCreationTimeUtc(string path); DateTime FileGetLastWrite(string path); DateTime FileGetLastWriteUtc(string path); void EnsureFolder(string path); diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index bdd7c14ac..ad85d0c14 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -24,11 +24,14 @@ namespace NzbDrone.Common.EnvironmentInfo if (!IsMono) { Os = Os.Windows; - } + PathStringComparison = StringComparison.OrdinalIgnoreCase; + } else { Os = IsOsx ? Os.Osx : Os.Linux; + + PathStringComparison = StringComparison.Ordinal; } } @@ -40,6 +43,7 @@ namespace NzbDrone.Common.EnvironmentInfo public static bool IsWindows { get; private set; } public static Os Os { get; private set; } public static DayOfWeek FirstDayOfWeek { get; private set; } + public static StringComparison PathStringComparison { get; private set; } //Borrowed from: https://github.com/jpobst/Pinta/blob/master/Pinta.Core/Managers/SystemManager.cs //From Managed.Windows.Forms/XplatUI diff --git a/src/NzbDrone.Common/Http/HttpProvider.cs b/src/NzbDrone.Common/Http/HttpProvider.cs index 35d9f2eb7..6b04cb548 100644 --- a/src/NzbDrone.Common/Http/HttpProvider.cs +++ b/src/NzbDrone.Common/Http/HttpProvider.cs @@ -84,6 +84,7 @@ namespace NzbDrone.Common.Http public Stream DownloadStream(string url, NetworkCredential credential = null) { var request = (HttpWebRequest)WebRequest.Create(url); + request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; request.UserAgent = _userAgent; request.Timeout = 20 * 1000; diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index f2b61218e..c75eeecc3 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -60,6 +60,7 @@ </ItemGroup> <ItemGroup> <Compile Include="ArchiveProvider.cs" /> + <Compile Include="ConvertBase32.cs" /> <Compile Include="Cache\Cached.cs" /> <Compile Include="Cache\CacheManager.cs" /> <Compile Include="Cache\ICached.cs" /> diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index a5c171039..053a22edb 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -39,14 +39,39 @@ namespace NzbDrone.Common public static bool PathEquals(this string firstPath, string secondPath) { - if (OsInfo.IsMono) + if (firstPath.Equals(secondPath, OsInfo.PathStringComparison)) return true; + return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath(), OsInfo.PathStringComparison); + } + + public static string GetRelativePath(this string parentPath, string childPath) + { + if (!parentPath.IsParentPath(childPath)) { - if (firstPath.Equals(secondPath)) return true; - return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath()); + throw new NzbDrone.Common.Exceptions.NotParentException("{0} is not a child of {1}", childPath, parentPath); } - if (firstPath.Equals(secondPath, StringComparison.OrdinalIgnoreCase)) return true; - return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath(), StringComparison.OrdinalIgnoreCase); + return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); + } + + public static bool IsParentPath(this string parentPath, string childPath) + { + parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); + childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); + + var parent = new DirectoryInfo(parentPath); + var child = new DirectoryInfo(childPath); + + while (child.Parent != null) + { + if (child.Parent.FullName.Equals(parent.FullName, OsInfo.PathStringComparison)) + { + return true; + } + + child = child.Parent; + } + + return false; } private static readonly Regex WindowsPathWithDriveRegex = new Regex(@"^[a-zA-Z]:\\", RegexOptions.Compiled); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index 48710e6f9..0fc46ab1f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -67,7 +67,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.GetMock<IHistoryService>().Setup(c => c.GetBestQualityInHistory(It.IsAny<QualityProfile>(), 3)).Returns<QualityModel>(null); Mocker.GetMock<IProvideDownloadClient>() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock<IDownloadClient>().Object); + .Setup(c => c.GetDownloadClients()) + .Returns(new IDownloadClient[] { Mocker.GetMock<IDownloadClient>().Object }); } private void WithFirstReportUpgradable() @@ -83,7 +84,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenSabnzbdDownloadClient() { Mocker.GetMock<IProvideDownloadClient>() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve<Sabnzbd>()); + .Setup(c => c.GetDownloadClients()) + .Returns(new IDownloadClient[] { Mocker.Resolve<Sabnzbd>() }); } private void GivenMostRecentForEpisode(HistoryEventType eventType) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs index 4a3be3627..a41d36669 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/NotInQueueSpecificationFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Queue; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -18,7 +19,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private Series _series; private Episode _episode; private RemoteEpisode _remoteEpisode; - private Mock<IDownloadClient> _downloadClient; private Series _otherSeries; private Episode _otherEpisode; @@ -50,34 +50,30 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Episodes = new List<Episode> { _episode }) .With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD)}) .Build(); - - _downloadClient = Mocker.GetMock<IDownloadClient>(); - - Mocker.GetMock<IProvideDownloadClient>() - .Setup(s => s.GetDownloadClient()) - .Returns(_downloadClient.Object); } private void GivenEmptyQueue() { - _downloadClient.Setup(s => s.GetQueue()) - .Returns(new List<QueueItem>()); + Mocker.GetMock<IQueueService>() + .Setup(s => s.GetQueue()) + .Returns(new List<Queue.Queue>()); } private void GivenQueue(IEnumerable<RemoteEpisode> remoteEpisodes) { - var queue = new List<QueueItem>(); + var queue = new List<Queue.Queue>(); foreach (var remoteEpisode in remoteEpisodes) { - queue.Add(new QueueItem - { - RemoteEpisode = remoteEpisode + queue.Add(new Queue.Queue + { + RemoteEpisode = remoteEpisode }); } - _downloadClient.Setup(s => s.GetQueue()) - .Returns(queue); + Mocker.GetMock<IQueueService>() + .Setup(s => s.GetQueue()) + .Returns(queue); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs new file mode 100644 index 000000000..df1218139 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Download +{ + [TestFixture] + public class CompletedDownloadServiceFixture : CoreTest<DownloadTrackingService> + { + private List<DownloadClientItem> _completed; + + [SetUp] + public void Setup() + { + _completed = Builder<DownloadClientItem>.CreateListOfSize(1) + .All() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .Build() + .ToList(); + + Mocker.GetMock<IProvideDownloadClient>() + .Setup(c => c.GetDownloadClients()) + .Returns( new IDownloadClient[] { Mocker.GetMock<IDownloadClient>().Object }); + + Mocker.GetMock<IDownloadClient>() + .SetupGet(c => c.Definition) + .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); + + Mocker.GetMock<IConfigService>() + .SetupGet(s => s.EnableCompletedDownloadHandling) + .Returns(true); + + Mocker.GetMock<IConfigService>() + .SetupGet(s => s.RemoveCompletedDownloads) + .Returns(true); + + Mocker.GetMock<IHistoryService>() + .Setup(s => s.Failed()) + .Returns(new List<History.History>()); + + Mocker.SetConstant<ICompletedDownloadService>(Mocker.Resolve<CompletedDownloadService>()); + } + + private void GivenNoGrabbedHistory() + { + Mocker.GetMock<IHistoryService>() + .Setup(s => s.Grabbed()) + .Returns(new List<History.History>()); + } + + private void GivenGrabbedHistory(List<History.History> history) + { + Mocker.GetMock<IHistoryService>() + .Setup(s => s.Grabbed()) + .Returns(history); + } + + private void GivenNoImportedHistory() + { + Mocker.GetMock<IHistoryService>() + .Setup(s => s.Imported()) + .Returns(new List<History.History>()); + } + + private void GivenImportedHistory(List<History.History> importedHistory) + { + Mocker.GetMock<IHistoryService>() + .Setup(s => s.Imported()) + .Returns(importedHistory); + } + + private void GivenCompletedDownloadClientHistory(bool hasStorage = true) + { + Mocker.GetMock<IDownloadClient>() + .Setup(s => s.GetItems()) + .Returns(_completed); + + Mocker.GetMock<IDiskProvider>() + .Setup(c => c.FolderExists(It.IsAny<string>())) + .Returns(hasStorage); + } + + private void GivenCompletedImport() + { + Mocker.GetMock<IDownloadedEpisodesImportService>() + .Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>())) + .Returns(new List<Core.MediaFiles.EpisodeImport.ImportDecision>() { new Core.MediaFiles.EpisodeImport.ImportDecision(null) }); + } + + private void GivenFailedImport() + { + Mocker.GetMock<IDownloadedEpisodesImportService>() + .Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>())) + .Returns(new List<Core.MediaFiles.EpisodeImport.ImportDecision>()); + } + + private void VerifyNoImports() + { + Mocker.GetMock<IDownloadedEpisodesImportService>() + .Verify(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()), Times.Never()); + } + + private void VerifyImports() + { + Mocker.GetMock<IDownloadedEpisodesImportService>() + .Verify(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()), Times.Once()); + } + + [Test] + public void should_process_if_matching_history_is_not_found_but_category_specified() + { + _completed.First().Category = "tv"; + + GivenCompletedDownloadClientHistory(); + GivenNoGrabbedHistory(); + GivenNoImportedHistory(); + GivenCompletedImport(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyImports(); + } + + [Test] + public void should_not_process_if_matching_history_is_not_found_and_no_category_specified() + { + _completed.First().Category = null; + + GivenCompletedDownloadClientHistory(); + GivenNoGrabbedHistory(); + GivenNoImportedHistory(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_not_process_if_grabbed_history_contains_null_downloadclient_id() + { + _completed.First().Category = null; + + GivenCompletedDownloadClientHistory(); + + var historyGrabbed = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", null); + + GivenGrabbedHistory(historyGrabbed); + GivenNoImportedHistory(); + GivenFailedImport(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_process_if_failed_history_contains_null_downloadclient_id() + { + GivenCompletedDownloadClientHistory(); + + var historyGrabbed = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + GivenGrabbedHistory(historyGrabbed); + + var historyImported = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + historyImported.First().Data.Add("downloadClient", "SabnzbdClient"); + historyImported.First().Data.Add("downloadClientId", null); + + GivenImportedHistory(historyImported); + GivenCompletedImport(); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyImports(); + } + + [Test] + public void should_not_process_if_already_added_to_history_as_imported() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenImportedHistory(history); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + } + + [Test] + public void should_process_if_not_already_in_imported_history() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyImports(); + } + + [Test] + public void should_not_process_if_storage_directory_does_not_exist() + { + GivenCompletedDownloadClientHistory(false); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + VerifyNoImports(); + } + + [Test] + public void should_not_process_if_storage_directory_in_drone_factory() + { + GivenCompletedDownloadClientHistory(true); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + + Mocker.GetMock<IConfigService>() + .SetupGet(v => v.DownloadedEpisodesFolder) + .Returns(@"C:\DropFolder".AsOsAgnostic()); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + VerifyNoImports(); + } + + [Test] + public void should_not_remove_if_config_disabled() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Mocker.GetMock<IConfigService>() + .SetupGet(s => s.RemoveCompletedDownloads) + .Returns(false); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock<IDiskProvider>() + .Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never()); + } + + [Test] + public void should_not_remove_while_readonly() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + _completed.First().IsReadOnly = true; + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock<IDiskProvider>() + .Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never()); + } + + [Test] + public void should_not_remove_if_imported_failed() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenFailedImport(); + + _completed.First().IsReadOnly = true; + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock<IDiskProvider>() + .Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never()); + } + + [Test] + public void should_remove_if_imported() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder<History.History>.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock<IDiskProvider>() + .Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs new file mode 100644 index 000000000..f19023b9c --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/Blackhole/UsenetBlackholeFixture.cs @@ -0,0 +1,118 @@ +using System.IO; +using System.Net; +using System.Linq; +using Moq; +using NUnit.Framework; +using FluentAssertions; +using NzbDrone.Test.Common; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Download.Clients.UsenetBlackhole; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.Blackhole +{ + + [TestFixture] + public class UsenetBlackholeFixture : DownloadClientFixtureBase<UsenetBlackhole> + { + protected string _completedDownloadFolder; + protected string _blackholeFolder; + protected string _filePath; + + [SetUp] + public void Setup() + { + _completedDownloadFolder = @"c:\blackhole\completed".AsOsAgnostic(); + _blackholeFolder = @"c:\blackhole\nzb".AsOsAgnostic(); + _filePath = (@"c:\blackhole\nzb\" + _title + ".nzb").AsOsAgnostic(); + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new UsenetBlackholeSettings + { + NzbFolder = _blackholeFolder, + WatchFolder = _completedDownloadFolder + }; + } + + protected void WithSuccessfulDownload() + { + + } + + protected void WithFailedDownload() + { + Mocker.GetMock<IHttpProvider>() + .Setup(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>())) + .Throws(new WebException()); + } + + protected void GivenCompletedItem() + { + var targetDir = Path.Combine(_completedDownloadFolder, _title); + Mocker.GetMock<IDiskProvider>() + .Setup(c => c.GetDirectories(_completedDownloadFolder)) + .Returns(new[] { targetDir }); + + Mocker.GetMock<IDiskProvider>() + .Setup(c => c.GetFiles(targetDir, SearchOption.AllDirectories)) + .Returns(new[] { Path.Combine(_completedDownloadFolder, "somefile.mkv") }); + + Mocker.GetMock<IDiskProvider>() + .Setup(c => c.GetFileSize(It.IsAny<string>())) + .Returns(1000000); + } + + [Test] + public void completed_download_should_have_required_properties() + { + GivenCompletedItem(); + + var result = Subject.GetItems().Single(); + + VerifyCompleted(result); + } + + [Test] + public void Download_should_download_file_if_it_doesnt_exist() + { + var remoteEpisode = CreateRemoteEpisode(); + + Subject.Download(remoteEpisode); + + Mocker.GetMock<IHttpProvider>().Verify(c => c.DownloadFile(_downloadUrl, _filePath), Times.Once()); + } + + [Test] + public void Download_should_replace_illegal_characters_in_title() + { + var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]"; + var expectedFilename = Path.Combine(_blackholeFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV]" + Path.GetExtension(_filePath)); + + var remoteEpisode = CreateRemoteEpisode(); + remoteEpisode.Release.Title = illegalTitle; + + Subject.Download(remoteEpisode); + + Mocker.GetMock<IHttpProvider>().Verify(c => c.DownloadFile(It.IsAny<string>(), expectedFilename), Times.Once()); + } + + [Test] + public void GetItems_should_considered_locked_files_downloading() + { + GivenCompletedItem(); + + Mocker.GetMock<IDiskProvider>() + .Setup(c => c.IsFileLocked(It.IsAny<string>())) + .Returns(true); + + var result = Subject.GetItems().Single(); + + result.Status.Should().Be(DownloadItemStatus.Downloading); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs deleted file mode 100644 index 6e993c320..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/BlackholeProviderFixture.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.IO; -using System.Net; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Clients; -using NzbDrone.Core.Download.Clients.Blackhole; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests -{ - [TestFixture] - public class BlackholeProviderFixture : CoreTest<Blackhole> - { - private const string _nzbUrl = "http://www.nzbs.com/url"; - private const string _title = "some_nzb_title"; - private string _blackHoleFolder; - private string _nzbPath; - private RemoteEpisode _remoteEpisode; - - [SetUp] - public void Setup() - { - _blackHoleFolder = @"c:\nzb\blackhole\".AsOsAgnostic(); - _nzbPath = @"c:\nzb\blackhole\some_nzb_title.nzb".AsOsAgnostic(); - - _remoteEpisode = new RemoteEpisode(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = _title; - _remoteEpisode.Release.DownloadUrl = _nzbUrl; - - Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new FolderSettings - { - Folder = _blackHoleFolder - }; - } - - private void WithExistingFile() - { - Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(_nzbPath)).Returns(true); - } - - private void WithFailedDownload() - { - Mocker.GetMock<IHttpProvider>().Setup(c => c.DownloadFile(It.IsAny<string>(), It.IsAny<string>())).Throws(new WebException()); - } - - [Test] - public void DownloadNzb_should_download_file_if_it_doesnt_exist() - { - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock<IHttpProvider>().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath), Times.Once()); - } - - [Test] - public void should_replace_illegal_characters_in_title() - { - var illegalTitle = "Saturday Night Live - S38E08 - Jeremy Renner/Maroon 5 [SDTV]"; - var expectedFilename = Path.Combine(_blackHoleFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV].nzb"); - _remoteEpisode.Release.Title = illegalTitle; - - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock<IHttpProvider>().Verify(c => c.DownloadFile(It.IsAny<string>(), expectedFilename), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs new file mode 100644 index 000000000..30ed654bb --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -0,0 +1,106 @@ +using System; +using System.Text; +using System.Linq; +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using FluentAssertions; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests +{ + public abstract class DownloadClientFixtureBase<TSubject> : CoreTest<TSubject> + where TSubject : class, IDownloadClient + { + protected readonly string _title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE"; + protected readonly string _downloadUrl = "http://somewhere.com/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.ext"; + + [SetUp] + public void SetupBase() + { + Mocker.GetMock<IParsingService>() + .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), null)) + .Returns(CreateRemoteEpisode()); + } + + protected virtual RemoteEpisode CreateRemoteEpisode() + { + var remoteEpisode = new RemoteEpisode(); + remoteEpisode.Release = new ReleaseInfo(); + remoteEpisode.Release.Title = _title; + remoteEpisode.Release.DownloadUrl = _downloadUrl; + remoteEpisode.Release.DownloadProtocol = Subject.Protocol; + + remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo(); + remoteEpisode.ParsedEpisodeInfo.FullSeason = false; + + remoteEpisode.Episodes = new List<Episode>(); + + remoteEpisode.Series = new Series(); + + return remoteEpisode; + } + + protected void VerifyIdentifiable(DownloadClientItem downloadClientItem) + { + downloadClientItem.DownloadClient.Should().Be(Subject.Definition.Name); + downloadClientItem.DownloadClientId.Should().NotBeNullOrEmpty(); + + downloadClientItem.Title.Should().NotBeNullOrEmpty(); + + downloadClientItem.RemoteEpisode.Should().NotBeNull(); + + } + + protected void VerifyQueued(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + downloadClientItem.RemainingSize.Should().NotBe(0); + //downloadClientItem.RemainingTime.Should().NotBe(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Queued); + } + + protected void VerifyPaused(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.RemainingSize.Should().NotBe(0); + //downloadClientItem.RemainingTime.Should().NotBe(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Paused); + } + + protected void VerifyDownloading(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.RemainingSize.Should().NotBe(0); + //downloadClientItem.RemainingTime.Should().NotBe(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Downloading); + } + + protected void VerifyCompleted(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.Title.Should().NotBeNullOrEmpty(); + downloadClientItem.RemainingSize.Should().Be(0); + downloadClientItem.RemainingTime.Should().Be(TimeSpan.Zero); + //downloadClientItem.OutputPath.Should().NotBeNullOrEmpty(); + downloadClientItem.Status.Should().Be(DownloadItemStatus.Completed); + } + + protected void VerifyFailed(DownloadClientItem downloadClientItem) + { + VerifyIdentifiable(downloadClientItem); + + downloadClientItem.Status.Should().Be(DownloadItemStatus.Failed); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs deleted file mode 100644 index 848bc237e..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/DownloadNzbFixture.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using FizzWare.NBuilder; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Clients.Nzbget; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests -{ - public class DownloadNzbFixture : CoreTest<Nzbget> - { - private const string _url = "http://www.nzbdrone.com"; - private const string _title = "30.Rock.S01E01.Pilot.720p.hdtv"; - private RemoteEpisode _remoteEpisode; - - [SetUp] - public void Setup() - { - _remoteEpisode = new RemoteEpisode(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = _title; - _remoteEpisode.Release.DownloadUrl = _url; - - _remoteEpisode.Episodes = Builder<Episode>.CreateListOfSize(1) - .All() - .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) - .Build() - .ToList(); - - Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new NzbgetSettings - { - Host = "localhost", - Port = 6789, - Username = "nzbget", - Password = "pass", - TvCategory = "tv", - RecentTvPriority = (int)NzbgetPriority.High - }; - } - - [Test] - public void should_add_item_to_queue() - { - Mocker.GetMock<INzbgetProxy>() - .Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<NzbgetSettings>())) - .Returns("id"); - - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock<INzbgetProxy>() - .Verify(v => v.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<NzbgetSettings>()), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs new file mode 100644 index 000000000..e29186110 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs @@ -0,0 +1,207 @@ +using System; +using System.IO; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Nzbget; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using System.Collections.Generic; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests +{ + [TestFixture] + public class NzbgetFixture : DownloadClientFixtureBase<Nzbget> + { + private NzbgetQueueItem _queued; + private NzbgetHistoryItem _failed; + private NzbgetHistoryItem _completed; + + [SetUp] + public void Setup() + { + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new NzbgetSettings + { + Host = "192.168.5.55", + Port = 2222, + Username = "admin", + Password = "pass", + TvCategory = "tv", + RecentTvPriority = (int)NzbgetPriority.High + }; + + _queued = new NzbgetQueueItem + { + FileSizeLo = 1000, + RemainingSizeLo = 10, + Category = "tv", + NzbName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + Parameters = new List<NzbgetParameter> { new NzbgetParameter { Name = "drone", Value = "id" } } + }; + + _failed = new NzbgetHistoryItem + { + FileSizeLo = 1000, + Category = "tv", + Name = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + DestDir = "somedirectory", + Parameters = new List<NzbgetParameter> { new NzbgetParameter { Name = "drone", Value = "id" } }, + ParStatus = "Some Error" + }; + + _completed = new NzbgetHistoryItem + { + FileSizeLo = 1000, + Category = "tv", + Name = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + DestDir = "somedirectory", + Parameters = new List<NzbgetParameter> { new NzbgetParameter { Name = "drone", Value = "id" } }, + ParStatus = "SUCCESS", + ScriptStatus = "NONE" + }; + } + + protected void WithFailedDownload() + { + Mocker.GetMock<INzbgetProxy>() + .Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<int>(), It.IsAny<NzbgetSettings>())) + .Returns((String)null); + } + + protected void WithSuccessfulDownload() + { + Mocker.GetMock<INzbgetProxy>() + .Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<int>(), It.IsAny<NzbgetSettings>())) + .Returns(Guid.NewGuid().ToString().Replace("-", "")); + } + + protected virtual void WithQueue(NzbgetQueueItem queue) + { + var list = new List<NzbgetQueueItem>(); + + if (queue != null) + { + list.Add(queue); + } + + Mocker.GetMock<INzbgetProxy>() + .Setup(s => s.GetQueue(It.IsAny<NzbgetSettings>())) + .Returns(list); + } + + protected virtual void WithHistory(NzbgetHistoryItem history) + { + var list = new List<NzbgetHistoryItem>(); + + if (history != null) + { + list.Add(history); + } + + Mocker.GetMock<INzbgetProxy>() + .Setup(s => s.GetHistory(It.IsAny<NzbgetSettings>())) + .Returns(list); + } + + [Test] + public void GetItems_should_return_no_items_when_queue_is_empty() + { + WithQueue(null); + WithHistory(null); + + Subject.GetItems().Should().BeEmpty(); + } + + [Test] + public void queued_item_should_have_required_properties() + { + _queued.ActiveDownloads = 0; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyQueued(result); + } + + [Test] + public void paused_item_should_have_required_properties() + { + _queued.PausedSizeLo = _queued.FileSizeLo; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyPaused(result); + } + + [Test] + public void downloading_item_should_have_required_properties() + { + _queued.ActiveDownloads = 1; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyDownloading(result); + } + + [Test] + public void completed_download_should_have_required_properties() + { + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + VerifyCompleted(result); + } + + [Test] + public void failed_item_should_have_required_properties() + { + WithQueue(null); + WithHistory(_failed); + + var result = Subject.GetItems().Single(); + + VerifyFailed(result); + } + + [Test] + public void Download_should_return_unique_id() + { + WithSuccessfulDownload(); + + var remoteEpisode = CreateRemoteEpisode(); + + var id = Subject.Download(remoteEpisode); + + id.Should().NotBeNullOrEmpty(); + } + + [Test] + public void GetItems_should_ignore_downloads_from_other_categories() + { + _completed.Category = "mycat"; + + WithQueue(null); + WithHistory(_completed); + + var items = Subject.GetItems(); + + items.Should().BeEmpty(); + } + } +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs deleted file mode 100644 index ec7befef0..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/QueueFixture.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Download; -using NzbDrone.Core.Download.Clients.Nzbget; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests -{ - public class QueueFixture : CoreTest<Nzbget> - { - private List<NzbgetQueueItem> _queue; - - [SetUp] - public void Setup() - { - _queue = Builder<NzbgetQueueItem>.CreateListOfSize(5) - .All() - .With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb") - .With(q => q.Parameters = new List<NzbgetParameter> - { - new NzbgetParameter { Name = "drone", Value = "id" } - }) - .Build() - .ToList(); - - Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new NzbgetSettings - { - Host = "localhost", - Port = 6789, - Username = "nzbget", - Password = "pass", - TvCategory = "tv", - RecentTvPriority = (int)NzbgetPriority.High - }; - } - - private void WithFullQueue() - { - Mocker.GetMock<INzbgetProxy>() - .Setup(s => s.GetQueue(It.IsAny<NzbgetSettings>())) - .Returns(_queue); - } - - private void WithEmptyQueue() - { - Mocker.GetMock<INzbgetProxy>() - .Setup(s => s.GetQueue(It.IsAny<NzbgetSettings>())) - .Returns(new List<NzbgetQueueItem>()); - } - - [Test] - public void should_return_no_items_when_queue_is_empty() - { - WithEmptyQueue(); - - Subject.GetQueue() - .Should() - .BeEmpty(); - } - - [Test] - public void should_return_item_when_queue_has_item() - { - WithFullQueue(); - - Mocker.GetMock<IParsingService>() - .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), 0, null)) - .Returns(new RemoteEpisode {Series = new Series()}); - - Subject.GetQueue() - .Should() - .HaveCount(_queue.Count); - } - } -} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index 5903fa993..bae8867fe 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests [Test] public void should_download_file_if_it_doesnt_exist() { - Subject.DownloadNzb(_remoteEpisode); + Subject.Download(_remoteEpisode); Mocker.GetMock<IHttpProvider>().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath), Times.Once()); } @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests { WithFailedDownload(); - Assert.Throws<WebException>(() => Subject.DownloadNzb(_remoteEpisode)); + Assert.Throws<WebException>(() => Subject.Download(_remoteEpisode)); } [Test] @@ -84,7 +84,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _remoteEpisode.Release.Title = "30 Rock - Season 1"; _remoteEpisode.ParsedEpisodeInfo.FullSeason = true; - Assert.Throws<NotImplementedException>(() => Subject.DownloadNzb(_remoteEpisode)); + Assert.Throws<NotSupportedException>(() => Subject.Download(_remoteEpisode)); + } + + [Test] + public void should_throw_item_is_removed() + { + Assert.Throws<NotSupportedException>(() => Subject.RemoveItem("")); } [Test] @@ -94,7 +100,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests var expectedFilename = Path.Combine(_pneumaticFolder, "Saturday Night Live - S38E08 - Jeremy Renner+Maroon 5 [SDTV].nzb"); _remoteEpisode.Release.Title = illegalTitle; - Subject.DownloadNzb(_remoteEpisode); + Subject.Download(_remoteEpisode); Mocker.GetMock<IHttpProvider>().Verify(c => c.DownloadFile(It.IsAny<string>(), expectedFilename), Times.Once()); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index fe9529ef1..45260547e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -12,30 +12,20 @@ using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using System.Collections.Generic; namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests { [TestFixture] - public class SabnzbdFixture : CoreTest<Sabnzbd> + public class SabnzbdFixture : DownloadClientFixtureBase<Sabnzbd> { - private const string URL = "http://www.nzbclub.com/nzb_download.aspx?mid=1950232"; - private const string TITLE = "My Series Name - 5x2-5x3 - My title [Bluray720p] [Proper]"; - private RemoteEpisode _remoteEpisode; + private SabnzbdQueue _queued; + private SabnzbdHistory _failed; + private SabnzbdHistory _completed; [SetUp] public void Setup() { - _remoteEpisode = new RemoteEpisode(); - _remoteEpisode.Release = new ReleaseInfo(); - _remoteEpisode.Release.Title = TITLE; - _remoteEpisode.Release.DownloadUrl = URL; - - _remoteEpisode.Episodes = Builder<Episode>.CreateListOfSize(1) - .All() - .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) - .Build() - .ToList(); - Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = new SabnzbdSettings { @@ -47,16 +37,219 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests TvCategory = "tv", RecentTvPriority = (int)SabnzbdPriority.High }; + _queued = new SabnzbdQueue + { + Paused = false, + Items = new List<SabnzbdQueueItem>() + { + new SabnzbdQueueItem + { + Status = SabnzbdDownloadStatus.Downloading, + Size = 1000, + Sizeleft = 10, + Timeleft = TimeSpan.FromSeconds(10), + Category = "tv", + Id = "sabnzbd_nzb12345", + Title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE" + } + } + }; + + _failed = new SabnzbdHistory + { + Items = new List<SabnzbdHistoryItem>() + { + new SabnzbdHistoryItem + { + Status = SabnzbdDownloadStatus.Failed, + Size = 1000, + Category = "tv", + Id = "sabnzbd_nzb12345", + Title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE" + } + } + }; + + _completed = new SabnzbdHistory + { + Items = new List<SabnzbdHistoryItem>() + { + new SabnzbdHistoryItem + { + Status = SabnzbdDownloadStatus.Completed, + Size = 1000, + Category = "tv", + Id = "sabnzbd_nzb12345", + Title = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + Storage = "somedirectory" + } + } + }; + } + + protected void WithFailedDownload() + { + Mocker.GetMock<ISabnzbdProxy>() + .Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<int>(), It.IsAny<SabnzbdSettings>())) + .Returns((SabnzbdAddResponse)null); + } + + protected void WithSuccessfulDownload() + { + Mocker.GetMock<ISabnzbdProxy>() + .Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), It.IsAny<int>(), It.IsAny<SabnzbdSettings>())) + .Returns(new SabnzbdAddResponse() + { + Status = true, + Ids = new List<string> { "sabznbd_nzo12345" } + }); + } + + protected virtual void WithQueue(SabnzbdQueue queue) + { + if (queue == null) + { + queue = new SabnzbdQueue() { Items = new List<SabnzbdQueueItem>() }; + } + + Mocker.GetMock<ISabnzbdProxy>() + .Setup(s => s.GetQueue(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SabnzbdSettings>())) + .Returns(queue); + } + + protected virtual void WithHistory(SabnzbdHistory history) + { + if (history == null) + history = new SabnzbdHistory() { Items = new List<SabnzbdHistoryItem>() }; + + Mocker.GetMock<ISabnzbdProxy>() + .Setup(s => s.GetHistory(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<SabnzbdSettings>())) + .Returns(history); } [Test] - public void downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_true() + public void GetItems_should_return_no_items_when_queue_is_empty() + { + WithQueue(null); + WithHistory(null); + + Subject.GetItems().Should().BeEmpty(); + } + + [TestCase(SabnzbdDownloadStatus.Grabbing)] + [TestCase(SabnzbdDownloadStatus.Queued)] + public void queued_item_should_have_required_properties(SabnzbdDownloadStatus status) + { + _queued.Items.First().Status = status; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyQueued(result); + result.RemainingTime.Should().NotBe(TimeSpan.Zero); + } + + [TestCase(SabnzbdDownloadStatus.Paused)] + public void paused_item_should_have_required_properties(SabnzbdDownloadStatus status) + { + _queued.Items.First().Status = status; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyPaused(result); + } + + [TestCase(SabnzbdDownloadStatus.Checking)] + [TestCase(SabnzbdDownloadStatus.Downloading)] + [TestCase(SabnzbdDownloadStatus.QuickCheck)] + [TestCase(SabnzbdDownloadStatus.Verifying)] + [TestCase(SabnzbdDownloadStatus.Repairing)] + [TestCase(SabnzbdDownloadStatus.Fetching)] + [TestCase(SabnzbdDownloadStatus.Extracting)] + [TestCase(SabnzbdDownloadStatus.Moving)] + [TestCase(SabnzbdDownloadStatus.Running)] + public void downloading_item_should_have_required_properties(SabnzbdDownloadStatus status) + { + _queued.Items.First().Status = status; + + WithQueue(_queued); + WithHistory(null); + + var result = Subject.GetItems().Single(); + + VerifyDownloading(result); + result.RemainingTime.Should().NotBe(TimeSpan.Zero); + } + + [Test] + public void completed_download_should_have_required_properties() + { + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + VerifyCompleted(result); + } + + [Test] + public void failed_item_should_have_required_properties() + { + _completed.Items.First().Status = SabnzbdDownloadStatus.Failed; + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + VerifyFailed(result); + } + + [Test] + public void Download_should_return_unique_id() + { + WithSuccessfulDownload(); + + var remoteEpisode = CreateRemoteEpisode(); + + var id = Subject.Download(remoteEpisode); + + id.Should().NotBeNullOrEmpty(); + } + + [Test] + public void GetItems_should_ignore_downloads_from_other_categories() + { + _completed.Items.First().Category = "myowncat"; + + WithQueue(null); + WithHistory(_completed); + + var items = Subject.GetItems(); + + items.Should().BeEmpty(); + } + + [Test] + public void Download_should_use_sabRecentTvPriority_when_recentEpisode_is_true() { Mocker.GetMock<ISabnzbdProxy>() .Setup(s => s.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), (int)SabnzbdPriority.High, It.IsAny<SabnzbdSettings>())) .Returns(new SabnzbdAddResponse()); - Subject.DownloadNzb(_remoteEpisode); + var remoteEpisode = CreateRemoteEpisode(); + remoteEpisode.Episodes = Builder<Episode>.CreateListOfSize(1) + .All() + .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) + .Build() + .ToList(); + + Subject.Download(remoteEpisode); Mocker.GetMock<ISabnzbdProxy>() .Verify(v => v.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), (int)SabnzbdPriority.High, It.IsAny<SabnzbdSettings>()), Times.Once()); diff --git a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs index 0d3755468..eb9736870 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadServiceFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; +using System.Collections.Generic; namespace NzbDrone.Core.Test.Download { @@ -16,12 +17,19 @@ namespace NzbDrone.Core.Test.Download public class DownloadServiceFixture : CoreTest<DownloadService> { private RemoteEpisode _parseResult; - + private List<IDownloadClient> _downloadClients; [SetUp] public void Setup() { + _downloadClients = new List<IDownloadClient>(); + Mocker.GetMock<IProvideDownloadClient>() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock<IDownloadClient>().Object); + .Setup(v => v.GetDownloadClients()) + .Returns(_downloadClients); + + Mocker.GetMock<IProvideDownloadClient>() + .Setup(v => v.GetDownloadClient(It.IsAny<Indexers.DownloadProtocol>())) + .Returns<Indexers.DownloadProtocol>(v => _downloadClients.FirstOrDefault(d => d.Protocol == v)); var episodes = Builder<Episode>.CreateListOfSize(2) .TheFirst(1).With(s => s.Id = 12) @@ -29,31 +37,43 @@ namespace NzbDrone.Core.Test.Download .All().With(s => s.SeriesId = 5) .Build().ToList(); + var releaseInfo = Builder<ReleaseInfo>.CreateNew() + .With(v => v.DownloadProtocol = Indexers.DownloadProtocol.Usenet) + .Build(); + _parseResult = Builder<RemoteEpisode>.CreateNew() .With(c => c.Series = Builder<Series>.CreateNew().Build()) - .With(c => c.Release = Builder<ReleaseInfo>.CreateNew().Build()) + .With(c => c.Release = releaseInfo) .With(c => c.Episodes = episodes) .Build(); } - private void WithSuccessfulAdd() + private Mock<IDownloadClient> WithUsenetClient() { - Mocker.GetMock<IDownloadClient>() - .Setup(s => s.DownloadNzb(It.IsAny<RemoteEpisode>())); + var mock = new Mock<IDownloadClient>(Moq.MockBehavior.Default); + _downloadClients.Add(mock.Object); + + mock.SetupGet(v => v.Protocol).Returns(Indexers.DownloadProtocol.Usenet); + + return mock; } - private void WithFailedAdd() + private Mock<IDownloadClient> WithTorrentClient() { - Mocker.GetMock<IDownloadClient>() - .Setup(s => s.DownloadNzb(It.IsAny<RemoteEpisode>())) - .Throws(new WebException()); + var mock = new Mock<IDownloadClient>(Moq.MockBehavior.Default); + _downloadClients.Add(mock.Object); + + mock.SetupGet(v => v.Protocol).Returns(Indexers.DownloadProtocol.Torrent); + + return mock; } [Test] public void Download_report_should_publish_on_grab_event() { - WithSuccessfulAdd(); - + var mock = WithUsenetClient(); + mock.Setup(s => s.Download(It.IsAny<RemoteEpisode>())); + Subject.DownloadReport(_parseResult); VerifyEventPublished<EpisodeGrabbedEvent>(); @@ -62,18 +82,20 @@ namespace NzbDrone.Core.Test.Download [Test] public void Download_report_should_grab_using_client() { - WithSuccessfulAdd(); - + var mock = WithUsenetClient(); + mock.Setup(s => s.Download(It.IsAny<RemoteEpisode>())); + Subject.DownloadReport(_parseResult); - Mocker.GetMock<IDownloadClient>() - .Verify(s => s.DownloadNzb(It.IsAny<RemoteEpisode>()), Times.Once()); + mock.Verify(s => s.Download(It.IsAny<RemoteEpisode>()), Times.Once()); } [Test] public void Download_report_should_not_publish_on_failed_grab_event() { - WithFailedAdd(); + var mock = WithUsenetClient(); + mock.Setup(s => s.Download(It.IsAny<RemoteEpisode>())) + .Throws(new WebException()); Assert.Throws<WebException>(() => Subject.DownloadReport(_parseResult)); @@ -83,15 +105,38 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_attempt_download_if_client_isnt_configure() { - Mocker.GetMock<IProvideDownloadClient>() - .Setup(c => c.GetDownloadClient()).Returns((IDownloadClient)null); - Subject.DownloadReport(_parseResult); - Mocker.GetMock<IDownloadClient>().Verify(c => c.DownloadNzb(It.IsAny<RemoteEpisode>()), Times.Never()); + Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteEpisode>()), Times.Never()); VerifyEventNotPublished<EpisodeGrabbedEvent>(); ExceptionVerification.ExpectedWarns(1); } + + [Test] + public void should_send_download_to_correct_usenet_client() + { + var mockTorrent = WithTorrentClient(); + var mockUsenet = WithUsenetClient(); + + Subject.DownloadReport(_parseResult); + + mockTorrent.Verify(c => c.Download(It.IsAny<RemoteEpisode>()), Times.Never()); + mockUsenet.Verify(c => c.Download(It.IsAny<RemoteEpisode>()), Times.Once()); + } + + [Test] + public void should_send_download_to_correct_torrent_client() + { + var mockTorrent = WithTorrentClient(); + var mockUsenet = WithUsenetClient(); + + _parseResult.Release.DownloadProtocol = Indexers.DownloadProtocol.Torrent; + + Subject.DownloadReport(_parseResult); + + mockTorrent.Verify(c => c.Download(It.IsAny<RemoteEpisode>()), Times.Once()); + mockUsenet.Verify(c => c.Download(It.IsAny<RemoteEpisode>()), Times.Never()); + } } } diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs index dec70e91f..44e563718 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs @@ -13,32 +13,43 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Download { [TestFixture] - public class FailedDownloadServiceFixture : CoreTest<FailedDownloadService> + public class FailedDownloadServiceFixture : CoreTest<DownloadTrackingService> { - private List<HistoryItem> _completed; - private List<HistoryItem> _failed; + private List<DownloadClientItem> _completed; + private List<DownloadClientItem> _failed; [SetUp] public void Setup() { - _completed = Builder<HistoryItem>.CreateListOfSize(5) + _completed = Builder<DownloadClientItem>.CreateListOfSize(5) .All() - .With(h => h.Status = HistoryStatus.Completed) + .With(h => h.Status = DownloadItemStatus.Completed) .Build() .ToList(); - _failed = Builder<HistoryItem>.CreateListOfSize(1) + _failed = Builder<DownloadClientItem>.CreateListOfSize(1) .All() - .With(h => h.Status = HistoryStatus.Failed) + .With(h => h.Status = DownloadItemStatus.Failed) .Build() .ToList(); Mocker.GetMock<IProvideDownloadClient>() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock<IDownloadClient>().Object); + .Setup(c => c.GetDownloadClients()) + .Returns( new IDownloadClient[] { Mocker.GetMock<IDownloadClient>().Object }); + + Mocker.GetMock<IDownloadClient>() + .SetupGet(c => c.Definition) + .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); Mocker.GetMock<IConfigService>() .SetupGet(s => s.EnableFailedDownloadHandling) .Returns(true); + + Mocker.GetMock<IHistoryService>() + .Setup(s => s.Imported()) + .Returns(new List<History.History>()); + + Mocker.SetConstant<IFailedDownloadService>(Mocker.Resolve<FailedDownloadService>()); } private void GivenNoGrabbedHistory() @@ -72,7 +83,7 @@ namespace NzbDrone.Core.Test.Download private void GivenFailedDownloadClientHistory() { Mocker.GetMock<IDownloadClient>() - .Setup(s => s.GetHistory(0, 20)) + .Setup(s => s.GetItems()) .Returns(_failed); } @@ -102,10 +113,10 @@ namespace NzbDrone.Core.Test.Download public void should_not_process_if_no_download_client_history() { Mocker.GetMock<IDownloadClient>() - .Setup(s => s.GetHistory(0, 20)) - .Returns(new List<HistoryItem>()); + .Setup(s => s.GetItems()) + .Returns(new List<DownloadClientItem>()); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); Mocker.GetMock<IHistoryService>() .Verify(s => s.BetweenDates(It.IsAny<DateTime>(), It.IsAny<DateTime>(), HistoryEventType.Grabbed), @@ -117,11 +128,14 @@ namespace NzbDrone.Core.Test.Download [Test] public void should_not_process_if_no_failed_items_in_download_client_history() { + GivenNoGrabbedHistory(); + GivenNoFailedHistory(); + Mocker.GetMock<IDownloadClient>() - .Setup(s => s.GetHistory(0, 20)) + .Setup(s => s.GetItems()) .Returns(_completed); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); Mocker.GetMock<IHistoryService>() .Verify(s => s.BetweenDates(It.IsAny<DateTime>(), It.IsAny<DateTime>(), HistoryEventType.Grabbed), @@ -136,7 +150,7 @@ namespace NzbDrone.Core.Test.Download GivenNoGrabbedHistory(); GivenFailedDownloadClientHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -156,7 +170,7 @@ namespace NzbDrone.Core.Test.Download GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -171,7 +185,7 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); GivenGrabbedHistory(historyGrabbed); @@ -184,7 +198,7 @@ namespace NzbDrone.Core.Test.Download GivenFailedHistory(historyFailed); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -202,9 +216,9 @@ namespace NzbDrone.Core.Test.Download GivenFailedHistory(history); history.First().Data.Add("downloadClient", "SabnzbdClient"); - history.First().Data.Add("downloadClientId", _failed.First().Id); + history.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -222,9 +236,9 @@ namespace NzbDrone.Core.Test.Download GivenNoFailedHistory(); history.First().Data.Add("downloadClient", "SabnzbdClient"); - history.First().Data.Add("downloadClientId", _failed.First().Id); + history.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -244,10 +258,10 @@ namespace NzbDrone.Core.Test.Download history.ForEach(h => { h.Data.Add("downloadClient", "SabnzbdClient"); - h.Data.Add("downloadClientId", _failed.First().Id); + h.Data.Add("downloadClientId", _failed.First().DownloadClientId); }); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(2); } @@ -259,7 +273,7 @@ namespace NzbDrone.Core.Test.Download .SetupGet(s => s.EnableFailedDownloadHandling) .Returns(false); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -276,7 +290,7 @@ namespace NzbDrone.Core.Test.Download _failed.First().Message = "Unpacking failed, write error or disk is full?"; - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } @@ -291,12 +305,12 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -311,13 +325,13 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); historyGrabbed.First().Data.Add("ageHours", "48"); GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -332,14 +346,14 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); historyGrabbed.First().Data.Add("ageHours", "48"); GivenGrabbedHistory(historyGrabbed); GivenNoFailedHistory(); GivenGracePeriod(6); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyFailedDownloads(); } @@ -354,7 +368,7 @@ namespace NzbDrone.Core.Test.Download .ToList(); historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); - historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId); historyGrabbed.First().Data.Add("ageHours", "1"); GivenGrabbedHistory(historyGrabbed); @@ -362,7 +376,7 @@ namespace NzbDrone.Core.Test.Download GivenGracePeriod(6); GivenRetryLimit(1); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); VerifyNoFailedDownloads(); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs index 9b0982976..a573d2662 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs @@ -15,8 +15,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks public void should_return_warning_when_download_client_has_not_been_configured() { Mocker.GetMock<IProvideDownloadClient>() - .Setup(s => s.GetDownloadClient()) - .Returns((IDownloadClient)null); + .Setup(s => s.GetDownloadClients()) + .Returns(new IDownloadClient[0]); Subject.Check().ShouldBeWarning(); } @@ -26,12 +26,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { var downloadClient = Mocker.GetMock<IDownloadClient>(); - downloadClient.Setup(s => s.GetQueue()) + downloadClient.Setup(s => s.GetItems()) .Throws<Exception>(); Mocker.GetMock<IProvideDownloadClient>() - .Setup(s => s.GetDownloadClient()) - .Returns(downloadClient.Object); + .Setup(s => s.GetDownloadClients()) + .Returns(new IDownloadClient[] { downloadClient.Object }); Subject.Check().ShouldBeError(); } @@ -41,12 +41,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { var downloadClient = Mocker.GetMock<IDownloadClient>(); - downloadClient.Setup(s => s.GetQueue()) - .Returns(new List<QueueItem>()); + downloadClient.Setup(s => s.GetItems()) + .Returns(new List<DownloadClientItem>()); Mocker.GetMock<IProvideDownloadClient>() - .Setup(s => s.GetDownloadClient()) - .Returns(downloadClient.Object); + .Setup(s => s.GetDownloadClients()) + .Returns(new IDownloadClient[] { downloadClient.Object }); Subject.Check().ShouldBeOk(); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs index c3a9ef0c3..b21d29eae 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs @@ -25,17 +25,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER)) .Returns(exists); } - - [Test] - public void should_return_warning_when_drone_factory_folder_is_not_configured() - { - Mocker.GetMock<IConfigService>() - .SetupGet(s => s.DownloadedEpisodesFolder) - .Returns(""); - - Subject.Check().ShouldBeWarning(); - } - + [Test] public void should_return_error_when_drone_factory_folder_does_not_exist() { diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs new file mode 100644 index 000000000..236470dc1 --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using Moq; +using FizzWare.NBuilder; +using NUnit.Framework; +using NzbDrone.Test.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Download; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class ImportMechanismCheckFixture : CoreTest<ImportMechanismCheck> + { + private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted"; + + private IList<TrackedDownload> _completed; + + private void GivenCompletedDownloadHandling(bool? enabled = null) + { + if (enabled.HasValue) + { + Mocker.GetMock<IConfigService>() + .Setup(s => s.IsDefined("EnableCompletedDownloadHandling")) + .Returns(true); + + Mocker.GetMock<IConfigService>() + .SetupGet(s => s.EnableCompletedDownloadHandling) + .Returns(enabled.Value); + } + + _completed = Builder<TrackedDownload>.CreateListOfSize(1) + .All() + .With(v => v.State == TrackedDownloadState.Downloading) + .With(v => v.DownloadItem = new DownloadClientItem()) + .With(v => v.DownloadItem.Status = DownloadItemStatus.Completed) + .With(v => v.DownloadItem.OutputPath = @"C:\Test\DropFolder\myfile.mkv".AsOsAgnostic()) + .Build(); + + Mocker.GetMock<IDownloadTrackingService>() + .Setup(v => v.GetCompletedDownloads()) + .Returns(_completed.ToList()); + } + + private void GivenDroneFactoryFolder(bool exists = false) + { + Mocker.GetMock<IConfigService>() + .SetupGet(s => s.DownloadedEpisodesFolder) + .Returns(DRONE_FACTORY_FOLDER.AsOsAgnostic()); + + Mocker.GetMock<IDiskProvider>() + .Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER.AsOsAgnostic())) + .Returns(exists); + } + + [Test] + public void should_return_warning_when_completed_download_handling_not_configured() + { + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_warning_when_both_completeddownloadhandling_and_dronefactory_are_not_configured() + { + GivenCompletedDownloadHandling(false); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_warning_when_downloadclient_drops_in_dronefactory_folder() + { + GivenCompletedDownloadHandling(true); + GivenDroneFactoryFolder(true); + + _completed.First().DownloadItem.OutputPath = (DRONE_FACTORY_FOLDER + @"\myfile.mkv").AsOsAgnostic(); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_ok_when_no_issues_found() + { + GivenCompletedDownloadHandling(true); + GivenDroneFactoryFolder(true); + + Subject.Check().ShouldBeOk(); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs index 6a7a65736..ae5915799 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IndexerServiceFixture.cs @@ -29,37 +29,6 @@ namespace NzbDrone.Core.Test.IndexerTests Mocker.SetConstant<IEnumerable<IIndexer>>(_indexers); } - [Test] - public void should_create_default_indexer_on_startup() - { - IList<IndexerDefinition> storedIndexers = null; - - Mocker.GetMock<IIndexerRepository>() - .Setup(c => c.InsertMany(It.IsAny<IList<IndexerDefinition>>())) - .Callback<IList<IndexerDefinition>>(indexers => storedIndexers = indexers); - - Subject.Handle(new ApplicationStartedEvent()); - - storedIndexers.Should().NotBeEmpty(); - storedIndexers.Select(c => c.Name).Should().OnlyHaveUniqueItems(); - storedIndexers.Select(c => c.Enable).Should().NotBeEmpty(); - storedIndexers.Select(c => c.Implementation).Should().NotContainNulls(); - } - - [Test] - public void getting_list_of_indexers() - { - Mocker.SetConstant<IIndexerRepository>(Mocker.Resolve<IndexerRepository>()); - - Subject.Handle(new ApplicationStartedEvent()); - - var indexers = Subject.All().ToList(); - indexers.Should().NotBeEmpty(); - indexers.Should().NotContain(c => c.Settings == null); - indexers.Should().NotContain(c => c.Name == null); - indexers.Select(c => c.Name).Should().OnlyHaveUniqueItems(); - } - [Test] public void should_remove_missing_indexers_on_startup() { diff --git a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs index 1d12f233e..83047365c 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/IntegrationTests/IndexerIntegrationTests.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using FluentAssertions; using NzbDrone.Core.Indexers; -using NzbDrone.Core.Indexers.Eztv; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Wombles; using NzbDrone.Core.Parser.Model; @@ -37,39 +36,6 @@ namespace NzbDrone.Core.Test.IndexerTests.IntegrationTests ValidateResult(result, skipSize: true, skipInfo: true); } - - [Test] - public void extv_rss() - { - var indexer = new Eztv(); - indexer.Definition = new IndexerDefinition - { - Name = "Eztv", - Settings = NullConfig.Instance - }; - - var result = Subject.FetchRss(indexer); - - ValidateTorrentResult(result, skipSize: false, skipInfo: true); - } - - [Test] - public void nzbsorg_rss() - { - var indexer = new Newznab(); - - indexer.Definition = new IndexerDefinition(); - indexer.Definition.Name = "nzbs.org"; - indexer.Definition.Settings = new NewznabSettings - { - ApiKey = "64d61d3cfd4b75e51d01cbc7c6a78275", - Url = "http://nzbs.org" - }; - - var result = Subject.FetchRss(indexer); - - ValidateResult(result); - } private void ValidateResult(IList<ReleaseInfo> reports, bool skipSize = false, bool skipInfo = false) { diff --git a/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs index 7fb252ff3..74fda4de8 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/SeasonSearchFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.IndexerTests indexer.Setup(s => s.GetSeasonSearchUrls(It.IsAny<String>(), It.IsAny<Int32>(), It.IsAny<Int32>(), It.IsAny<Int32>())) .Returns(new List<string> { "http://www.nzbdrone.com" }); - indexer.SetupGet(s => s.SupportsPaging).Returns(paging); + indexer.SetupGet(s => s.SupportedPageSize).Returns(paging ? 100 : 0); var definition = new IndexerDefinition(); definition.Name = "Test"; diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index a0eecb805..3cc90f748 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns("c:\\drop\\".AsOsAgnostic()); Mocker.GetMock<IImportApprovedEpisodes>() - .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true)) + .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null)) .Returns(new List<ImportDecision>()); } @@ -77,6 +77,8 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_skip_if_file_is_in_use_by_another_process() { + GivenValidSeries(); + Mocker.GetMock<IDiskProvider>().Setup(c => c.IsFileLocked(It.IsAny<string>())) .Returns(true); @@ -122,7 +124,7 @@ namespace NzbDrone.Core.Test.MediaFiles public void should_not_delete_folder_if_no_files_were_imported() { Mocker.GetMock<IImportApprovedEpisodes>() - .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false)) + .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null)) .Returns(new List<ImportDecision>()); Subject.Execute(new DownloadedEpisodesScanCommand()); @@ -132,7 +134,7 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_delete_folder_if_files_were_imported_and_video_files_remain() + public void should_not_delete_folder_if_files_were_imported_and_video_files_remain() { GivenValidSeries(); @@ -146,7 +148,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(imported); Mocker.GetMock<IImportApprovedEpisodes>() - .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true)) + .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null)) .Returns(imported); Subject.Execute(new DownloadedEpisodesScanCommand()); @@ -172,7 +174,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(imported); Mocker.GetMock<IImportApprovedEpisodes>() - .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true)) + .Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null)) .Returns(imported); Mocker.GetMock<ISampleService>() @@ -211,13 +213,13 @@ namespace NzbDrone.Core.Test.MediaFiles private void VerifyNoImport() { - Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true), + Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null), Times.Never()); } private void VerifyImport() { - Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true), + Mocker.GetMock<IImportApprovedEpisodes>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null), Times.Once()); } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs deleted file mode 100644 index 2ede2be18..000000000 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotInUseSpecificationFixture.cs +++ /dev/null @@ -1,75 +0,0 @@ -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Common.Disk; -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.MediaFiles.EpisodeImport.Specifications -{ - [TestFixture] - public class NotInUseSpecificationFixture : CoreTest<NotInUseSpecification> - { - private LocalEpisode _localEpisode; - - [SetUp] - public void Setup() - { - _localEpisode = new LocalEpisode - { - Path = @"C:\Test\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(), - Size = 100, - Series = Builder<Series>.CreateNew().Build() - }; - } - - private void GivenChildOfSeries() - { - _localEpisode.ExistingFile = true; - } - - [Test] - public void should_return_true_if_file_is_under_series_folder() - { - GivenChildOfSeries(); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - - [Test] - public void should_not_check_for_file_in_use_if_child_of_series_folder() - { - GivenChildOfSeries(); - - Subject.IsSatisfiedBy(_localEpisode); - - Mocker.GetMock<IDiskProvider>() - .Verify(v => v.IsFileLocked(It.IsAny<string>()), Times.Never()); - } - - [Test] - public void should_return_false_if_file_is_in_use() - { - Mocker.GetMock<IDiskProvider>() - .Setup(s => s.IsFileLocked(It.IsAny<string>())) - .Returns(true); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); - } - - [Test] - public void should_return_true_if_file_is_not_in_use() - { - Mocker.GetMock<IDiskProvider>() - .Setup(s => s.IsFileLocked(It.IsAny<string>())) - .Returns(false); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - } -} diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index a1c9a22e1..904a0e5c7 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -57,20 +57,20 @@ namespace NzbDrone.Core.Test.MediaFiles } Mocker.GetMock<IUpgradeMediaFiles>() - .Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>())) + .Setup(s => s.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), It.IsAny<LocalEpisode>(), false)) .Returns(new EpisodeFileMoveResult()); } [Test] public void should_return_empty_list_if_there_are_no_approved_decisions() { - Subject.Import(_rejectedDecisions).Should().BeEmpty(); + Subject.Import(_rejectedDecisions, false).Should().BeEmpty(); } [Test] public void should_import_each_approved() { - Subject.Import(_approvedDecisions).Should().HaveCount(5); + Subject.Import(_approvedDecisions, false).Should().HaveCount(5); } [Test] @@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.MediaFiles all.AddRange(_rejectedDecisions); all.AddRange(_approvedDecisions); - Subject.Import(all).Should().HaveCount(5); + Subject.Import(all, false).Should().HaveCount(5); } [Test] @@ -90,7 +90,7 @@ namespace NzbDrone.Core.Test.MediaFiles all.AddRange(_approvedDecisions); all.Add(new ImportDecision(_approvedDecisions.First().LocalEpisode)); - Subject.Import(all).Should().HaveCount(5); + Subject.Import(all, false).Should().HaveCount(5); } [Test] @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.Import(new List<ImportDecision> {_approvedDecisions.First()}, true); Mocker.GetMock<IUpgradeMediaFiles>() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode), + .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Once()); } @@ -115,10 +115,10 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_not_move_existing_files() { - Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }); + Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false); Mocker.GetMock<IUpgradeMediaFiles>() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode), + .Verify(v => v.UpgradeEpisodeFile(It.IsAny<EpisodeFile>(), _approvedDecisions.First().LocalEpisode, false), Times.Never()); } @@ -143,7 +143,7 @@ namespace NzbDrone.Core.Test.MediaFiles all.Add(fileDecision); all.Add(sampleDecision); - var results = Subject.Import(all); + var results = Subject.Import(all, false); results.Should().HaveCount(1); results.Should().ContainSingle(d => d.LocalEpisode.Size == fileDecision.LocalEpisode.Size); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 82fc19ecc..862f9cd46 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -115,18 +115,20 @@ <Compile Include="DecisionEngineTests\Search\SeriesSpecificationFixture.cs" /> <Compile Include="Download\DownloadApprovedReportsTests\DownloadApprovedFixture.cs" /> <Compile Include="Download\DownloadApprovedReportsTests\GetQualifiedReportsFixture.cs" /> - <Compile Include="Download\DownloadClientTests\BlackholeProviderFixture.cs" /> - <Compile Include="Download\DownloadClientTests\NzbgetTests\DownloadNzbFixture.cs" /> - <Compile Include="Download\DownloadClientTests\NzbgetTests\QueueFixture.cs" /> + <Compile Include="Download\DownloadClientTests\Blackhole\UsenetBlackholeFixture.cs" /> + <Compile Include="Download\DownloadClientTests\DownloadClientFixtureBase.cs" /> + <Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" /> <Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" /> <Compile Include="Download\DownloadClientTests\SabnzbdTests\SabnzbdFixture.cs" /> <Compile Include="Download\DownloadServiceFixture.cs" /> + <Compile Include="Download\CompletedDownloadServiceFixture.cs" /> <Compile Include="Download\FailedDownloadServiceFixture.cs" /> <Compile Include="Framework\CoreTest.cs" /> <Compile Include="Framework\DbTest.cs" /> <Compile Include="Framework\NBuilderExtensions.cs" /> <Compile Include="HealthCheck\Checks\RootFolderCheckFixture.cs" /> <Compile Include="HealthCheck\Checks\DownloadClientCheckFixture.cs" /> + <Compile Include="HealthCheck\Checks\ImportMechanismCheckFixture.cs" /> <Compile Include="HealthCheck\Checks\UpdateCheckFixture.cs" /> <Compile Include="HealthCheck\Checks\IndexerCheckFixture.cs" /> <Compile Include="HealthCheck\Checks\DroneFactoryCheckFixture.cs" /> @@ -156,7 +158,6 @@ <Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecificationFixture.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecificationFixture.cs" /> <Compile Include="MediaFiles\EpisodeImport\ImportDecisionMakerFixture.cs" /> - <Compile Include="MediaFiles\EpisodeImport\Specifications\NotInUseSpecificationFixture.cs" /> <Compile Include="MediaFiles\EpisodeImport\SampleServiceFixture.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecificationFixture.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecificationFixture.cs" /> diff --git a/src/NzbDrone.Core.Test/app.config b/src/NzbDrone.Core.Test/app.config new file mode 100644 index 000000000..a6a2b7fa9 --- /dev/null +++ b/src/NzbDrone.Core.Test/app.config @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration /> \ No newline at end of file diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index ce3252221..af776e904 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -72,6 +72,11 @@ namespace NzbDrone.Core.Configuration _eventAggregator.PublishEvent(new ConfigSavedEvent()); } + public Boolean IsDefined(String key) + { + return _repository.Get(key.ToLower()) != null; + } + public String DownloadedEpisodesFolder { get { return GetValue(ConfigKey.DownloadedEpisodesFolder.ToString()); } @@ -117,6 +122,27 @@ namespace NzbDrone.Core.Configuration set { SetValue("AutoDownloadPropers", value); } } + public Boolean EnableCompletedDownloadHandling + { + get { return GetValueBoolean("EnableCompletedDownloadHandling", false); } + + set { SetValue("EnableCompletedDownloadHandling", value); } + } + + public Boolean RemoveCompletedDownloads + { + get { return GetValueBoolean("RemoveCompletedDownloads", false); } + + set { SetValue("RemoveCompletedDownloads", value); } + } + + public Boolean EnableFailedDownloadHandling + { + get { return GetValueBoolean("EnableFailedDownloadHandling", true); } + + set { SetValue("EnableFailedDownloadHandling", value); } + } + public Boolean AutoRedownloadFailed { get { return GetValueBoolean("AutoRedownloadFailed", true); } @@ -152,13 +178,6 @@ namespace NzbDrone.Core.Configuration set { SetValue("BlacklistRetryLimit", value); } } - public Boolean EnableFailedDownloadHandling - { - get { return GetValueBoolean("EnableFailedDownloadHandling", true); } - - set { SetValue("EnableFailedDownloadHandling", value); } - } - public Boolean CreateEmptySeriesFolders { get { return GetValueBoolean("CreateEmptySeriesFolders", false); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 10a1843a5..a295a1ac3 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -11,15 +11,20 @@ namespace NzbDrone.Core.Configuration Dictionary<String, Object> AllWithDefaults(); void SaveConfigDictionary(Dictionary<string, object> configValues); + Boolean IsDefined(String key); + //Download Client String DownloadedEpisodesFolder { get; set; } String DownloadClientWorkingFolders { get; set; } Int32 DownloadedEpisodesScanInterval { get; set; } - //Failed Download Handling (Download client) + //Completed/Failed Download Handling (Download client) + Boolean EnableCompletedDownloadHandling { get; set; } + Boolean RemoveCompletedDownloads { get; set; } + + Boolean EnableFailedDownloadHandling { get; set; } Boolean AutoRedownloadFailed { get; set; } Boolean RemoveFailedDownloads { get; set; } - Boolean EnableFailedDownloadHandling { get; set; } Int32 BlacklistGracePeriod { get; set; } Int32 BlacklistRetryInterval { get; set; } Int32 BlacklistRetryLimit { get; set; } diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 74ab43f69..edb84e624 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -37,7 +37,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity<Config>().RegisterModel("Config"); Mapper.Entity<RootFolder>().RegisterModel("RootFolders").Ignore(r => r.FreeSpace); - Mapper.Entity<IndexerDefinition>().RegisterModel("Indexers"); + Mapper.Entity<IndexerDefinition>().RegisterModel("Indexers") + .Ignore(s => s.Protocol); Mapper.Entity<ScheduledTask>().RegisterModel("ScheduledTasks"); Mapper.Entity<NotificationDefinition>().RegisterModel("Notifications"); Mapper.Entity<MetadataDefinition>().RegisterModel("Metadata"); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs index 4fe2010de..92f73d562 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotInQueueSpecification.cs @@ -5,17 +5,18 @@ using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Queue; namespace NzbDrone.Core.DecisionEngine.Specifications { public class NotInQueueSpecification : IDecisionEngineSpecification { - private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IQueueService _queueService; private readonly Logger _logger; - public NotInQueueSpecification(IProvideDownloadClient downloadClientProvider, Logger logger) + public NotInQueueSpecification(IQueueService queueService, Logger logger) { - _downloadClientProvider = downloadClientProvider; + _queueService = queueService; _logger = logger; } @@ -29,15 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { - var downloadClient = _downloadClientProvider.GetDownloadClient(); - - if (downloadClient == null) - { - _logger.Warn("Download client isn't configured yet."); - return true; - } - - var queue = downloadClient.GetQueue().Select(q => q.RemoteEpisode); + var queue = _queueService.GetQueue().Select(q => q.RemoteEpisode); if (IsInQueue(subject, queue)) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 0132f4adf..9dd1fc5c4 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -1,3 +1,4 @@ +using System.Linq; using NLog; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Sabnzbd; @@ -41,9 +42,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return true; } - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var downloadClients = _downloadClientProvider.GetDownloadClients(); - if (downloadClient != null && downloadClient.GetType() == typeof (Sabnzbd)) + foreach (var downloadClient in downloadClients.OfType<Sabnzbd>()) { _logger.Debug("Performing history status check on report"); foreach (var episode in subject.Episodes) diff --git a/src/NzbDrone.Core/Download/CheckForFailedDownloadCommand.cs b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs similarity index 61% rename from src/NzbDrone.Core/Download/CheckForFailedDownloadCommand.cs rename to src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs index a1714d35f..7dc987d84 100644 --- a/src/NzbDrone.Core/Download/CheckForFailedDownloadCommand.cs +++ b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Core.Download { - public class CheckForFailedDownloadCommand : Command + public class CheckForFinishedDownloadCommand : Command { } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs deleted file mode 100644 index 057556420..000000000 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using NLog; -using NzbDrone.Common; -using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Messaging.Commands; -using NzbDrone.Core.Organizer; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download.Clients.Blackhole -{ - public class Blackhole : DownloadClientBase<FolderSettings>, IExecute<TestBlackholeCommand> - { - private readonly IDiskProvider _diskProvider; - private readonly IHttpProvider _httpProvider; - private readonly Logger _logger; - - public Blackhole(IDiskProvider diskProvider, IHttpProvider httpProvider, Logger logger) - { - _diskProvider = diskProvider; - _httpProvider = httpProvider; - _logger = logger; - } - - public override string DownloadNzb(RemoteEpisode remoteEpisode) - { - var url = remoteEpisode.Release.DownloadUrl; - var title = remoteEpisode.Release.Title; - - title = FileNameBuilder.CleanFilename(title); - - var filename = Path.Combine(Settings.Folder, title + ".nzb"); - - - _logger.Debug("Downloading NZB from: {0} to: {1}", url, filename); - _httpProvider.DownloadFile(url, filename); - _logger.Debug("NZB Download succeeded, saved to: {0}", filename); - - return null; - } - - public override IEnumerable<QueueItem> GetQueue() - { - return new QueueItem[0]; - } - - public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10) - { - return new HistoryItem[0]; - } - - public override void RemoveFromQueue(string id) - { - } - - public override void RemoveFromHistory(string id) - { - } - - public override void RetryDownload(string id) - { - throw new NotImplementedException(); - } - - public override void Test() - { - PerformTest(Settings.Folder); - } - - private void PerformTest(string folder) - { - var testPath = Path.Combine(folder, "drone_test.txt"); - _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); - _diskProvider.DeleteFile(testPath); - } - - public void Execute(TestBlackholeCommand message) - { - PerformTest(message.Folder); - } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs index 38292bb26..88b6c7b25 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs @@ -5,15 +5,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { public class NzbgetQueueItem { - private string _nzbName; public Int32 NzbId { get; set; } public Int32 FirstId { get; set; } public Int32 LastId { get; set; } public string NzbName { get; set; } public String Category { get; set; } - public Int32 FileSizeMb { get; set; } - public Int32 RemainingSizeMb { get; set; } - public Int32 PausedSizeMb { get; set; } + public UInt32 FileSizeLo { get; set; } + public UInt32 FileSizeHi { get; set; } + public UInt32 RemainingSizeLo { get; set; } + public UInt32 RemainingSizeHi { get; set; } + public UInt32 PausedSizeLo { get; set; } + public UInt32 PausedSizeHi { get; set; } + public Int32 ActiveDownloads { get; set; } public List<NzbgetParameter> Parameters { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 531f56898..f7032256a 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -14,22 +15,27 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public class Nzbget : DownloadClientBase<NzbgetSettings>, IExecute<TestNzbgetCommand> { private readonly INzbgetProxy _proxy; - private readonly IParsingService _parsingService; private readonly IHttpProvider _httpProvider; - private readonly Logger _logger; public Nzbget(INzbgetProxy proxy, IParsingService parsingService, IHttpProvider httpProvider, Logger logger) + : base(parsingService, logger) { _proxy = proxy; - _parsingService = parsingService; _httpProvider = httpProvider; - _logger = logger; } - public override string DownloadNzb(RemoteEpisode remoteEpisode) + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title + ".nzb"; @@ -48,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } } - public override IEnumerable<QueueItem> GetQueue() + private IEnumerable<DownloadClientItem> GetQueue() { List<NzbgetQueueItem> queue; @@ -59,36 +65,42 @@ namespace NzbDrone.Core.Download.Clients.Nzbget catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty<QueueItem>(); + return Enumerable.Empty<DownloadClientItem>(); } - var queueItems = new List<QueueItem>(); + var queueItems = new List<DownloadClientItem>(); foreach (var item in queue) { var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); - var queueItem = new QueueItem(); - queueItem.Id = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(); + var queueItem = new DownloadClientItem(); + queueItem.DownloadClientId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(); queueItem.Title = item.NzbName; - queueItem.Size = item.FileSizeMb; - queueItem.Sizeleft = item.RemainingSizeMb; - queueItem.Status = item.FileSizeMb == item.PausedSizeMb ? "paused" : "queued"; + queueItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); + queueItem.RemainingSize = MakeInt64(item.RemainingSizeHi, item.RemainingSizeLo); + queueItem.Category = item.Category; - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); - if (parsedEpisodeInfo == null) continue; + if (queueItem.TotalSize == MakeInt64(item.PausedSizeHi, item.PausedSizeLo)) + { + queueItem.Status = DownloadItemStatus.Paused; + } + else if (item.ActiveDownloads == 0 && queueItem.RemainingSize != 0) + { + queueItem.Status = DownloadItemStatus.Queued; + } + else + { + queueItem.Status = DownloadItemStatus.Downloading; + } - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); - if (remoteEpisode.Series == null) continue; - - queueItem.RemoteEpisode = remoteEpisode; queueItems.Add(queueItem); } return queueItems; } - public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10) + private IEnumerable<DownloadClientItem> GetHistory() { List<NzbgetHistoryItem> history; @@ -99,10 +111,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty<HistoryItem>(); + return Enumerable.Empty<DownloadClientItem>(); } - var historyItems = new List<HistoryItem>(); + var historyItems = new List<DownloadClientItem>(); var successStatues = new[] {"SUCCESS", "NONE"}; foreach (var item in history) @@ -110,15 +122,15 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); var status = successStatues.Contains(item.ParStatus) && successStatues.Contains(item.ScriptStatus) - ? HistoryStatus.Completed - : HistoryStatus.Failed; + ? DownloadItemStatus.Completed + : DownloadItemStatus.Failed; - var historyItem = new HistoryItem(); - historyItem.Id = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); + var historyItem = new DownloadClientItem(); + historyItem.DownloadClient = Definition.Name; + historyItem.DownloadClientId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); historyItem.Title = item.Name; - historyItem.Size = item.FileSizeMb.ToString(); //Why is this a string? - historyItem.DownloadTime = 0; - historyItem.Storage = item.DestDir; + historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); + historyItem.OutputPath = item.DestDir; historyItem.Category = item.Category; historyItem.Message = String.Format("PAR Status: {0} - Script Status: {1}", item.ParStatus, item.ScriptStatus); historyItem.Status = status; @@ -129,12 +141,20 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return historyItems; } - public override void RemoveFromQueue(string id) + public override IEnumerable<DownloadClientItem> GetItems() { - throw new NotImplementedException(); + foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) + { + if (downloadClientItem.Category != Settings.TvCategory) continue; + + downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); + if (downloadClientItem.RemoteEpisode == null) continue; + + yield return downloadClientItem; + } } - public override void RemoveFromHistory(string id) + public override void RemoveItem(string id) { _proxy.RemoveFromHistory(id, Settings); } @@ -161,5 +181,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _proxy.GetVersion(settings); } + + // Javascript doesn't support 64 bit integers natively so json officially doesn't either. + // NzbGet api thus sends it in two 32 bit chunks. Here we join the two chunks back together. + // Simplified decimal example: "42" splits into "4" and "2". To join them I shift (<<) the "4" 1 digit to the left = "40". combine it with "2". which becomes "42" again. + private Int64 MakeInt64(UInt32 high, UInt32 low) + { + Int64 result = high; + + result = (result << 32) | (Int64)low; + + return result; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs index af90178a8..bce08e208 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs @@ -5,11 +5,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { public class NzbgetHistoryItem { - private string _nzbName; public Int32 Id { get; set; } public String Name { get; set; } public String Category { get; set; } - public Int32 FileSizeMb { get; set; } + public UInt32 FileSizeLo { get; set; } + public UInt32 FileSizeHi { get; set; } public String ParStatus { get; set; } public String ScriptStatus { get; set; } public String DestDir { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs index 805b4d19a..f596d6b25 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/TestNzbgetCommand.cs @@ -13,9 +13,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } } + public String Host { get; set; } public Int32 Port { get; set; } public String Username { get; set; } public String Password { get; set; } + public String TvCategory { get; set; } + public Int32 RecentTvPriority { get; set; } + public Int32 OlderTvPriority { get; set; } + public Boolean UseSsl { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 639b4e545..c62a6bca0 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -7,8 +7,10 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.Clients.Pneumatic @@ -21,22 +23,34 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic private static readonly Logger logger = NzbDroneLogger.GetLogger(); - public Pneumatic(IConfigService configService, IHttpProvider httpProvider, - IDiskProvider diskProvider) + public Pneumatic(IConfigService configService, + IHttpProvider httpProvider, + IDiskProvider diskProvider, + IParsingService parsingService, + Logger logger) + : base(parsingService, logger) { _configService = configService; _httpProvider = httpProvider; _diskProvider = diskProvider; } - public override string DownloadNzb(RemoteEpisode remoteEpisode) + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; if (remoteEpisode.ParsedEpisodeInfo.FullSeason) { - throw new NotImplementedException("Full season releases are not supported with Pneumatic."); + throw new NotSupportedException("Full season releases are not supported with Pneumatic."); } title = FileNameBuilder.CleanFilename(title); @@ -63,27 +77,19 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic } } - public override IEnumerable<QueueItem> GetQueue() + public override IEnumerable<DownloadClientItem> GetItems() { - return new QueueItem[0]; + return new DownloadClientItem[0]; } - - public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10) - { - return new HistoryItem[0]; - } - - public override void RemoveFromQueue(string id) - { - } - - public override void RemoveFromHistory(string id) + + public override void RemoveItem(string id) { + throw new NotSupportedException(); } public override void RetryDownload(string id) { - throw new NotImplementedException(); + throw new NotSupportedException(); } public override void Test() diff --git a/src/NzbDrone.Core/Download/Clients/FolderSettings.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs similarity index 94% rename from src/NzbDrone.Core/Download/Clients/FolderSettings.cs rename to src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs index cacb847ea..00ebb3b93 100644 --- a/src/NzbDrone.Core/Download/Clients/FolderSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation.Paths; -namespace NzbDrone.Core.Download.Clients +namespace NzbDrone.Core.Download.Clients.Pneumatic { public class FolderSettingsValidator : AbstractValidator<FolderSettings> { diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 931b919cf..af3af9a37 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common; -using NzbDrone.Common.Cache; using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -15,25 +15,27 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public class Sabnzbd : DownloadClientBase<SabnzbdSettings>, IExecute<TestSabnzbdCommand> { private readonly IHttpProvider _httpProvider; - private readonly IParsingService _parsingService; private readonly ISabnzbdProxy _proxy; - private readonly ICached<IEnumerable<QueueItem>> _queueCache; - private readonly Logger _logger; public Sabnzbd(IHttpProvider httpProvider, - ICacheManager cacheManager, IParsingService parsingService, ISabnzbdProxy proxy, Logger logger) + : base(parsingService, logger) { _httpProvider = httpProvider; - _parsingService = parsingService; _proxy = proxy; - _queueCache = cacheManager.GetCache<IEnumerable<QueueItem>>(GetType(), "queue"); - _logger = logger; } - public override string DownloadNzb(RemoteEpisode remoteEpisode) + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; @@ -54,76 +56,104 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } } - public override IEnumerable<QueueItem> GetQueue() + private IEnumerable<DownloadClientItem> GetQueue() { - return _queueCache.Get("queue", () => + SabnzbdQueue sabQueue; + + try { - SabnzbdQueue sabQueue; + sabQueue = _proxy.GetQueue(0, 0, Settings); + } + catch (DownloadClientException ex) + { + _logger.ErrorException(ex.Message, ex); + return Enumerable.Empty<DownloadClientItem>(); + } - try + var queueItems = new List<DownloadClientItem>(); + + foreach (var sabQueueItem in sabQueue.Items) + { + var queueItem = new DownloadClientItem(); + queueItem.DownloadClient = Definition.Name; + queueItem.DownloadClientId = sabQueueItem.Id; + queueItem.Category = sabQueueItem.Category; + queueItem.Title = sabQueueItem.Title; + queueItem.TotalSize = (long)(sabQueueItem.Size * 1024 * 1024); + queueItem.RemainingSize = (long)(sabQueueItem.Sizeleft * 1024 * 1024); + queueItem.RemainingTime = sabQueueItem.Timeleft; + + if (sabQueue.Paused || sabQueueItem.Status == SabnzbdDownloadStatus.Paused) { - sabQueue = _proxy.GetQueue(0, 0, Settings); + queueItem.Status = DownloadItemStatus.Paused; } - catch (DownloadClientException ex) + else if (sabQueueItem.Status == SabnzbdDownloadStatus.Queued || sabQueueItem.Status == SabnzbdDownloadStatus.Grabbing) { - _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty<QueueItem>(); + queueItem.Status = DownloadItemStatus.Queued; + } + else + { + queueItem.Status = DownloadItemStatus.Downloading; } - var queueItems = new List<QueueItem>(); - - foreach (var sabQueueItem in sabQueue.Items) + if (queueItem.Title.StartsWith("ENCRYPTED /")) { - var queueItem = new QueueItem(); - queueItem.Id = sabQueueItem.Id; - queueItem.Title = sabQueueItem.Title; - queueItem.Size = sabQueueItem.Size; - queueItem.Sizeleft = sabQueueItem.Sizeleft; - queueItem.Timeleft = sabQueueItem.Timeleft; - queueItem.Status = sabQueueItem.Status; - - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title.Replace("ENCRYPTED / ", "")); - if (parsedEpisodeInfo == null) continue; - - var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); - if (remoteEpisode.Series == null) continue; - - queueItem.RemoteEpisode = remoteEpisode; - - queueItems.Add(queueItem); + queueItem.Title = queueItem.Title.Substring(11); + queueItem.IsEncrypted = true; } - return queueItems; - }, TimeSpan.FromSeconds(10)); + queueItems.Add(queueItem); + } + + return queueItems; } - public override IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10) + private IEnumerable<DownloadClientItem> GetHistory() { SabnzbdHistory sabHistory; try { - sabHistory = _proxy.GetHistory(start, limit, Settings); + sabHistory = _proxy.GetHistory(0, 0, Settings); } catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty<HistoryItem>(); + return Enumerable.Empty<DownloadClientItem>(); } - var historyItems = new List<HistoryItem>(); + var historyItems = new List<DownloadClientItem>(); foreach (var sabHistoryItem in sabHistory.Items) { - var historyItem = new HistoryItem(); - historyItem.Id = sabHistoryItem.Id; - historyItem.Title = sabHistoryItem.Title; - historyItem.Size = sabHistoryItem.Size; - historyItem.DownloadTime = sabHistoryItem.DownloadTime; - historyItem.Storage = sabHistoryItem.Storage; - historyItem.Category = sabHistoryItem.Category; - historyItem.Message = sabHistoryItem.FailMessage; - historyItem.Status = sabHistoryItem.Status == "Failed" ? HistoryStatus.Failed : HistoryStatus.Completed; + var historyItem = new DownloadClientItem + { + DownloadClient = Definition.Name, + DownloadClientId = sabHistoryItem.Id, + Category = sabHistoryItem.Category, + Title = sabHistoryItem.Title, + + TotalSize = sabHistoryItem.Size, + RemainingSize = 0, + DownloadTime = TimeSpan.FromSeconds(sabHistoryItem.DownloadTime), + RemainingTime = TimeSpan.Zero, + + OutputPath = sabHistoryItem.Storage, + Message = sabHistoryItem.FailMessage + }; + + if (sabHistoryItem.Status == SabnzbdDownloadStatus.Failed) + { + historyItem.Status = DownloadItemStatus.Failed; + } + else if (sabHistoryItem.Status == SabnzbdDownloadStatus.Completed) + { + historyItem.Status = DownloadItemStatus.Completed; + } + else // Verifying/Moving etc + { + historyItem.Status = DownloadItemStatus.Downloading; + } historyItems.Add(historyItem); } @@ -131,14 +161,29 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return historyItems; } - public override void RemoveFromQueue(string id) + public override IEnumerable<DownloadClientItem> GetItems() { - _proxy.RemoveFrom("queue", id, Settings); + foreach (var downloadClientItem in GetQueue().Concat(GetHistory())) + { + if (downloadClientItem.Category != Settings.TvCategory) continue; + + downloadClientItem.RemoteEpisode = GetRemoteEpisode(downloadClientItem.Title); + if (downloadClientItem.RemoteEpisode == null) continue; + + yield return downloadClientItem; + } } - public override void RemoveFromHistory(string id) + public override void RemoveItem(string id) { - _proxy.RemoveFrom("history", id, Settings); + if (GetQueue().Any(v => v.DownloadClientId == id)) + { + _proxy.RemoveFrom("queue", id, Settings); + } + else + { + _proxy.RemoveFrom("history", id, Settings); + } } public override void RetryDownload(string id) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdDownloadStatus.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdDownloadStatus.cs new file mode 100644 index 000000000..16a3853ec --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdDownloadStatus.cs @@ -0,0 +1,22 @@ +using System; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd +{ + public enum SabnzbdDownloadStatus + { + Grabbing, + Queued, + Paused, + Checking, + Downloading, + QuickCheck, + Verifying, + Repairing, + Fetching, // Fetching additional blocks + Extracting, + Moving, + Running, // Running PP Script + Completed, + Failed + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs index 166b25c94..5a5f80ceb 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdHistoryItem.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Sabnzbd { @@ -7,7 +8,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [JsonProperty(PropertyName = "fail_message")] public string FailMessage { get; set; } - public string Size { get; set; } + [JsonProperty(PropertyName = "bytes")] + public Int64 Size { get; set; } public string Category { get; set; } [JsonProperty(PropertyName = "nzb_name")] @@ -17,7 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public int DownloadTime { get; set; } public string Storage { get; set; } - public string Status { get; set; } + public SabnzbdDownloadStatus Status { get; set; } [JsonProperty(PropertyName = "nzo_id")] public string Id { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 4c699ab64..52259fd7a 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -167,7 +167,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd result.Error = response.Content.Replace("error: ", ""); } - + if (result.Failed) throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error); } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs index a3a74452f..78e80f52c 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdQueueItem.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { public class SabnzbdQueueItem { - public string Status { get; set; } + public SabnzbdDownloadStatus Status { get; set; } public int Index { get; set; } [JsonConverter(typeof(SabnzbdQueueTimeConverter))] @@ -15,8 +15,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd [JsonProperty(PropertyName = "mb")] public decimal Size { get; set; } - private string _title; - [JsonProperty(PropertyName = "filename")] public string Title { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs index 2c1d2eb9d..458b62f3a 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/TestSabnzbdCommand.cs @@ -13,11 +13,15 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd } } + public String Host { get; set; } public Int32 Port { get; set; } public String ApiKey { get; set; } public String Username { get; set; } public String Password { get; set; } + public String TvCategory { get; set; } + public Int32 RecentTvPriority { get; set; } + public Int32 OlderTvPriority { get; set; } public Boolean UseSsl { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/TestUsenetBlackholeCommand.cs similarity index 51% rename from src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs rename to src/NzbDrone.Core/Download/Clients/UsenetBlackhole/TestUsenetBlackholeCommand.cs index 10898f80a..e4db46d4a 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/TestBlackholeCommand.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/TestUsenetBlackholeCommand.cs @@ -1,9 +1,9 @@ using System; using NzbDrone.Core.Messaging.Commands; -namespace NzbDrone.Core.Download.Clients.Blackhole +namespace NzbDrone.Core.Download.Clients.UsenetBlackhole { - public class TestBlackholeCommand : Command + public class TestUsenetBlackholeCommand : Command { public override bool SendUpdatesToClient { @@ -13,6 +13,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole } } - public String Folder { get; set; } + public String NzbFolder { get; set; } + public String WatchFolder { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs new file mode 100644 index 000000000..d534429ad --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Http; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Core.Download.Clients.UsenetBlackhole +{ + public class UsenetBlackhole : DownloadClientBase<UsenetBlackholeSettings>, IExecute<TestUsenetBlackholeCommand> + { + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; + private readonly IHttpProvider _httpProvider; + + public UsenetBlackhole(IDiskProvider diskProvider, + IDiskScanService diskScanService, + IParsingService parsingService, + IHttpProvider httpProvider, + Logger logger) + : base(parsingService, logger) + { + _diskProvider = diskProvider; + _diskScanService = diskScanService; + _httpProvider = httpProvider; + } + + public override DownloadProtocol Protocol + { + get + { + return DownloadProtocol.Usenet; + } + } + + public override string Download(RemoteEpisode remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title; + + title = FileNameBuilder.CleanFilename(title); + + var filename = Path.Combine(Settings.NzbFolder, title + ".nzb"); + + _logger.Debug("Downloading NZB from: {0} to: {1}", url, filename); + _httpProvider.DownloadFile(url, filename); + _logger.Debug("NZB Download succeeded, saved to: {0}", filename); + + return null; + } + + public override IEnumerable<DownloadClientItem> GetItems() + { + foreach (var folder in _diskProvider.GetDirectories(Settings.WatchFolder)) + { + var title = FileNameBuilder.CleanFilename(Path.GetFileName(folder)); + + var files = _diskProvider.GetFiles(folder, SearchOption.AllDirectories); + + var historyItem = new DownloadClientItem + { + DownloadClient = Definition.Name, + DownloadClientId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTimeUtc(folder).Ticks, + Title = title, + + TotalSize = files.Select(_diskProvider.GetFileSize).Sum(), + + OutputPath = folder + }; + + if (files.Any(_diskProvider.IsFileLocked)) + { + historyItem.Status = DownloadItemStatus.Downloading; + } + else + { + historyItem.Status = DownloadItemStatus.Completed; + } + + historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); + if (historyItem.RemoteEpisode == null) continue; + + yield return historyItem; + } + + foreach (var videoFile in _diskScanService.GetVideoFiles(Settings.WatchFolder, false)) + { + var title = FileNameBuilder.CleanFilename(Path.GetFileName(videoFile)); + + var historyItem = new DownloadClientItem + { + DownloadClient = Definition.Name, + DownloadClientId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWriteUtc(videoFile).Ticks, + Title = title, + + TotalSize = _diskProvider.GetFileSize(videoFile), + + OutputPath = videoFile + }; + + if (_diskProvider.IsFileLocked(videoFile)) + { + historyItem.Status = DownloadItemStatus.Downloading; + } + else + { + historyItem.Status = DownloadItemStatus.Completed; + } + + historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); + if (historyItem.RemoteEpisode == null) continue; + + yield return historyItem; + } + } + + public override void RemoveItem(string id) + { + throw new NotSupportedException(); + } + + public override void RetryDownload(string id) + { + throw new NotSupportedException(); + } + + public override void Test() + { + PerformTest(Settings.NzbFolder); + PerformTest(Settings.WatchFolder); + } + + private void PerformTest(string folder) + { + var testPath = Path.Combine(folder, "drone_test.txt"); + _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); + _diskProvider.DeleteFile(testPath); + } + + public void Execute(TestUsenetBlackholeCommand message) + { + PerformTest(Settings.NzbFolder); + PerformTest(Settings.WatchFolder); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs new file mode 100644 index 000000000..ae518f1ec --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs @@ -0,0 +1,35 @@ +using System; +using FluentValidation; +using FluentValidation.Results; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation.Paths; + +namespace NzbDrone.Core.Download.Clients.UsenetBlackhole +{ + public class UsenetBlackholeSettingsValidator : AbstractValidator<UsenetBlackholeSettings> + { + public UsenetBlackholeSettingsValidator() + { + //Todo: Validate that the path actually exists + RuleFor(c => c.NzbFolder).IsValidPath(); + } + } + + public class UsenetBlackholeSettings : IProviderConfig + { + private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator(); + + [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path)] + public String NzbFolder { get; set; } + + [FieldDefinition(1, Label = "Watch Folder", Type = FieldType.Path)] + public String WatchFolder { get; set; } + + public ValidationResult Validate() + { + return Validator.Validate(this); + } + } +} diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs new file mode 100644 index 000000000..c6aa2b0c6 --- /dev/null +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.History; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using System.IO; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Download +{ + public interface ICompletedDownloadService + { + void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> importedHistory); + } + + public class CompletedDownloadService : ICompletedDownloadService + { + private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; + private readonly IDiskProvider _diskProvider; + private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly Logger _logger; + + public CompletedDownloadService(IEventAggregator eventAggregator, + IConfigService configService, + IDiskProvider diskProvider, + IDownloadedEpisodesImportService downloadedEpisodesImportService, + Logger logger) + { + _eventAggregator = eventAggregator; + _configService = configService; + _diskProvider = diskProvider; + _downloadedEpisodesImportService = downloadedEpisodesImportService; + _logger = logger; + } + + private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId) + { + return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID))) + .ToList(); + } + + public void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> importedHistory) + { + if (!_configService.EnableCompletedDownloadHandling) + { + return; + } + + if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Completed && trackedDownload.State == TrackedDownloadState.Downloading) + { + var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (!grabbedItems.Any() && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) + { + _logger.Trace("Ignoring download that wasn't grabbed by drone: " + trackedDownload.DownloadItem.Title); + return; + } + + var importedItems = GetHistoryItems(importedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (importedItems.Any()) + { + trackedDownload.State = TrackedDownloadState.Imported; + + _logger.Debug("Already added to history as imported: " + trackedDownload.DownloadItem.Title); + } + else + { + string downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; + string downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; + if (downloadItemOutputPath.IsNullOrWhiteSpace()) + { + _logger.Trace("Storage path not specified: " + trackedDownload.DownloadItem.Title); + return; + } + + if (!downloadedEpisodesFolder.IsNullOrWhiteSpace() && (downloadedEpisodesFolder.PathEquals(downloadItemOutputPath) || downloadedEpisodesFolder.IsParentPath(downloadItemOutputPath))) + { + _logger.Trace("Storage path inside drone factory, ignoring download: " + trackedDownload.DownloadItem.Title); + return; + } + + if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath)) + { + var decisions = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem); + + if (decisions.Any()) + { + trackedDownload.State = TrackedDownloadState.Imported; + } + } + else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath)) + { + var decisions = _downloadedEpisodesImportService.ProcessFile(new FileInfo(trackedDownload.DownloadItem.OutputPath), trackedDownload.DownloadItem); + + if (decisions.Any()) + { + trackedDownload.State = TrackedDownloadState.Imported; + } + } + else + { + _logger.Debug("Storage path does not exist: " + trackedDownload.DownloadItem.Title); + return; + } + } + } + + if (_configService.RemoveCompletedDownloads && trackedDownload.State == TrackedDownloadState.Imported && !trackedDownload.DownloadItem.IsReadOnly) + { + try + { + _logger.Info("Removing completed download from history: {0}", trackedDownload.DownloadItem.Title); + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId); + + if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath)) + { + _logger.Info("Removing completed download directory: {0}", trackedDownload.DownloadItem.OutputPath); + _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath, true); + } + else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath)) + { + _logger.Info("Removing completed download file: {0}", trackedDownload.DownloadItem.OutputPath); + _diskProvider.DeleteFile(trackedDownload.DownloadItem.OutputPath); + } + + trackedDownload.State = TrackedDownloadState.Removed; + } + catch (NotSupportedException) + { + _logger.Debug("Removing item not supported by your download client"); + } + } + } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 8cf5a0717..410286aee 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -1,12 +1,20 @@ using System; +using System.Linq; using System.Collections.Generic; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; +using NLog; namespace NzbDrone.Core.Download { - public abstract class DownloadClientBase<TSettings> : IDownloadClient where TSettings : IProviderConfig, new() + public abstract class DownloadClientBase<TSettings> : IDownloadClient + where TSettings : IProviderConfig, new() { + private readonly IParsingService _parsingService; + protected readonly Logger _logger; + public Type ConfigContract { get @@ -33,17 +41,39 @@ namespace NzbDrone.Core.Download } } + protected DownloadClientBase(IParsingService parsingService, Logger logger) + { + _parsingService = parsingService; + _logger = logger; + } + public override string ToString() { return GetType().Name; } - public abstract string DownloadNzb(RemoteEpisode remoteEpisode); - public abstract IEnumerable<QueueItem> GetQueue(); - public abstract IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10); - public abstract void RemoveFromQueue(string id); - public abstract void RemoveFromHistory(string id); + + + public abstract DownloadProtocol Protocol + { + get; + } + + public abstract string Download(RemoteEpisode remoteEpisode); + public abstract IEnumerable<DownloadClientItem> GetItems(); + public abstract void RemoveItem(string id); public abstract void RetryDownload(string id); public abstract void Test(); + + protected RemoteEpisode GetRemoteEpisode(String title) + { + var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); + if (parsedEpisodeInfo == null) return null; + + var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); + if (remoteEpisode.Series == null) return null; + + return remoteEpisode; + } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index 25b4ee1c8..c038b5177 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Download { public interface IDownloadClientFactory : IProviderFactory<IDownloadClient, DownloadClientDefinition> { - List<IDownloadClient> Enabled(); + } public class DownloadClientFactory : ProviderFactory<IDownloadClient, DownloadClientDefinition>, IDownloadClientFactory @@ -22,9 +22,18 @@ namespace NzbDrone.Core.Download _providerRepository = providerRepository; } - public List<IDownloadClient> Enabled() + protected override List<DownloadClientDefinition> Active() { - return GetAvailableProviders().Where(n => ((DownloadClientDefinition)n.Definition).Enable).ToList(); + return base.Active().Where(c => c.Enable).ToList(); + } + + protected override DownloadClientDefinition GetTemplate(IDownloadClient provider) + { + var definition = base.GetTemplate(provider); + + definition.Protocol = provider.Protocol; + + return definition; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs new file mode 100644 index 000000000..e8b8b6fc5 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -0,0 +1,29 @@ +using NzbDrone.Core.Parser.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download +{ + public class DownloadClientItem + { + public string DownloadClient { get; set; } + public string DownloadClientId { get; set; } + public string Category { get; set; } + public string Title { get; set; } + + public long TotalSize { get; set; } + public long RemainingSize { get; set; } + public TimeSpan DownloadTime { get; set; } + public TimeSpan RemainingTime { get; set; } + + public string OutputPath { get; set; } + public string Message { get; set; } + + public DownloadItemStatus Status { get; set; } + public bool IsEncrypted { get; set; } + public bool IsReadOnly { get; set; } + public RemoteEpisode RemoteEpisode { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index 8fae72188..d0da96f6d 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -1,10 +1,14 @@ -using System.Linq; +using System; +using System.Linq; +using System.Collections.Generic; +using NzbDrone.Core.Indexers; namespace NzbDrone.Core.Download { public interface IProvideDownloadClient { - IDownloadClient GetDownloadClient(); + IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol); + IEnumerable<IDownloadClient> GetDownloadClients(); } public class DownloadClientProvider : IProvideDownloadClient @@ -16,9 +20,14 @@ namespace NzbDrone.Core.Download _downloadClientFactory = downloadClientFactory; } - public IDownloadClient GetDownloadClient() + public IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol) { - return _downloadClientFactory.Enabled().FirstOrDefault(); + return _downloadClientFactory.GetAvailableProviders().FirstOrDefault(v => v.Protocol == downloadProtocol); + } + + public IEnumerable<IDownloadClient> GetDownloadClients() + { + return _downloadClientFactory.GetAvailableProviders(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadItemStatus.cs b/src/NzbDrone.Core/Download/DownloadItemStatus.cs new file mode 100644 index 000000000..4ea8f4342 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadItemStatus.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download +{ + public enum DownloadItemStatus + { + Queued = 0, + Paused = 1, + Downloading = 2, + Completed = 3, + Failed = 4 + } +} diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index b8598cec1..874683db3 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -19,7 +19,6 @@ namespace NzbDrone.Core.Download private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; - public DownloadService(IProvideDownloadClient downloadClientProvider, IEventAggregator eventAggregator, Logger logger) { @@ -34,15 +33,15 @@ namespace NzbDrone.Core.Download Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); var downloadTitle = remoteEpisode.Release.Title; - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var downloadClient = _downloadClientProvider.GetDownloadClient(remoteEpisode.Release.DownloadProtocol); if (downloadClient == null) { - _logger.Warn("Download client isn't configured yet."); + _logger.Warn("{0} Download client isn't configured yet.", remoteEpisode.Release.DownloadProtocol); return; } - var downloadClientId = downloadClient.DownloadNzb(remoteEpisode); + var downloadClientId = downloadClient.Download(remoteEpisode); var episodeGrabbedEvent = new EpisodeGrabbedEvent(remoteEpisode); episodeGrabbedEvent.DownloadClient = downloadClient.GetType().Name; diff --git a/src/NzbDrone.Core/Download/DownloadTrackingService.cs b/src/NzbDrone.Core/Download/DownloadTrackingService.cs new file mode 100644 index 000000000..d06017125 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadTrackingService.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Common.Cache; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Queue; + +namespace NzbDrone.Core.Download +{ + public interface IDownloadTrackingService + { + List<TrackedDownload> GetTrackedDownloads(); + List<TrackedDownload> GetCompletedDownloads(); + List<TrackedDownload> GetQueuedDownloads(); + } + + public class DownloadTrackingService : IDownloadTrackingService, IExecute<CheckForFinishedDownloadCommand>, IHandle<ApplicationStartedEvent> + { + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IHistoryService _historyService; + private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly ICompletedDownloadService _completedDownloadService; + private readonly Logger _logger; + + private readonly ICached<TrackedDownload> _trackedDownloads; + private readonly ICached<List<TrackedDownload>> _queuedDownloads; + + public static string DOWNLOAD_CLIENT = "downloadClient"; + public static string DOWNLOAD_CLIENT_ID = "downloadClientId"; + + public DownloadTrackingService(IProvideDownloadClient downloadClientProvider, + IHistoryService historyService, + IEventAggregator eventAggregator, + IConfigService configService, + ICacheManager cacheManager, + IFailedDownloadService failedDownloadService, + ICompletedDownloadService completedDownloadService, + Logger logger) + { + _downloadClientProvider = downloadClientProvider; + _historyService = historyService; + _eventAggregator = eventAggregator; + _configService = configService; + _failedDownloadService = failedDownloadService; + _completedDownloadService = completedDownloadService; + _logger = logger; + + _trackedDownloads = cacheManager.GetCache<TrackedDownload>(GetType()); + _queuedDownloads = cacheManager.GetCache<List<TrackedDownload>>(GetType(), "queued"); + } + + public List<TrackedDownload> GetTrackedDownloads() + { + return _trackedDownloads.Values.ToList(); + } + + public List<TrackedDownload> GetCompletedDownloads() + { + return _trackedDownloads.Values.Where(v => v.State == TrackedDownloadState.Downloading && v.DownloadItem.Status == DownloadItemStatus.Completed).ToList(); + } + + public List<TrackedDownload> GetQueuedDownloads() + { + return _queuedDownloads.Get("queued", () => + { + UpdateTrackedDownloads(); + + var enabledFailedDownloadHandling = _configService.EnableFailedDownloadHandling; + var enabledCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling; + + return _trackedDownloads.Values + .Where(v => v.State == TrackedDownloadState.Downloading) + .Where(v => + v.DownloadItem.Status == DownloadItemStatus.Queued || + v.DownloadItem.Status == DownloadItemStatus.Paused || + v.DownloadItem.Status == DownloadItemStatus.Downloading || + v.DownloadItem.Status == DownloadItemStatus.Failed && enabledFailedDownloadHandling || + v.DownloadItem.Status == DownloadItemStatus.Completed && enabledCompletedDownloadHandling) + .ToList(); + + }, TimeSpan.FromSeconds(5.0)); + } + + private TrackedDownload GetTrackedDownload(IDownloadClient downloadClient, DownloadClientItem queueItem) + { + var id = String.Format("{0}-{1}", downloadClient.Definition.Id, queueItem.DownloadClientId); + var trackedDownload = _trackedDownloads.Get(id, () => new TrackedDownload + { + TrackingId = id, + DownloadClient = downloadClient.Definition.Id, + StartedTracking = DateTime.UtcNow, + State = TrackedDownloadState.Unknown + }); + + trackedDownload.DownloadItem = queueItem; + + return trackedDownload; + } + + private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId) + { + return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID))) + .ToList(); + } + + + private Boolean UpdateTrackedDownloads() + { + var downloadClients = _downloadClientProvider.GetDownloadClients(); + + var oldTrackedDownloads = new HashSet<TrackedDownload>(_trackedDownloads.Values); + var newTrackedDownloads = new HashSet<TrackedDownload>(); + + var stateChanged = false; + + foreach (var downloadClient in downloadClients) + { + var downloadClientHistory = downloadClient.GetItems().Select(v => GetTrackedDownload(downloadClient, v)).ToList(); + foreach (var trackedDownload in downloadClientHistory) + { + if (!oldTrackedDownloads.Contains(trackedDownload)) + { + _logger.Trace("Started tracking download from history: {0}", trackedDownload.TrackingId); + stateChanged = true; + } + + newTrackedDownloads.Add(trackedDownload); + } + } + + foreach (var item in oldTrackedDownloads.Except(newTrackedDownloads)) + { + if (item.State != TrackedDownloadState.Removed) + { + item.State = TrackedDownloadState.Removed; + stateChanged = true; + + _logger.Debug("Item removed from download client by user: {0}", item.TrackingId); + } + } + + foreach (var item in newTrackedDownloads.Union(oldTrackedDownloads).Where(v => v.State == TrackedDownloadState.Removed)) + { + _trackedDownloads.Remove(item.TrackingId); + + _logger.Trace("Stopped tracking download: {0}", item.TrackingId); + } + + _queuedDownloads.Clear(); + + return stateChanged; + } + + private void ProcessTrackedDownloads() + { + var grabbedHistory = _historyService.Grabbed(); + var failedHistory = _historyService.Failed(); + var importedHistory = _historyService.Imported(); + + var stateChanged = UpdateTrackedDownloads(); + + var downloadClients = _downloadClientProvider.GetDownloadClients(); + var trackedDownloads = _trackedDownloads.Values.ToArray(); + + foreach (var trackedDownload in trackedDownloads) + { + var downloadClient = downloadClients.Single(v => v.Definition.Id == trackedDownload.DownloadClient); + + var state = trackedDownload.State; + + if (trackedDownload.State == TrackedDownloadState.Unknown) + { + trackedDownload.State = TrackedDownloadState.Downloading; + } + + _failedDownloadService.CheckForFailedItem(downloadClient, trackedDownload, grabbedHistory, failedHistory); + _completedDownloadService.CheckForCompletedItem(downloadClient, trackedDownload, grabbedHistory, importedHistory); + + if (state != trackedDownload.State) + { + stateChanged = true; + } + } + + if (stateChanged) + { + _eventAggregator.PublishEvent(new UpdateQueueEvent()); + } + } + + public void Execute(CheckForFinishedDownloadCommand message) + { + ProcessTrackedDownloads(); + } + + public void Handle(ApplicationStartedEvent message) + { + ProcessTrackedDownloads(); + } + } +} diff --git a/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs deleted file mode 100644 index 0475ceaf2..000000000 --- a/src/NzbDrone.Core/Download/Events/DownloadFailedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Download.Events -{ - public class DownloadFailedEvent : IEvent - { - public Int32 SeriesId { get; set; } - public List<Int32> EpisodeIds { get; set; } - public QualityModel Quality { get; set; } - public String SourceTitle { get; set; } - public String DownloadClient { get; set; } - public String DownloadClientId { get; set; } - public String Message { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs b/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs deleted file mode 100644 index 887e42362..000000000 --- a/src/NzbDrone.Core/Download/Events/EpisodeGrabbedEvent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using NzbDrone.Common.Messaging; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download.Events -{ - public class EpisodeGrabbedEvent : IEvent - { - public RemoteEpisode Episode { get; private set; } - public String DownloadClient { get; set; } - public String DownloadClientId { get; set; } - - public EpisodeGrabbedEvent(RemoteEpisode episode) - { - Episode = episode; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownload.cs b/src/NzbDrone.Core/Download/FailedDownload.cs deleted file mode 100644 index eead58f05..000000000 --- a/src/NzbDrone.Core/Download/FailedDownload.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download -{ - public class FailedDownload - { - public HistoryItem DownloadClientHistoryItem { get; set; } - public DateTime LastRetry { get; set; } - public Int32 RetryCount { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index e5be96880..cc25cf872 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -14,35 +14,25 @@ namespace NzbDrone.Core.Download public interface IFailedDownloadService { void MarkAsFailed(int historyId); + void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> failedHistory); } - public class FailedDownloadService : IFailedDownloadService, IExecute<CheckForFailedDownloadCommand> + public class FailedDownloadService : IFailedDownloadService { - private readonly IProvideDownloadClient _downloadClientProvider; private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; private readonly Logger _logger; - private readonly ICached<FailedDownload> _failedDownloads; - - private static string DOWNLOAD_CLIENT = "downloadClient"; - private static string DOWNLOAD_CLIENT_ID = "downloadClientId"; - - public FailedDownloadService(IProvideDownloadClient downloadClientProvider, - IHistoryService historyService, + public FailedDownloadService(IHistoryService historyService, IEventAggregator eventAggregator, IConfigService configService, - ICacheManager cacheManager, Logger logger) { - _downloadClientProvider = downloadClientProvider; _historyService = historyService; _eventAggregator = eventAggregator; _configService = configService; _logger = logger; - - _failedDownloads = cacheManager.GetCache<FailedDownload>(GetType()); } public void MarkAsFailed(int historyId) @@ -51,149 +41,92 @@ namespace NzbDrone.Core.Download PublishDownloadFailedEvent(new List<History.History> { item }, "Manually marked as failed"); } - private void CheckQueue(List<History.History> grabbedHistory, List<History.History> failedHistory) + public void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> failedHistory) { - var downloadClient = GetDownloadClient(); - - if (downloadClient == null) + if (!_configService.EnableFailedDownloadHandling) { return; } - var downloadClientQueue = downloadClient.GetQueue().ToList(); - var failedItems = downloadClientQueue.Where(q => q.Title.StartsWith("ENCRYPTED / ")).ToList(); - - if (!failedItems.Any()) + if (trackedDownload.DownloadItem.IsEncrypted && trackedDownload.State == TrackedDownloadState.Downloading) { - _logger.Debug("Yay! No encrypted downloads"); - return; - } + var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId); - foreach (var failedItem in failedItems) - { - var failedLocal = failedItem; - var historyItems = GetHistoryItems(grabbedHistory, failedLocal.Id); - - if (!historyItems.Any()) + if (!grabbedItems.Any()) { - _logger.Debug("Unable to find matching history item"); - continue; + _logger.Debug("Download was not grabbed by drone, ignoring."); + return; } - if (failedHistory.Any(h => failedLocal.Id.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID)))) + trackedDownload.State = TrackedDownloadState.DownloadFailed; + + var failedItems = GetHistoryItems(failedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (failedItems.Any()) { _logger.Debug("Already added to history as failed"); - continue; } - - PublishDownloadFailedEvent(historyItems, "Encrypted download detected"); - - if (_configService.RemoveFailedDownloads) + else { - _logger.Info("Removing encrypted download from queue: {0}", failedItem.Title.Replace("ENCRYPTED / ", "")); - downloadClient.RemoveFromQueue(failedItem.Id); + PublishDownloadFailedEvent(grabbedItems, "Encrypted download detected"); } } - } - private void CheckHistory(List<History.History> grabbedHistory, List<History.History> failedHistory) - { - var downloadClient = GetDownloadClient(); - - if (downloadClient == null) + if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed && trackedDownload.State == TrackedDownloadState.Downloading) { - return; - } + var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId); - var downloadClientHistory = downloadClient.GetHistory(0, 20).ToList(); - var failedItems = downloadClientHistory.Where(h => h.Status == HistoryStatus.Failed).ToList(); - - if (!failedItems.Any()) - { - _logger.Debug("Yay! No failed downloads"); - return; - } - - foreach (var failedItem in failedItems) - { - var failedLocal = failedItem; - var historyItems = GetHistoryItems(grabbedHistory, failedLocal.Id); - - if (!historyItems.Any()) + if (!grabbedItems.Any()) { - _logger.Debug("Unable to find matching history item"); - continue; + _logger.Debug("Download was not grabbed by drone, ignoring."); + return; } //TODO: Make this more configurable (ignore failure reasons) to support changes and other failures that should be ignored - if (failedLocal.Message.Equals("Unpacking failed, write error or disk is full?", + if (trackedDownload.DownloadItem.Message.Equals("Unpacking failed, write error or disk is full?", StringComparison.InvariantCultureIgnoreCase)) { _logger.Debug("Failed due to lack of disk space, do not blacklist"); - continue; + return; } - if (FailedDownloadForRecentRelease(failedItem, historyItems)) + if (FailedDownloadForRecentRelease(downloadClient, trackedDownload, grabbedItems)) { _logger.Debug("Recent release Failed, do not blacklist"); - continue; + return; } - - if (failedHistory.Any(h => failedLocal.Id.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID)))) + + trackedDownload.State = TrackedDownloadState.DownloadFailed; + + var failedItems = GetHistoryItems(failedHistory, trackedDownload.DownloadItem.DownloadClientId); + + if (failedItems.Any()) { _logger.Debug("Already added to history as failed"); - continue; } - - PublishDownloadFailedEvent(historyItems, failedItem.Message); - - if (_configService.RemoveFailedDownloads) + else { - _logger.Info("Removing failed download from history: {0}", failedItem.Title); - downloadClient.RemoveFromHistory(failedItem.Id); + PublishDownloadFailedEvent(grabbedItems, trackedDownload.DownloadItem.Message); + } + } + + if (_configService.RemoveFailedDownloads && trackedDownload.State == TrackedDownloadState.DownloadFailed) + { + try + { + _logger.Info("Removing failed download from client: {0}", trackedDownload.DownloadItem.Title); + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId); + + trackedDownload.State = TrackedDownloadState.Removed; + } + catch (NotSupportedException) + { + _logger.Debug("Removing item not supported by your download client"); } } } - private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId) - { - return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID))) - .ToList(); - } - - private void PublishDownloadFailedEvent(List<History.History> historyItems, string message) - { - var historyItem = historyItems.First(); - - var downloadFailedEvent = new DownloadFailedEvent - { - SeriesId = historyItem.SeriesId, - EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), - Quality = historyItem.Quality, - SourceTitle = historyItem.SourceTitle, - DownloadClient = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT), - DownloadClientId = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID), - Message = message - }; - - downloadFailedEvent.Data = downloadFailedEvent.Data.Merge(historyItem.Data); - - _eventAggregator.PublishEvent(downloadFailedEvent); - } - - private IDownloadClient GetDownloadClient() - { - var downloadClient = _downloadClientProvider.GetDownloadClient(); - - if (downloadClient == null) - { - _logger.Debug("No download client is configured"); - } - - return downloadClient; - } - - private bool FailedDownloadForRecentRelease(HistoryItem failedDownloadHistoryItem, List<History.History> matchingHistoryItems) + private bool FailedDownloadForRecentRelease(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> matchingHistoryItems) { double ageHours; @@ -209,31 +142,23 @@ namespace NzbDrone.Core.Download return false; } - var tracked = _failedDownloads.Get(failedDownloadHistoryItem.Id, () => new FailedDownload - { - DownloadClientHistoryItem = failedDownloadHistoryItem, - LastRetry = DateTime.UtcNow - } - ); - - if (tracked.RetryCount >= _configService.BlacklistRetryLimit) + if (trackedDownload.RetryCount >= _configService.BlacklistRetryLimit) { _logger.Debug("Retry limit reached"); return false; } - if (tracked.LastRetry.AddMinutes(_configService.BlacklistRetryInterval) < DateTime.UtcNow) + if (trackedDownload.RetryCount == 0 || trackedDownload.LastRetry.AddMinutes(_configService.BlacklistRetryInterval) < DateTime.UtcNow) { _logger.Debug("Retrying failed release"); - tracked.LastRetry = DateTime.UtcNow; - tracked.RetryCount++; + trackedDownload.LastRetry = DateTime.UtcNow; + trackedDownload.RetryCount++; try { - GetDownloadClient().RetryDownload(failedDownloadHistoryItem.Id); + downloadClient.RetryDownload(trackedDownload.DownloadItem.DownloadClientId); } - - catch (NotImplementedException ex) + catch (NotSupportedException ex) { _logger.Debug("Retrying failed downloads is not supported by your download client"); return false; @@ -243,19 +168,30 @@ namespace NzbDrone.Core.Download return true; } - public void Execute(CheckForFailedDownloadCommand message) + private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId) { - if (!_configService.EnableFailedDownloadHandling) + return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID))) + .ToList(); + } + + private void PublishDownloadFailedEvent(List<History.History> historyItems, string message) + { + var historyItem = historyItems.First(); + + var downloadFailedEvent = new DownloadFailedEvent { - _logger.Debug("Failed Download Handling is not enabled"); - return; - } + SeriesId = historyItem.SeriesId, + EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), + Quality = historyItem.Quality, + SourceTitle = historyItem.SourceTitle, + DownloadClient = historyItem.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT), + DownloadClientId = historyItem.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID), + Message = message + }; - var grabbedHistory = _historyService.Grabbed(); - var failedHistory = _historyService.Failed(); + downloadFailedEvent.Data = downloadFailedEvent.Data.Merge(historyItem.Data); - CheckQueue(grabbedHistory, failedHistory); - CheckHistory(grabbedHistory, failedHistory); + _eventAggregator.PublishEvent(downloadFailedEvent); } } } diff --git a/src/NzbDrone.Core/Download/HistoryItem.cs b/src/NzbDrone.Core/Download/HistoryItem.cs deleted file mode 100644 index 9475b527d..000000000 --- a/src/NzbDrone.Core/Download/HistoryItem.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download -{ - public class HistoryItem - { - public String Id { get; set; } - public String Title { get; set; } - public String Size { get; set; } - public String Category { get; set; } - public Int32 DownloadTime { get; set; } - public String Storage { get; set; } - public String Message { get; set; } - public HistoryStatus Status { get; set; } - } - - public enum HistoryStatus - { - Completed = 0, - Failed = 1 - } -} diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index d246e9645..aab29cde8 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; @@ -6,11 +7,11 @@ namespace NzbDrone.Core.Download { public interface IDownloadClient : IProvider { - string DownloadNzb(RemoteEpisode remoteEpisode); - IEnumerable<QueueItem> GetQueue(); - IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 0); - void RemoveFromQueue(string id); - void RemoveFromHistory(string id); + DownloadProtocol Protocol { get; } + + string Download(RemoteEpisode remoteEpisode); + IEnumerable<DownloadClientItem> GetItems(); + void RemoveItem(string id); void RetryDownload(string id); void Test(); } diff --git a/src/NzbDrone.Core/Download/QueueItem.cs b/src/NzbDrone.Core/Download/QueueItem.cs deleted file mode 100644 index 9112680b9..000000000 --- a/src/NzbDrone.Core/Download/QueueItem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.Download -{ - public class QueueItem - { - public string Id { get; set; } - public decimal Size { get; set; } - public string Title { get; set; } - public decimal Sizeleft { get; set; } - public TimeSpan Timeleft { get; set; } - public String Status { get; set; } - public RemoteEpisode RemoteEpisode { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownload.cs new file mode 100644 index 000000000..9d490c51e --- /dev/null +++ b/src/NzbDrone.Core/Download/TrackedDownload.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Download +{ + public class TrackedDownload + { + public String TrackingId { get; set; } + public Int32 DownloadClient { get; set; } + public DownloadClientItem DownloadItem { get; set; } + public TrackedDownloadState State { get; set; } + public DateTime StartedTracking { get; set; } + public DateTime LastRetry { get; set; } + public Int32 RetryCount { get; set; } + } + + public enum TrackedDownloadState + { + Unknown, + Downloading, + Imported, + DownloadFailed, + Removed + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs index b45ee036f..f86bdf679 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using NzbDrone.Core.Download; namespace NzbDrone.Core.HealthCheck.Checks @@ -14,16 +15,19 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var downloadClients = _downloadClientProvider.GetDownloadClients(); - if (downloadClient == null) + if (downloadClients.Count() == 0) { return new HealthCheck(GetType(), HealthCheckResult.Warning, "No download client is available"); } try { - downloadClient.GetQueue(); + foreach (var downloadClient in downloadClients) + { + downloadClient.GetItems(); + } } catch (Exception) { diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs index b26cf3404..f539b58e1 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.HealthCheck.Checks if (droneFactoryFolder.IsNullOrWhiteSpace()) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Drone factory folder is not configured"); + return new HealthCheck(GetType()); } if (!_diskProvider.FolderExists(droneFactoryFolder)) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs new file mode 100644 index 000000000..d78b97d93 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Download; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + public class ImportMechanismCheck : HealthCheckBase + { + private readonly IConfigService _configService; + private readonly IDownloadTrackingService _downloadTrackingService; + + public ImportMechanismCheck(IConfigService configService, IDownloadTrackingService downloadTrackingService) + { + _configService = configService; + _downloadTrackingService = downloadTrackingService; + } + + public override HealthCheck Check() + { + if (!_configService.IsDefined("EnableCompletedDownloadHandling")) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Completed Download Handling is disabled"); + } + + var droneFactoryFolder = _configService.DownloadedEpisodesFolder; + + if (!_configService.EnableCompletedDownloadHandling && droneFactoryFolder.IsNullOrWhiteSpace()) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling or configure Drone factory"); + } + + if (_configService.EnableCompletedDownloadHandling && !droneFactoryFolder.IsNullOrWhiteSpace() && _downloadTrackingService.GetCompletedDownloads().Any(v => droneFactoryFolder.PathEquals(v.DownloadItem.OutputPath) || droneFactoryFolder.IsParentPath(v.DownloadItem.OutputPath))) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Download Client has history items in Drone Factory conflicting with Completed Download Handling"); + } + + return new HealthCheck(GetType()); + } + } +} diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index dead6df7c..b76a3be12 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.History List<History> BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List<History> Failed(); List<History> Grabbed(); + List<History> Imported(); History MostRecentForEpisode(int episodeId); List<History> FindBySourceTitle(string sourceTitle); } @@ -62,6 +63,11 @@ namespace NzbDrone.Core.History return Query.Where(h => h.EventType == HistoryEventType.Grabbed); } + public List<History> Imported() + { + return Query.Where(h => h.EventType == HistoryEventType.DownloadFolderImported); + } + public History MostRecentForEpisode(int episodeId) { return Query.Where(h => h.EpisodeId == episodeId) diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index c95bc233a..37824b5d1 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.History List<History> BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List<History> Failed(); List<History> Grabbed(); + List<History> Imported(); History MostRecentForEpisode(int episodeId); History Get(int id); List<History> FindBySourceTitle(string sourceTitle); @@ -62,6 +63,11 @@ namespace NzbDrone.Core.History return _historyRepository.Grabbed(); } + public List<History> Imported() + { + return _historyRepository.Imported(); + } + public History MostRecentForEpisode(int episodeId) { return _historyRepository.MostRecentForEpisode(episodeId); @@ -149,6 +155,8 @@ namespace NzbDrone.Core.History //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); history.Data.Add("DroppedPath", message.EpisodeInfo.Path); history.Data.Add("ImportedPath", message.ImportedEpisode.Path); + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("DownloadClientId", message.DownloadClientId); _historyRepository.Insert(history); } diff --git a/src/NzbDrone.Core/Indexers/DownloadProtocol.cs b/src/NzbDrone.Core/Indexers/DownloadProtocol.cs new file mode 100644 index 000000000..eac150ce6 --- /dev/null +++ b/src/NzbDrone.Core/Indexers/DownloadProtocol.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Indexers +{ + public enum DownloadProtocol + { + Usenet = 1, + Torrent = 2 + } +} diff --git a/src/NzbDrone.Core/Indexers/DownloadProtocols.cs b/src/NzbDrone.Core/Indexers/DownloadProtocols.cs deleted file mode 100644 index 4fff5e07d..000000000 --- a/src/NzbDrone.Core/Indexers/DownloadProtocols.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Indexers -{ - public enum DownloadProtocols - { - Nzb = 0, - Torrent =1 - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs b/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs deleted file mode 100644 index f926d911e..000000000 --- a/src/NzbDrone.Core/Indexers/Eztv/Eztv.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.Indexers.Eztv -{ - public class Eztv : IndexerBase<NullConfig> - { - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Torrent; - } - } - - public override bool SupportsPaging - { - get - { - return false; - } - } - - public override IParseFeed Parser - { - get - { - return new BasicTorrentRssParser(); - } - } - - public override IEnumerable<string> RecentFeed - { - get - { - yield return "http://www.ezrss.it/feed/"; - } - } - - public override IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber) - { - yield return string.Format("http://www.ezrss.it/search/index.php?show_name={0}&season={1}&episode={2}&mode=rss", seriesTitle, seasonNumber, episodeNumber); - } - - public override IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset) - { - yield return string.Format("http://www.ezrss.it/search/index.php?show_name={0}&season={1}&mode=rss", seriesTitle, seasonNumber); - - } - - public override IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date) - { - //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 141145e9f..fd70a2473 100644 --- a/src/NzbDrone.Core/Indexers/IIndexer.cs +++ b/src/NzbDrone.Core/Indexers/IIndexer.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Indexers { IParseFeed Parser { get; } DownloadProtocol Protocol { get; } + Int32 SupportedPageSize { get; } Boolean SupportsPaging { get; } Boolean SupportsSearching { get; } diff --git a/src/NzbDrone.Core/Indexers/IndexerBase.cs b/src/NzbDrone.Core/Indexers/IndexerBase.cs index 96fe2b837..bb73432c6 100644 --- a/src/NzbDrone.Core/Indexers/IndexerBase.cs +++ b/src/NzbDrone.Core/Indexers/IndexerBase.cs @@ -34,8 +34,10 @@ namespace NzbDrone.Core.Indexers public abstract DownloadProtocol Protocol { get; } - public abstract bool SupportsPaging { get; } - public virtual bool SupportsSearching { get { return true; } } + public virtual Boolean SupportsFeed { get { return true; } } + public virtual Int32 SupportedPageSize { get { return 0; } } + public bool SupportsPaging { get { return SupportedPageSize > 0; } } + public virtual Boolean SupportsSearching { get { return true; } } protected TSettings Settings { @@ -58,10 +60,4 @@ namespace NzbDrone.Core.Indexers return Definition.Name; } } - - public enum DownloadProtocol - { - Usenet = 1, - Torrent = 2 - } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs index 4a061129e..c1509952e 100644 --- a/src/NzbDrone.Core/Indexers/IndexerDefinition.cs +++ b/src/NzbDrone.Core/Indexers/IndexerDefinition.cs @@ -5,5 +5,7 @@ namespace NzbDrone.Core.Indexers public class IndexerDefinition : ProviderDefinition { public bool Enable { get; set; } + + public DownloadProtocol Protocol { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index 03d0450b7..bf3627dfb 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -31,17 +31,7 @@ namespace NzbDrone.Core.Indexers protected override void InitializeProviders() { - var definitions = _providers.Where(c => c.Protocol == DownloadProtocol.Usenet) - .SelectMany(indexer => indexer.DefaultDefinitions); - var currentProviders = All(); - - var newProviders = definitions.Where(def => currentProviders.All(c => c.Implementation != def.Implementation)).ToList(); - - if (newProviders.Any()) - { - _providerRepository.InsertMany(newProviders.Cast<IndexerDefinition>().ToList()); - } } protected override List<IndexerDefinition> Active() @@ -59,5 +49,14 @@ namespace NzbDrone.Core.Indexers return base.Create(definition); } + + protected override IndexerDefinition GetTemplate(IIndexer provider) + { + var definition = base.GetTemplate(provider); + + definition.Protocol = provider.Protocol; + + return definition; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs index 678a5be1a..154c160a0 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -63,11 +63,9 @@ namespace NzbDrone.Core.Indexers _logger.Info("{0} offset {1}. Found {2}", indexer, searchCriteria, result.Count); - if (result.Count > 90 && - offset < 900 && - indexer.SupportsPaging) + if (indexer.SupportsPaging && result.Count >= indexer.SupportedPageSize && offset < 900) { - result.AddRange(Fetch(indexer, searchCriteria, offset + 100)); + result.AddRange(Fetch(indexer, searchCriteria, offset + indexer.SupportedPageSize)); } return result; @@ -152,7 +150,11 @@ namespace NzbDrone.Core.Indexers } } - result.ForEach(c => c.Indexer = indexer.Definition.Name); + result.ForEach(c => + { + c.Indexer = indexer.Definition.Name; + c.DownloadProtocol = indexer.Protocol; + }); return result; } diff --git a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs index c4c8288a8..9adcd88aa 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/Newznab.cs @@ -7,6 +7,9 @@ namespace NzbDrone.Core.Indexers.Newznab { public class Newznab : IndexerBase<NewznabSettings> { + public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } + public override Int32 SupportedPageSize { get { return 100; } } + public override IParseFeed Parser { get @@ -72,14 +75,6 @@ namespace NzbDrone.Core.Indexers.Newznab return settings; } - public override bool SupportsPaging - { - get - { - return true; - } - } - public override IEnumerable<string> RecentFeed { get @@ -140,14 +135,6 @@ namespace NzbDrone.Core.Indexers.Newznab return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, offset)); } - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Usenet; - } - } - private static string NewsnabifyTitle(string title) { return title.Replace("+", "%20"); diff --git a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs index 689138c03..6978213f8 100644 --- a/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs +++ b/src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs @@ -5,13 +5,7 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs { public class Omgwtfnzbs : IndexerBase<OmgwtfnzbsSettings> { - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Usenet; - } - } + public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } public override IParseFeed Parser { @@ -25,7 +19,6 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs { get { - yield return String.Format("http://rss.omgwtfnzbs.org/rss-search.php?catid=19,20&user={0}&api={1}&eng=1", Settings.Username, Settings.ApiKey); } @@ -71,13 +64,5 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs { return new List<string>(); } - - public override bool SupportsPaging - { - get - { - return false; - } - } } } diff --git a/src/NzbDrone.Core/Indexers/RssParserBase.cs b/src/NzbDrone.Core/Indexers/RssParserBase.cs index 0988c4f97..8b300c6d5 100644 --- a/src/NzbDrone.Core/Indexers/RssParserBase.cs +++ b/src/NzbDrone.Core/Indexers/RssParserBase.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Indexers try { var reportInfo = ParseFeedItem(item.StripNameSpace(), url); + if (reportInfo != null) { reportInfo.DownloadUrl = GetNzbUrl(item); @@ -69,7 +70,7 @@ namespace NzbDrone.Core.Indexers var reportInfo = CreateNewReleaseInfo(); reportInfo.Title = title; - reportInfo.PublishDate = item.PublishDate(); + reportInfo.PublishDate = GetPublishDate(item); reportInfo.DownloadUrl = GetNzbUrl(item); reportInfo.InfoUrl = GetNzbInfoUrl(item); @@ -92,6 +93,11 @@ namespace NzbDrone.Core.Indexers return item.Title(); } + protected virtual DateTime GetPublishDate(XElement item) + { + return item.PublishDate(); + } + protected virtual string GetNzbUrl(XElement item) { return item.Links().First(); diff --git a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs index f61c7ffba..8565ef9b9 100644 --- a/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs +++ b/src/NzbDrone.Core/Indexers/Wombles/Wombles.cs @@ -6,29 +6,8 @@ namespace NzbDrone.Core.Indexers.Wombles { public class Wombles : IndexerBase<NullConfig> { - public override DownloadProtocol Protocol - { - get - { - return DownloadProtocol.Usenet; - } - } - - public override bool SupportsPaging - { - get - { - return false; - } - } - - public override bool SupportsSearching - { - get - { - return false; - } - } + public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } + public override bool SupportsSearching { get { return false; } } public override IParseFeed Parser { diff --git a/src/NzbDrone.Core/Indexers/XElementExtensions.cs b/src/NzbDrone.Core/Indexers/XElementExtensions.cs index 254c9ae6f..fc3f29dbc 100644 --- a/src/NzbDrone.Core/Indexers/XElementExtensions.cs +++ b/src/NzbDrone.Core/Indexers/XElementExtensions.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Indexers { private static readonly Logger Logger = NzbDroneLogger.GetLogger(); - private static readonly Regex RemoveTimeZoneRegex = new Regex(@"\s[A-Z]{2,4}$", RegexOptions.Compiled); + public static readonly Regex RemoveTimeZoneRegex = new Regex(@"\s[A-Z]{2,4}$", RegexOptions.Compiled); public static string Title(this XElement item) { @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Indexers return long.Parse(item.TryGetValue("length")); } - private static string TryGetValue(this XElement item, string elementName, string defaultValue = "") + public static string TryGetValue(this XElement item, string elementName, string defaultValue = "") { var element = item.Element(elementName); diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index 459b2ddb5..788e6df0c 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Jobs var defaultTasks = new[] { new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName}, - new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName}, + new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName}, new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index a4ee5ff99..c8d94c2ed 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -95,7 +95,7 @@ namespace NzbDrone.Core.MediaFiles decisionsStopwatch.Stop(); _logger.Trace("Import decisions complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed); - _importApprovedEpisodes.Import(decisions); + _importApprovedEpisodes.Import(decisions, false); _logger.Info("Completed scanning disk for {0}", series.Title); _eventAggregator.PublishEvent(new SeriesScannedEvent(series)); diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 54d9bb45f..1c7235237 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -14,10 +14,17 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; namespace NzbDrone.Core.MediaFiles { - public class DownloadedEpisodesImportService : IExecute<DownloadedEpisodesScanCommand> + public interface IDownloadedEpisodesImportService + { + List<ImportDecision> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem); + List<ImportDecision> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem); + } + + public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService, IExecute<DownloadedEpisodesScanCommand> { private readonly IDiskProvider _diskProvider; private readonly IDiskScanService _diskScanService; @@ -50,9 +57,53 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } + public List<ImportDecision> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem) + { + var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); + var series = _parsingService.GetSeries(cleanedUpName); + var quality = QualityParser.ParseQuality(cleanedUpName); + _logger.Debug("{0} folder quality: {1}", cleanedUpName, quality); + + if (series == null) + { + _logger.Debug("Unknown Series {0}", cleanedUpName); + return new List<ImportDecision>(); + } + + var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality); + + var importedDecisions = _importApprovedEpisodes.Import(decisions, true, downloadClientItem); + + if (!downloadClientItem.IsReadOnly && importedDecisions.Any() && ShouldDeleteFolder(directoryInfo)) + { + _logger.Debug("Deleting folder after importing valid files"); + _diskProvider.DeleteFolder(directoryInfo.FullName, true); + } + + return importedDecisions; + } + + public List<ImportDecision> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem) + { + var series = _parsingService.GetSeries(Path.GetFileNameWithoutExtension(fileInfo.Name)); + + if (series == null) + { + _logger.Debug("Unknown Series for file: {0}", fileInfo.Name); + return new List<ImportDecision>(); + } + + var decisions = _importDecisionMaker.GetImportDecisions(new List<string>() { fileInfo.FullName }, series, true, null); + + var importedDecisions = _importApprovedEpisodes.Import(decisions, true, downloadClientItem); + + return importedDecisions; + } + private void ProcessDownloadedEpisodesFolder() { - //TODO: We should also process the download client's category folder var downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; if (String.IsNullOrEmpty(downloadedEpisodesFolder)) @@ -100,6 +151,15 @@ namespace NzbDrone.Core.MediaFiles var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); + foreach (var videoFile in videoFiles) + { + if (_diskProvider.IsFileLocked(videoFile)) + { + _logger.Debug("[{0}] is currently locked by another process, skipping", videoFile); + return new List<ImportDecision>(); + } + } + return ProcessFiles(series, quality, videoFiles); } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 5e3086835..24f1ab54e 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.MediaFiles { EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series); EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode); + EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode); } public class EpisodeFileMovingService : IMoveEpisodeFiles @@ -53,7 +54,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug("Renaming episode file: {0} to {1}", episodeFile, filePath); - return MoveFile(episodeFile, series, episodes, filePath); + return TransferFile(episodeFile, series, episodes, filePath, false); } public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) @@ -63,10 +64,20 @@ namespace NzbDrone.Core.MediaFiles _logger.Debug("Moving episode file: {0} to {1}", episodeFile, filePath); - return MoveFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath); + return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, false); } - private EpisodeFile MoveFile(EpisodeFile episodeFile, Series series, List<Episode> episodes, string destinationFilename) + public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) + { + var newFileName = _buildFileNames.BuildFilename(localEpisode.Episodes, localEpisode.Series, episodeFile); + var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(episodeFile.Path)); + + _logger.Debug("Copying episode file: {0} to {1}", episodeFile, filePath); + + return TransferFile(episodeFile, localEpisode.Series, localEpisode.Episodes, filePath, true); + } + + private EpisodeFile TransferFile(EpisodeFile episodeFile, Series series, List<Episode> episodes, string destinationFilename, bool copyOnly) { Ensure.That(episodeFile, () => episodeFile).IsNotNull(); Ensure.That(series,() => series).IsNotNull(); @@ -103,8 +114,16 @@ namespace NzbDrone.Core.MediaFiles } } - _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); - _diskProvider.MoveFile(episodeFile.Path, destinationFilename); + if (copyOnly) + { + _logger.Debug("Copying [{0}] > [{1}]", episodeFile.Path, destinationFilename); + _diskProvider.CopyFile(episodeFile.Path, destinationFilename); + } + else + { + _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); + _diskProvider.MoveFile(episodeFile.Path, destinationFilename); + } episodeFile.Path = destinationFilename; _updateEpisodeFileService.ChangeFileDateForFile(episodeFile, series, episodes); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index a62dd7536..598093ff7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -9,13 +9,14 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Download; namespace NzbDrone.Core.MediaFiles.EpisodeImport { public interface IImportApprovedEpisodes { - List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownloads = false); + List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem historyItem = null); } public class ImportApprovedEpisodes : IImportApprovedEpisodes @@ -39,14 +40,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _logger = logger; } - public List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownload = false) + public List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem historyItem = null) { var qualifiedImports = decisions.Where(c => c.Approved) - .GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s - .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.QualityProfile)) - .ThenByDescending(c => c.LocalEpisode.Size)) - .SelectMany(c => c) - .ToList(); + .GroupBy(c => c.LocalEpisode.Series.Id, (i, s) => s + .OrderByDescending(c => c.LocalEpisode.Quality, new QualityModelComparer(s.First().LocalEpisode.Series.QualityProfile)) + .ThenByDescending(c => c.LocalEpisode.Size)) + .SelectMany(c => c) + .ToList(); var imported = new List<ImportDecision>(); @@ -78,15 +79,23 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport if (newDownload) { + bool copyOnly = historyItem != null && historyItem.IsReadOnly; episodeFile.SceneName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath()); - var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode); + var moveResult = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode, copyOnly); oldFiles = moveResult.OldFiles; } _mediaFileService.Add(episodeFile); imported.Add(importDecision); - _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload)); + if (historyItem != null) + { + _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, historyItem.DownloadClient, historyItem.DownloadClientId)); + } + else + { + _eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload)); + } if (newDownload) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs deleted file mode 100644 index 8eb2beaed..000000000 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotInUseSpecification.cs +++ /dev/null @@ -1,37 +0,0 @@ -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Core.Parser.Model; - -namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications -{ - public class NotInUseSpecification : IImportDecisionEngineSpecification - { - private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; - - public NotInUseSpecification(IDiskProvider diskProvider, Logger logger) - { - _diskProvider = diskProvider; - _logger = logger; - } - - public string RejectionReason { get { return "File is in use"; } } - - public bool IsSatisfiedBy(LocalEpisode localEpisode) - { - if (localEpisode.ExistingFile) - { - _logger.Debug("{0} is in series folder, skipping in use check", localEpisode.Path); - return true; - } - - if (_diskProvider.IsFileLocked(localEpisode.Path)) - { - _logger.Debug("{0} is in use"); - return false; - } - - return true; - } - } -} diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs index d7c1ee6e7..445a1b3a8 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs @@ -9,6 +9,8 @@ namespace NzbDrone.Core.MediaFiles.Events public LocalEpisode EpisodeInfo { get; private set; } public EpisodeFile ImportedEpisode { get; private set; } public Boolean NewDownload { get; set; } + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload) { @@ -16,5 +18,14 @@ namespace NzbDrone.Core.MediaFiles.Events ImportedEpisode = importedEpisode; NewDownload = newDownload; } + + public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadClientId) + { + EpisodeInfo = episodeInfo; + ImportedEpisode = importedEpisode; + NewDownload = newDownload; + DownloadClient = downloadClient; + DownloadClientId = downloadClientId; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs index a51121511..750091d94 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileTableCleanupService.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.MediaFiles continue; } - if (!DiskProviderBase.IsParent(series.Path, episodeFile.Path)) + if (!series.Path.IsParentPath(episodeFile.Path)) { _logger.Debug("File [{0}] does not belong to this series, removing from db", episodeFile.Path); _mediaFileService.Delete(episodeFile); diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index c3cf4d208..924d60b9c 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.MediaFiles { public interface IUpgradeMediaFiles { - EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode); + EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false); } public class UpgradeMediaFileService : IUpgradeMediaFiles @@ -32,7 +32,7 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } - public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) + public EpisodeFileMoveResult UpgradeEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode, bool copyOnly = false) { var moveFileResult = new EpisodeFileMoveResult(); var existingFiles = localEpisode.Episodes @@ -54,7 +54,14 @@ namespace NzbDrone.Core.MediaFiles _mediaFileService.Delete(file, true); } - moveFileResult.EpisodeFile = _episodeFileMover.MoveEpisodeFile(episodeFile, localEpisode); + if (copyOnly) + { + moveFileResult.EpisodeFile = _episodeFileMover.CopyEpisodeFile(episodeFile, localEpisode); + } + else + { + moveFileResult.EpisodeFile = _episodeFileMover.MoveEpisodeFile(episodeFile, localEpisode); + } return moveFileResult; } diff --git a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs index 7031b7ff9..58237b927 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox if (!newFilename.PathEquals(existingFilename)) { _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); updatedMetadataFiles.Add(metadataFile); } @@ -93,7 +93,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox { SeriesId = series.Id, Consumer = GetType().Name, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + RelativePath = series.Path.GetRelativePath(path) }; //Series and season images are both named folder.jpg, only season ones sit in season folders diff --git a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs index a8399c0b8..f542eaf39 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv if (!newFilename.PathEquals(existingFilename)) { _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); updatedMetadataFiles.Add(metadataFile); } @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv { SeriesId = series.Id, Consumer = GetType().Name, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + RelativePath = series.Path.GetRelativePath(path) }; //Series and season images are both named folder.jpg, only season ones sit in season folders diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index 310ffa5df..ee3c13e9d 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -71,7 +71,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc if (!newFilename.PathEquals(existingFilename)) { _diskProvider.MoveFile(existingFilename, newFilename); - metadataFile.RelativePath = DiskProviderBase.GetRelativePath(series.Path, newFilename); + metadataFile.RelativePath = series.Path.GetRelativePath(newFilename); updatedMetadataFiles.Add(metadataFile); } @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc { SeriesId = series.Id, Consumer = GetType().Name, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, path) + RelativePath = series.Path.GetRelativePath(path) }; if (SeriesImagesRegex.IsMatch(filename)) diff --git a/src/NzbDrone.Core/MetaData/MetadataService.cs b/src/NzbDrone.Core/MetaData/MetadataService.cs index ca428d4fc..f6794d6d3 100644 --- a/src/NzbDrone.Core/MetaData/MetadataService.cs +++ b/src/NzbDrone.Core/MetaData/MetadataService.cs @@ -160,7 +160,7 @@ namespace NzbDrone.Core.Metadata _diskProvider.WriteAllText(seriesMetadata.Path, seriesMetadata.Contents); metadata.Hash = hash; - metadata.RelativePath = DiskProviderBase.GetRelativePath(series.Path, seriesMetadata.Path); + metadata.RelativePath = series.Path.GetRelativePath(seriesMetadata.Path); return metadata; } @@ -174,7 +174,7 @@ namespace NzbDrone.Core.Metadata return null; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, episodeMetadata.Path); + var relativePath = series.Path.GetRelativePath(episodeMetadata.Path); var existingMetadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.EpisodeMetadata && c.EpisodeFileId == episodeFile.Id); @@ -226,7 +226,7 @@ namespace NzbDrone.Core.Metadata continue; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + var relativePath = series.Path.GetRelativePath(image.Path); var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeriesImage && c.RelativePath == relativePath) ?? @@ -260,7 +260,7 @@ namespace NzbDrone.Core.Metadata continue; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + var relativePath = series.Path.GetRelativePath(image.Path); var metadata = existingMetadataFiles.SingleOrDefault(c => c.Type == MetadataType.SeasonImage && c.SeasonNumber == season.SeasonNumber && @@ -295,7 +295,7 @@ namespace NzbDrone.Core.Metadata continue; } - var relativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path); + var relativePath = series.Path.GetRelativePath(image.Path); var existingMetadata = existingMetadataFiles.FirstOrDefault(c => c.Type == MetadataType.EpisodeImage && c.EpisodeFileId == episodeFile.Id); @@ -319,7 +319,7 @@ namespace NzbDrone.Core.Metadata EpisodeFileId = episodeFile.Id, Consumer = consumer.GetType().Name, Type = MetadataType.EpisodeImage, - RelativePath = DiskProviderBase.GetRelativePath(series.Path, image.Path) + RelativePath = series.Path.GetRelativePath(image.Path) }; DownloadImage(series, image.Url, image.Path); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 389d24a9d..38edae8a0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -65,6 +65,10 @@ <Reference Include="Omu.ValueInjecter"> <HintPath>..\packages\valueinjecter.2.3.3\lib\net35\Omu.ValueInjecter.dll</HintPath> </Reference> + <Reference Include="RestSharp, Version=104.4.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Data" /> @@ -90,9 +94,6 @@ <Reference Include="Prowlin"> <HintPath>..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll</HintPath> </Reference> - <Reference Include="RestSharp"> - <HintPath>..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll</HintPath> - </Reference> <Reference Include="System.Data.SQLite"> <HintPath>..\Libraries\Sqlite\System.Data.SQLite.dll</HintPath> </Reference> @@ -238,10 +239,12 @@ <Compile Include="DecisionEngine\Specifications\RssSync\HistorySpecification.cs" /> <Compile Include="DiskSpace\DiskSpace.cs" /> <Compile Include="DiskSpace\DiskSpaceService.cs" /> - <Compile Include="Download\Clients\Blackhole\Blackhole.cs" /> - <Compile Include="Download\Clients\Blackhole\TestBlackholeCommand.cs" /> + <Compile Include="Download\Clients\Sabnzbd\SabnzbdDownloadStatus.cs" /> + <Compile Include="Download\Clients\UsenetBlackhole\UsenetBlackhole.cs" /> + <Compile Include="Download\Clients\UsenetBlackhole\TestUsenetBlackholeCommand.cs" /> + <Compile Include="Download\Clients\UsenetBlackhole\UsenetBlackholeSettings.cs" /> <Compile Include="Download\Clients\DownloadClientException.cs" /> - <Compile Include="Download\Clients\FolderSettings.cs" /> + <Compile Include="Download\Clients\Pneumatic\PneumaticSettings.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetHistoryItem.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetParameter.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" /> @@ -253,6 +256,7 @@ <Compile Include="Download\Clients\Sabnzbd\Responses\SabnzbdVersionResponse.cs" /> <Compile Include="Download\Clients\Sabnzbd\SabnzbdSettings.cs" /> <Compile Include="Download\Clients\Sabnzbd\TestSabnzbdCommand.cs" /> + <Compile Include="Download\CompletedDownloadService.cs" /> <Compile Include="Download\DownloadClientBase.cs" /> <Compile Include="Download\DownloadClientDefinition.cs" /> <Compile Include="Download\DownloadClientFactory.cs" /> @@ -262,15 +266,16 @@ <Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" /> <Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdQueueTimeConverter.cs" /> <Compile Include="Download\Clients\Sabnzbd\SabnzbdProxy.cs" /> - <Compile Include="Download\CheckForFailedDownloadCommand.cs" /> - <Compile Include="Download\FailedDownload.cs" /> - <Compile Include="Download\HistoryItem.cs" /> + <Compile Include="Download\CheckForFinishedDownloadCommand.cs" /> + <Compile Include="Download\DownloadClientItem.cs" /> + <Compile Include="Download\FailedDownloadService.cs" /> + <Compile Include="Download\DownloadItemStatus.cs" /> + <Compile Include="Download\TrackedDownload.cs" /> <Compile Include="Download\DownloadFailedEvent.cs" /> <Compile Include="Download\DownloadApprovedReports.cs" /> <Compile Include="Download\DownloadClientProvider.cs" /> <Compile Include="Download\DownloadClientType.cs" /> - <Compile Include="Download\FailedDownloadService.cs" /> - <Compile Include="Download\QueueItem.cs" /> + <Compile Include="Download\DownloadTrackingService.cs" /> <Compile Include="Download\RedownloadFailedDownloadService.cs" /> <Compile Include="Exceptions\BadRequestException.cs" /> <Compile Include="Exceptions\DownstreamException.cs" /> @@ -278,6 +283,7 @@ <Compile Include="Exceptions\StatusCodeToExceptions.cs" /> <Compile Include="HealthCheck\CheckHealthCommand.cs" /> <Compile Include="HealthCheck\Checks\DownloadClientCheck.cs" /> + <Compile Include="HealthCheck\Checks\ImportMechanismCheck.cs" /> <Compile Include="HealthCheck\Checks\MonoVersionCheck.cs" /> <Compile Include="HealthCheck\Checks\DroneFactoryCheck.cs" /> <Compile Include="HealthCheck\Checks\IndexerCheck.cs" /> @@ -307,10 +313,9 @@ <Compile Include="IndexerSearch\SeasonSearchCommand.cs" /> <Compile Include="IndexerSearch\SeasonSearchService.cs" /> <Compile Include="Indexers\BasicTorrentRssParser.cs" /> - <Compile Include="Indexers\DownloadProtocols.cs" /> + <Compile Include="Indexers\DownloadProtocol.cs" /> <Compile Include="Indexers\Exceptions\ApiKeyException.cs" /> <Compile Include="Indexers\Exceptions\RequestLimitReachedException.cs" /> - <Compile Include="Indexers\Eztv\Eztv.cs" /> <Compile Include="Indexers\FetchAndParseRssService.cs" /> <Compile Include="Indexers\IIndexer.cs" /> <Compile Include="Indexers\IndexerSettingUpdatedEvent.cs" /> @@ -440,7 +445,6 @@ <Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" /> <Compile Include="MediaFiles\EpisodeImport\ImportApprovedEpisodes.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\NotUnpackingSpecification.cs" /> - <Compile Include="MediaFiles\EpisodeImport\Specifications\NotInUseSpecification.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\UpgradeSpecification.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\NotSampleSpecification.cs" /> @@ -754,9 +758,7 @@ <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> - <ItemGroup> - <Folder Include="Download\Clients\uTorrent\" /> - </ItemGroup> + <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> <PostBuildEvent> diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index d2b6201c8..4fb3b2b7a 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -1,4 +1,5 @@ using System; +using NzbDrone.Core.Indexers; namespace NzbDrone.Core.Parser.Model { @@ -10,6 +11,7 @@ namespace NzbDrone.Core.Parser.Model public string InfoUrl { get; set; } public string CommentUrl { get; set; } public String Indexer { get; set; } + public DownloadProtocol DownloadProtocol { get; set; } public DateTime PublishDate { get; set; } diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index ce507912e..05f0359ab 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using NLog; +using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.IndexerSearch.Definitions; @@ -76,7 +77,7 @@ namespace NzbDrone.Core.Parser Episodes = episodes, Path = filename, ParsedEpisodeInfo = parsedEpisodeInfo, - ExistingFile = DiskProviderBase.IsParent(series.Path, filename) + ExistingFile = series.Path.IsParentPath(filename) }; } diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 733ff2301..a1b2ff7f3 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -2,6 +2,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Queue { @@ -15,5 +16,6 @@ namespace NzbDrone.Core.Queue public Decimal Sizeleft { get; set; } public TimeSpan Timeleft { get; set; } public String Status { get; set; } + public RemoteEpisode RemoteEpisode { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index e195f57e4..018c9f01f 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using NLog; using NzbDrone.Core.Download; @@ -12,39 +13,23 @@ namespace NzbDrone.Core.Queue public class QueueService : IQueueService { - private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IDownloadTrackingService _downloadTrackingService; private readonly Logger _logger; - public QueueService(IProvideDownloadClient downloadClientProvider, Logger logger) + public QueueService(IDownloadTrackingService downloadTrackingService, Logger logger) { - _downloadClientProvider = downloadClientProvider; + _downloadTrackingService = downloadTrackingService; _logger = logger; } public List<Queue> GetQueue() { - var downloadClient = _downloadClientProvider.GetDownloadClient(); + var queueItems = _downloadTrackingService.GetQueuedDownloads().Select(v => v.DownloadItem).ToList(); - if (downloadClient == null) - { - _logger.Debug("Download client is not configured."); - return new List<Queue>(); - } - - try - { - var queueItems = downloadClient.GetQueue(); - - return MapQueue(queueItems); - } - catch (Exception ex) - { - _logger.Error("Error getting queue from download client: " + downloadClient.ToString(), ex); - return new List<Queue>(); - } + return MapQueue(queueItems); } - private List<Queue> MapQueue(IEnumerable<QueueItem> queueItems) + private List<Queue> MapQueue(IEnumerable<DownloadClientItem> queueItems) { var queued = new List<Queue>(); @@ -53,15 +38,16 @@ namespace NzbDrone.Core.Queue foreach (var episode in queueItem.RemoteEpisode.Episodes) { var queue = new Queue(); - queue.Id = queueItem.Id.GetHashCode() + episode.Id; + queue.Id = queueItem.DownloadClientId.GetHashCode() + episode.Id; queue.Series = queueItem.RemoteEpisode.Series; queue.Episode = episode; queue.Quality = queueItem.RemoteEpisode.ParsedEpisodeInfo.Quality; queue.Title = queueItem.Title; - queue.Size = queueItem.Size; - queue.Sizeleft = queueItem.Sizeleft; - queue.Timeleft = queueItem.Timeleft; - queue.Status = queueItem.Status; + queue.Size = queueItem.TotalSize; + queue.Sizeleft = queueItem.RemainingSize; + queue.Timeleft = queueItem.RemainingTime; + queue.Status = queueItem.Status.ToString(); + queue.RemoteEpisode = queueItem.RemoteEpisode; queued.Add(queue); } } diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index bdce9edd9..0c7530ac5 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -40,12 +40,7 @@ namespace NzbDrone.Core.ThingiProvider public List<TProviderDefinition> Templates() { - return _providers.Select(p => new TProviderDefinition() - { - ConfigContract = p.ConfigContract.Name, - Implementation = p.GetType().Name, - Settings = (IProviderConfig)Activator.CreateInstance(p.ConfigContract) - }).ToList(); + return _providers.Select(GetTemplate).ToList(); } public List<TProvider> GetAvailableProviders() @@ -87,6 +82,18 @@ namespace NzbDrone.Core.ThingiProvider return _providers.Select(c => c.GetType()).SingleOrDefault(c => c.Name.Equals(definition.Implementation, StringComparison.InvariantCultureIgnoreCase)); } + protected virtual TProviderDefinition GetTemplate(TProvider provider) + { + var definition = new TProviderDefinition() + { + ConfigContract = provider.ConfigContract.Name, + Implementation = provider.GetType().Name, + Settings = (IProviderConfig)Activator.CreateInstance(provider.ConfigContract) + }; + + return definition; + } + public void Handle(ApplicationStartedEvent message) { _logger.Debug("Initializing Providers. Count {0}", _providers.Count); diff --git a/src/NzbDrone.Core/packages.config b/src/NzbDrone.Core/packages.config index 36fbe4d0a..f657add80 100644 --- a/src/NzbDrone.Core/packages.config +++ b/src/NzbDrone.Core/packages.config @@ -7,6 +7,6 @@ <package id="Newtonsoft.Json" version="5.0.8" targetFramework="net40" /> <package id="NLog" version="2.1.0" targetFramework="net40" /> <package id="Prowlin" version="0.9.4456.26422" targetFramework="net40" /> - <package id="RestSharp" version="104.3.3" targetFramework="net40" /> + <package id="RestSharp" version="104.4.0" targetFramework="net40" /> <package id="valueinjecter" version="2.3.3" targetFramework="net40" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index fffac3b0a..c51816d43 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -68,6 +68,17 @@ namespace NzbDrone.Integration.Test _runner.Start(); InitRestClients(); + + // Add Wombles + var wombles = Indexers.Post(new Api.Indexers.IndexerResource + { + Enable = true, + ConfigContract = "NullConfig", + Implementation = "Wombles", + Name = "Wombles", + Protocol = Core.Indexers.DownloadProtocol.Usenet, + Fields = new List<Api.ClientSchema.Field>() + }); } private void InitRestClients() diff --git a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj index 3f04a0cc5..837beaf72 100644 --- a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj @@ -44,6 +44,10 @@ <Reference Include="nunit.framework"> <HintPath>..\packages\NUnit.2.6.2\lib\nunit.framework.dll</HintPath> </Reference> + <Reference Include="RestSharp, Version=104.4.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> @@ -84,9 +88,6 @@ <Reference Include="Owin"> <HintPath>..\packages\Owin.1.0\lib\net40\Owin.dll</HintPath> </Reference> - <Reference Include="RestSharp"> - <HintPath>..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="Client\ClientBase.cs" /> diff --git a/src/NzbDrone.Integration.Test/packages.config b/src/NzbDrone.Integration.Test/packages.config index 0013824cf..39bda9dde 100644 --- a/src/NzbDrone.Integration.Test/packages.config +++ b/src/NzbDrone.Integration.Test/packages.config @@ -13,5 +13,5 @@ <package id="NLog" version="2.1.0" targetFramework="net40" /> <package id="NUnit" version="2.6.2" targetFramework="net40" /> <package id="Owin" version="1.0" targetFramework="net40" /> - <package id="RestSharp" version="104.3.3" targetFramework="net40" /> + <package id="RestSharp" version="104.4.0" targetFramework="net40" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 437a9bde4..d9eebe47a 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -23,8 +23,8 @@ namespace NzbDrone.Test.Common.AutoMoq { public readonly MockBehavior DefaultBehavior = MockBehavior.Default; public Type ResolveType; - private IUnityContainer container; - private IDictionary<Type, object> registeredMocks; + private IUnityContainer _container; + private IDictionary<Type, object> _registeredMocks; public AutoMoqer() { @@ -46,7 +46,7 @@ namespace NzbDrone.Test.Common.AutoMoq public virtual T Resolve<T>() { ResolveType = typeof(T); - var result = container.Resolve<T>(); + var result = _container.Resolve<T>(); SetConstant(result); ResolveType = null; return result; @@ -78,13 +78,13 @@ namespace NzbDrone.Test.Common.AutoMoq public virtual void SetMock(Type type, Mock mock) { - if (registeredMocks.ContainsKey(type) == false) - registeredMocks.Add(type, mock); + if (_registeredMocks.ContainsKey(type) == false) + _registeredMocks.Add(type, mock); } public virtual void SetConstant<T>(T instance) { - container.RegisterInstance(instance); + _container.RegisterInstance(instance); SetMock(instance.GetType(), null); } @@ -120,7 +120,7 @@ namespace NzbDrone.Test.Common.AutoMoq public void VerifyAllMocks() { - foreach (var registeredMock in registeredMocks) + foreach (var registeredMock in _registeredMocks) { var mock = registeredMock.Value as Mock; if (mock != null) @@ -132,12 +132,12 @@ namespace NzbDrone.Test.Common.AutoMoq private void SetupAutoMoqer(IUnityContainer container) { - this.container = container; + _container = container; container.RegisterInstance(this); RegisterPlatformLibrary(container); - registeredMocks = new Dictionary<Type, object>(); + _registeredMocks = new Dictionary<Type, object>(); AddTheAutoMockingContainerExtensionToTheContainer(container); } @@ -149,19 +149,19 @@ namespace NzbDrone.Test.Common.AutoMoq private Mock<T> TheRegisteredMockForThisType<T>(Type type) where T : class { - return (Mock<T>)registeredMocks.Where(x => x.Key == type).First().Value; + return (Mock<T>)_registeredMocks.Where(x => x.Key == type).First().Value; } private void CreateANewMockAndRegisterIt<T>(Type type, MockBehavior behavior) where T : class { var mock = new Mock<T>(behavior); - container.RegisterInstance(mock.Object); + _container.RegisterInstance(mock.Object); SetMock(type, mock); } private bool GetMockHasNotBeenCalledForThisType(Type type) { - return registeredMocks.ContainsKey(type) == false; + return _registeredMocks.ContainsKey(type) == false; } private static Type GetTheMockType<T>() where T : class diff --git a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj index e871e8478..017ace6e1 100644 --- a/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/NzbDrone.Test.Common.csproj @@ -43,6 +43,10 @@ <Reference Include="Moq"> <HintPath>..\packages\Moq.4.0.10827\lib\NET40\Moq.dll</HintPath> </Reference> + <Reference Include="RestSharp, Version=104.4.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> @@ -68,9 +72,6 @@ <Reference Include="nunit.framework"> <HintPath>..\packages\NUnit.2.6.2\lib\nunit.framework.dll</HintPath> </Reference> - <Reference Include="RestSharp"> - <HintPath>..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll</HintPath> - </Reference> </ItemGroup> <ItemGroup> <Compile Include="AutoMoq\AutoMoqer.cs" /> diff --git a/src/NzbDrone.Test.Common/packages.config b/src/NzbDrone.Test.Common/packages.config index 28a2e5f1d..5cea9507c 100644 --- a/src/NzbDrone.Test.Common/packages.config +++ b/src/NzbDrone.Test.Common/packages.config @@ -6,6 +6,6 @@ <package id="Newtonsoft.Json" version="5.0.8" targetFramework="net40" /> <package id="NLog" version="2.1.0" targetFramework="net40" /> <package id="NUnit" version="2.6.2" targetFramework="net40" /> - <package id="RestSharp" version="104.3.3" targetFramework="net40" /> + <package id="RestSharp" version="104.4.0" targetFramework="net40" /> <package id="Unity" version="2.1.505.2" targetFramework="net40" /> </packages> \ No newline at end of file diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index 43a879bdb..095cae5e8 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -1,6 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30110.0 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test.Common", "Test.Common", "{47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97}" diff --git a/src/UI/History/Queue/QueueStatusCell.js b/src/UI/History/Queue/QueueStatusCell.js index 2c01a0246..6a8903deb 100644 --- a/src/UI/History/Queue/QueueStatusCell.js +++ b/src/UI/History/Queue/QueueStatusCell.js @@ -26,6 +26,11 @@ define( title = 'Queued'; } + if (status === 'completed') { + icon = 'icon-inbox'; + title = 'Downloaded'; + } + this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title)); } diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js index e6f557dc1..48fd1cbee 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js @@ -1,23 +1,14 @@ 'use strict'; define([ - 'marionette', + 'Settings/ThingyAddCollectionView', + 'Settings/ThingyHeaderGroupView', 'Settings/DownloadClient/Add/DownloadClientAddItemView' -], function (Marionette, AddItemView) { +], function (ThingyAddCollectionView, ThingyHeaderGroupView, AddItemView) { - return Marionette.CompositeView.extend({ - itemView : AddItemView, + return ThingyAddCollectionView.extend({ + itemView : ThingyHeaderGroupView.extend({ itemView: AddItemView }), itemViewContainer: '.add-download-client .items', - template : 'Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate', - - itemViewOptions: function () { - return { - downloadClientCollection: this.downloadClientCollection - }; - }, - - initialize: function (options) { - this.downloadClientCollection = options.downloadClientCollection; - } + template : 'Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate' }); }); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js index beab52273..4f8b07c7d 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js @@ -1,10 +1,11 @@ 'use strict'; define([ + 'jquery', 'AppLayout', 'marionette', 'Settings/DownloadClient/Edit/DownloadClientEditView' -], function (AppLayout, Marionette, EditView) { +], function ($, AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ template: 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', @@ -15,7 +16,7 @@ define([ }, initialize: function (options) { - this.downloadClientCollection = options.downloadClientCollection; + this.targetCollection = options.targetCollection; }, _add: function (e) { @@ -25,11 +26,11 @@ define([ this.model.set({ id : undefined, - name : this.model.get('implementationName'), + name : this.model.get('implementation'), enable : true }); - var editView = new EditView({ model: this.model, downloadClientCollection: this.downloadClientCollection }); + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); AppLayout.modalRegion.show(editView); } }); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js b/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js new file mode 100644 index 000000000..f37861d72 --- /dev/null +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js @@ -0,0 +1,36 @@ +'use strict'; + +define([ + 'underscore', + 'AppLayout', + 'backbone', + 'Settings/DownloadClient/DownloadClientCollection', + 'Settings/DownloadClient/Add/DownloadClientAddCollectionView' +], function (_, AppLayout, Backbone, SchemaCollection, AddCollectionView) { + return ({ + + open: function (collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var groupedSchemaCollection = new Backbone.Collection(); + + schemaCollection.on('sync', function() { + + var groups = schemaCollection.groupBy(function(model, iterator) { return model.get('protocol'); }); + + var modelCollection = _.map(groups, function(values, key, list) { + return { 'header': key, collection: values }; + }); + + groupedSchemaCollection.reset(modelCollection); + }); + + var view = new AddCollectionView({ collection: groupedSchemaCollection, targetCollection: collection }); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/DownloadClient/Add/SchemaModal.js b/src/UI/Settings/DownloadClient/Add/SchemaModal.js deleted file mode 100644 index dac0dca63..000000000 --- a/src/UI/Settings/DownloadClient/Add/SchemaModal.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; -define([ - 'AppLayout', - 'Settings/DownloadClient/DownloadClientCollection', - 'Settings/DownloadClient/Add/DownloadClientAddCollectionView' -], function (AppLayout, DownloadClientCollection, DownloadClientAddCollectionView) { - return ({ - - open: function (collection) { - var schemaCollection = new DownloadClientCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - - var view = new DownloadClientAddCollectionView({ collection: schemaCollection, downloadClientCollection: collection}); - AppLayout.modalRegion.show(view); - } - }); -}); diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js index 502d57e7f..2c8f951db 100644 --- a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js +++ b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js @@ -1,23 +1,23 @@ 'use strict'; -define( - [ - 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate', - events: { - 'click .x-confirm-delete': '_delete' - }, +define([ + 'vent', + 'marionette' +], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate', - _delete: function () { - this.model.destroy({ - wait : true, - success: function () { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } - }); + events: { + 'click .x-confirm-delete': '_delete' + }, + + _delete: function () { + this.model.destroy({ + wait : true, + success: function () { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollection.js b/src/UI/Settings/DownloadClient/DownloadClientCollection.js index 6166da3e4..6e32ea832 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientCollection.js +++ b/src/UI/Settings/DownloadClient/DownloadClientCollection.js @@ -1,12 +1,31 @@ 'use strict'; -define( - [ - 'backbone', - 'Settings/DownloadClient/DownloadClientModel' - ], function (Backbone, DownloadClientModel) { - return Backbone.Collection.extend({ - model: DownloadClientModel, - url : window.NzbDrone.ApiRoot + '/downloadclient' - }); +define([ + 'backbone', + 'Settings/DownloadClient/DownloadClientModel' +], function (Backbone, DownloadClientModel) { + + return Backbone.Collection.extend({ + model: DownloadClientModel, + url : window.NzbDrone.ApiRoot + '/downloadclient', + + comparator : function(left, right, collection) { + + var result = 0; + + if (left.get('protocol')) { + result = -left.get('protocol').localeCompare(right.get('protocol')); + } + + if (result === 0 && left.get('name')) { + result = left.get('name').localeCompare(right.get('name')); + } + + if (result === 0) { + result = left.get('implementation').localeCompare(right.get('implementation')); + } + + return result; + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js index 4a11cb167..505d1ebfd 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js +++ b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js @@ -1,31 +1,29 @@ 'use strict'; -define( - [ - 'underscore', - 'AppLayout', - 'marionette', - 'Settings/DownloadClient/DownloadClientItemView', - 'Settings/DownloadClient/Add/SchemaModal' - ], function (_, AppLayout, Marionette, DownloadClientItemView, SchemaModal) { - return Marionette.CompositeView.extend({ - itemView : DownloadClientItemView, - itemViewContainer: '#x-download-clients', - template : 'Settings/DownloadClient/DownloadClientCollectionViewTemplate', - ui: { - 'addCard': '.x-add-card' - }, +define([ + 'marionette', + 'Settings/DownloadClient/DownloadClientItemView', + 'Settings/DownloadClient/Add/DownloadClientSchemaModal' +], function (Marionette, ItemView, SchemaModal) { + return Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer: '.download-client-list', + template : 'Settings/DownloadClient/DownloadClientCollectionViewTemplate', - events: { - 'click .x-add-card': '_openSchemaModal' - }, + ui: { + 'addCard': '.x-add-card' + }, - appendHtml: function (collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, + events: { + 'click .x-add-card': '_openSchemaModal' + }, - _openSchemaModal: function () { - SchemaModal.open(this.collection); - } - }); + appendHtml: function (collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal: function () { + SchemaModal.open(this.collection); + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html index be4c04f09..a5ebbecef 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.html @@ -2,7 +2,7 @@ <legend>Download Clients</legend> <div class="row"> <div class="col-md-12"> - <ul id="x-download-clients" class="download-client-list thingies"> + <ul class="download-client-list thingies"> <li> <div class="download-client-item thingy add-card x-add-card"> <span class="center well"> diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemView.js b/src/UI/Settings/DownloadClient/DownloadClientItemView.js index ae552f53c..0d8bf3315 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientItemView.js +++ b/src/UI/Settings/DownloadClient/DownloadClientItemView.js @@ -1,27 +1,26 @@ 'use strict'; -define( - [ - 'AppLayout', - 'marionette', - 'Settings/DownloadClient/Edit/DownloadClientEditView' - ], function (AppLayout, Marionette, EditView) { +define([ + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/Edit/DownloadClientEditView' +], function (AppLayout, Marionette, EditView) { - return Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/DownloadClientItemViewTemplate', - tagName : 'li', + return Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/DownloadClientItemViewTemplate', + tagName : 'li', - events: { - 'click' : '_edit' - }, + events: { + 'click' : '_edit' + }, - initialize: function () { - this.listenTo(this.model, 'sync', this.render); - }, + initialize: function () { + this.listenTo(this.model, 'sync', this.render); + }, - _edit: function () { - var view = new EditView({ model: this.model, downloadClientCollection: this.model.collection }); - AppLayout.modalRegion.show(view); - } - }); + _edit: function () { + var view = new EditView({ model: this.model, targetCollection: this.model.collection}); + AppLayout.modalRegion.show(view); + } }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayout.js b/src/UI/Settings/DownloadClient/DownloadClientLayout.js index e632371dc..e9510f160 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientLayout.js +++ b/src/UI/Settings/DownloadClient/DownloadClientLayout.js @@ -1,32 +1,31 @@ 'use strict'; -define( - [ - 'marionette', - 'Settings/DownloadClient/DownloadClientCollection', - 'Settings/DownloadClient/DownloadClientCollectionView', - 'Settings/DownloadClient/Options/DownloadClientOptionsView', - 'Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView' - ], function (Marionette, DownloadClientCollection, DownloadClientCollectionView, DownloadClientOptionsView, FailedDownloadHandlingView) { +define([ + 'marionette', + 'Settings/DownloadClient/DownloadClientCollection', + 'Settings/DownloadClient/DownloadClientCollectionView', + 'Settings/DownloadClient/DroneFactory/DroneFactoryView', + 'Settings/DownloadClient/DownloadHandling/DownloadHandlingView' +], function (Marionette, DownloadClientCollection, CollectionView, DroneFactoryView, DownloadHandlingView) { - return Marionette.Layout.extend({ - template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', + return Marionette.Layout.extend({ + template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', - regions: { - downloadClients : '#x-download-clients-region', - downloadClientOptions : '#x-download-client-options-region', - failedDownloadHandling : '#x-failed-download-handling-region' - }, + regions: { + downloadClients : '#x-download-clients-region', + downloadHandling : '#x-download-handling-region', + droneFactory : '#x-dronefactory-region' + }, - initialize: function () { - this.downloadClientCollection = new DownloadClientCollection(); - this.downloadClientCollection.fetch(); - }, + initialize: function () { + this.downloadClientsCollection = new DownloadClientCollection(); + this.downloadClientsCollection.fetch(); + }, - onShow: function () { - this.downloadClients.show(new DownloadClientCollectionView({ collection: this.downloadClientCollection })); - this.downloadClientOptions.show(new DownloadClientOptionsView({ model: this.model })); - this.failedDownloadHandling.show(new FailedDownloadHandlingView({ model: this.model })); - } - }); - }); \ No newline at end of file + onShow: function () { + this.downloadClients.show(new CollectionView({ collection: this.downloadClientsCollection })); + this.downloadHandling.show(new DownloadHandlingView({ model: this.model })); + this.droneFactory.show(new DroneFactoryView({ model: this.model })); + } + }); +}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html index 365590417..89c20761e 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html @@ -1,6 +1,5 @@ <div id="x-download-clients-region"></div> <div class="form-horizontal"> - <div id="x-download-client-options-region"></div> - <div id="x-failed-download-handling-region"></div> + <div id="x-download-handling-region"></div> + <div id="x-dronefactory-region"></div> </div> - diff --git a/src/UI/Settings/DownloadClient/DownloadClientModel.js b/src/UI/Settings/DownloadClient/DownloadClientModel.js index 5e08858af..3702cf7dc 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientModel.js +++ b/src/UI/Settings/DownloadClient/DownloadClientModel.js @@ -1,10 +1,9 @@ 'use strict'; -define( - [ - 'backbone.deepmodel' - ], function (DeepModel) { - return DeepModel.DeepModel.extend({ - }); +define([ + 'backbone.deepmodel' +], function (DeepModel) { + return DeepModel.DeepModel.extend({ + }); - +}); diff --git a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js index 8a3b066b3..ab2a40f89 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js +++ b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js @@ -1,11 +1,11 @@ 'use strict'; -define( - [ - 'Settings/SettingsModelBase' - ], function (SettingsModelBase) { - return SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/downloadclient', - successMessage: 'Download client settings saved', - errorMessage : 'Failed to save download client settings' - }); + +define([ + 'Settings/SettingsModelBase' +], function (SettingsModelBase) { + return SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/downloadclient', + successMessage: 'Download client settings saved', + errorMessage : 'Failed to save download client settings' }); +}); diff --git a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js new file mode 100644 index 000000000..d88d628ef --- /dev/null +++ b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js @@ -0,0 +1,60 @@ +'use strict'; +define( + [ + 'marionette', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function (Marionette, AsModelBoundView, AsValidatedView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate', + + ui: { + completedDownloadHandlingCheckbox : '.x-completed-download-handling', + completedDownloadOptions : '.x-completed-download-options', + failedDownloadHandlingCheckbox : '.x-failed-download-handling', + failedDownloadOptions : '.x-failed-download-options' + }, + + events: { + 'change .x-completed-download-handling' : '_setCompletedDownloadOptionsVisibility', + 'change .x-failed-download-handling' : '_setFailedDownloadOptionsVisibility' + }, + + onRender: function () { + if (!this.ui.completedDownloadHandlingCheckbox.prop('checked')) { + this.ui.completedDownloadOptions.hide(); + } + if (!this.ui.failedDownloadHandlingCheckbox.prop('checked')) { + this.ui.failedDownloadOptions.hide(); + } + }, + + _setCompletedDownloadOptionsVisibility: function () { + var checked = this.ui.completedDownloadHandlingCheckbox.prop('checked'); + if (checked) { + this.ui.completedDownloadOptions.slideDown(); + } + + else { + this.ui.completedDownloadOptions.slideUp(); + } + }, + + _setFailedDownloadOptionsVisibility: function () { + var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); + if (checked) { + this.ui.failedDownloadOptions.slideDown(); + } + + else { + this.ui.failedDownloadOptions.slideUp(); + } + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; + }); diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html similarity index 62% rename from src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html rename to src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html index 0bf3acc39..4a0eb27af 100644 --- a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html @@ -1,6 +1,56 @@ -<fieldset class="advanced-setting"> - <legend>Failed Download Handling</legend> +<fieldset> + <legend>Download Client Import Handling</legend> + <div class="form-group"> + <label class="col-sm-3 control-label">Enable</label> + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="enableCompletedDownloadHandling" class="x-completed-download-handling"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Import completed downloads in download client history"/> + <i class="icon-nd-form-warning" title="Download client history items that are stored in the drone factory will be ignored. Configure the Drone Factory for a different path"/> + </span> + </div> + </div> + </div> + + <div class="x-completed-download-options advanced-setting""> + <div class="form-group"> + <label class="col-sm-3 control-label">Remove</label> + + <div class="col-sm-8"> + <div class="input-group"> + <label class="checkbox toggle well"> + <input type="checkbox" name="removeCompletedDownloads"/> + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + + <span class="help-inline-checkbox"> + <i class="icon-nd-form-info" title="Remove imported downloads from download client history"/> + </span> + </div> + </div> + </div> + </div> +</fieldset> + +<fieldset class="advanced-setting"> + <legend>Download Client Failed Handling</legend> + <div class="form-group"> <label class="col-sm-3 control-label">Enable</label> @@ -40,7 +90,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release when a download fails?"/> + <i class="icon-nd-form-info" title="Automatically search for and attempt to download another release"/> </span> </div> </div> @@ -62,7 +112,7 @@ </label> <span class="help-inline-checkbox"> - <i class="icon-nd-form-info" title="Automatically remove failed downloads from history and encrypted downloads from queue?"/> + <i class="icon-nd-form-info" title="Remove failed downloads from download client history"/> </span> </div> </div> diff --git a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js similarity index 86% rename from src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js rename to src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js index 444bed1d8..100b9b46c 100644 --- a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsView.js +++ b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js @@ -8,7 +8,7 @@ define( ], function (Marionette, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate', + template: 'Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate', ui: { droneFactory : '.x-path' diff --git a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.html similarity index 86% rename from src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html rename to src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.html index f5feb5a57..afd61ea24 100644 --- a/src/UI/Settings/DownloadClient/Options/DownloadClientOptionsViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.html @@ -1,10 +1,10 @@ <fieldset> - <legend>Options</legend> + <legend>Drone Factory Options</legend> <div class="form-group"> <label class="col-sm-3 control-label">Drone Factory</label> <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-nd-form-info" title="The folder where your download client downloads TV shows to (Completed Download Directory)"/> + <i class="icon-nd-form-info" title="Optional folder to periodically scan for available imports"/> <i class="icon-nd-form-warning" title="Do not use the folder that contains some or all of your sorted and named TV shows - doing so could cause data loss"></i> </div> diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js index 6f75aaf8f..98fa8eea0 100644 --- a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js +++ b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js @@ -1,97 +1,96 @@ 'use strict'; -define( - [ - 'vent', - 'AppLayout', - 'marionette', - 'Settings/DownloadClient/Delete/DownloadClientDeleteView', - 'Commands/CommandController', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView', - 'underscore', - 'Form/FormBuilder', - 'Mixins/AutoComplete', - 'bootstrap' - ], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { +define([ + 'vent', + 'AppLayout', + 'marionette', + 'Settings/DownloadClient/Delete/DownloadClientDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'underscore', + 'Form/FormBuilder', + 'Mixins/AutoComplete', + 'bootstrap' +], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { - var view = Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', + var view = Marionette.ItemView.extend({ + template: 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', - ui: { - path : '.x-path', - modalBody : '.modal-body' - }, + ui: { + path : '.x-path', + modalBody : '.modal-body' + }, - events: { - 'click .x-save' : '_save', - 'click .x-save-and-add': '_saveAndAdd', - 'click .x-delete' : '_delete', - 'click .x-back' : '_back', - 'click .x-test' : '_test' - }, + events: { + 'click .x-save' : '_save', + 'click .x-save-and-add': '_saveAndAdd', + 'click .x-delete' : '_delete', + 'click .x-back' : '_back', + 'click .x-test' : '_test' + }, - initialize: function (options) { - this.downloadClientCollection = options.downloadClientCollection; - }, + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, - onShow: function () { - //Hack to deal with modals not overflowing - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.autoComplete('/directories'); - }, - - _save: function () { - var self = this; - var promise = this.model.save(); - - if (promise) { - promise.done(function () { - self.downloadClientCollection.add(self.model, { merge: true }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - } - }, - - _saveAndAdd: function () { - var self = this; - var promise = this.model.save(); - - if (promise) { - promise.done(function () { - self.notificationCollection.add(self.model, { merge: true }); - - require('Settings/DownloadClient/Add/SchemaModal').open(self.downloadClientCollection); - }); - } - }, - - _delete: function () { - var view = new DeleteView({ model: this.model }); - AppLayout.modalRegion.show(view); - }, - - _back: function () { - require('Settings/DownloadClient/Add/SchemaModal').open(this.downloadClientCollection); - }, - - _test: function () { - var testCommand = 'test{0}'.format(this.model.get('implementation')); - var properties = {}; - - _.each(this.model.get('fields'), function (field) { - properties[field.name] = field.value; - }); - - CommandController.Execute(testCommand, properties); + onShow: function () { + //Hack to deal with modals not overflowing + if (this.ui.path.length > 0) { + this.ui.modalBody.addClass('modal-overflow'); } - }); - AsModelBoundView.call(view); - AsValidatedView.call(view); + this.ui.path.autoComplete('/directories'); + }, - return view; + _save: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _saveAndAdd: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + + require('Settings/DownloadClient/Add/DownloadClientSchemaModal').open(self.targetCollection); + }); + } + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _back: function () { + require('Settings/DownloadClient/Add/DownloadClientSchemaModal').open(this.targetCollection); + }, + + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; + + _.each(this.model.get('fields'), function (field) { + properties[field.name] = field.value; + }); + + CommandController.Execute(testCommand, properties); + } }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; +}); diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js deleted file mode 100644 index 9af62d5dc..000000000 --- a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingView.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; -define( - [ - 'marionette', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView' - ], function (Marionette, AsModelBoundView, AsValidatedView) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate', - - ui: { - failedDownloadHandlingCheckbox: '.x-failed-download-handling', - failedDownloadOptions : '.x-failed-download-options' - }, - - events: { - 'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility' - }, - - _setFailedDownloadOptionsVisibility: function () { - var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); - if (checked) { - this.ui.failedDownloadOptions.slideDown(); - } - - else { - this.ui.failedDownloadOptions.slideUp(); - } - } - }); - - AsModelBoundView.call(view); - AsValidatedView.call(view); - - return view; - }); diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js b/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js new file mode 100644 index 000000000..35edef28d --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js @@ -0,0 +1,14 @@ +'use strict'; + +define([ + 'Settings/ThingyAddCollectionView', + 'Settings/ThingyHeaderGroupView', + 'Settings/Indexers/Add/IndexerAddItemView' +], function (ThingyAddCollectionView, ThingyHeaderGroupView, AddItemView) { + + return ThingyAddCollectionView.extend({ + itemView : ThingyHeaderGroupView.extend({ itemView: AddItemView }), + itemViewContainer: '.add-indexer .items', + template : 'Settings/Indexers/Add/IndexerAddCollectionViewTemplate' + }); +}); diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.html b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.html new file mode 100644 index 000000000..95d3ceb9a --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.html @@ -0,0 +1,16 @@ +<div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Add Indexer</h3> + </div> + <div class="modal-body"> + <div class="add-indexer add-thingies"> + <ul class="items"></ul> + </div> + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> + </div> +</div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js new file mode 100644 index 000000000..859f2a008 --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js @@ -0,0 +1,37 @@ +'use strict'; + +define([ + 'jquery', + 'AppLayout', + 'marionette', + 'Settings/Indexers/Edit/IndexerEditView' +], function ($, AppLayout, Marionette, EditView) { + + return Marionette.ItemView.extend({ + template: 'Settings/Indexers/Add/IndexerAddItemViewTemplate', + tagName : 'li', + + events: { + 'click': '_add' + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, + + _add: function (e) { + if (this.$(e.target).hasClass('icon-info-sign')) { + return; + } + + this.model.set({ + id : undefined, + name : this.model.get('implementation'), + enable : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + } + }); +}); diff --git a/src/UI/Settings/Notifications/AddItemTemplate.html b/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html similarity index 100% rename from src/UI/Settings/Notifications/AddItemTemplate.html rename to src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html diff --git a/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js b/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js new file mode 100644 index 000000000..f702481b7 --- /dev/null +++ b/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js @@ -0,0 +1,36 @@ +'use strict'; + +define([ + 'underscore', + 'AppLayout', + 'backbone', + 'Settings/Indexers/IndexerCollection', + 'Settings/Indexers/Add/IndexerAddCollectionView' +], function (_, AppLayout, Backbone, SchemaCollection, AddCollectionView) { + return ({ + + open: function (collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var groupedSchemaCollection = new Backbone.Collection(); + + schemaCollection.on('sync', function() { + + var groups = schemaCollection.groupBy(function(model, iterator) { return model.get('protocol'); }); + + var modelCollection = _.map(groups, function(values, key, list) { + return { 'header': key, collection: values }; + }); + + groupedSchemaCollection.reset(modelCollection); + }); + + var view = new AddCollectionView({ collection: groupedSchemaCollection, targetCollection: collection }); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/Indexers/Collection.js b/src/UI/Settings/Indexers/Collection.js deleted file mode 100644 index fc20e436a..000000000 --- a/src/UI/Settings/Indexers/Collection.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -define( - [ - 'backbone', - 'Settings/Indexers/Model', - ], function (Backbone, IndexerModel) { - return Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/indexer', - model: IndexerModel - }); - }); diff --git a/src/UI/Settings/Indexers/CollectionView.js b/src/UI/Settings/Indexers/CollectionView.js deleted file mode 100644 index 662dd5298..000000000 --- a/src/UI/Settings/Indexers/CollectionView.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; -define( - [ - 'AppLayout', - 'marionette', - 'Settings/Indexers/ItemView', - 'Settings/Indexers/EditView', - 'Settings/Indexers/Collection', - 'underscore' - ], function (AppLayout, Marionette, IndexerItemView, IndexerEditView, IndexerCollection, _) { - return Marionette.CompositeView.extend({ - itemView : IndexerItemView, - itemViewContainer: '#x-indexers', - template : 'Settings/Indexers/CollectionTemplate', - - ui: { - 'addCard': '.x-add-card' - }, - - events: { - 'click .x-add-card': '_openSchemaModal' - }, - - appendHtml: function (collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal: function () { - var self = this; - var schemaCollection = new IndexerCollection(); - var originalUrl = schemaCollection.url; - - schemaCollection.url = schemaCollection.url + '/schema'; - - schemaCollection.fetch({ - success: function (collection) { - collection.url = originalUrl; - var model = _.first(collection.models); - - model.set({ - id : undefined, - name : '', - enable: true - }); - - var view = new IndexerEditView({ model: model, indexerCollection: self.collection}); - AppLayout.modalRegion.show(view); - } - }); - } - }); - }); diff --git a/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js b/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js new file mode 100644 index 000000000..adcb5236f --- /dev/null +++ b/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'vent', + 'marionette' +], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/Indexers/Delete/IndexerDeleteViewTemplate', + + events: { + 'click .x-confirm-delete': '_delete' + }, + + _delete: function () { + this.model.destroy({ + wait : true, + success: function () { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } + }); +}); diff --git a/src/UI/Settings/Indexers/DeleteViewTemplate.html b/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.html similarity index 100% rename from src/UI/Settings/Indexers/DeleteViewTemplate.html rename to src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.html diff --git a/src/UI/Settings/Indexers/DeleteView.js b/src/UI/Settings/Indexers/DeleteView.js deleted file mode 100644 index b230684eb..000000000 --- a/src/UI/Settings/Indexers/DeleteView.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; -define( - [ - 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ - template: 'Settings/Indexers/DeleteViewTemplate', - - events: { - 'click .x-confirm-delete': '_removeNotification' - }, - - _removeNotification: function () { - this.model.destroy({ - wait : true, - success: function () { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } - }); - }); diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditView.js b/src/UI/Settings/Indexers/Edit/IndexerEditView.js new file mode 100644 index 000000000..2f4a8b90d --- /dev/null +++ b/src/UI/Settings/Indexers/Edit/IndexerEditView.js @@ -0,0 +1,86 @@ +'use strict'; + +define([ + 'vent', + 'AppLayout', + 'marionette', + 'Settings/Indexers/Delete/IndexerDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'underscore', + 'Form/FormBuilder', + 'Mixins/AutoComplete', + 'bootstrap' +], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Indexers/Edit/IndexerEditViewTemplate', + + events: { + 'click .x-save' : '_save', + 'click .x-save-and-add': '_saveAndAdd', + 'click .x-delete' : '_delete', + 'click .x-back' : '_back', + 'click .x-test' : '_test' + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, + + _save: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _saveAndAdd: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + + require('Settings/Indexers/Add/IndexerSchemaModal').open(self.targetCollection); + }); + } + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _back: function () { + if (this.model.isNew()) { + this.model.destroy(); + } + + require('Settings/Indexers/Add/IndexerSchemaModal').open(this.targetCollection); + }, + + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; + + _.each(this.model.get('fields'), function (field) { + properties[field.name] = field.value; + }); + + CommandController.Execute(testCommand, properties); + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; +}); diff --git a/src/UI/Settings/Indexers/EditTemplate.html b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html similarity index 73% rename from src/UI/Settings/Indexers/EditTemplate.html rename to src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html index 7e7eee4e0..25ea543df 100644 --- a/src/UI/Settings/Indexers/EditTemplate.html +++ b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html @@ -1,14 +1,14 @@ <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close x-cancel"aria-hidden="true">×</button> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> {{#if id}} - <h3>Edit</h3> + <h3>Edit - {{implementation}}</h3> {{else}} - <h3>Add Newznab</h3> + <h3>Add - {{implementation}}</h3> {{/if}} </div> - <div class="modal-body"> + <div class="modal-body indexer-modal"> <div class="form-horizontal"> <div class="form-group"> <label class="col-sm-3 control-label">Name</label> @@ -41,12 +41,14 @@ </div> <div class="modal-footer"> {{#if id}} - <button class="btn btn-danger pull-left x-remove">delete</button> + <button class="btn btn-danger pull-left x-delete">delete</button> + {{else}} + <button class="btn pull-left x-back">back</button> {{/if}} - <span class="x-activity"></span> - - <button class="btn x-cancel">cancel</button> + <!-- Testing is currently not yet supported for indexers, but leaving the infrastructure for later --> + <!-- <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> --> + <button class="btn" data-dismiss="modal">cancel</button> <div class="btn-group"> <button class="btn btn-primary x-save">save</button> diff --git a/src/UI/Settings/Indexers/EditView.js b/src/UI/Settings/Indexers/EditView.js deleted file mode 100644 index dca99a2ac..000000000 --- a/src/UI/Settings/Indexers/EditView.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -define( - [ - 'vent', - 'marionette', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView', - 'underscore' - ], function (vent, Marionette, AsModelBoundView, AsValidatedView, _) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Indexers/EditTemplate', - - ui: { - activity: '.x-activity' - }, - - events: { - 'click .x-save' : '_save', - 'click .x-save-and-add': '_saveAndAdd', - 'click .x-cancel' : '_cancel' - }, - - initialize: function (options) { - this.indexerCollection = options.indexerCollection; - }, - - _save: function () { - this.ui.activity.html('<i class="icon-nd-spinner"></i>'); - - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.indexerCollection.add(self.model, { merge: true }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - - promise.fail(function () { - self.ui.activity.empty(); - }); - } - }, - - _saveAndAdd: function () { - this.ui.activity.html('<i class="icon-nd-spinner"></i>'); - - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.indexerCollection.add(self.model, { merge: true }); - - self.model.set({ - id : undefined, - name : '', - enable: false - }); - - _.each(self.model.get('fields'), function (value, key, list) { - self.model.set('fields.' + key + '.value', ''); - }); - }); - - promise.fail(function () { - self.ui.activity.empty(); - }); - } - }, - - _cancel: function () { - if (this.model.isNew()) { - this.model.destroy(); - vent.trigger(vent.Commands.CloseModalCommand); - } - } - }); - - AsModelBoundView.call(view); - AsValidatedView.call(view); - - return view; - }); diff --git a/src/UI/Settings/Indexers/IndexerCollection.js b/src/UI/Settings/Indexers/IndexerCollection.js new file mode 100644 index 000000000..4a3f2492e --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerCollection.js @@ -0,0 +1,31 @@ +'use strict'; + +define([ + 'backbone', + 'Settings/Indexers/IndexerModel' +], function (Backbone, IndexerModel) { + + return Backbone.Collection.extend({ + model: IndexerModel, + url : window.NzbDrone.ApiRoot + '/indexer', + + comparator : function(left, right, collection) { + + var result = 0; + + if (left.get('protocol')) { + result = -left.get('protocol').localeCompare(right.get('protocol')); + } + + if (result === 0 && left.get('name')) { + result = left.get('name').localeCompare(right.get('name')); + } + + if (result === 0) { + result = left.get('implementation').localeCompare(right.get('implementation')); + } + + return result; + } + }); +}); diff --git a/src/UI/Settings/Indexers/IndexerCollectionView.js b/src/UI/Settings/Indexers/IndexerCollectionView.js new file mode 100644 index 000000000..7bfd67322 --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerCollectionView.js @@ -0,0 +1,29 @@ +'use strict'; + +define([ + 'marionette', + 'Settings/Indexers/IndexerItemView', + 'Settings/Indexers/Add/IndexerSchemaModal' +], function (Marionette, ItemView, SchemaModal) { + return Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer: '.indexer-list', + template : 'Settings/Indexers/IndexerCollectionViewTemplate', + + ui: { + 'addCard': '.x-add-card' + }, + + events: { + 'click .x-add-card': '_openSchemaModal' + }, + + appendHtml: function (collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal: function () { + SchemaModal.open(this.collection); + } + }); +}); diff --git a/src/UI/Settings/Indexers/CollectionTemplate.html b/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.html similarity index 57% rename from src/UI/Settings/Indexers/CollectionTemplate.html rename to src/UI/Settings/Indexers/IndexerCollectionViewTemplate.html index 657ee83d7..09e4e129b 100644 --- a/src/UI/Settings/Indexers/CollectionTemplate.html +++ b/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.html @@ -2,11 +2,11 @@ <legend>Indexers</legend> <div class="row"> <div class="col-md-12"> - <ul id="x-indexers" class="indexer-list thingies"> + <ul class="indexer-list thingies"> <li> - <div class="indexer-settings-item add-card x-add-card"> + <div class="indexer-item thingy add-card x-add-card"> <span class="center well"> - <i class="icon-plus" title="Add Newznab"/> + <i class="icon-plus" title="Add Indexer"/> </span> </div> </li> diff --git a/src/UI/Settings/Indexers/IndexerItemView.js b/src/UI/Settings/Indexers/IndexerItemView.js new file mode 100644 index 000000000..a85a73b21 --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerItemView.js @@ -0,0 +1,26 @@ +'use strict'; + +define([ + 'AppLayout', + 'marionette', + 'Settings/Indexers/Edit/IndexerEditView' +], function (AppLayout, Marionette, EditView) { + + return Marionette.ItemView.extend({ + template: 'Settings/Indexers/IndexerItemViewTemplate', + tagName : 'li', + + events: { + 'click' : '_edit' + }, + + initialize: function () { + this.listenTo(this.model, 'sync', this.render); + }, + + _edit: function () { + var view = new EditView({ model: this.model, targetCollection: this.model.collection }); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/Indexers/IndexerItemViewTemplate.html b/src/UI/Settings/Indexers/IndexerItemViewTemplate.html new file mode 100644 index 000000000..d1b3cf807 --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerItemViewTemplate.html @@ -0,0 +1,13 @@ +<div class="indexer-item thingy" title="Click to edit"> + <div> + <h3>{{name}}</h3> + </div> + + <div class="settings"> + {{#if enable}} + <span class="label label-success">Enabled</span> + {{else}} + <span class="label label-default">Not Enabled</span> + {{/if}} + </div> +</div> diff --git a/src/UI/Settings/Indexers/IndexerLayout.js b/src/UI/Settings/Indexers/IndexerLayout.js index eca151eb1..9f3b4c209 100644 --- a/src/UI/Settings/Indexers/IndexerLayout.js +++ b/src/UI/Settings/Indexers/IndexerLayout.js @@ -1,28 +1,28 @@ 'use strict'; -define( - [ - 'marionette', - 'Settings/Indexers/CollectionView', - 'Settings/Indexers/Options/IndexerOptionsView' - ], function (Marionette, CollectionView, OptionsView) { - return Marionette.Layout.extend({ - template: 'Settings/Indexers/IndexerLayoutTemplate', +define([ + 'marionette', + 'Settings/Indexers/IndexerCollection', + 'Settings/Indexers/IndexerCollectionView', + 'Settings/Indexers/Options/IndexerOptionsView' +], function (Marionette, IndexerCollection, CollectionView, OptionsView) { - regions: { - indexersRegion : '#indexers-collection', - indexerOptions : '#indexer-options' - }, + return Marionette.Layout.extend({ + template: 'Settings/Indexers/IndexerLayoutTemplate', - initialize: function (options) { - this.settings = options.settings; - this.indexersCollection = options.indexersCollection; - }, + regions: { + indexers : '#x-indexers-region', + indexerOptions : '#x-indexer-options-region' + }, - onShow: function () { - this.indexersRegion.show(new CollectionView({ collection: this.indexersCollection })); - this.indexerOptions.show(new OptionsView({ model: this.settings })); - } - }); + initialize: function (options) { + this.indexersCollection = new IndexerCollection(); + this.indexersCollection.fetch(); + }, + + onShow: function () { + this.indexers.show(new CollectionView({ collection: this.indexersCollection })); + this.indexerOptions.show(new OptionsView({ model: this.model })); + } }); - +}); diff --git a/src/UI/Settings/Indexers/IndexerLayoutTemplate.html b/src/UI/Settings/Indexers/IndexerLayoutTemplate.html index a0c6402a8..91bfbbdef 100644 --- a/src/UI/Settings/Indexers/IndexerLayoutTemplate.html +++ b/src/UI/Settings/Indexers/IndexerLayoutTemplate.html @@ -1,5 +1,4 @@ -<div id="indexers-collection"></div> - +<div id="x-indexers-region"></div> <div class="form-horizontal"> - <div id="indexer-options"></div> + <div id="x-indexer-options-region"></div> </div> diff --git a/src/UI/Settings/Indexers/IndexerModel.js b/src/UI/Settings/Indexers/IndexerModel.js new file mode 100644 index 000000000..3702cf7dc --- /dev/null +++ b/src/UI/Settings/Indexers/IndexerModel.js @@ -0,0 +1,9 @@ +'use strict'; + +define([ + 'backbone.deepmodel' +], function (DeepModel) { + return DeepModel.DeepModel.extend({ + + }); +}); diff --git a/src/UI/Settings/Indexers/IndexerSettingsModel.js b/src/UI/Settings/Indexers/IndexerSettingsModel.js index 34ede06ee..ce3a654ea 100644 --- a/src/UI/Settings/Indexers/IndexerSettingsModel.js +++ b/src/UI/Settings/Indexers/IndexerSettingsModel.js @@ -1,11 +1,11 @@ 'use strict'; -define( - [ - 'Settings/SettingsModelBase' - ], function (SettingsModelBase) { - return SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/indexer', - successMessage: 'Indexer settings saved', - errorMessage : 'Failed to save indexer settings' - }); + +define([ + 'Settings/SettingsModelBase' +], function (SettingsModelBase) { + return SettingsModelBase.extend({ + url : window.NzbDrone.ApiRoot + '/config/indexer', + successMessage: 'Indexer settings saved', + errorMessage : 'Failed to save indexer settings' }); +}); diff --git a/src/UI/Settings/Indexers/ItemTemplate.html b/src/UI/Settings/Indexers/ItemTemplate.html deleted file mode 100644 index 4f38978ad..000000000 --- a/src/UI/Settings/Indexers/ItemTemplate.html +++ /dev/null @@ -1,37 +0,0 @@ -<div class="indexer-settings-item thingy"> - <div> - <h3>{{name}}</h3> - {{#if_eq implementation compare="Newznab"}} - <span class="btn-group pull-right"> - <button class="btn btn-xs btn-icon-only x-delete"> - <i class="icon-nd-delete"/> - </button> - </span> - {{/if_eq}} - </div> - - <div class="form-group"> - <label class="control-label">Enable</label> - - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - - {{formBuilder}} - - {{#if_eq name compare="WomblesIndex"}} - <div class="alert"> - <i class="icon-nd-warning"></i> - Does not support searching - </div> - {{/if_eq}} -</div> diff --git a/src/UI/Settings/Indexers/ItemView.js b/src/UI/Settings/Indexers/ItemView.js deleted file mode 100644 index 23ab0d00b..000000000 --- a/src/UI/Settings/Indexers/ItemView.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -define( - [ - 'AppLayout', - 'marionette', - 'Settings/Indexers/DeleteView', - 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView' - ], function (AppLayout, Marionette, DeleteView, AsModelBoundView, AsValidatedView) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/Indexers/ItemTemplate', - tagName : 'li', - - events: { - 'click .x-delete': '_deleteIndexer' - }, - - _deleteIndexer: function () { - var view = new DeleteView({ model: this.model}); - AppLayout.modalRegion.show(view); - } - }); - - AsModelBoundView.call(view); - return AsValidatedView.call(view); - - }); diff --git a/src/UI/Settings/Indexers/Model.js b/src/UI/Settings/Indexers/Model.js deleted file mode 100644 index ecfee73b8..000000000 --- a/src/UI/Settings/Indexers/Model.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; -define([ - 'Settings/SettingsModelBase'], function (ModelBase) { - return ModelBase.extend({ - - baseInitialize: ModelBase.prototype.initialize, - - initialize: function () { - var name = this.get('name'); - - if (name) { - this.successMessage = 'Saved indexer: ' + name; - this.errorMessage = 'Couldn\'t save indexer: ' + name; - } - - else { - this.successMessage = 'Saved indexer'; - this.errorMessage = 'Couldn\'t save indexer'; - } - - this.baseInitialize.call(this); - } - }); -}); diff --git a/src/UI/Settings/Indexers/indexers.less b/src/UI/Settings/Indexers/indexers.less index 281e70e90..d478f0e64 100644 --- a/src/UI/Settings/Indexers/indexers.less +++ b/src/UI/Settings/Indexers/indexers.less @@ -1,29 +1,33 @@ -.indexer-settings-item { +@import "../../Shared/Styles/clickable.less"; - width: 220px; - height: 295px; +.indexer-list { + li { + display: inline-block; + vertical-align: top; + } +} + +.indexer-item { + + .clickable; + + width: 290px; + height: 90px; padding: 10px 15px; - h3 { - width: 175px; - overflow: visible; - } - &.add-card { - margin-top: 10px; - margin-left: 10px; - .center { - margin-top: 90px; + margin-top: -3px; } } +} - /* Super hack to keep using form builder, this should be dead when we do proper modals for editing */ - .col-sm-1, .col-sm-3, .col-sm-5 { - display : block; - width : 100%; - padding: 0px; - float: none; - position: inherit; +.modal-overflow { + overflow-y: visible; +} + +.add-indexer { + li { + width: 33%; } } \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js b/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js new file mode 100644 index 000000000..713e05c2c --- /dev/null +++ b/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js @@ -0,0 +1,13 @@ +'use strict'; + +define([ + 'Settings/ThingyAddCollectionView', + 'Settings/Notifications/Add/NotificationAddItemView' +], function (ThingyAddCollectionView, AddItemView) { + + return ThingyAddCollectionView.extend({ + itemView : AddItemView, + itemViewContainer: '.add-notifications .items', + template : 'Settings/Notifications/Add/NotificationAddCollectionViewTemplate' + }); +}); diff --git a/src/UI/Settings/Notifications/AddTemplate.html b/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.html similarity index 100% rename from src/UI/Settings/Notifications/AddTemplate.html rename to src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.html diff --git a/src/UI/Settings/Notifications/AddItemView.js b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js similarity index 53% rename from src/UI/Settings/Notifications/AddItemView.js rename to src/UI/Settings/Notifications/Add/NotificationAddItemView.js index 2c031b3e0..5c8c0ec9f 100644 --- a/src/UI/Settings/Notifications/AddItemView.js +++ b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js @@ -1,37 +1,38 @@ 'use strict'; define([ + 'jquery', 'AppLayout', 'marionette', - 'Settings/Notifications/NotificationEditView' -], function (AppLayout, Marionette, EditView) { + 'Settings/Notifications/Edit/NotificationEditView' +], function ($, AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ - template: 'Settings/Notifications/AddItemTemplate', + template: 'Settings/Notifications/Add/NotificationAddItemViewTemplate', tagName : 'li', events: { - 'click': 'addNotification' + 'click': '_add' }, initialize: function (options) { - this.notificationCollection = options.notificationCollection; + this.targetCollection = options.targetCollection; }, - addNotification: function (e) { + _add: function (e) { if (this.$(e.target).hasClass('icon-info-sign')) { return; } this.model.set({ id : undefined, - name : this.model.get('implementationName'), + name : this.model.get('implementation'), onGrab : true, onDownload : true, onUpgrade : true }); - var editView = new EditView({ model: this.model, notificationCollection: this.notificationCollection }); + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); AppLayout.modalRegion.show(editView); } }); diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html new file mode 100644 index 000000000..f892a4d01 --- /dev/null +++ b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html @@ -0,0 +1,6 @@ +<div class="add-thingy"> + {{implementation}} + {{#if link}} + <a href="{{link}}"><i class="icon-info-sign"/></a> + {{/if}} +</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js b/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js new file mode 100644 index 000000000..f931ac924 --- /dev/null +++ b/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js @@ -0,0 +1,21 @@ +'use strict'; + +define([ + 'AppLayout', + 'Settings/Notifications/NotificationCollection', + 'Settings/Notifications/Add/NotificationAddCollectionView' +], function (AppLayout, SchemaCollection, AddCollectionView) { + return ({ + + open: function (collection) { + var schemaCollection = new SchemaCollection(); + var originalUrl = schemaCollection.url; + schemaCollection.url = schemaCollection.url + '/schema'; + schemaCollection.fetch(); + schemaCollection.url = originalUrl; + + var view = new AddCollectionView({ collection: schemaCollection, targetCollection: collection}); + AppLayout.modalRegion.show(view); + } + }); +}); diff --git a/src/UI/Settings/Notifications/AddView.js b/src/UI/Settings/Notifications/AddView.js deleted file mode 100644 index 17e1064d2..000000000 --- a/src/UI/Settings/Notifications/AddView.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -define([ - 'marionette', - 'Settings/Notifications/AddItemView' -], function (Marionette, AddItemView) { - - return Marionette.CompositeView.extend({ - itemView : AddItemView, - itemViewContainer: '.add-notifications .items', - template : 'Settings/Notifications/AddTemplate', - - itemViewOptions: function () { - return { - notificationCollection: this.notificationCollection - }; - }, - - initialize: function (options) { - this.notificationCollection = options.notificationCollection; - } - }); -}); diff --git a/src/UI/Settings/Notifications/Collection.js b/src/UI/Settings/Notifications/Collection.js deleted file mode 100644 index a045020ab..000000000 --- a/src/UI/Settings/Notifications/Collection.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -define( - [ - 'backbone', - 'Settings/Notifications/Model' - ], function (Backbone, NotificationModel) { - return Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/notification', - model: NotificationModel - }); - }); diff --git a/src/UI/Settings/Notifications/CollectionTemplate.html b/src/UI/Settings/Notifications/CollectionTemplate.html deleted file mode 100644 index b9cfad00d..000000000 --- a/src/UI/Settings/Notifications/CollectionTemplate.html +++ /dev/null @@ -1,13 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <ul class="notifications thingies"> - <li> - <div class="notification-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-plus" title="Add Connection"/> - </span> - </div> - </li> - </ul> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/CollectionView.js b/src/UI/Settings/Notifications/CollectionView.js deleted file mode 100644 index efae31c1b..000000000 --- a/src/UI/Settings/Notifications/CollectionView.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; -define([ - 'marionette', - 'Settings/Notifications/NotificationsItemView', - 'Settings/Notifications/SchemaModal' -], function (Marionette, NotificationItemView, SchemaModal) { - return Marionette.CompositeView.extend({ - itemView : NotificationItemView, - itemViewContainer: '.notifications', - template : 'Settings/Notifications/CollectionTemplate', - - ui: { - 'addCard': '.x-add-card' - }, - - events: { - 'click .x-add-card': '_openSchemaModal' - }, - - appendHtml: function(collectionView, itemView, index){ - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal: function () { - SchemaModal.open(this.collection); - } - }); -}); diff --git a/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js b/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js new file mode 100644 index 000000000..858fcf85e --- /dev/null +++ b/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'vent', + 'marionette' +], function (vent, Marionette) { + return Marionette.ItemView.extend({ + template: 'Settings/Notifications/Delete/NotificationDeleteViewTemplate', + + events: { + 'click .x-confirm-delete': '_delete' + }, + + _delete: function () { + this.model.destroy({ + wait : true, + success: function () { + vent.trigger(vent.Commands.CloseModalCommand); + } + }); + } + }); +}); diff --git a/src/UI/Settings/Notifications/DeleteTemplate.html b/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.html similarity index 100% rename from src/UI/Settings/Notifications/DeleteTemplate.html rename to src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.html diff --git a/src/UI/Settings/Notifications/DeleteView.js b/src/UI/Settings/Notifications/DeleteView.js deleted file mode 100644 index 24a03f776..000000000 --- a/src/UI/Settings/Notifications/DeleteView.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; -define( - [ - 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ - template: 'Settings/Notifications/DeleteTemplate', - - events: { - 'click .x-confirm-delete': '_removeNotification' - }, - - _removeNotification: function () { - this.model.destroy({ - wait : true, - success: function () { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } - }); - }); diff --git a/src/UI/Settings/Notifications/Edit/NotificationEditView.js b/src/UI/Settings/Notifications/Edit/NotificationEditView.js new file mode 100644 index 000000000..0855d5482 --- /dev/null +++ b/src/UI/Settings/Notifications/Edit/NotificationEditView.js @@ -0,0 +1,113 @@ +'use strict'; + +define([ + 'vent', + 'AppLayout', + 'marionette', + 'Settings/Notifications/Delete/NotificationDeleteView', + 'Commands/CommandController', + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView', + 'underscore', + 'Form/FormBuilder' +], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, _) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/Notifications/Edit/NotificationEditViewTemplate', + + ui: { + onDownloadToggle: '.x-on-download', + onUpgradeSection: '.x-on-upgrade' + }, + + events: { + 'click .x-save' : '_save', + 'click .x-save-and-add': '_saveAndAdd', + 'click .x-delete' : '_delete', + 'click .x-back' : '_back', + 'click .x-cancel' : '_cancel', + 'click .x-test' : '_test', + 'change .x-on-download': '_onDownloadChanged' + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + }, + + onRender: function () { + this._onDownloadChanged(); + }, + + _save: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + vent.trigger(vent.Commands.CloseModalCommand); + }); + } + }, + + _saveAndAdd: function () { + var self = this; + var promise = this.model.save(); + + if (promise) { + promise.done(function () { + self.targetCollection.add(self.model, { merge: true }); + + require('Settings/Notifications/Add/NotificationSchemaModal').open(self.targetCollection); + }); + } + }, + + _delete: function () { + var view = new DeleteView({ model: this.model }); + AppLayout.modalRegion.show(view); + }, + + _back: function () { + if (this.model.isNew()) { + this.model.destroy(); + } + + require('Settings/Notifications/Add/NotificationSchemaModal').open(this.targetCollection); + }, + + _cancel: function () { + if (this.model.isNew()) { + this.model.destroy(); + } + }, + + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; + + _.each(this.model.get('fields'), function (field) { + properties[field.name] = field.value; + }); + + CommandController.Execute(testCommand, properties); + }, + + _onDownloadChanged: function () { + var checked = this.ui.onDownloadToggle.prop('checked'); + + if (checked) { + this.ui.onUpgradeSection.show(); + } + + else { + this.ui.onUpgradeSection.hide(); + } + } + }); + + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; +}); diff --git a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html b/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.html similarity index 94% rename from src/UI/Settings/Notifications/NotificationEditViewTemplate.html rename to src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.html index a68594879..79a4f4ba7 100644 --- a/src/UI/Settings/Notifications/NotificationEditViewTemplate.html +++ b/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.html @@ -1,14 +1,14 @@ <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close x-cancel" aria-hidden="true">×</button> + <button type="button" class="close x-cancel" data-dismiss="modal" aria-hidden="true">×</button> {{#if id}} <h3>Edit - {{implementation}}</h3> {{else}} <h3>Add - {{implementation}}</h3> {{/if}} </div> - <div class="modal-body"> + <div class="modal-body notification-modal"> <div class="form-horizontal"> <div class="form-group"> <label class="col-sm-3 control-label">Name</label> @@ -95,7 +95,7 @@ {{/if}} <button class="btn x-test">test <i class="x-test-icon icon-nd-test"/></button> - <button class="btn x-cancel">cancel</button> + <button class="btn x-cancel" data-dismiss="modal">cancel</button> <div class="btn-group"> <button class="btn btn-primary x-save">save</button> diff --git a/src/UI/Settings/Notifications/NotificationCollection.js b/src/UI/Settings/Notifications/NotificationCollection.js new file mode 100644 index 000000000..9a729c937 --- /dev/null +++ b/src/UI/Settings/Notifications/NotificationCollection.js @@ -0,0 +1,13 @@ +'use strict'; + +define([ + 'backbone', + 'Settings/Notifications/NotificationModel' +], function (Backbone, NotificationModel) { + + return Backbone.Collection.extend({ + model: NotificationModel, + url : window.NzbDrone.ApiRoot + '/notification' + + }); +}); diff --git a/src/UI/Settings/Notifications/NotificationCollectionView.js b/src/UI/Settings/Notifications/NotificationCollectionView.js new file mode 100644 index 000000000..ee74ef0c4 --- /dev/null +++ b/src/UI/Settings/Notifications/NotificationCollectionView.js @@ -0,0 +1,29 @@ +'use strict'; + +define([ + 'marionette', + 'Settings/Notifications/NotificationItemView', + 'Settings/Notifications/Add/NotificationSchemaModal' +], function (Marionette, ItemView, SchemaModal) { + return Marionette.CompositeView.extend({ + itemView : ItemView, + itemViewContainer: '.notification-list', + template : 'Settings/Notifications/NotificationCollectionViewTemplate', + + ui: { + 'addCard': '.x-add-card' + }, + + events: { + 'click .x-add-card': '_openSchemaModal' + }, + + appendHtml: function (collectionView, itemView, index) { + collectionView.ui.addCard.parent('li').before(itemView.el); + }, + + _openSchemaModal: function () { + SchemaModal.open(this.collection); + } + }); +}); diff --git a/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.html b/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.html new file mode 100644 index 000000000..512f1b422 --- /dev/null +++ b/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.html @@ -0,0 +1,16 @@ +<fieldset> + <legend>Connections</legend> + <div class="row"> + <div class="col-md-12"> + <ul class="notification-list thingies"> + <li> + <div class="notification-item thingy add-card x-add-card"> + <span class="center well"> + <i class="icon-plus" title="Add Connection"/> + </span> + </div> + </li> + </ul> + </div> + </div> +</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationEditView.js b/src/UI/Settings/Notifications/NotificationEditView.js deleted file mode 100644 index a11c507f3..000000000 --- a/src/UI/Settings/Notifications/NotificationEditView.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -define( - [ - 'vent', - 'AppLayout', - 'marionette', - 'Settings/Notifications/DeleteView', - 'Commands/CommandController', - 'Mixins/AsModelBoundView', - 'underscore', - 'Form/FormBuilder' - - ], function (vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, _) { - - var model = Marionette.ItemView.extend({ - template: 'Settings/Notifications/NotificationEditViewTemplate', - - ui: { - onDownloadToggle: '.x-on-download', - onUpgradeSection: '.x-on-upgrade' - }, - - events: { - 'click .x-save' : '_saveClient', - 'click .x-save-and-add': '_saveAndAddNotification', - 'click .x-delete' : '_deleteNotification', - 'click .x-back' : '_back', - 'click .x-test' : '_test', - 'click .x-cancel' : '_cancel', - 'change .x-on-download': '_onDownloadChanged' - }, - - initialize: function (options) { - this.notificationCollection = options.notificationCollection; - }, - - onRender: function () { - this._onDownloadChanged(); - }, - - _saveClient: function () { - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.notificationCollection.add(self.model, { merge: true }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - } - }, - - _saveAndAddNotification: function () { - var self = this; - var promise = this.model.saveSettings(); - - if (promise) { - promise.done(function () { - self.notificationCollection.add(self.model, { merge: true }); - - require('Settings/Notifications/SchemaModal').open(self.notificationCollection); - }); - } - }, - - _cancel: function () { - if (this.model.isNew()) { - this.model.destroy(); - } - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _deleteNotification: function () { - var view = new DeleteView({ model: this.model }); - AppLayout.modalRegion.show(view); - }, - - _back: function () { - if (this.model.isNew()) { - this.model.destroy(); - } - - require('Settings/Notifications/SchemaModal').open(this.notificationCollection); - }, - - _test: function () { - var testCommand = 'test{0}'.format(this.model.get('implementation')); - var properties = {}; - - _.each(this.model.get('fields'), function (field) { - properties[field.name] = field.value; - }); - - CommandController.Execute(testCommand, properties); - }, - - _onDownloadChanged: function () { - var checked = this.ui.onDownloadToggle.prop('checked'); - - if (checked) { - this.ui.onUpgradeSection.show(); - } - - else { - this.ui.onUpgradeSection.hide(); - } - } - }); - - return AsModelBoundView.call(model); - }); diff --git a/src/UI/Settings/Notifications/NotificationsItemView.js b/src/UI/Settings/Notifications/NotificationItemView.js similarity index 65% rename from src/UI/Settings/Notifications/NotificationsItemView.js rename to src/UI/Settings/Notifications/NotificationItemView.js index 3cde28bf9..c91a00adb 100644 --- a/src/UI/Settings/Notifications/NotificationsItemView.js +++ b/src/UI/Settings/Notifications/NotificationItemView.js @@ -3,8 +3,7 @@ define([ 'AppLayout', 'marionette', - 'Settings/Notifications/NotificationEditView' - + 'Settings/Notifications/Edit/NotificationEditView' ], function (AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ @@ -12,15 +11,15 @@ define([ tagName : 'li', events: { - 'click' : '_editNotification' + 'click' : '_edit' }, initialize: function () { this.listenTo(this.model, 'sync', this.render); }, - _editNotification: function () { - var view = new EditView({ model: this.model, notificationCollection: this.model.collection}); + _edit: function () { + var view = new EditView({ model: this.model, targetCollection: this.model.collection}); AppLayout.modalRegion.show(view); } }); diff --git a/src/UI/Settings/Notifications/Model.js b/src/UI/Settings/Notifications/NotificationModel.js similarity index 74% rename from src/UI/Settings/Notifications/Model.js rename to src/UI/Settings/Notifications/NotificationModel.js index b384b0c3d..9eb8e5552 100644 --- a/src/UI/Settings/Notifications/Model.js +++ b/src/UI/Settings/Notifications/NotificationModel.js @@ -1,6 +1,7 @@ 'use strict'; define([ - 'Settings/SettingsModelBase'], function (ModelBase) { + 'Settings/SettingsModelBase' +], function (ModelBase) { return ModelBase.extend({ successMessage: 'Notification Saved', diff --git a/src/UI/Settings/Notifications/SchemaModal.js b/src/UI/Settings/Notifications/SchemaModal.js deleted file mode 100644 index 923072ec4..000000000 --- a/src/UI/Settings/Notifications/SchemaModal.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; -define([ - 'AppLayout', - 'Settings/Notifications/Collection', - 'Settings/Notifications/AddView' -], function (AppLayout, NotificationCollection, AddSelectionNotificationView) { - return ({ - - open: function (collection) { - var schemaCollection = new NotificationCollection(); - var orginalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = orginalUrl; - - var view = new AddSelectionNotificationView({ collection: schemaCollection, notificationCollection: collection}); - AppLayout.modalRegion.show(view); - } - }); -}); diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index c8066d71b..eddb56ecc 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -12,12 +12,12 @@ define( 'Settings/MediaManagement/MediaManagementSettingsModel', 'Settings/Quality/QualityLayout', 'Settings/Indexers/IndexerLayout', - 'Settings/Indexers/Collection', + 'Settings/Indexers/IndexerCollection', 'Settings/Indexers/IndexerSettingsModel', 'Settings/DownloadClient/DownloadClientLayout', 'Settings/DownloadClient/DownloadClientSettingsModel', - 'Settings/Notifications/CollectionView', - 'Settings/Notifications/Collection', + 'Settings/Notifications/NotificationCollectionView', + 'Settings/Notifications/NotificationCollection', 'Settings/Metadata/MetadataLayout', 'Settings/General/GeneralView', 'Shared/LoadingView', @@ -93,7 +93,6 @@ define( this.mediaManagementSettings = new MediaManagementSettingsModel(); this.namingSettings = new NamingModel(); this.indexerSettings = new IndexerSettingsModel(); - this.indexerCollection = new IndexerCollection(); this.downloadClientSettings = new DownloadClientSettingsModel(); this.notificationCollection = new NotificationCollection(); this.generalSettings = new GeneralSettingsModel(); @@ -102,7 +101,6 @@ define( this.mediaManagementSettings.fetch(), this.namingSettings.fetch(), this.indexerSettings.fetch(), - this.indexerCollection.fetch(), this.downloadClientSettings.fetch(), this.notificationCollection.fetch(), this.generalSettings.fetch() @@ -112,7 +110,7 @@ define( self.loading.$el.hide(); self.mediaManagement.show(new MediaManagementLayout({ settings: self.mediaManagementSettings, namingSettings: self.namingSettings })); self.quality.show(new QualityLayout()); - self.indexers.show(new IndexerLayout({ settings: self.indexerSettings, indexersCollection: self.indexerCollection })); + self.indexers.show(new IndexerLayout({ model: self.indexerSettings })); self.downloadClient.show(new DownloadClientLayout({ model: self.downloadClientSettings })); self.notifications.show(new NotificationCollectionView({ collection: self.notificationCollection })); self.metadata.show(new MetadataLayout()); diff --git a/src/UI/Settings/ThingyAddCollectionView.js b/src/UI/Settings/ThingyAddCollectionView.js new file mode 100644 index 000000000..eb0a56e60 --- /dev/null +++ b/src/UI/Settings/ThingyAddCollectionView.js @@ -0,0 +1,18 @@ +'use strict'; + +define([ + 'marionette' +], function (Marionette) { + + return Marionette.CompositeView.extend({ + itemViewOptions : function () { + return { + targetCollection: this.targetCollection || this.options.targetCollection + }; + }, + + initialize: function (options) { + this.targetCollection = options.targetCollection; + } + }); +}); diff --git a/src/UI/Settings/ThingyHeaderGroupView.js b/src/UI/Settings/ThingyHeaderGroupView.js new file mode 100644 index 000000000..aec24a3a1 --- /dev/null +++ b/src/UI/Settings/ThingyHeaderGroupView.js @@ -0,0 +1,23 @@ +'use strict'; + +define([ + 'backbone', + 'marionette' +], function (Backbone, Marionette) { + + return Marionette.CompositeView.extend({ + itemViewContainer: '.item-list', + template: 'Settings/ThingyHeaderGroupViewTemplate', + tagName : 'div', + + itemViewOptions: function () { + return { + targetCollection: this.targetCollection || this.options.targetCollection + }; + }, + + initialize: function () { + this.collection = new Backbone.Collection(this.model.get('collection')); + } + }); +}); diff --git a/src/UI/Settings/ThingyHeaderGroupViewTemplate.html b/src/UI/Settings/ThingyHeaderGroupViewTemplate.html new file mode 100644 index 000000000..c3c233e52 --- /dev/null +++ b/src/UI/Settings/ThingyHeaderGroupViewTemplate.html @@ -0,0 +1,2 @@ +<legend>{{header}}</legend> +<ul class="item-list" /> \ No newline at end of file diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less index a9ccce814..69b8def1d 100644 --- a/src/UI/Settings/thingy.less +++ b/src/UI/Settings/thingy.less @@ -25,6 +25,11 @@ .add-thingies { text-align: center; + legend { + text-align: left; + text-transform: capitalize; + } + .items { list-style-type: none; margin: 0px; From b8c9f6d42e7d84e57417bfe60c3cb91fdb6039c4 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Fri, 23 May 2014 21:18:49 +0200 Subject: [PATCH 07/16] Fixed detection of failed unpack for nzbget proxy. --- .../Download/Clients/Nzbget/Nzbget.cs | 24 +++++++++++++------ .../Clients/Nzbget/NzbgetHistoryItem.cs | 4 ++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index f7032256a..828242bc9 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -115,15 +115,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } var historyItems = new List<DownloadClientItem>(); - var successStatues = new[] {"SUCCESS", "NONE"}; + var successStatus = new[] {"SUCCESS", "NONE"}; foreach (var item in history) { var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); - var status = successStatues.Contains(item.ParStatus) && - successStatues.Contains(item.ScriptStatus) - ? DownloadItemStatus.Completed - : DownloadItemStatus.Failed; var historyItem = new DownloadClientItem(); historyItem.DownloadClient = Definition.Name; @@ -132,8 +128,22 @@ namespace NzbDrone.Core.Download.Clients.Nzbget historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); historyItem.OutputPath = item.DestDir; historyItem.Category = item.Category; - historyItem.Message = String.Format("PAR Status: {0} - Script Status: {1}", item.ParStatus, item.ScriptStatus); - historyItem.Status = status; + historyItem.Message = String.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus); + historyItem.Status = DownloadItemStatus.Completed; + + if (!successStatus.Contains(item.ParStatus) || + !successStatus.Contains(item.UnpackStatus) || + !successStatus.Contains(item.MoveStatus) || + !successStatus.Contains(item.ScriptStatus) || + !successStatus.Contains(item.DeleteStatus) || + !successStatus.Contains(item.MarkStatus)) + { + historyItem.Status = DownloadItemStatus.Failed; + } + else if (item.MoveStatus != "SUCCESS") + { + historyItem.Status = DownloadItemStatus.Queued; + } historyItems.Add(historyItem); } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs index bce08e208..f02c483f3 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetHistoryItem.cs @@ -11,7 +11,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public UInt32 FileSizeLo { get; set; } public UInt32 FileSizeHi { get; set; } public String ParStatus { get; set; } + public String UnpackStatus { get; set; } + public String MoveStatus { get; set; } public String ScriptStatus { get; set; } + public String DeleteStatus { get; set; } + public String MarkStatus { get; set; } public String DestDir { get; set; } public List<NzbgetParameter> Parameters { get; set; } } From f304ad50d1859c685ca847024cf4875742a96d67 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 24 May 2014 12:48:55 +0200 Subject: [PATCH 08/16] New: Updated Nzbget Download Client proxy with time estimation for both download and post-processing stages. --- .../NzbgetTests/NzbgetFixture.cs | 26 +++- .../Checks/ImportMechanismCheckFixture.cs | 2 +- .../Clients/Nzbget/NzbGetQueueItem.cs | 4 +- .../Download/Clients/Nzbget/Nzbget.cs | 61 +++++++-- .../Clients/Nzbget/NzbgetBooleanResponse.cs | 10 -- .../Clients/Nzbget/NzbgetGlobalStatus.cs | 19 +++ .../Clients/Nzbget/NzbgetPostQueueItem.cs | 19 +++ .../Download/Clients/Nzbget/NzbgetProxy.cs | 30 ++++- ...zbgetListResponse.cs => NzbgetResponse.cs} | 6 +- .../Clients/Nzbget/VersionResponse.cs | 10 -- .../UsenetBlackhole/UsenetBlackhole.cs | 2 + .../Download/DownloadClientItem.cs | 21 +-- .../Download/DownloadTrackingService.cs | 121 +++++++++--------- src/NzbDrone.Core/NzbDrone.Core.csproj | 6 +- 14 files changed, 218 insertions(+), 119 deletions(-) delete mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetGlobalStatus.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPostQueueItem.cs rename src/NzbDrone.Core/Download/Clients/Nzbget/{NzbgetListResponse.cs => NzbgetResponse.cs} (57%) delete mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs index e29186110..0dffee397 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs @@ -52,7 +52,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests Name = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", DestDir = "somedirectory", Parameters = new List<NzbgetParameter> { new NzbgetParameter { Name = "drone", Value = "id" } }, - ParStatus = "Some Error" + ParStatus = "Some Error", + UnpackStatus = "NONE", + MoveStatus = "NONE", + ScriptStatus = "NONE", + DeleteStatus = "NONE", + MarkStatus = "NONE" }; _completed = new NzbgetHistoryItem @@ -63,8 +68,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests DestDir = "somedirectory", Parameters = new List<NzbgetParameter> { new NzbgetParameter { Name = "drone", Value = "id" } }, ParStatus = "SUCCESS", - ScriptStatus = "NONE" + UnpackStatus = "NONE", + MoveStatus = "SUCCESS", + ScriptStatus = "NONE", + DeleteStatus = "NONE", + MarkStatus = "NONE" }; + + Mocker.GetMock<INzbgetProxy>() + .Setup(s => s.GetGlobalStatus(It.IsAny<NzbgetSettings>())) + .Returns(new NzbgetGlobalStatus + { + DownloadRate = 7000000 + }); } protected void WithFailedDownload() @@ -93,6 +109,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests Mocker.GetMock<INzbgetProxy>() .Setup(s => s.GetQueue(It.IsAny<NzbgetSettings>())) .Returns(list); + + Mocker.GetMock<INzbgetProxy>() + .Setup(s => s.GetPostQueue(It.IsAny<NzbgetSettings>())) + .Returns(new List<NzbgetPostQueueItem>()); } protected virtual void WithHistory(NzbgetHistoryItem history) @@ -134,7 +154,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests [Test] public void paused_item_should_have_required_properties() { - _queued.PausedSizeLo = _queued.FileSizeLo; + _queued.PausedSizeLo = _queued.RemainingSizeLo; WithQueue(_queued); WithHistory(null); diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs index 236470dc1..4e0724e84 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Mocker.GetMock<IDownloadTrackingService>() .Setup(v => v.GetCompletedDownloads()) - .Returns(_completed.ToList()); + .Returns(_completed.ToArray()); } private void GivenDroneFactoryFolder(bool exists = false) diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs index 88b6c7b25..a951bd57d 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbGetQueueItem.cs @@ -8,7 +8,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public Int32 NzbId { get; set; } public Int32 FirstId { get; set; } public Int32 LastId { get; set; } - public string NzbName { get; set; } + public String NzbName { get; set; } public String Category { get; set; } public UInt32 FileSizeLo { get; set; } public UInt32 FileSizeHi { get; set; } @@ -16,6 +16,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public UInt32 RemainingSizeHi { get; set; } public UInt32 PausedSizeLo { get; set; } public UInt32 PausedSizeHi { get; set; } + public Int32 MinPriority { get; set; } + public Int32 MaxPriority { get; set; } public Int32 ActiveDownloads { get; set; } public List<NzbgetParameter> Parameters { get; set; } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 828242bc9..bef490cf3 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -56,11 +56,15 @@ namespace NzbDrone.Core.Download.Clients.Nzbget private IEnumerable<DownloadClientItem> GetQueue() { + NzbgetGlobalStatus globalStatus; List<NzbgetQueueItem> queue; + Dictionary<Int32, NzbgetPostQueueItem> postQueue; try { + globalStatus = _proxy.GetGlobalStatus(Settings); queue = _proxy.GetQueue(Settings); + postQueue = _proxy.GetPostQueue(Settings).ToDictionary(v => v.NzbId); } catch (DownloadClientException ex) { @@ -70,28 +74,57 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var queueItems = new List<DownloadClientItem>(); + Int64 totalRemainingSize = 0; + foreach (var item in queue) { + var postQueueItem = postQueue.GetValueOrDefault(item.NzbId); + + Int64 totalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); + Int64 pausedSize = MakeInt64(item.PausedSizeHi, item.PausedSizeLo); + Int64 remainingSize = MakeInt64(item.RemainingSizeHi, item.RemainingSizeLo); + var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); var queueItem = new DownloadClientItem(); queueItem.DownloadClientId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString(); queueItem.Title = item.NzbName; - queueItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); - queueItem.RemainingSize = MakeInt64(item.RemainingSizeHi, item.RemainingSizeLo); + queueItem.TotalSize = totalSize; queueItem.Category = item.Category; - if (queueItem.TotalSize == MakeInt64(item.PausedSizeHi, item.PausedSizeLo)) + if (postQueueItem != null) + { + queueItem.Status = DownloadItemStatus.Downloading; + queueItem.Message = postQueueItem.ProgressLabel; + + if (postQueueItem.StageProgress != 0) + { + queueItem.RemainingTime = TimeSpan.FromSeconds(postQueueItem.StageTimeSec * 1000 / postQueueItem.StageProgress - postQueueItem.StageTimeSec); + } + } + else if (globalStatus.DownloadPaused || remainingSize == pausedSize) { queueItem.Status = DownloadItemStatus.Paused; - } - else if (item.ActiveDownloads == 0 && queueItem.RemainingSize != 0) - { - queueItem.Status = DownloadItemStatus.Queued; + queueItem.RemainingSize = remainingSize; } else { - queueItem.Status = DownloadItemStatus.Downloading; + if (item.ActiveDownloads == 0 && remainingSize != 0) + { + queueItem.Status = DownloadItemStatus.Queued; + } + else + { + queueItem.Status = DownloadItemStatus.Downloading; + } + + queueItem.RemainingSize = remainingSize - pausedSize; + + if (globalStatus.DownloadRate != 0) + { + queueItem.RemainingTime = TimeSpan.FromSeconds((totalRemainingSize + queueItem.RemainingSize) / globalStatus.DownloadRate); + totalRemainingSize += queueItem.RemainingSize; + } } queueItems.Add(queueItem); @@ -130,13 +163,17 @@ namespace NzbDrone.Core.Download.Clients.Nzbget historyItem.Category = item.Category; historyItem.Message = String.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus); historyItem.Status = DownloadItemStatus.Completed; + historyItem.RemainingTime = TimeSpan.Zero; + + if (item.DeleteStatus == "MANUAL") + { + continue; + } if (!successStatus.Contains(item.ParStatus) || !successStatus.Contains(item.UnpackStatus) || !successStatus.Contains(item.MoveStatus) || - !successStatus.Contains(item.ScriptStatus) || - !successStatus.Contains(item.DeleteStatus) || - !successStatus.Contains(item.MarkStatus)) + !successStatus.Contains(item.ScriptStatus)) { historyItem.Status = DownloadItemStatus.Failed; } @@ -179,7 +216,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _proxy.GetVersion(Settings); } - private VersionResponse GetVersion(string host = null, int port = 0, string username = null, string password = null) + private String GetVersion(string host = null, int port = 0, string username = null, string password = null) { return _proxy.GetVersion(Settings); } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs deleted file mode 100644 index 6c536ba7d..000000000 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetBooleanResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download.Clients.Nzbget -{ - public class NzbgetBooleanResponse - { - public String Version { get; set; } - public Boolean Result { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetGlobalStatus.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetGlobalStatus.cs new file mode 100644 index 000000000..fcf5f6e46 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetGlobalStatus.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetGlobalStatus + { + public UInt32 RemainingSizeLo { get; set; } + public UInt32 RemainingSizeHi { get; set; } + public UInt32 DownloadedSizeLo { get; set; } + public UInt32 DownloadedSizeHi { get; set; } + public UInt32 DownloadRate { get; set; } + public UInt32 AverageDownloadRate { get; set; } + public UInt32 DownloadLimit { get; set; } + public Boolean DownloadPaused { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPostQueueItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPostQueueItem.cs new file mode 100644 index 000000000..450e07eab --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetPostQueueItem.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetPostQueueItem + { + public Int32 NzbId { get; set; } + public String NzbName { get; set; } + public String Stage { get; set; } + public String ProgressLabel { get; set; } + public Int32 FileProgress { get; set; } + public Int32 StageProgress { get; set; } + public Int32 TotalTimeSec { get; set; } + public Int32 StageTimeSec { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index c1c176e4b..5d6e34bb6 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -13,9 +13,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget public interface INzbgetProxy { string DownloadNzb(Stream nzb, string title, string category, int priority, NzbgetSettings settings); + NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings); List<NzbgetQueueItem> GetQueue(NzbgetSettings settings); + List<NzbgetPostQueueItem> GetPostQueue(NzbgetSettings settings); List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings); - VersionResponse GetVersion(NzbgetSettings settings); + String GetVersion(NzbgetSettings settings); void RemoveFromHistory(string id, NzbgetSettings settings); void RetryDownload(string id, NzbgetSettings settings); } @@ -34,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var parameters = new object[] { title, category, priority, false, Convert.ToBase64String(nzb.ToBytes()) }; var request = BuildRequest(new JsonRequest("append", parameters)); - var response = Json.Deserialize<NzbgetBooleanResponse>(ProcessRequest(request, settings)); + var response = Json.Deserialize<NzbgetResponse<Boolean>>(ProcessRequest(request, settings)); _logger.Debug("Queue Response: [{0}]", response.Result); if (!response.Result) @@ -61,25 +63,39 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return droneId; } + public NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings) + { + var request = BuildRequest(new JsonRequest("status")); + + return Json.Deserialize<NzbgetResponse<NzbgetGlobalStatus>>(ProcessRequest(request, settings)).Result; + } + public List<NzbgetQueueItem> GetQueue(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("listgroups")); - return Json.Deserialize<NzbgetListResponse<NzbgetQueueItem>>(ProcessRequest(request, settings)).QueueItems; + return Json.Deserialize<NzbgetResponse<List<NzbgetQueueItem>>>(ProcessRequest(request, settings)).Result; + } + + public List<NzbgetPostQueueItem> GetPostQueue(NzbgetSettings settings) + { + var request = BuildRequest(new JsonRequest("postqueue")); + + return Json.Deserialize<NzbgetResponse<List<NzbgetPostQueueItem>>>(ProcessRequest(request, settings)).Result; } public List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("history")); - return Json.Deserialize<NzbgetListResponse<NzbgetHistoryItem>>(ProcessRequest(request, settings)).QueueItems; + return Json.Deserialize<NzbgetResponse<List<NzbgetHistoryItem>>>(ProcessRequest(request, settings)).Result; } - public VersionResponse GetVersion(NzbgetSettings settings) + public String GetVersion(NzbgetSettings settings) { var request = BuildRequest(new JsonRequest("version")); - return Json.Deserialize<VersionResponse>(ProcessRequest(request, settings)); + return Json.Deserialize<NzbgetResponse<String>>(ProcessRequest(request, settings)).Version; } public void RemoveFromHistory(string id, NzbgetSettings settings) @@ -120,7 +136,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { var parameters = new object[] { command, offset, editText, id }; var request = BuildRequest(new JsonRequest("editqueue", parameters)); - var response = Json.Deserialize<NzbgetBooleanResponse>(ProcessRequest(request, settings)); + var response = Json.Deserialize<NzbgetResponse<Boolean>>(ProcessRequest(request, settings)); return response.Result; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetResponse.cs similarity index 57% rename from src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs rename to src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetResponse.cs index bb51dbcc6..d13f53fa5 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetListResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetResponse.cs @@ -4,11 +4,11 @@ using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Nzbget { - public class NzbgetListResponse<T> + public class NzbgetResponse<T> { public String Version { get; set; } - [JsonProperty(PropertyName = "result")] - public List<T> QueueItems { get; set; } + public T Result { get; set; } + } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs deleted file mode 100644 index 780fd90ad..000000000 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/VersionResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace NzbDrone.Core.Download.Clients.Nzbget -{ - public class VersionResponse - { - public String Version { get; set; } - public String Result { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index d534429ad..ec1865c2f 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -83,6 +83,8 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole else { historyItem.Status = DownloadItemStatus.Completed; + + historyItem.RemainingTime = TimeSpan.Zero; } historyItem.RemoteEpisode = GetRemoteEpisode(historyItem.Title); diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index e8b8b6fc5..6cc2dbb2f 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -8,22 +8,23 @@ namespace NzbDrone.Core.Download { public class DownloadClientItem { - public string DownloadClient { get; set; } - public string DownloadClientId { get; set; } - public string Category { get; set; } - public string Title { get; set; } + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } + public String Category { get; set; } + public String Title { get; set; } - public long TotalSize { get; set; } - public long RemainingSize { get; set; } + public Int64 TotalSize { get; set; } + public Int64 RemainingSize { get; set; } public TimeSpan DownloadTime { get; set; } public TimeSpan RemainingTime { get; set; } - public string OutputPath { get; set; } - public string Message { get; set; } + public String OutputPath { get; set; } + public String Message { get; set; } public DownloadItemStatus Status { get; set; } - public bool IsEncrypted { get; set; } - public bool IsReadOnly { get; set; } + public Boolean IsEncrypted { get; set; } + public Boolean IsReadOnly { get; set; } + public RemoteEpisode RemoteEpisode { get; set; } } } diff --git a/src/NzbDrone.Core/Download/DownloadTrackingService.cs b/src/NzbDrone.Core/Download/DownloadTrackingService.cs index d06017125..78bb5ec5f 100644 --- a/src/NzbDrone.Core/Download/DownloadTrackingService.cs +++ b/src/NzbDrone.Core/Download/DownloadTrackingService.cs @@ -15,12 +15,12 @@ namespace NzbDrone.Core.Download { public interface IDownloadTrackingService { - List<TrackedDownload> GetTrackedDownloads(); - List<TrackedDownload> GetCompletedDownloads(); - List<TrackedDownload> GetQueuedDownloads(); + TrackedDownload[] GetTrackedDownloads(); + TrackedDownload[] GetCompletedDownloads(); + TrackedDownload[] GetQueuedDownloads(); } - public class DownloadTrackingService : IDownloadTrackingService, IExecute<CheckForFinishedDownloadCommand>, IHandle<ApplicationStartedEvent> + public class DownloadTrackingService : IDownloadTrackingService, IExecute<CheckForFinishedDownloadCommand>, IHandle<ApplicationStartedEvent>, IHandle<EpisodeGrabbedEvent> { private readonly IProvideDownloadClient _downloadClientProvider; private readonly IHistoryService _historyService; @@ -30,8 +30,7 @@ namespace NzbDrone.Core.Download private readonly ICompletedDownloadService _completedDownloadService; private readonly Logger _logger; - private readonly ICached<TrackedDownload> _trackedDownloads; - private readonly ICached<List<TrackedDownload>> _queuedDownloads; + private readonly ICached<TrackedDownload[]> _trackedDownloadCache; public static string DOWNLOAD_CLIENT = "downloadClient"; public static string DOWNLOAD_CLIENT_ID = "downloadClientId"; @@ -53,108 +52,105 @@ namespace NzbDrone.Core.Download _completedDownloadService = completedDownloadService; _logger = logger; - _trackedDownloads = cacheManager.GetCache<TrackedDownload>(GetType()); - _queuedDownloads = cacheManager.GetCache<List<TrackedDownload>>(GetType(), "queued"); + _trackedDownloadCache = cacheManager.GetCache<TrackedDownload[]>(GetType()); } - public List<TrackedDownload> GetTrackedDownloads() + public TrackedDownload[] GetTrackedDownloads() { - return _trackedDownloads.Values.ToList(); + return _trackedDownloadCache.Get("tracked", () => new TrackedDownload[0]); } - public List<TrackedDownload> GetCompletedDownloads() + public TrackedDownload[] GetCompletedDownloads() { - return _trackedDownloads.Values.Where(v => v.State == TrackedDownloadState.Downloading && v.DownloadItem.Status == DownloadItemStatus.Completed).ToList(); + return GetTrackedDownloads() + .Where(v => v.State == TrackedDownloadState.Downloading && v.DownloadItem.Status == DownloadItemStatus.Completed) + .ToArray(); } - public List<TrackedDownload> GetQueuedDownloads() + public TrackedDownload[] GetQueuedDownloads() { - return _queuedDownloads.Get("queued", () => + return _trackedDownloadCache.Get("queued", () => { UpdateTrackedDownloads(); - var enabledFailedDownloadHandling = _configService.EnableFailedDownloadHandling; - var enabledCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling; - - return _trackedDownloads.Values - .Where(v => v.State == TrackedDownloadState.Downloading) - .Where(v => - v.DownloadItem.Status == DownloadItemStatus.Queued || - v.DownloadItem.Status == DownloadItemStatus.Paused || - v.DownloadItem.Status == DownloadItemStatus.Downloading || - v.DownloadItem.Status == DownloadItemStatus.Failed && enabledFailedDownloadHandling || - v.DownloadItem.Status == DownloadItemStatus.Completed && enabledCompletedDownloadHandling) - .ToList(); + return FilterQueuedDownloads(GetTrackedDownloads()); }, TimeSpan.FromSeconds(5.0)); } - private TrackedDownload GetTrackedDownload(IDownloadClient downloadClient, DownloadClientItem queueItem) + private TrackedDownload[] FilterQueuedDownloads(IEnumerable<TrackedDownload> trackedDownloads) { - var id = String.Format("{0}-{1}", downloadClient.Definition.Id, queueItem.DownloadClientId); - var trackedDownload = _trackedDownloads.Get(id, () => new TrackedDownload - { - TrackingId = id, - DownloadClient = downloadClient.Definition.Id, - StartedTracking = DateTime.UtcNow, - State = TrackedDownloadState.Unknown - }); + var enabledFailedDownloadHandling = _configService.EnableFailedDownloadHandling; + var enabledCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling; - trackedDownload.DownloadItem = queueItem; - - return trackedDownload; + return trackedDownloads + .Where(v => v.State == TrackedDownloadState.Downloading) + .Where(v => + v.DownloadItem.Status == DownloadItemStatus.Queued || + v.DownloadItem.Status == DownloadItemStatus.Paused || + v.DownloadItem.Status == DownloadItemStatus.Downloading || + v.DownloadItem.Status == DownloadItemStatus.Failed && enabledFailedDownloadHandling || + v.DownloadItem.Status == DownloadItemStatus.Completed && enabledCompletedDownloadHandling) + .ToArray(); } - + private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId) { return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID))) .ToList(); } - private Boolean UpdateTrackedDownloads() { var downloadClients = _downloadClientProvider.GetDownloadClients(); - var oldTrackedDownloads = new HashSet<TrackedDownload>(_trackedDownloads.Values); - var newTrackedDownloads = new HashSet<TrackedDownload>(); + var oldTrackedDownloads = GetTrackedDownloads().ToDictionary(v => v.TrackingId); + var newTrackedDownloads = new List<TrackedDownload>(); var stateChanged = false; foreach (var downloadClient in downloadClients) { - var downloadClientHistory = downloadClient.GetItems().Select(v => GetTrackedDownload(downloadClient, v)).ToList(); - foreach (var trackedDownload in downloadClientHistory) + var downloadClientHistory = downloadClient.GetItems().ToList(); + foreach (var downloadItem in downloadClientHistory) { - if (!oldTrackedDownloads.Contains(trackedDownload)) + var trackingId = String.Format("{0}-{1}", downloadClient.Definition.Id, downloadItem.DownloadClientId); + TrackedDownload trackedDownload; + + if (!oldTrackedDownloads.TryGetValue(trackingId, out trackedDownload)) { - _logger.Trace("Started tracking download from history: {0}", trackedDownload.TrackingId); + trackedDownload = new TrackedDownload + { + TrackingId = trackingId, + DownloadClient = downloadClient.Definition.Id, + StartedTracking = DateTime.UtcNow, + State = TrackedDownloadState.Unknown + }; + + _logger.Trace("Started tracking download from history: {0}: {1}", trackedDownload.TrackingId, downloadItem.Title); stateChanged = true; } + trackedDownload.DownloadItem = downloadItem; + newTrackedDownloads.Add(trackedDownload); } } - foreach (var item in oldTrackedDownloads.Except(newTrackedDownloads)) + foreach (var downloadItem in oldTrackedDownloads.Values.Except(newTrackedDownloads)) { - if (item.State != TrackedDownloadState.Removed) + if (downloadItem.State != TrackedDownloadState.Removed) { - item.State = TrackedDownloadState.Removed; + downloadItem.State = TrackedDownloadState.Removed; stateChanged = true; - _logger.Debug("Item removed from download client by user: {0}", item.TrackingId); + _logger.Debug("Item removed from download client by user: {0}: {1}", downloadItem.TrackingId, downloadItem.DownloadItem.Title); } + + _logger.Trace("Stopped tracking download: {0}: {1}", downloadItem.TrackingId, downloadItem.DownloadItem.Title); } - foreach (var item in newTrackedDownloads.Union(oldTrackedDownloads).Where(v => v.State == TrackedDownloadState.Removed)) - { - _trackedDownloads.Remove(item.TrackingId); - - _logger.Trace("Stopped tracking download: {0}", item.TrackingId); - } - - _queuedDownloads.Clear(); + _trackedDownloadCache.Set("tracked", newTrackedDownloads.ToArray()); return stateChanged; } @@ -168,7 +164,7 @@ namespace NzbDrone.Core.Download var stateChanged = UpdateTrackedDownloads(); var downloadClients = _downloadClientProvider.GetDownloadClients(); - var trackedDownloads = _trackedDownloads.Values.ToArray(); + var trackedDownloads = GetTrackedDownloads(); foreach (var trackedDownload in trackedDownloads) { @@ -190,6 +186,8 @@ namespace NzbDrone.Core.Download } } + _trackedDownloadCache.Set("queued", FilterQueuedDownloads(trackedDownloads), TimeSpan.FromSeconds(5.0)); + if (stateChanged) { _eventAggregator.PublishEvent(new UpdateQueueEvent()); @@ -205,5 +203,10 @@ namespace NzbDrone.Core.Download { ProcessTrackedDownloads(); } + + public void Handle(EpisodeGrabbedEvent message) + { + ProcessTrackedDownloads(); + } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 38edae8a0..b46015936 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -239,6 +239,8 @@ <Compile Include="DecisionEngine\Specifications\RssSync\HistorySpecification.cs" /> <Compile Include="DiskSpace\DiskSpace.cs" /> <Compile Include="DiskSpace\DiskSpaceService.cs" /> + <Compile Include="Download\Clients\Nzbget\NzbgetGlobalStatus.cs" /> + <Compile Include="Download\Clients\Nzbget\NzbgetPostQueueItem.cs" /> <Compile Include="Download\Clients\Sabnzbd\SabnzbdDownloadStatus.cs" /> <Compile Include="Download\Clients\UsenetBlackhole\UsenetBlackhole.cs" /> <Compile Include="Download\Clients\UsenetBlackhole\TestUsenetBlackholeCommand.cs" /> @@ -525,13 +527,11 @@ <Compile Include="Instrumentation\LogService.cs" /> <Compile Include="Instrumentation\DatabaseTarget.cs" /> <Compile Include="MediaFiles\MediaInfo\MediaInfoModel.cs" /> - <Compile Include="Download\Clients\Nzbget\NzbgetBooleanResponse.cs" /> <Compile Include="Download\Clients\Nzbget\ErrorModel.cs" /> <Compile Include="Download\Clients\Nzbget\JsonError.cs" /> - <Compile Include="Download\Clients\Nzbget\NzbgetListResponse.cs" /> + <Compile Include="Download\Clients\Nzbget\NzbgetResponse.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetQueueItem.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetPriority.cs" /> - <Compile Include="Download\Clients\Nzbget\VersionResponse.cs" /> <Compile Include="Organizer\NamingConfig.cs" /> <Compile Include="Parser\Language.cs" /> <Compile Include="Parser\Model\LocalEpisode.cs" /> From cb0f7792f2f7f4b238e903b9e130f7992e3754e2 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 24 May 2014 12:50:34 +0200 Subject: [PATCH 09/16] Queue UI no longer shows unknown ETAs as 0:00:00. --- src/NzbDrone.Api/Queue/QueueResource.cs | 2 +- .../Download/DownloadClientItem.cs | 4 ++-- src/NzbDrone.Core/Queue/Queue.cs | 2 +- src/NzbDrone.Core/Queue/QueueService.cs | 5 ++++- src/UI/Cells/cells.less | 1 + src/UI/History/Queue/TimeleftCell.js | 17 +++++++++++------ 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index 0adfe1e79..5d8cc2642 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Api.Queue public Decimal Size { get; set; } public String Title { get; set; } public Decimal Sizeleft { get; set; } - public TimeSpan Timeleft { get; set; } + public TimeSpan? Timeleft { get; set; } public String Status { get; set; } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 6cc2dbb2f..8b9365c9c 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -15,8 +15,8 @@ namespace NzbDrone.Core.Download public Int64 TotalSize { get; set; } public Int64 RemainingSize { get; set; } - public TimeSpan DownloadTime { get; set; } - public TimeSpan RemainingTime { get; set; } + public TimeSpan? DownloadTime { get; set; } + public TimeSpan? RemainingTime { get; set; } public String OutputPath { get; set; } public String Message { get; set; } diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index a1b2ff7f3..c8cde78d4 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -14,7 +14,7 @@ namespace NzbDrone.Core.Queue public Decimal Size { get; set; } public String Title { get; set; } public Decimal Sizeleft { get; set; } - public TimeSpan Timeleft { get; set; } + public TimeSpan? Timeleft { get; set; } public String Status { get; set; } public RemoteEpisode RemoteEpisode { get; set; } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 018c9f01f..ead4f083c 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -24,7 +24,10 @@ namespace NzbDrone.Core.Queue public List<Queue> GetQueue() { - var queueItems = _downloadTrackingService.GetQueuedDownloads().Select(v => v.DownloadItem).ToList(); + var queueItems = _downloadTrackingService.GetQueuedDownloads() + .Select(v => v.DownloadItem) + .OrderBy(v => v.RemainingTime) + .ToList(); return MapQueue(queueItems); } diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 4105f32a1..42442e6af 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -137,6 +137,7 @@ td.episode-status-cell, td.quality-cell { .timeleft-cell { cursor : default; width : 80px; + text-align: center; } .queue-status-cell { diff --git a/src/UI/History/Queue/TimeleftCell.js b/src/UI/History/Queue/TimeleftCell.js index 9ead67f63..813014001 100644 --- a/src/UI/History/Queue/TimeleftCell.js +++ b/src/UI/History/Queue/TimeleftCell.js @@ -2,8 +2,9 @@ define( [ - 'Cells/NzbDroneCell' - ], function (NzbDroneCell) { + 'Cells/NzbDroneCell', + 'filesize' + ], function (NzbDroneCell, fileSize) { return NzbDroneCell.extend({ className: 'timeleft-cell', @@ -14,11 +15,15 @@ define( if (this.cellValue) { var timeleft = this.cellValue.get('timeleft'); - var size = this.cellValue.get('size'); - var sizeleft = this.cellValue.get('sizeleft'); + var totalSize = fileSize(this.cellValue.get('size'), 1, false); + var remainingSize = fileSize(this.cellValue.get('sizeleft'), 1, false); - this.$el.html(timeleft); - this.$el.attr('title', '{0} MB / {1} MB'.format(sizeleft, size)); + if (timeleft === undefined) { + this.$el.html("-"); + } else { + this.$el.html(timeleft); + } + this.$el.attr('title', '{0} / {1}'.format(remainingSize, totalSize)); } return this; From 67dcfad5dc3c18528e5dc16bc9d3f1b35a7cd1a5 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 24 May 2014 21:02:25 +0200 Subject: [PATCH 10/16] Sabnzbd Download Client now reports paused items as having unknown remaining time. --- src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index af3af9a37..998f8876b 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -86,6 +86,8 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (sabQueue.Paused || sabQueueItem.Status == SabnzbdDownloadStatus.Paused) { queueItem.Status = DownloadItemStatus.Paused; + + queueItem.RemainingTime = null; } else if (sabQueueItem.Status == SabnzbdDownloadStatus.Queued || sabQueueItem.Status == SabnzbdDownloadStatus.Grabbing) { From ab154d924e2cf365f8534f4224cfb8c9c2d42ae5 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sun, 25 May 2014 23:58:41 +0200 Subject: [PATCH 11/16] Added db migration to deal with the settings changes. --- .../PneumaticProviderFixture.cs | 4 +- .../051_rename_download_client_settings.cs | 88 +++++++++++++++++++ .../Download/Clients/Pneumatic/Pneumatic.cs | 8 +- .../Clients/Pneumatic/PneumaticSettings.cs | 14 +-- .../UsenetBlackhole/UsenetBlackhole.cs | 4 +- .../UsenetBlackholeSettings.cs | 1 + src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 7 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs index bae8867fe..a0e33f1bd 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PneumaticProviderFixture.cs @@ -45,9 +45,9 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests _remoteEpisode.ParsedEpisodeInfo.FullSeason = false; Subject.Definition = new DownloadClientDefinition(); - Subject.Definition.Settings = new FolderSettings + Subject.Definition.Settings = new PneumaticSettings { - Folder = _pneumaticFolder + NzbFolder = _pneumaticFolder }; } diff --git a/src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs b/src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs new file mode 100644 index 000000000..ccc3f3527 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs @@ -0,0 +1,88 @@ +using NzbDrone.Core.Datastore.Migration.Framework; +using FluentMigrator; +using System.Data; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Download.Clients.UsenetBlackhole; +using Newtonsoft.Json; +using System; +using NzbDrone.Core.Download.Clients.Pneumatic; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(51)] + public class rename_download_client_settings : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertFolderSettings); + } + + private void ConvertFolderSettings(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand downloadClientsCmd = conn.CreateCommand()) + { + downloadClientsCmd.Transaction = tran; + downloadClientsCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; + var downloadedEpisodesFolder = downloadClientsCmd.ExecuteScalar() as String; + + downloadClientsCmd.Transaction = tran; + downloadClientsCmd.CommandText = @"SELECT Id, Implementation, Settings, ConfigContract FROM DownloadClients WHERE ConfigContract = 'FolderSettings'"; + using (IDataReader downloadClientReader = downloadClientsCmd.ExecuteReader()) + { + while (downloadClientReader.Read()) + { + var id = downloadClientReader.GetInt32(0); + var implementation = downloadClientReader.GetString(1); + var settings = downloadClientReader.GetString(2); + var configContract = downloadClientReader.GetString(3); + + var settingsJson = JsonConvert.DeserializeObject(settings) as Newtonsoft.Json.Linq.JObject; + + if (implementation == "Blackhole") + { + var newSettings = new + { + NzbFolder = settingsJson.Value<String>("folder"), + WatchFolder = downloadedEpisodesFolder + }.ToJson(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE DownloadClients SET Implementation = ?, Settings = ?, ConfigContract = ? WHERE Id = ?"; + updateCmd.AddParameter("UsenetBlackhole"); + updateCmd.AddParameter(newSettings); + updateCmd.AddParameter("UsenetBlackholeSettings"); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + else if (implementation == "Pneumatic") + { + var newSettings = new + { + NzbFolder = settingsJson.Value<String>("folder") + }.ToJson(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE DownloadClients SET Settings = ?, ConfigContract = ? WHERE Id = ?"; + updateCmd.AddParameter(newSettings); + updateCmd.AddParameter("PneumaticSettings"); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + else + { + throw new NotSupportedException(); + } + } + } + } +} + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index c62a6bca0..7e3bb56de 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -15,7 +15,7 @@ using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.Clients.Pneumatic { - public class Pneumatic : DownloadClientBase<FolderSettings>, IExecute<TestPneumaticCommand> + public class Pneumatic : DownloadClientBase<PneumaticSettings>, IExecute<TestPneumaticCommand> { private readonly IConfigService _configService; private readonly IHttpProvider _httpProvider; @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic title = FileNameBuilder.CleanFilename(title); //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) - var filename = Path.Combine(Settings.Folder, title + ".nzb"); + var filename = Path.Combine(Settings.NzbFolder, title + ".nzb"); logger.Debug("Downloading NZB from: {0} to: {1}", url, filename); _httpProvider.DownloadFile(url, filename); @@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { get { - return !string.IsNullOrWhiteSpace(Settings.Folder); + return !string.IsNullOrWhiteSpace(Settings.NzbFolder); } } @@ -94,7 +94,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic public override void Test() { - PerformTest(Settings.Folder); + PerformTest(Settings.NzbFolder); } private void PerformTest(string folder) diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs index 00ebb3b93..29b414c26 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/PneumaticSettings.cs @@ -8,21 +8,21 @@ using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Core.Download.Clients.Pneumatic { - public class FolderSettingsValidator : AbstractValidator<FolderSettings> + public class PneumaticSettingsValidator : AbstractValidator<PneumaticSettings> { - public FolderSettingsValidator() + public PneumaticSettingsValidator() { //Todo: Validate that the path actually exists - RuleFor(c => c.Folder).IsValidPath(); + RuleFor(c => c.NzbFolder).IsValidPath(); } } - public class FolderSettings : IProviderConfig + public class PneumaticSettings : IProviderConfig { - private static readonly FolderSettingsValidator Validator = new FolderSettingsValidator(); + private static readonly PneumaticSettingsValidator Validator = new PneumaticSettingsValidator(); - [FieldDefinition(0, Label = "Folder", Type = FieldType.Path)] - public String Folder { get; set; } + [FieldDefinition(0, Label = "Nzb Folder", Type = FieldType.Path)] + public String NzbFolder { get; set; } public ValidationResult Validate() { diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index ec1865c2f..260d99198 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -149,8 +149,8 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole public void Execute(TestUsenetBlackholeCommand message) { - PerformTest(Settings.NzbFolder); - PerformTest(Settings.WatchFolder); + PerformTest(message.NzbFolder); + PerformTest(message.WatchFolder); } } } diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs index ae518f1ec..dd5371af8 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackholeSettings.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole { //Todo: Validate that the path actually exists RuleFor(c => c.NzbFolder).IsValidPath(); + RuleFor(c => c.WatchFolder).IsValidPath(); } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b46015936..4549f34a1 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -197,6 +197,7 @@ <Compile Include="Datastore\Migration\048_add_title_to_scenemappings.cs" /> <Compile Include="Datastore\Migration\049_fix_dognzb_url.cs" /> <Compile Include="Datastore\Migration\050_add_hash_to_metadata_files.cs" /> + <Compile Include="Datastore\Migration\051_rename_download_client_settings.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" /> From c6e33bc463db8b970ff0979fc3c4e9b8e1cdeb72 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Mon, 26 May 2014 08:31:00 +0200 Subject: [PATCH 12/16] Processed comments. --- src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs | 6 +++--- src/UI/History/Queue/TimeleftCell.js | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index bef490cf3..b4f67bec3 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -80,9 +80,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { var postQueueItem = postQueue.GetValueOrDefault(item.NzbId); - Int64 totalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); - Int64 pausedSize = MakeInt64(item.PausedSizeHi, item.PausedSizeLo); - Int64 remainingSize = MakeInt64(item.RemainingSizeHi, item.RemainingSizeLo); + var totalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); + var pausedSize = MakeInt64(item.PausedSizeHi, item.PausedSizeLo); + var remainingSize = MakeInt64(item.RemainingSizeHi, item.RemainingSizeLo); var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone"); diff --git a/src/UI/History/Queue/TimeleftCell.js b/src/UI/History/Queue/TimeleftCell.js index 813014001..a4d6e4544 100644 --- a/src/UI/History/Queue/TimeleftCell.js +++ b/src/UI/History/Queue/TimeleftCell.js @@ -20,7 +20,8 @@ define( if (timeleft === undefined) { this.$el.html("-"); - } else { + } + else { this.$el.html(timeleft); } this.$el.attr('title', '{0} / {1}'.format(remainingSize, totalSize)); From 1b96a43037f7e01604790e0d639e3a453464dad0 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Tue, 27 May 2014 23:04:13 +0200 Subject: [PATCH 13/16] Fixed performance issues with the QueueModule and limited the number of items the Download Client will fetch as history. --- src/NzbDrone.Api/Queue/QueueResource.cs | 6 ++++-- .../DownloadClientTests/DownloadClientFixtureBase.cs | 5 +++++ src/NzbDrone.Core/Configuration/ConfigService.cs | 7 +++++++ src/NzbDrone.Core/Configuration/IConfigService.cs | 1 + src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs | 6 ++++-- src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs | 8 +++----- src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs | 8 +++++--- .../Download/Clients/UsenetBlackhole/UsenetBlackhole.cs | 6 ++++-- src/NzbDrone.Core/Download/DownloadClientBase.cs | 5 ++++- 9 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index 5d8cc2642..d47dbbd8f 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -2,13 +2,15 @@ using NzbDrone.Api.REST; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; +using NzbDrone.Api.Series; +using NzbDrone.Api.Episodes; namespace NzbDrone.Api.Queue { public class QueueResource : RestResource { - public Core.Tv.Series Series { get; set; } - public Episode Episode { get; set; } + public SeriesResource Series { get; set; } + public EpisodeResource Episode { get; set; } public QualityModel Quality { get; set; } public Decimal Size { get; set; } public String Title { get; set; } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index 30ed654bb..2ba721adb 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; using NzbDrone.Core.Download; +using NzbDrone.Core.Configuration; namespace NzbDrone.Core.Test.Download.DownloadClientTests { @@ -22,6 +23,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests [SetUp] public void SetupBase() { + Mocker.GetMock<IConfigService>() + .SetupGet(s => s.DownloadClientHistoryLimit) + .Returns(30); + Mocker.GetMock<IParsingService>() .Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<int>(), null)) .Returns(CreateRemoteEpisode()); diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index af776e904..e1462a561 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -205,6 +205,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("DownloadedEpisodesScanInterval", value); } } + public Int32 DownloadClientHistoryLimit + { + get { return GetValueInt("DownloadClientHistoryLimit", 30); } + + set { SetValue("DownloadClientHistoryLimit", value); } + } + public Boolean SkipFreeSpaceCheckWhenImporting { get { return GetValueBoolean("SkipFreeSpaceCheckWhenImporting", false); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index a295a1ac3..f471762c5 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Configuration String DownloadedEpisodesFolder { get; set; } String DownloadClientWorkingFolders { get; set; } Int32 DownloadedEpisodesScanInterval { get; set; } + Int32 DownloadClientHistoryLimit { get; set; } //Completed/Failed Download Handling (Download client) Boolean EnableCompletedDownloadHandling { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index b4f67bec3..2fb35f2a0 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; @@ -18,10 +19,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget private readonly IHttpProvider _httpProvider; public Nzbget(INzbgetProxy proxy, + IConfigService configService, IParsingService parsingService, IHttpProvider httpProvider, Logger logger) - : base(parsingService, logger) + : base(configService, parsingService, logger) { _proxy = proxy; _httpProvider = httpProvider; @@ -139,7 +141,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget try { - history = _proxy.GetHistory(Settings); + history = _proxy.GetHistory(Settings).Take(_configService.DownloadClientHistoryLimit).ToList(); } catch (DownloadClientException ex) { diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 7e3bb56de..2753d9056 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -17,20 +17,18 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { public class Pneumatic : DownloadClientBase<PneumaticSettings>, IExecute<TestPneumaticCommand> { - private readonly IConfigService _configService; private readonly IHttpProvider _httpProvider; private readonly IDiskProvider _diskProvider; private static readonly Logger logger = NzbDroneLogger.GetLogger(); - public Pneumatic(IConfigService configService, - IHttpProvider httpProvider, + public Pneumatic(IHttpProvider httpProvider, IDiskProvider diskProvider, + IConfigService configService, IParsingService parsingService, Logger logger) - : base(parsingService, logger) + : base(configService, parsingService, logger) { - _configService = configService; _httpProvider = httpProvider; _diskProvider = diskProvider; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 998f8876b..d97914a45 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; @@ -18,10 +19,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd private readonly ISabnzbdProxy _proxy; public Sabnzbd(IHttpProvider httpProvider, - IParsingService parsingService, ISabnzbdProxy proxy, + IConfigService configService, + IParsingService parsingService, Logger logger) - : base(parsingService, logger) + : base(configService, parsingService, logger) { _httpProvider = httpProvider; _proxy = proxy; @@ -116,7 +118,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd try { - sabHistory = _proxy.GetHistory(0, 0, Settings); + sabHistory = _proxy.GetHistory(0, _configService.DownloadClientHistoryLimit, Settings); } catch (DownloadClientException ex) { diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index 260d99198..036be4338 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Organizer; @@ -23,10 +24,11 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole public UsenetBlackhole(IDiskProvider diskProvider, IDiskScanService diskScanService, - IParsingService parsingService, IHttpProvider httpProvider, + IConfigService configService, + IParsingService parsingService, Logger logger) - : base(parsingService, logger) + : base(configService, parsingService, logger) { _diskProvider = diskProvider; _diskScanService = diskScanService; diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 410286aee..453dcc479 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Configuration; using NLog; namespace NzbDrone.Core.Download @@ -12,6 +13,7 @@ namespace NzbDrone.Core.Download public abstract class DownloadClientBase<TSettings> : IDownloadClient where TSettings : IProviderConfig, new() { + protected readonly IConfigService _configService; private readonly IParsingService _parsingService; protected readonly Logger _logger; @@ -41,8 +43,9 @@ namespace NzbDrone.Core.Download } } - protected DownloadClientBase(IParsingService parsingService, Logger logger) + protected DownloadClientBase(IConfigService configService, IParsingService parsingService, Logger logger) { + _configService = configService; _parsingService = parsingService; _logger = logger; } From 1a63b1caba85dfd0cf1d09aff177bc4355edc2f0 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Thu, 29 May 2014 00:48:37 +0200 Subject: [PATCH 14/16] Updated migration to attempt associate old grabbed & imported events and associate drone factory imports during CompletedDownloadHandling. --- .../CompletedDownloadServiceFixture.cs | 61 ++++- .../Migration/051_download_client_import.cs | 224 ++++++++++++++++++ .../051_rename_download_client_settings.cs | 88 ------- .../Framework/NzbDroneMigrationBase.cs | 4 +- .../Download/CompletedDownloadService.cs | 33 ++- src/NzbDrone.Core/History/HistoryService.cs | 8 + .../DownloadedEpisodesImportService.cs | 12 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 2 +- 8 files changed, 331 insertions(+), 101 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs delete mode 100644 src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs index df1218139..0d7b6d1e9 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -13,6 +13,8 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Test.Common; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Test.Download { @@ -28,6 +30,10 @@ namespace NzbDrone.Core.Test.Download .All() .With(h => h.Status = DownloadItemStatus.Completed) .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .With(h => h.RemoteEpisode = new RemoteEpisode + { + Episodes = new List<Episode> { new Episode { Id = 1 } } + }) .Build() .ToList(); @@ -255,7 +261,9 @@ namespace NzbDrone.Core.Test.Download history.First().Data.Add("downloadClient", "SabnzbdClient"); history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); - + + Subject.Execute(new CheckForFinishedDownloadCommand()); + VerifyNoImports(); } @@ -278,9 +286,60 @@ namespace NzbDrone.Core.Test.Download history.First().Data.Add("downloadClient", "SabnzbdClient"); history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + Subject.Execute(new CheckForFinishedDownloadCommand()); + VerifyNoImports(); } + [Test] + public void should_process_as_already_imported_if_drone_factory_import_history_exists() + { + GivenCompletedDownloadClientHistory(false); + + _completed.Clear(); + _completed.AddRange(Builder<DownloadClientItem>.CreateListOfSize(2) + .All() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .With(h => h.RemoteEpisode = new RemoteEpisode + { + Episodes = new List<Episode> { new Episode { Id = 1 } } + }) + .Build()); + + var grabbedHistory = Builder<History.History>.CreateListOfSize(2) + .All() + .With(d => d.Data["downloadClient"] = "SabnzbdClient") + .TheFirst(1) + .With(d => d.Data["downloadClientId"] = _completed.First().DownloadClientId) + .With(d => d.SourceTitle = "Droned.S01E01.720p-LAZY") + .TheLast(1) + .With(d => d.Data["downloadClientId"] = _completed.Last().DownloadClientId) + .With(d => d.SourceTitle = "Droned.S01E01.Proper.720p-LAZY") + .Build() + .ToList(); + + var importedHistory = Builder<History.History>.CreateListOfSize(2) + .All() + .With(d => d.EpisodeId = 1) + .TheFirst(1) + .With(d => d.Data["droppedPath"] = @"C:\mydownload\Droned.S01E01.720p-LAZY\lzy-dr101.mkv".AsOsAgnostic()) + .TheLast(1) + .With(d => d.Data["droppedPath"] = @"C:\mydownload\Droned.S01E01.Proper.720p-LAZY\lzy-dr101.mkv".AsOsAgnostic()) + .Build() + .ToList(); + + GivenGrabbedHistory(grabbedHistory); + GivenImportedHistory(importedHistory); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + VerifyNoImports(); + + Mocker.GetMock<IHistoryService>() + .Verify(v => v.UpdateHistoryData(It.IsAny<int>(), It.IsAny<Dictionary<String, String>>()), Times.Exactly(2)); + } + [Test] public void should_not_remove_if_config_disabled() { diff --git a/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs b/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs new file mode 100644 index 000000000..f801eda79 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs @@ -0,0 +1,224 @@ +using System; +using System.Data; +using System.Linq; +using System.Collections.Generic; +using FluentMigrator; +using Newtonsoft.Json; +using NzbDrone.Common; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.IO; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(51)] + public class download_client_import : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertFolderSettings); + + Execute.WithConnection(AssociateImportedHistoryItems); + } + + private void ConvertFolderSettings(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand downloadClientsCmd = conn.CreateCommand()) + { + downloadClientsCmd.Transaction = tran; + downloadClientsCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; + var downloadedEpisodesFolder = downloadClientsCmd.ExecuteScalar() as String; + + downloadClientsCmd.Transaction = tran; + downloadClientsCmd.CommandText = @"SELECT Id, Implementation, Settings, ConfigContract FROM DownloadClients WHERE ConfigContract = 'FolderSettings'"; + using (IDataReader downloadClientReader = downloadClientsCmd.ExecuteReader()) + { + while (downloadClientReader.Read()) + { + var id = downloadClientReader.GetInt32(0); + var implementation = downloadClientReader.GetString(1); + var settings = downloadClientReader.GetString(2); + var configContract = downloadClientReader.GetString(3); + + var settingsJson = JsonConvert.DeserializeObject(settings) as Newtonsoft.Json.Linq.JObject; + + if (implementation == "Blackhole") + { + var newSettings = new + { + NzbFolder = settingsJson.Value<String>("folder"), + WatchFolder = downloadedEpisodesFolder + }.ToJson(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE DownloadClients SET Implementation = ?, Settings = ?, ConfigContract = ? WHERE Id = ?"; + updateCmd.AddParameter("UsenetBlackhole"); + updateCmd.AddParameter(newSettings); + updateCmd.AddParameter("UsenetBlackholeSettings"); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + else if (implementation == "Pneumatic") + { + var newSettings = new + { + NzbFolder = settingsJson.Value<String>("folder") + }.ToJson(); + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "UPDATE DownloadClients SET Settings = ?, ConfigContract = ? WHERE Id = ?"; + updateCmd.AddParameter(newSettings); + updateCmd.AddParameter("PneumaticSettings"); + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + else + { + using (IDbCommand updateCmd = conn.CreateCommand()) + { + updateCmd.Transaction = tran; + updateCmd.CommandText = "DELETE FROM DownloadClients WHERE Id = ?"; + updateCmd.AddParameter(id); + + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } + + private sealed class MigrationHistoryItem + { + public Int32 Id { get; set; } + public Int32 EpisodeId { get; set; } + public Int32 SeriesId { get; set; } + public String SourceTitle { get; set; } + public DateTime Date { get; set; } + public Dictionary<String, String> Data { get; set; } + public MigrationHistoryEventType EventType { get; set; } + } + + private enum MigrationHistoryEventType + { + Unknown = 0, + Grabbed = 1, + SeriesFolderImported = 2, + DownloadFolderImported = 3, + DownloadFailed = 4 + } + + private void AssociateImportedHistoryItems(IDbConnection conn, IDbTransaction tran) + { + var historyItems = new List<MigrationHistoryItem>(); + + using (IDbCommand historyCmd = conn.CreateCommand()) + { + historyCmd.Transaction = tran; + historyCmd.CommandText = @"SELECT Id, EpisodeId, SeriesId, SourceTitle, Date, Data, EventType FROM History WHERE EventType NOT NULL"; + using (IDataReader historyRead = historyCmd.ExecuteReader()) + { + while (historyRead.Read()) + { + historyItems.Add(new MigrationHistoryItem + { + Id = historyRead.GetInt32(0), + EpisodeId = historyRead.GetInt32(1), + SeriesId = historyRead.GetInt32(2), + SourceTitle = historyRead.GetString(3), + Date = historyRead.GetDateTime(4), + Data = Json.Deserialize<Dictionary<String, String>>(historyRead.GetString(5)), + EventType = (MigrationHistoryEventType)historyRead.GetInt32(6) + }); + } + } + } + + var numHistoryItemsNotAssociated = historyItems.Count(v => v.EventType == MigrationHistoryEventType.DownloadFolderImported && + v.Data.GetValueOrDefault("downloadClientId") == null); + + if (numHistoryItemsNotAssociated == 0) + { + return; + } + + var historyItemsToAssociate = new Dictionary<MigrationHistoryItem, MigrationHistoryItem>(); + + var historyItemsLookup = historyItems.ToLookup(v => v.EpisodeId); + + foreach (var historyItemGroup in historyItemsLookup) + { + var list = historyItemGroup.ToList(); + + for (int i = 0; i < list.Count - 1; i++) + { + var grabbedEvent = list[i]; + if (grabbedEvent.EventType != MigrationHistoryEventType.Grabbed) continue; + if (grabbedEvent.Data.GetValueOrDefault("downloadClient") == null || grabbedEvent.Data.GetValueOrDefault("downloadClientId") == null) continue; + + // Check if it is already associated with a failed/imported event. + int j; + for (j = i + 1; j < list.Count;j++) + { + if (list[j].EventType != MigrationHistoryEventType.DownloadFolderImported && + list[j].EventType != MigrationHistoryEventType.DownloadFailed) + { + continue; + } + + if (list[j].Data.ContainsKey("downloadClient") && list[j].Data["downloadClient"] == grabbedEvent.Data["downloadClient"] && + list[j].Data.ContainsKey("downloadClientId") && list[j].Data["downloadClientId"] == grabbedEvent.Data["downloadClientId"]) + { + break; + } + } + + if (j != list.Count) + { + list.RemoveAt(j); + list.RemoveAt(i--); + continue; + } + + var importedEvent = list[i + 1]; + if (importedEvent.EventType != MigrationHistoryEventType.DownloadFolderImported) continue; + + var droppedPath = importedEvent.Data.GetValueOrDefault("droppedPath"); + if (droppedPath != null && new FileInfo(droppedPath).Directory.Name == grabbedEvent.SourceTitle) + { + historyItemsToAssociate[importedEvent] = grabbedEvent; + + list.RemoveAt(i + 1); + list.RemoveAt(i--); + } + } + } + + foreach (var pair in historyItemsToAssociate) + { + using (IDbCommand updateHistoryCmd = conn.CreateCommand()) + { + pair.Key.Data["downloadClient"] = pair.Value.Data["downloadClient"]; + pair.Key.Data["downloadClientId"] = pair.Value.Data["downloadClientId"]; + + updateHistoryCmd.Transaction = tran; + updateHistoryCmd.CommandText = "UPDATE History SET Data = ? WHERE Id = ?"; + updateHistoryCmd.AddParameter(pair.Key.Data.ToJson()); + updateHistoryCmd.AddParameter(pair.Key.Id); + + updateHistoryCmd.ExecuteNonQuery(); + } + } + + _logger.Info("Updated old History items. {0}/{1} old ImportedEvents were associated with GrabbedEvents.", historyItemsToAssociate.Count, numHistoryItemsNotAssociated); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs b/src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs deleted file mode 100644 index ccc3f3527..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/051_rename_download_client_settings.cs +++ /dev/null @@ -1,88 +0,0 @@ -using NzbDrone.Core.Datastore.Migration.Framework; -using FluentMigrator; -using System.Data; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Download.Clients.UsenetBlackhole; -using Newtonsoft.Json; -using System; -using NzbDrone.Core.Download.Clients.Pneumatic; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(51)] - public class rename_download_client_settings : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(ConvertFolderSettings); - } - - private void ConvertFolderSettings(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand downloadClientsCmd = conn.CreateCommand()) - { - downloadClientsCmd.Transaction = tran; - downloadClientsCmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; - var downloadedEpisodesFolder = downloadClientsCmd.ExecuteScalar() as String; - - downloadClientsCmd.Transaction = tran; - downloadClientsCmd.CommandText = @"SELECT Id, Implementation, Settings, ConfigContract FROM DownloadClients WHERE ConfigContract = 'FolderSettings'"; - using (IDataReader downloadClientReader = downloadClientsCmd.ExecuteReader()) - { - while (downloadClientReader.Read()) - { - var id = downloadClientReader.GetInt32(0); - var implementation = downloadClientReader.GetString(1); - var settings = downloadClientReader.GetString(2); - var configContract = downloadClientReader.GetString(3); - - var settingsJson = JsonConvert.DeserializeObject(settings) as Newtonsoft.Json.Linq.JObject; - - if (implementation == "Blackhole") - { - var newSettings = new - { - NzbFolder = settingsJson.Value<String>("folder"), - WatchFolder = downloadedEpisodesFolder - }.ToJson(); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE DownloadClients SET Implementation = ?, Settings = ?, ConfigContract = ? WHERE Id = ?"; - updateCmd.AddParameter("UsenetBlackhole"); - updateCmd.AddParameter(newSettings); - updateCmd.AddParameter("UsenetBlackholeSettings"); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - else if (implementation == "Pneumatic") - { - var newSettings = new - { - NzbFolder = settingsJson.Value<String>("folder") - }.ToJson(); - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - updateCmd.Transaction = tran; - updateCmd.CommandText = "UPDATE DownloadClients SET Settings = ?, ConfigContract = ? WHERE Id = ?"; - updateCmd.AddParameter(newSettings); - updateCmd.AddParameter("PneumaticSettings"); - updateCmd.AddParameter(id); - - updateCmd.ExecuteNonQuery(); - } - } - else - { - throw new NotSupportedException(); - } - } - } - } -} - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs index da9dde57a..5655649cd 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs @@ -6,11 +6,11 @@ namespace NzbDrone.Core.Datastore.Migration.Framework { public abstract class NzbDroneMigrationBase : FluentMigrator.Migration { - private Logger _logger; + protected readonly Logger _logger; protected NzbDroneMigrationBase() { - _logger = NzbDroneLogger.GetLogger(); + _logger = NzbDroneLogger.GetLogger(this); } protected virtual void MainDbUpgrade() diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index c6aa2b0c6..7ae4b422b 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -30,18 +30,21 @@ namespace NzbDrone.Core.Download private readonly IConfigService _configService; private readonly IDiskProvider _diskProvider; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; + private readonly IHistoryService _historyService; private readonly Logger _logger; public CompletedDownloadService(IEventAggregator eventAggregator, IConfigService configService, IDiskProvider diskProvider, IDownloadedEpisodesImportService downloadedEpisodesImportService, + IHistoryService historyService, Logger logger) { _eventAggregator = eventAggregator; _configService = configService; _diskProvider = diskProvider; _downloadedEpisodesImportService = downloadedEpisodesImportService; + _historyService = historyService; _logger = logger; } @@ -67,7 +70,7 @@ namespace NzbDrone.Core.Download _logger.Trace("Ignoring download that wasn't grabbed by drone: " + trackedDownload.DownloadItem.Title); return; } - + var importedItems = GetHistoryItems(importedHistory, trackedDownload.DownloadItem.DownloadClientId); if (importedItems.Any()) @@ -112,6 +115,34 @@ namespace NzbDrone.Core.Download } else { + if (grabbedItems.Any()) + { + var episodeIds = trackedDownload.DownloadItem.RemoteEpisode.Episodes.Select(v => v.Id).ToList(); + + // Check if we can associate it with a previous drone factory import. + importedItems = importedHistory.Where(v => v.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID) == null && + episodeIds.Contains(v.EpisodeId) && + v.Data.GetValueOrDefault("droppedPath") != null && + new FileInfo(v.Data["droppedPath"]).Directory.Name == grabbedItems.First().SourceTitle + ).ToList(); + if (importedItems.Count == 1) + { + var importedFile = new FileInfo(importedItems.First().Data["droppedPath"]); + + if (importedFile.Directory.Name == grabbedItems.First().SourceTitle) + { + trackedDownload.State = TrackedDownloadState.Imported; + + importedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT] = grabbedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT]; + importedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID] = grabbedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID]; + _historyService.UpdateHistoryData(importedItems.First().Id, importedItems.First().Data); + + _logger.Debug("Storage path does not exist, but found probable drone factory ImportEvent: " + trackedDownload.DownloadItem.Title); + return; + } + } + } + _logger.Debug("Storage path does not exist: " + trackedDownload.DownloadItem.Title); return; } diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 37824b5d1..e9846031e 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -25,6 +25,7 @@ namespace NzbDrone.Core.History History MostRecentForEpisode(int episodeId); History Get(int id); List<History> FindBySourceTitle(string sourceTitle); + void UpdateHistoryData(Int32 historyId, Dictionary<String, String> data); } public class HistoryService : IHistoryService, IHandle<EpisodeGrabbedEvent>, IHandle<EpisodeImportedEvent>, IHandle<DownloadFailedEvent> @@ -101,6 +102,13 @@ namespace NzbDrone.Core.History .FirstOrDefault(); } + public void UpdateHistoryData(Int32 historyId, Dictionary<String, String> data) + { + var history = _historyRepository.Get(historyId); + history.Data = data; + _historyRepository.Update(history); + } + public void Handle(EpisodeGrabbedEvent message) { foreach (var episode in message.Episode.Episodes) diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 1c7235237..a765c45df 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -160,7 +160,8 @@ namespace NzbDrone.Core.MediaFiles } } - return ProcessFiles(series, quality, videoFiles); + var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality); + return _importApprovedEpisodes.Import(decisions, true); } private void ProcessVideoFile(string videoFile) @@ -179,13 +180,8 @@ namespace NzbDrone.Core.MediaFiles return; } - ProcessFiles(series, null, videoFile); - } - - private List<ImportDecision> ProcessFiles(Series series, QualityModel quality, params string[] videoFiles) - { - var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality); - return _importApprovedEpisodes.Import(decisions, true); + var decisions = _importDecisionMaker.GetImportDecisions(new [] { videoFile }.ToList(), series, true, null); + _importApprovedEpisodes.Import(decisions, true); } private void ProcessFolder(string path) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 4549f34a1..76f49c4cd 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -197,7 +197,7 @@ <Compile Include="Datastore\Migration\048_add_title_to_scenemappings.cs" /> <Compile Include="Datastore\Migration\049_fix_dognzb_url.cs" /> <Compile Include="Datastore\Migration\050_add_hash_to_metadata_files.cs" /> - <Compile Include="Datastore\Migration\051_rename_download_client_settings.cs" /> + <Compile Include="Datastore\Migration\051_download_client_import.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" /> From 68352e0340c4428028db088d37d8f896247bd713 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Fri, 30 May 2014 17:54:02 +0200 Subject: [PATCH 15/16] Updated migration to enable completed download handling for new users (those with no drone factory configured) --- .../Configuration/ConfigServiceFixture.cs | 2 ++ .../Migration/051_download_client_import.cs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs index 35caa1216..5f686eac3 100644 --- a/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Configuration/ConfigServiceFixture.cs @@ -14,6 +14,8 @@ namespace NzbDrone.Core.Test.Configuration public void SetUp() { Mocker.SetConstant<IConfigRepository>(Mocker.Resolve<ConfigRepository>()); + + Db.All<Config>().ForEach(Db.Delete); } [Test] diff --git a/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs b/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs index f801eda79..c773e6224 100644 --- a/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs +++ b/src/NzbDrone.Core/Datastore/Migration/051_download_client_import.cs @@ -16,11 +16,30 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { + Execute.WithConnection(EnableCompletedDownloadHandlingForNewUsers); + Execute.WithConnection(ConvertFolderSettings); Execute.WithConnection(AssociateImportedHistoryItems); } + private void EnableCompletedDownloadHandlingForNewUsers(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = @"SELECT Value FROM Config WHERE Key = 'downloadedepisodesfolder'"; + + var result = cmd.ExecuteScalar(); + + if (result == null) + { + cmd.CommandText = @"INSERT INTO Config (Key, Value) VALUES ('enablecompleteddownloadhandling', 'True')"; + cmd.ExecuteNonQuery(); + } + } + } + private void ConvertFolderSettings(IDbConnection conn, IDbTransaction tran) { using (IDbCommand downloadClientsCmd = conn.CreateCommand()) From 0e7fc2e697dc5fd115cf795279cfe1eaaf34b761 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Tue, 3 Jun 2014 23:04:28 +0200 Subject: [PATCH 16/16] New: Health Check errors now have links to the wiki pages. --- src/NzbDrone.Api/Health/HealthResource.cs | 1 + .../HealthCheck/Checks/RootFolderCheck.cs | 4 +-- .../HealthCheck/Checks/UpdateCheck.cs | 3 +- src/NzbDrone.Core/HealthCheck/HealthCheck.cs | 20 ++++++++++++- src/UI/System/Info/Health/HealthLayout.js | 15 ++++++++-- src/UI/System/Info/Health/HealthWikiCell.js | 29 +++++++++++++++++++ src/UI/jQuery/RouteBinder.js | 6 ++-- 7 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 src/UI/System/Info/Health/HealthWikiCell.js diff --git a/src/NzbDrone.Api/Health/HealthResource.cs b/src/NzbDrone.Api/Health/HealthResource.cs index a5bec7c06..281a55e60 100644 --- a/src/NzbDrone.Api/Health/HealthResource.cs +++ b/src/NzbDrone.Api/Health/HealthResource.cs @@ -8,5 +8,6 @@ namespace NzbDrone.Api.Health { public HealthCheckResult Type { get; set; } public String Message { get; set; } + public Uri WikiUrl { get; set; } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs index 0c44b0947..59a79c4cc 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs @@ -28,11 +28,11 @@ namespace NzbDrone.Core.HealthCheck.Checks { if (missingRootFolders.Count == 1) { - return new HealthCheck(GetType(), HealthCheckResult.Error, "Missing root folder: " + missingRootFolders.First()); + return new HealthCheck(GetType(), HealthCheckResult.Error, "Missing root folder: " + missingRootFolders.First(), "#missing-root-folder"); } var message = String.Format("Multiple root folders are missing: {0}", String.Join(" | ", missingRootFolders)); - return new HealthCheck(GetType(), HealthCheckResult.Error, message); + return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#missing-root-folder"); } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index 0eb187b29..350595138 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -33,8 +33,7 @@ namespace NzbDrone.Core.HealthCheck.Checks } catch (Exception) { - return new HealthCheck(GetType(), HealthCheckResult.Error, - "Unable to update, running from write-protected folder"); + return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to update, running from write-protected folder"); } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs index 183849ecb..31f0a3035 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs @@ -1,13 +1,17 @@ using System; +using System.Text.RegularExpressions; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.HealthCheck { public class HealthCheck : ModelBase { + private static readonly Regex CleanFragmentRegex = new Regex("[^a-z ]", RegexOptions.Compiled); + public Type Source { get; set; } public HealthCheckResult Type { get; set; } public String Message { get; set; } + public Uri WikiUrl { get; set; } public HealthCheck(Type source) { @@ -15,11 +19,25 @@ namespace NzbDrone.Core.HealthCheck Type = HealthCheckResult.Ok; } - public HealthCheck(Type source, HealthCheckResult type, string message) + public HealthCheck(Type source, HealthCheckResult type, String message, String wikiFragment = null) { Source = source; Type = type; Message = message; + WikiUrl = MakeWikiUrl(wikiFragment ?? MakeWikiFragment(message)); + } + + private static String MakeWikiFragment(String message) + { + return "#" + CleanFragmentRegex.Replace(message.ToLower(), String.Empty).Replace(' ', '-'); + } + + private static Uri MakeWikiUrl(String fragment) + { + var rootUri = new Uri("https://github.com/NzbDrone/NzbDrone/wiki/Health-checks"); + var fragmentUri = new Uri(fragment, UriKind.Relative); + + return new Uri(rootUri, fragmentUri); } } diff --git a/src/UI/System/Info/Health/HealthLayout.js b/src/UI/System/Info/Health/HealthLayout.js index 2a0a44461..935087741 100644 --- a/src/UI/System/Info/Health/HealthLayout.js +++ b/src/UI/System/Info/Health/HealthLayout.js @@ -5,8 +5,9 @@ define( 'backgrid', 'Health/HealthCollection', 'System/Info/Health/HealthCell', + 'System/Info/Health/HealthWikiCell', 'System/Info/Health/HealthOkView' - ], function (Marionette, Backgrid, HealthCollection, HealthCell, HealthOkView) { + ], function (Marionette, Backgrid, HealthCollection, HealthCell, HealthWikiCell, HealthOkView) { return Marionette.Layout.extend({ template: 'System/Info/Health/HealthLayoutTemplate', @@ -19,12 +20,20 @@ define( { name: 'type', label: '', - cell: HealthCell + cell: HealthCell, + sortable: false }, { name: 'message', label: 'Message', - cell: 'string' + cell: 'string', + sortable: false + }, + { + name: 'wikiUrl', + label: '', + cell: HealthWikiCell, + sortable: false } ], diff --git a/src/UI/System/Info/Health/HealthWikiCell.js b/src/UI/System/Info/Health/HealthWikiCell.js new file mode 100644 index 000000000..e6efd8c22 --- /dev/null +++ b/src/UI/System/Info/Health/HealthWikiCell.js @@ -0,0 +1,29 @@ +'use strict'; +define( + [ + 'jquery', + 'backgrid' + ], function ($, Backgrid) { + return Backgrid.UriCell.extend({ + + className: 'wiki-link-cell', + + title: 'Read the Wiki for more information', + + text: 'Wiki', + + render: function () { + this.$el.empty(); + var rawValue = this.model.get(this.column.get("name")); + var formattedValue = this.formatter.fromRaw(rawValue, this.model); + this.$el.append($("<a>", { + tabIndex: -1, + href: rawValue, + title: this.title || formattedValue, + target: this.target + }).text(this.text)); + this.delegateEvents(); + return this; + } + }); + }); diff --git a/src/UI/jQuery/RouteBinder.js b/src/UI/jQuery/RouteBinder.js index f4b541102..e3e85c068 100644 --- a/src/UI/jQuery/RouteBinder.js +++ b/src/UI/jQuery/RouteBinder.js @@ -48,13 +48,15 @@ define( throw 'couldn\'t find route target'; } - if (!href.startsWith('http')) { var relativeHref = href.replace(StatusModel.get('urlBase'), ''); Backbone.history.navigate(relativeHref, { trigger: true }); } - + else if (href.contains('#')) { + //Open in new tab without dereferer (since it doesn't support fragments) + window.open(href, '_blank'); + } else { //Open in new tab window.open('http://www.dereferer.org/?' + encodeURI(href), '_blank');