From 7c5bc94f6eb835898ecc798918b4688630b00800 Mon Sep 17 00:00:00 2001 From: mythjuha Date: Sat, 19 Apr 2014 16:53:08 +0200 Subject: [PATCH 01/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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'); From 7b3bdee24f2422162924010f5b511785a7540878 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Tue, 3 Jun 2014 15:45:03 -0700 Subject: [PATCH 17/59] Fixed: path in rename preview --- src/UI/Rename/RenamePreviewItemViewTemplate.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI/Rename/RenamePreviewItemViewTemplate.html b/src/UI/Rename/RenamePreviewItemViewTemplate.html index fb161d465..f0fe50702 100644 --- a/src/UI/Rename/RenamePreviewItemViewTemplate.html +++ b/src/UI/Rename/RenamePreviewItemViewTemplate.html @@ -8,12 +8,12 @@ </div> </label> </div> - <div class="col-md-9"> + <div class="col-md-11"> <div class="row"> - <div class="col-md-9 file-path"><i class="icon-nd-existing" title="Existing path" /> {{existingPath}}</div> + <div class="col-md-12 file-path"><i class="icon-nd-existing" title="Existing path" /> {{existingPath}}</div> </div> <div class="row"> - <div class="col-md-9 file-path"><i class="icon-nd-suggested" title="Suggested path" /> {{newPath}}</div> + <div class="col-md-12 file-path"><i class="icon-nd-suggested" title="Suggested path" /> {{newPath}}</div> </div> </div> </div> From ce89469c9b449a4e1456675bfea99cd95d905b0f Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Tue, 3 Jun 2014 23:02:08 -0700 Subject: [PATCH 18/59] Drone factory folder not being set is now logged at trace --- src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index a765c45df..f73e9555c 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -108,7 +108,7 @@ namespace NzbDrone.Core.MediaFiles if (String.IsNullOrEmpty(downloadedEpisodesFolder)) { - _logger.Warn("Drone Factory folder is not configured"); + _logger.Trace("Drone Factory folder is not configured"); return; } From 341cb169ebb67d097a8a785e3c3f479d4f8d4ecd Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 4 Jun 2014 08:10:40 -0700 Subject: [PATCH 19/59] renamed completed/failed download handling sections --- .../DownloadHandling/DownloadHandlingViewTemplate.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html index 4a0eb27af..d32b8f72f 100644 --- a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.html @@ -1,5 +1,5 @@ <fieldset> - <legend>Download Client Import Handling</legend> + <legend>Completed Download Handling</legend> <div class="form-group"> <label class="col-sm-3 control-label">Enable</label> @@ -23,7 +23,7 @@ </div> </div> - <div class="x-completed-download-options advanced-setting""> + <div class="x-completed-download-options advanced-setting"> <div class="form-group"> <label class="col-sm-3 control-label">Remove</label> @@ -49,7 +49,7 @@ </fieldset> <fieldset class="advanced-setting"> - <legend>Download Client Failed Handling</legend> + <legend>Failed Download Handling</legend> <div class="form-group"> <label class="col-sm-3 control-label">Enable</label> From 9916479f027bf1a909d3ad12f6aea22d1a137970 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Wed, 4 Jun 2014 22:08:36 +0200 Subject: [PATCH 20/59] Sabnzbdproxy now returns the folder instead of the file in case of a single file download. --- .../SabnzbdTests/SabnzbdFixture.cs | 16 +++++++++++++++- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index 45260547e..9a5760d62 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using Moq; @@ -12,7 +13,7 @@ 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; +using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests { @@ -254,5 +255,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests Mocker.GetMock<ISabnzbdProxy>() .Verify(v => v.DownloadNzb(It.IsAny<Stream>(), It.IsAny<String>(), It.IsAny<String>(), (int)SabnzbdPriority.High, It.IsAny<SabnzbdSettings>()), Times.Once()); } + + [Test] + public void should_return_path_to_folder_instead_of_file() + { + _completed.Items.First().Storage = @"C:\sorted\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE\Droned.S01E01_Pilot_1080p_WEB-DL-DRONE.mkv".AsOsAgnostic(); + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"C:\sorted\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index d97914a45..86c690dd2 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using NLog; @@ -142,7 +143,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd DownloadTime = TimeSpan.FromSeconds(sabHistoryItem.DownloadTime), RemainingTime = TimeSpan.Zero, - OutputPath = sabHistoryItem.Storage, Message = sabHistoryItem.FailMessage }; @@ -159,6 +159,19 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd historyItem.Status = DownloadItemStatus.Downloading; } + if (!sabHistoryItem.Storage.IsNullOrWhiteSpace()) + { + var parent = Directory.GetParent(sabHistoryItem.Storage); + if (parent.Name == sabHistoryItem.Title) + { + historyItem.OutputPath = parent.FullName; + } + else + { + historyItem.OutputPath = sabHistoryItem.Storage; + } + } + historyItems.Add(historyItem); } From c04ae9f1d037e418a7d461c15221b8b47df1f893 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 24 May 2014 12:47:01 +0200 Subject: [PATCH 21/59] Fixed: Newznab parser will attempt to use the usenetdate for age determination instead of the feed publish date. --- .../Indexers/Newznab/NewznabParser.cs | 16 ++++++++++++++++ src/NzbDrone.Core/Indexers/XElementExtensions.cs | 11 ++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs b/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs index 85fe2d4e4..cc1919c80 100644 --- a/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs +++ b/src/NzbDrone.Core/Indexers/Newznab/NewznabParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using NzbDrone.Core.Parser.Model; +using System.Globalization; namespace NzbDrone.Core.Indexers.Newznab { @@ -19,6 +20,21 @@ namespace NzbDrone.Core.Indexers.Newznab return item.Comments().Replace("#comments", ""); } + protected override DateTime GetPublishDate(XElement item) + { + var attributes = item.Elements("attr").ToList(); + var usenetdateElement = attributes.SingleOrDefault(e => e.Attribute("name").Value.Equals("usenetdate", StringComparison.CurrentCultureIgnoreCase)); + + if (usenetdateElement != null) + { + var dateString = usenetdateElement.Attribute("value").Value; + + return XElementExtensions.ParseDate(dateString); + } + + return base.GetPublishDate(item); + } + protected override long GetSize(XElement item) { var attributes = item.Elements("attr").ToList(); diff --git a/src/NzbDrone.Core/Indexers/XElementExtensions.cs b/src/NzbDrone.Core/Indexers/XElementExtensions.cs index fc3f29dbc..fb24e526f 100644 --- a/src/NzbDrone.Core/Indexers/XElementExtensions.cs +++ b/src/NzbDrone.Core/Indexers/XElementExtensions.cs @@ -35,10 +35,8 @@ namespace NzbDrone.Core.Indexers return res; } - public static DateTime PublishDate(this XElement item) + public static DateTime ParseDate(string dateString) { - string dateString = item.TryGetValue("pubDate"); - try { DateTime result; @@ -56,6 +54,13 @@ namespace NzbDrone.Core.Indexers } } + public static DateTime PublishDate(this XElement item) + { + string dateString = item.TryGetValue("pubDate"); + + return ParseDate(dateString); + } + public static List<String> Links(this XElement item) { var elements = item.Elements("link"); From 50303ce470e153e069346112be57ab70d22b7bfa Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Fri, 30 May 2014 19:38:52 +0200 Subject: [PATCH 22/59] Fixed: Checks full path for _UNPACK_ prefix so that full season downloads are properly checked for unpacking. --- .../NotUnpackingSpecificationFixture.cs | 2 +- .../NotUnpackingSpecification.cs | 24 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs index 155420917..57c444521 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications private void GivenInWorkingFolder() { - _localEpisode.Path = @"C:\Test\Unsorted TV\_UNPACK_30.rock\30.rock.s01e01.avi".AsOsAgnostic(); + _localEpisode.Path = @"C:\Test\Unsorted TV\_UNPACK_30.rock\someSubFolder\30.rock.s01e01.avi".AsOsAgnostic(); } private void GivenLastWriteTimeUtc(DateTime time) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index 3c52b1bb6..97aa38e93 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -34,19 +34,25 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|')) { - if (Directory.GetParent(localEpisode.Path).Name.StartsWith(workingFolder)) + DirectoryInfo parent = Directory.GetParent(localEpisode.Path); + while (parent != null) { - if (OsInfo.IsMono) + if (parent.Name.StartsWith(workingFolder)) { - _logger.Debug("{0} is still being unpacked", localEpisode.Path); - return false; + if (OsInfo.IsMono) + { + _logger.Debug("{0} is still being unpacked", localEpisode.Path); + return false; + } + + if (_diskProvider.FileGetLastWriteUtc(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) + { + _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); + return false; + } } - if (_diskProvider.FileGetLastWriteUtc(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) - { - _logger.Debug("{0} appears to be unpacking still", localEpisode.Path); - return false; - } + parent = parent.Parent; } } From e28123eefd76f67eb9ad4afca214e11a3ab99993 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Fri, 30 May 2014 22:36:38 +0200 Subject: [PATCH 23/59] Fixed: Search results from trakt are now sorted based on similarity with the search query. Using a Levenshtein distance algorithm. --- .../LevenshteinDistanceFixture.cs | 50 +++++++++++++++++ .../NzbDrone.Common.Test.csproj | 1 + src/NzbDrone.Common/LevenstheinExtensions.cs | 55 +++++++++++++++++++ src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + .../MetadataSource/TraktProxy.cs | 4 +- 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs create mode 100644 src/NzbDrone.Common/LevenstheinExtensions.cs diff --git a/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs b/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs new file mode 100644 index 000000000..27fe63480 --- /dev/null +++ b/src/NzbDrone.Common.Test/LevenshteinDistanceFixture.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; +using System.IO; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Common.Test +{ + [TestFixture] + public class LevenshteinDistanceFixture : TestBase + { + [TestCase("", "", 0)] + [TestCase("abc", "abc", 0)] + [TestCase("abc", "abcd", 1)] + [TestCase("abcd", "abc", 1)] + [TestCase("abc", "abd", 1)] + [TestCase("abc", "adc", 1)] + [TestCase("abcdefgh", "abcghdef", 4)] + [TestCase("a.b.c.", "abc", 3)] + [TestCase("Agents Of SHIELD", "Marvel's Agents Of S.H.I.E.L.D.", 15)] + [TestCase("Agents of cracked", "Agents of shield", 6)] + [TestCase("ABCxxx", "ABC1xx", 1)] + [TestCase("ABC1xx", "ABCxxx", 1)] + public void LevenshteinDistance(String text, String other, Int32 expected) + { + text.LevenshteinDistance(other).Should().Be(expected); + } + + [TestCase("", "", 0)] + [TestCase("abc", "abc", 0)] + [TestCase("abc", "abcd", 1)] + [TestCase("abcd", "abc", 3)] + [TestCase("abc", "abd", 3)] + [TestCase("abc", "adc", 3)] + [TestCase("abcdefgh", "abcghdef", 8)] + [TestCase("a.b.c.", "abc", 0)] + [TestCase("Agents of shield", "Marvel's Agents Of S.H.I.E.L.D.", 9)] + [TestCase("Agents of shield", "Agents of cracked", 14)] + [TestCase("Agents of shield", "the shield", 24)] + [TestCase("ABCxxx", "ABC1xx", 3)] + [TestCase("ABC1xx", "ABCxxx", 3)] + public void LevenshteinDistanceClean(String text, String other, Int32 expected) + { + text.ToLower().LevenshteinDistanceClean(other.ToLower()).Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index ae5e48bf8..e7f6a681f 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -67,6 +67,7 @@ <Compile Include="EnsureTest\PathExtensionFixture.cs" /> <Compile Include="EnvironmentTests\StartupArgumentsFixture.cs" /> <Compile Include="EnvironmentTests\EnvironmentProviderTest.cs" /> + <Compile Include="LevenshteinDistanceFixture.cs" /> <Compile Include="ReflectionExtensions.cs" /> <Compile Include="PathExtensionFixture.cs" /> <Compile Include="DiskProviderTests\DiskProviderFixtureBase.cs" /> diff --git a/src/NzbDrone.Common/LevenstheinExtensions.cs b/src/NzbDrone.Common/LevenstheinExtensions.cs new file mode 100644 index 000000000..3bc54d5b2 --- /dev/null +++ b/src/NzbDrone.Common/LevenstheinExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using ICSharpCode.SharpZipLib.Zip; + +namespace NzbDrone.Common +{ + public static class LevenstheinExtensions + { + public static Int32 LevenshteinDistance(this String text, String other, Int32 costInsert = 1, Int32 costDelete = 1, Int32 costSubstitute = 1) + { + if (text == other) return 0; + if (text.Length == 0) return other.Length * costInsert; + if (other.Length == 0) return text.Length * costDelete; + + Int32[] matrix = new Int32[other.Length + 1]; + + for (var i = 1; i < matrix.Length; i++) + { + matrix[i] = i * costInsert; + } + + for (var i = 0; i < text.Length; i++) + { + Int32 topLeft = matrix[0]; + matrix[0] = matrix[0] + costDelete; + + for (var j = 0; j < other.Length; j++) + { + Int32 top = matrix[j]; + Int32 left = matrix[j + 1]; + + var sumIns = top + costInsert; + var sumDel = left + costDelete; + var sumSub = topLeft + (text[i] == other[j] ? 0 : costSubstitute); + + topLeft = matrix[j + 1]; + matrix[j + 1] = Math.Min(Math.Min(sumIns, sumDel), sumSub); + } + } + + return matrix[other.Length]; + } + + public static Int32 LevenshteinDistanceClean(this String expected, String other) + { + expected = expected.ToLower().Replace(".", ""); + other = other.ToLower().Replace(".", ""); + + return expected.LevenshteinDistance(other, 1, 3, 3); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index c75eeecc3..bab538ec7 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -114,6 +114,7 @@ <Compile Include="Serializer\IntConverter.cs" /> <Compile Include="Services.cs" /> <Compile Include="Extensions\StreamExtensions.cs" /> + <Compile Include="LevenstheinExtensions.cs" /> <Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" /> <Compile Include="Security\IgnoreCertErrorPolicy.cs" /> <Compile Include="StringExtensions.cs" /> diff --git a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs index 664778b63..22e11bec1 100644 --- a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -35,7 +35,9 @@ namespace NzbDrone.Core.MetadataSource var restRequest = new RestRequest(GetSearchTerm(title) + "/30/seasons"); var response = client.ExecuteAndValidate<List<Show>>(restRequest); - return response.Select(MapSeries).ToList(); + return response.Select(MapSeries) + .OrderBy(v => title.LevenshteinDistanceClean(v.Title)) + .ToList(); } catch (WebException ex) { From 6d2fac5cd05f47a267be71c9362e381d81f01b51 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 31 May 2014 17:53:04 +0200 Subject: [PATCH 24/59] New: Adding new series by tvdbid: or slug: is now possible. --- .../MetadataSource/TraktProxy.cs | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs index 22e11bec1..0236a4169 100644 --- a/src/NzbDrone.Core/MetadataSource/TraktProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/TraktProxy.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MetadataSource { private readonly Logger _logger; private static readonly Regex CollapseSpaceRegex = new Regex(@"\s+", RegexOptions.Compiled); - private static readonly Regex InvalidSearchCharRegex = new Regex(@"(?:\*|\(|\)|'|!|@)", RegexOptions.Compiled); + private static readonly Regex InvalidSearchCharRegex = new Regex(@"(?:\*|\(|\)|'|!|@|\+)", RegexOptions.Compiled); public TraktProxy(Logger logger) { @@ -31,13 +31,43 @@ namespace NzbDrone.Core.MetadataSource { try { - var client = BuildClient("search", "shows"); - var restRequest = new RestRequest(GetSearchTerm(title) + "/30/seasons"); - var response = client.ExecuteAndValidate<List<Show>>(restRequest); + if (title.StartsWith("tvdb:") || title.StartsWith("tvdbid:") || title.StartsWith("slug:")) + { + try + { + var slug = title.Split(':')[1]; - return response.Select(MapSeries) - .OrderBy(v => title.LevenshteinDistanceClean(v.Title)) - .ToList(); + if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace)) + { + return new List<Series>(); + } + + var client = BuildClient("show", "summary"); + var restRequest = new RestRequest(GetSearchTerm(slug) + "/extended"); + var response = client.ExecuteAndValidate<Show>(restRequest); + + return new List<Series> { MapSeries(response) }; + } + catch (RestException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.NotFound) + { + return new List<Series>(); + } + + throw; + } + } + else + { + var client = BuildClient("search", "shows"); + var restRequest = new RestRequest(GetSearchTerm(title) + "/30/seasons"); + var response = client.ExecuteAndValidate<List<Show>>(restRequest); + + return response.Select(MapSeries) + .OrderBy(v => title.LevenshteinDistanceClean(v.Title)) + .ToList(); + } } catch (WebException ex) { @@ -172,7 +202,6 @@ namespace NzbDrone.Core.MetadataSource phrase = CollapseSpaceRegex.Replace(phrase, " ").Trim().ToLower(); phrase = phrase.Trim('-'); phrase = HttpUtility.UrlEncode(phrase); - return phrase; } From 545bc756f24856a34709bddba284ffa24b870201 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 4 Jun 2014 08:38:26 -0700 Subject: [PATCH 25/59] Fixed: Error messages not being shown in the UI or being shown in the wrong place --- .../ParserTests/ReleaseGroupParserFixture.cs | 1 + src/UI/Mixins/AsModelBoundView.js | 2 +- src/UI/Mixins/AsValidatedView.js | 34 +++++++++++++------ .../DownloadClientLayoutTemplate.html | 1 + .../Indexers/Add/IndexerAddItemView.js | 2 +- .../Settings/Indexers/Edit/IndexerEditView.js | 20 ++++++++++- .../Edit/IndexerEditViewTemplate.html | 4 +-- .../MediaManagementLayoutTemplate.html | 2 +- .../Permissions/PermissionsViewTemplate.html | 4 +-- .../ControlPanel/ControlPanelController.js | 8 ++--- src/UI/Shared/Modal/ModalController.js | 10 +++--- src/UI/Shared/Modal/ModalRegion.js | 6 ++-- src/UI/jQuery/jquery.validation.js | 6 ++-- 13 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 7c1504d4a..d147bc47b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The Office - S01E01 - Pilot [HTDV-1080p]", "DRONE")] [TestCase("The.Walking.Dead.S04E13.720p.WEB-DL.AAC2.0.H.264-Cyphanix", "Cyphanix")] [TestCase("Arrow.S02E01.720p.WEB-DL.DD5.1.H.264.mkv", "DRONE")] + [TestCase("Series Title S01E01 Episode Title", "DRONE")] public void should_parse_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/UI/Mixins/AsModelBoundView.js b/src/UI/Mixins/AsModelBoundView.js index be70ec65b..0095370e0 100644 --- a/src/UI/Mixins/AsModelBoundView.js +++ b/src/UI/Mixins/AsModelBoundView.js @@ -30,7 +30,7 @@ define( } }; - this.prototype.beforeClose = function () { + this.prototype.onBeforeClose = function () { if (this._modelBinder) { this._modelBinder.unbind(); diff --git a/src/UI/Mixins/AsValidatedView.js b/src/UI/Mixins/AsValidatedView.js index 952c1da69..7a6cea293 100644 --- a/src/UI/Mixins/AsValidatedView.js +++ b/src/UI/Mixins/AsValidatedView.js @@ -12,19 +12,12 @@ define( var originalBeforeClose = this.prototype.onBeforeClose; var errorHandler = function (response) { - - if (response.status === 400) { - - var view = this; - var validationErrors = JSON.parse(response.responseText); - _.each(validationErrors, function (error) { - view.$el.processServerError(error); - }); - } + this.model.trigger('validation:failed', response); }; - var validatedSync = function (method, model,options) { - this.$el.removeAllErrors(); + var validatedSync = function (method, model, options) { + model.trigger('validation:sync'); +// this.$el.removeAllErrors(); arguments[2].isValidatedCall = true; return model._originalSync.apply(this, arguments).fail(errorHandler.bind(this)); }; @@ -39,6 +32,21 @@ define( this.prototype.onRender = function () { + this.listenTo(this.model, 'validation:sync', function () { + this.$el.removeAllErrors(); + }); + + this.listenTo(this.model, 'validation:failed', function (response) { + if (response.status === 400) { + + var view = this; + var validationErrors = JSON.parse(response.responseText); + _.each(validationErrors, function (error) { + view.$el.processServerError(error); + }); + } + }); + Validation.bind(this); this.bindToModelValidation = bindToModel.bind(this); @@ -55,6 +63,10 @@ define( if (this.model) { Validation.unbind(this); + + //If we don't do this the next time the model is used the sync is bound to an old view + this.model.sync = this.model._originalSync; + this.model._originalSync = undefined; } if (originalBeforeClose) { diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html index 89c20761e..7450b08d3 100644 --- a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html +++ b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.html @@ -1,5 +1,6 @@ <div id="x-download-clients-region"></div> <div class="form-horizontal"> + <div id="x-download-handling-region"></div> <div id="x-dronefactory-region"></div> </div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js index 859f2a008..217a1c999 100644 --- a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js +++ b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js @@ -26,7 +26,7 @@ define([ this.model.set({ id : undefined, - name : this.model.get('implementation'), + name : undefined, enable : true }); diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditView.js b/src/UI/Settings/Indexers/Edit/IndexerEditView.js index 2f4a8b90d..004750332 100644 --- a/src/UI/Settings/Indexers/Edit/IndexerEditView.js +++ b/src/UI/Settings/Indexers/Edit/IndexerEditView.js @@ -22,13 +22,18 @@ define([ 'click .x-save-and-add': '_saveAndAdd', 'click .x-delete' : '_delete', 'click .x-back' : '_back', - 'click .x-test' : '_test' + 'click .x-close' : '_close', + 'click .x-test' : '_test' }, initialize: function (options) { this.targetCollection = options.targetCollection; }, + onBeforeClose: function () { + window.alert('closing down!'); + }, + _save: function () { var self = this; var promise = this.model.save(); @@ -67,6 +72,19 @@ define([ require('Settings/Indexers/Add/IndexerSchemaModal').open(this.targetCollection); }, + _close: function () { + + if (this.model.isNew()) { + this.model.destroy(); + } + + else { + this.model.fetch(); + } + + vent.trigger(vent.Commands.CloseModalCommand); + }, + _test: function () { var testCommand = 'test{0}'.format(this.model.get('implementation')); var properties = {}; diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html index 25ea543df..75ec1d284 100644 --- a/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html +++ b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.html @@ -1,7 +1,7 @@ <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <button type="button" class="close x-close" aria-hidden="true">×</button> {{#if id}} <h3>Edit - {{implementation}}</h3> {{else}} @@ -48,7 +48,7 @@ <!-- 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> + <button class="btn x-close">cancel</button> <div class="btn-group"> <button class="btn btn-primary x-save">save</button> diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html index 05a416998..1126a78f6 100644 --- a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html +++ b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.html @@ -2,5 +2,5 @@ <div id="episode-naming"></div> <div id="sorting"></div> <div id="file-management"></div> - <div id="permissions"></div> + {{#if_mono}}<div id="permissions"></div>{{/if_mono}} </div> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html index fd8ddcc69..704863c1c 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.html @@ -1,5 +1,4 @@ -{{#if_mono}} -<fieldset class="advanced-setting"> +<fieldset class="advanced-setting"> <legend>Permissions</legend> <div class="form-group"> @@ -73,4 +72,3 @@ </div> </div> </fieldset> -{{/if_mono}} diff --git a/src/UI/Shared/ControlPanel/ControlPanelController.js b/src/UI/Shared/ControlPanel/ControlPanelController.js index 4e2a1100c..d09a34a4c 100644 --- a/src/UI/Shared/ControlPanel/ControlPanelController.js +++ b/src/UI/Shared/ControlPanel/ControlPanelController.js @@ -9,15 +9,15 @@ define( return Marionette.AppRouter.extend({ initialize: function () { - vent.on(vent.Commands.OpenControlPanelCommand, this._openControlPanel, this); - vent.on(vent.Commands.CloseControlPanelCommand, this._closeControlPanel, this); + vent.on(vent.Commands.OpenControlPanelCommand, this._openModal, this); + vent.on(vent.Commands.CloseControlPanelCommand, this._closeModal, this); }, - _openControlPanel: function (view) { + _openModal: function (view) { AppLayout.controlPanelRegion.show(view); }, - _closeControlPanel: function () { + _closeModal: function () { AppLayout.controlPanelRegion.closePanel(); } }); diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index 9fcedea9a..177092de3 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -15,8 +15,8 @@ define( return Marionette.AppRouter.extend({ initialize: function () { - vent.on(vent.Commands.OpenModalCommand, this._openControlPanel, this); - vent.on(vent.Commands.CloseModalCommand, this._closeControlPanel, this); + vent.on(vent.Commands.OpenModalCommand, this._openModal, this); + vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); @@ -25,12 +25,12 @@ define( vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); }, - _openControlPanel: function (view) { + _openModal: function (view) { AppLayout.modalRegion.show(view); }, - _closeControlPanel: function () { - AppLayout.modalRegion.closePanel(); + _closeModal: function () { + AppLayout.modalRegion.closeModal(); }, _editSeries: function (options) { diff --git a/src/UI/Shared/Modal/ModalRegion.js b/src/UI/Shared/Modal/ModalRegion.js index 5aef6748e..a39f73fe4 100644 --- a/src/UI/Shared/Modal/ModalRegion.js +++ b/src/UI/Shared/Modal/ModalRegion.js @@ -11,7 +11,7 @@ define( constructor: function () { Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); - this.on('show', this.showPanel, this); + this.on('show', this.showModal, this); }, getEl: function (selector) { @@ -20,7 +20,7 @@ define( return $el; }, - showPanel: function () { + showModal: function () { this.$el.addClass('modal fade'); //need tab index so close on escape works @@ -32,7 +32,7 @@ define( 'backdrop': 'static'}); }, - closePanel: function () { + closeModal: function () { $(this.el).modal('hide'); this.reset(); } diff --git a/src/UI/jQuery/jquery.validation.js b/src/UI/jQuery/jquery.validation.js index 1b180c588..4f836f657 100644 --- a/src/UI/jQuery/jquery.validation.js +++ b/src/UI/jQuery/jquery.validation.js @@ -16,7 +16,6 @@ define( return this.name.toLowerCase() === validationName; }); - if (input.length === 0) { input = this.find('[validation-name]').filter(function () { return $(this).attr('validation-name').toLowerCase() === validationName; @@ -58,7 +57,10 @@ define( }; $.fn.addFormError = function (error) { - this.find('.form-group').parent().prepend('<div class="alert alert-error validation-error">' + error.errorMessage + '</div>'); + var t1 = this.find('.form-horizontal'); + var t2 = this.find('.form-horizontal').parent(); + + this.prepend('<div class="alert alert-danger validation-error">' + error.errorMessage + '</div>'); }; $.fn.removeAllErrors = function () { From f2da193ef7b8c41d36c18c97fa416ff7d8396600 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 4 Jun 2014 17:10:33 -0700 Subject: [PATCH 26/59] More logging during output process --- src/NzbDrone.Update/UpdateApp.cs | 35 ++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index 3691f6fce..1cc323e67 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -53,19 +53,7 @@ namespace NzbDrone.Update public void Start(string[] args) { var startupContext = ParseArgs(args); - string targetFolder; - - if (startupContext.ExecutingApplication.IsNullOrWhiteSpace()) - { - var exeFileInfo = new FileInfo(_processProvider.GetProcessById(startupContext.ProcessId).StartPath); - targetFolder = exeFileInfo.Directory.FullName; - } - - else - { - var exeFileInfo = new FileInfo(startupContext.ExecutingApplication); - targetFolder = exeFileInfo.Directory.FullName; - } + var targetFolder = GetInstallationDirectory(startupContext); logger.Info("Starting update process. Target Path:{0}", targetFolder); _installUpdateService.Start(targetFolder, startupContext.ProcessId); @@ -122,5 +110,26 @@ namespace NzbDrone.Update logger.Debug("NzbDrone process ID: {0}", id); return id; } + + private string GetInstallationDirectory(UpdateStartupContext startupContext) + { + if (startupContext.ExecutingApplication.IsNullOrWhiteSpace()) + { + logger.Debug("Using process ID to find installation directory: {0}", startupContext.ProcessId); + var exeFileInfo = new FileInfo(_processProvider.GetProcessById(startupContext.ProcessId).StartPath); + logger.Debug("Executable location: {0}", exeFileInfo.FullName); + + return exeFileInfo.DirectoryName; + } + + else + { + logger.Debug("Using executing application: {0}", startupContext.ExecutingApplication); + var exeFileInfo = new FileInfo(startupContext.ExecutingApplication); + logger.Debug("Executable location: {0}", exeFileInfo.FullName); + + return exeFileInfo.DirectoryName; + } + } } } From 0b3e4c48f751a8e817830b276a780b0d5cb75e18 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 4 Jun 2014 21:20:58 -0700 Subject: [PATCH 27/59] Added some logging to PlexServerProxy --- .../Notifications/Plex/PlexServerProxy.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index 726cac410..d9c1102b4 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; +using NLog; using NzbDrone.Common; using NzbDrone.Common.Cache; using NzbDrone.Common.Serializer; @@ -19,10 +20,12 @@ namespace NzbDrone.Core.Notifications.Plex public class PlexServerProxy : IPlexServerProxy { private readonly ICached<String> _authCache; + private readonly Logger _logger; - public PlexServerProxy(ICacheManager cacheManager) + public PlexServerProxy(ICacheManager cacheManager, Logger logger) { _authCache = cacheManager.GetCache<String>(GetType(), "authCache"); + _logger = logger; } public List<PlexDirectory> GetTvSections(PlexServerSettings settings) @@ -32,6 +35,9 @@ namespace NzbDrone.Core.Notifications.Plex var response = client.Execute(request); + CheckForError(response.Content); + _logger.Trace("Sections response: {0}", response.Content); + return Json.Deserialize<PlexMediaContainer>(response.Content) .Directories .Where(d => d.Type == "show") @@ -45,6 +51,9 @@ namespace NzbDrone.Core.Notifications.Plex var client = GetPlexServerClient(settings); var response = client.Execute(request); + + CheckForError(response.Content); + _logger.Trace("Update response: {0}", response.Content); } private String Authenticate(string username, string password) @@ -53,6 +62,7 @@ namespace NzbDrone.Core.Notifications.Plex var client = GetMyPlexClient(username, password); var response = client.Execute(request); + CheckForError(response.Content); var user = Json.Deserialize<PlexUser>(JObject.Parse(response.Content).SelectToken("user").ToString()); From 69567de9a2b950cb1704f93ddb1588d85af1c956 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 4 Jun 2014 21:54:40 -0700 Subject: [PATCH 28/59] Some additional release group parsing tests --- .../ParserTests/ReleaseGroupParserFixture.cs | 4 ++++ src/NzbDrone.Core/Parser/Parser.cs | 18 +++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index d147bc47b..06a95be1b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -20,6 +20,10 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The.Walking.Dead.S04E13.720p.WEB-DL.AAC2.0.H.264-Cyphanix", "Cyphanix")] [TestCase("Arrow.S02E01.720p.WEB-DL.DD5.1.H.264.mkv", "DRONE")] [TestCase("Series Title S01E01 Episode Title", "DRONE")] + [TestCase("The Colbert Report - 2014-06-02 - Thomas Piketty.mkv", "DRONE")] + [TestCase("Real Time with Bill Maher S12E17 May 23, 2014.mp4", "DRONE")] + [TestCase("Reizen Waes - S01E08 - Transistrië, Zuid-Ossetië en Abchazië SDTV.avi", "DRONE")] + [TestCase("Simpsons 10x11 - Wild Barts Cant Be Broken [rl].avi", "DRONE")] public void should_parse_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 528b6ae27..f9345db2b 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -284,18 +284,22 @@ namespace NzbDrone.Core.Parser title = title.TrimEnd("-RP"); - string group; var matches = ReleaseGroupRegex.Matches(title); + if (matches.Count != 0) { - group = matches.OfType<Match>().Last().Groups["releasegroup"].Value; - } - else - { - return defaultReleaseGroup; + var group = matches.OfType<Match>().Last().Groups["releasegroup"].Value; + int groupIsNumeric; + + if (Int32.TryParse(group, out groupIsNumeric)) + { + return defaultReleaseGroup; + } + + return group; } - return group; + return defaultReleaseGroup; } public static string RemoveFileExtension(string title) From 6ce45b8c09104d45d439c7a9e7395be30d269466 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 5 Jun 2014 07:09:29 -0700 Subject: [PATCH 29/59] Plex proxy logging --- src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index d9c1102b4..d0355835f 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.Notifications.Plex var response = client.Execute(request); CheckForError(response.Content); - _logger.Trace("Sections response: {0}", response.Content); + _logger.Debug("Sections response: {0}", response.Content); return Json.Deserialize<PlexMediaContainer>(response.Content) .Directories @@ -53,7 +53,7 @@ namespace NzbDrone.Core.Notifications.Plex var response = client.Execute(request); CheckForError(response.Content); - _logger.Trace("Update response: {0}", response.Content); + _logger.Debug("Update response: {0}", response.Content); } private String Authenticate(string username, string password) @@ -64,6 +64,7 @@ namespace NzbDrone.Core.Notifications.Plex var response = client.Execute(request); CheckForError(response.Content); + _logger.Debug("Authentication Response: {0}", response.Content); var user = Json.Deserialize<PlexUser>(JObject.Parse(response.Content).SelectToken("user").ToString()); @@ -91,7 +92,6 @@ namespace NzbDrone.Core.Notifications.Plex request.AddHeader("X-Plex-Version", "0"); return request; - } private RestClient GetPlexServerClient(PlexServerSettings settings) From 5d5b12d80ef665356994158a93e895f74e6f0ba3 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 5 Jun 2014 15:36:01 -0700 Subject: [PATCH 30/59] Fixed: Updater being passed invalid path in some cases --- .../ParserTests/ReleaseGroupParserFixture.cs | 1 - src/NzbDrone.Core/Update/InstallUpdateService.cs | 4 ++-- src/NzbDrone.sln | 7 +------ 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 06a95be1b..2b7bd476b 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -4,7 +4,6 @@ using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ParserTests { - [TestFixture] public class ReleaseGroupParserFixture : CoreTest { diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 404ac8f12..17d32991b 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -128,7 +128,7 @@ namespace NzbDrone.Core.Update _diskProvider.DeleteFolder(_appFolderInfo.GetUpdateClientFolder(), true); _logger.ProgressInfo("Starting update script: {0}", _configFileProvider.UpdateScriptPath); - _processProvider.Start(scriptPath, GetUpdaterArgs(updateSandboxFolder.WrapInQuotes())); + _processProvider.Start(scriptPath, GetUpdaterArgs(updateSandboxFolder)); } private string GetUpdaterArgs(string updateSandboxFolder) @@ -136,7 +136,7 @@ namespace NzbDrone.Core.Update var processId = _processProvider.GetCurrentProcess().Id.ToString(); var executingApplication = _runtimeInfo.ExecutingApplication; - return String.Join(" ", processId, updateSandboxFolder.WrapInQuotes(), executingApplication.WrapInQuotes()); + return String.Join(" ", processId, updateSandboxFolder.TrimEnd(Path.DirectorySeparatorChar).WrapInQuotes(), executingApplication.WrapInQuotes()); } public void Execute(ApplicationUpdateCommand message) diff --git a/src/NzbDrone.sln b/src/NzbDrone.sln index 095cae5e8..b6ba578db 100644 --- a/src/NzbDrone.sln +++ b/src/NzbDrone.sln @@ -1,8 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30110.0 -MinimumVisualStudioVersion = 10.0.40219.1 +# Visual Studio 2012 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}" @@ -73,9 +71,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{0F0D4998-8F5D-4467-A909-BB192C4B3B4B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4EACDBBC-BCD7-4765-A57B-3E08331E4749}" - ProjectSection(SolutionItems) = preProject - NzbDrone.Common.Test\ServiceFactoryFixture.cs = NzbDrone.Common.Test\ServiceFactoryFixture.cs - EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows.Test", "NzbDrone.Windows.Test\NzbDrone.Windows.Test.csproj", "{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}" EndProject From cdb595a3cddcdf9e0a1b4e7a3f83fceae48cf87b Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 5 Jun 2014 15:40:19 -0700 Subject: [PATCH 31/59] Only accept the PID for now in the updater, ignore everything else --- src/NzbDrone.Update/UpdateApp.cs | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index 1cc323e67..9d994233f 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -71,30 +71,30 @@ namespace NzbDrone.Update ProcessId = ParseProcessId(args[0]) }; - if (args.Count() == 1) - { - return startupContext; - } - - else if (args.Count() == 3) - { - startupContext.UpdateLocation = args[1]; - startupContext.ExecutingApplication = args[2]; - } - - else - { - logger.Debug("Arguments:"); - - foreach (var arg in args) - { - logger.Debug(" {0}", arg); - } - - var message = String.Format("Number of arguments are unexpected, expected: 3, found: {0}", args.Count()); - - throw new ArgumentOutOfRangeException("args", message); - } +// if (args.Count() == 1) +// { +// return startupContext; +// } +// +// else if (args.Count() == 3) +// { +// startupContext.UpdateLocation = args[1]; +// startupContext.ExecutingApplication = args[2]; +// } +// +// else +// { +// logger.Debug("Arguments:"); +// +// foreach (var arg in args) +// { +// logger.Debug(" {0}", arg); +// } +// +// var message = String.Format("Number of arguments are unexpected, expected: 3, found: {0}", args.Count()); +// +// throw new ArgumentOutOfRangeException("args", message); +// } return startupContext; } From ddbcb883ca7562c0735ea7b493da4262fc553219 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 5 Jun 2014 16:28:28 -0700 Subject: [PATCH 32/59] Updating on mono --- src/NzbDrone.Update/UpdateApp.cs | 51 +++++++++++++++++--------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index 9d994233f..0631f9592 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -71,30 +71,33 @@ namespace NzbDrone.Update ProcessId = ParseProcessId(args[0]) }; -// if (args.Count() == 1) -// { -// return startupContext; -// } -// -// else if (args.Count() == 3) -// { -// startupContext.UpdateLocation = args[1]; -// startupContext.ExecutingApplication = args[2]; -// } -// -// else -// { -// logger.Debug("Arguments:"); -// -// foreach (var arg in args) -// { -// logger.Debug(" {0}", arg); -// } -// -// var message = String.Format("Number of arguments are unexpected, expected: 3, found: {0}", args.Count()); -// -// throw new ArgumentOutOfRangeException("args", message); -// } + if (OsInfo.IsMono) + { + if (args.Count() == 1) + { + return startupContext; + } + + else if (args.Count() == 3) + { + startupContext.UpdateLocation = args[1]; + startupContext.ExecutingApplication = args[2]; + } + + else + { + logger.Debug("Arguments:"); + + foreach (var arg in args) + { + logger.Debug(" {0}", arg); + } + + var message = String.Format("Number of arguments are unexpected, expected: 3, found: {0}", args.Count()); + + throw new ArgumentOutOfRangeException("args", message); + } + } return startupContext; } From 519b6debfb415c05e44abce702264bfdbeb64c9c Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 5 Jun 2014 23:38:05 -0700 Subject: [PATCH 33/59] Fixed validation that was causing add existing series to fail --- .../ReleaseIntegrationTest.cs | 5 -- .../RootFolders/RootFolderLayoutTemplate.html | 18 ++++---- src/UI/AddSeries/addSeries.less | 4 ++ src/UI/Mixins/AsValidatedView.js | 46 +++++++++++++------ .../Settings/Indexers/Edit/IndexerEditView.js | 4 -- src/UI/jQuery/jquery.validation.js | 4 +- 6 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs b/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs index a97bdf3b8..a4e1e4d40 100644 --- a/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs @@ -10,17 +10,13 @@ namespace NzbDrone.Integration.Test [Test] public void should_only_have_unknown_series_releases() { - var releases = Releases.All(); var indexers = Indexers.All(); - releases.Should().OnlyContain(c => c.Rejections.Contains("Unknown Series")); releases.Should().OnlyContain(c => BeValidRelease(c)); } - - private bool BeValidRelease(ReleaseResource releaseResource) { releaseResource.Age.Should().BeGreaterOrEqualTo(-1); @@ -33,6 +29,5 @@ namespace NzbDrone.Integration.Test return true; } - } } \ No newline at end of file diff --git a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html index 2e16ae6e3..1d0822902 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html +++ b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.html @@ -8,14 +8,16 @@ <div class="validation-errors"></div> <div class="alert alert-info">Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> - <div class="input-group x-path form-group"> - <span class="input-group-addon"> <i class="icon-folder-open"></i></span> - <input class="col-md-9 form-control" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> - <span class="input-group-btn "> - <button class="btn btn-success x-add"> - <i class="icon-ok"/> - </button> - </span> + <div class="form-group"> + <div class="input-group x-path"> + <span class="input-group-addon"> <i class="icon-folder-open"></i></span> + <input class="col-md-9 form-control" type="text" validation-name="path" placeholder="Enter path to folder that contains your shows"> + <span class="input-group-btn "> + <button class="btn btn-success x-add"> + <i class="icon-ok"/> + </button> + </span> + </div> </div> {{#if items}} diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index ee287bbac..0fa784a19 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -130,4 +130,8 @@ li.add-new:hover { overflow: auto; max-height: 300px; } + + .validation-errors { + display: none; + } } diff --git a/src/UI/Mixins/AsValidatedView.js b/src/UI/Mixins/AsValidatedView.js index 7a6cea293..203efa06b 100644 --- a/src/UI/Mixins/AsValidatedView.js +++ b/src/UI/Mixins/AsValidatedView.js @@ -12,12 +12,18 @@ define( var originalBeforeClose = this.prototype.onBeforeClose; var errorHandler = function (response) { - this.model.trigger('validation:failed', response); + if (this.model) { + this.model.trigger('validation:failed', response); + } + + else { + this.trigger('validation:failed', response); + } }; var validatedSync = function (method, model, options) { model.trigger('validation:sync'); -// this.$el.removeAllErrors(); + arguments[2].isValidatedCall = true; return model._originalSync.apply(this, arguments).fail(errorHandler.bind(this)); }; @@ -30,22 +36,34 @@ define( } }; + var validationFailed = function (response) { + if (response.status === 400) { + + var view = this; + var validationErrors = JSON.parse(response.responseText); + _.each(validationErrors, function (error) { + view.$el.processServerError(error); + }); + } + }; + this.prototype.onRender = function () { - this.listenTo(this.model, 'validation:sync', function () { - this.$el.removeAllErrors(); - }); + if (this.model) { + this.listenTo(this.model, 'validation:sync', function () { + this.$el.removeAllErrors(); + }); - this.listenTo(this.model, 'validation:failed', function (response) { - if (response.status === 400) { + this.listenTo(this.model, 'validation:failed', validationFailed); + } - var view = this; - var validationErrors = JSON.parse(response.responseText); - _.each(validationErrors, function (error) { - view.$el.processServerError(error); - }); - } - }); + else { + this.listenTo(this, 'validation:sync', function () { + this.$el.removeAllErrors(); + }); + + this.listenTo(this, 'validation:failed', validationFailed); + } Validation.bind(this); this.bindToModelValidation = bindToModel.bind(this); diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditView.js b/src/UI/Settings/Indexers/Edit/IndexerEditView.js index 004750332..4a77072d7 100644 --- a/src/UI/Settings/Indexers/Edit/IndexerEditView.js +++ b/src/UI/Settings/Indexers/Edit/IndexerEditView.js @@ -30,10 +30,6 @@ define([ this.targetCollection = options.targetCollection; }, - onBeforeClose: function () { - window.alert('closing down!'); - }, - _save: function () { var self = this; var promise = this.model.save(); diff --git a/src/UI/jQuery/jquery.validation.js b/src/UI/jQuery/jquery.validation.js index 4f836f657..210f94d69 100644 --- a/src/UI/jQuery/jquery.validation.js +++ b/src/UI/jQuery/jquery.validation.js @@ -9,7 +9,7 @@ define( var validationName = error.propertyName.toLowerCase(); this.find('.validation-errors') - .addClass('alert alert-error') + .addClass('alert alert-danger') .append('<div><i class="icon-exclamation-sign"></i>' + error.errorMessage + '</div>'); var input = this.find('[name]').filter(function () { @@ -65,7 +65,7 @@ define( $.fn.removeAllErrors = function () { this.find('.error').removeClass('error'); - this.find('.validation-errors').removeClass('alert').removeClass('alert-error').html(''); + this.find('.validation-errors').removeClass('alert').removeClass('alert-danger').html(''); this.find('.validation-error').remove(); return this.find('.help-inline.error-message').remove(); }; From ebd13bdda8791e71cfd92cd85bb1983ee8cd9425 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sat, 7 Jun 2014 12:19:37 -0700 Subject: [PATCH 34/59] New: Health check for AppData and Startup folder conflict --- .../Checks/AppDataLocationFixture.cs | 54 +++++++++++++++++++ .../HealthCheck/Checks/UpdateCheckFixture.cs | 23 +++++++- .../NzbDrone.Core.Test.csproj | 1 + .../Checks/AppDataLocationCheck.cs | 34 ++++++++++++ .../HealthCheck/Checks/UpdateCheck.cs | 14 +++-- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Update/InstallUpdateService.cs | 11 ++++ 7 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 src/NzbDrone.Core.Test/HealthCheck/Checks/AppDataLocationFixture.cs create mode 100644 src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/AppDataLocationFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/AppDataLocationFixture.cs new file mode 100644 index 000000000..ade6278e3 --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/AppDataLocationFixture.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class AppDataLocationFixture : CoreTest<AppDataLocationCheck> + { + [Test] + public void should_return_warning_when_app_data_is_child_of_startup_folder() + { + Mocker.GetMock<IAppFolderInfo>() + .Setup(s => s.StartUpFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Mocker.GetMock<IAppFolderInfo>() + .Setup(s => s.AppDataFolder) + .Returns(@"C:\NzbDrone\AppData".AsOsAgnostic()); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_warning_when_app_data_is_same_as_startup_folder() + { + Mocker.GetMock<IAppFolderInfo>() + .Setup(s => s.StartUpFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Mocker.GetMock<IAppFolderInfo>() + .Setup(s => s.AppDataFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Subject.Check().ShouldBeWarning(); + } + + [Test] + public void should_return_ok_when_no_conflict() + { + Mocker.GetMock<IAppFolderInfo>() + .Setup(s => s.StartUpFolder) + .Returns(@"C:\NzbDrone".AsOsAgnostic()); + + Mocker.GetMock<IAppFolderInfo>() + .Setup(s => s.AppDataFolder) + .Returns(@"C:\ProgramData\NzbDrone".AsOsAgnostic()); + + Subject.Check().ShouldBeOk(); + } + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs index 7e6fd213c..68123676a 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/UpdateCheckFixture.cs @@ -1,10 +1,9 @@ using System; -using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.HealthCheck; +using NzbDrone.Core.Configuration; using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.Test.Framework; @@ -28,5 +27,25 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks Subject.Check().ShouldBeError(); } + + [Test] + public void should_return_error_when_app_folder_is_write_protected_and_update_automatically_is_enabled() + { + MonoOnly(); + + Mocker.GetMock<IConfigFileProvider>() + .Setup(s => s.UpdateAutomatically) + .Returns(true); + + Mocker.GetMock<IAppFolderInfo>() + .Setup(s => s.StartUpFolder) + .Returns(@"/opt/nzbdrone"); + + Mocker.GetMock<IDiskProvider>() + .Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>())) + .Throws<Exception>(); + + Subject.Check().ShouldBeError(); + } } } diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 862f9cd46..818726309 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -126,6 +126,7 @@ <Compile Include="Framework\CoreTest.cs" /> <Compile Include="Framework\DbTest.cs" /> <Compile Include="Framework\NBuilderExtensions.cs" /> + <Compile Include="HealthCheck\Checks\AppDataLocationFixture.cs" /> <Compile Include="HealthCheck\Checks\RootFolderCheckFixture.cs" /> <Compile Include="HealthCheck\Checks\DownloadClientCheckFixture.cs" /> <Compile Include="HealthCheck\Checks\ImportMechanismCheckFixture.cs" /> diff --git a/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs new file mode 100644 index 000000000..7567ef095 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/AppDataLocationCheck.cs @@ -0,0 +1,34 @@ +using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + public class AppDataLocationCheck : HealthCheckBase + { + private readonly IAppFolderInfo _appFolderInfo; + + public AppDataLocationCheck(IAppFolderInfo appFolderInfo) + { + _appFolderInfo = appFolderInfo; + } + + public override HealthCheck Check() + { + if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) || + _appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder)) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Updating will not be possible to prevent deleting AppData on Update"); + } + + return new HealthCheck(GetType()); + } + + public override bool CheckOnConfigChange + { + get + { + return false; + } + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index 350595138..704eb2799 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -2,6 +2,7 @@ using System.IO; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; namespace NzbDrone.Core.HealthCheck.Checks @@ -11,19 +12,22 @@ namespace NzbDrone.Core.HealthCheck.Checks private readonly IDiskProvider _diskProvider; private readonly IAppFolderInfo _appFolderInfo; private readonly ICheckUpdateService _checkUpdateService; + private readonly IConfigFileProvider _configFileProvider; - public UpdateCheck(IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, ICheckUpdateService checkUpdateService) + public UpdateCheck(IDiskProvider diskProvider, + IAppFolderInfo appFolderInfo, + ICheckUpdateService checkUpdateService, + IConfigFileProvider configFileProvider) { _diskProvider = diskProvider; _appFolderInfo = appFolderInfo; _checkUpdateService = checkUpdateService; + _configFileProvider = configFileProvider; } - - + public override HealthCheck Check() { - //TODO: Check on mono as well - if (OsInfo.IsWindows) + if (OsInfo.IsWindows || (OsInfo.IsMono && _configFileProvider.UpdateAutomatically)) { try { diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 76f49c4cd..7dcfdebe4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -285,6 +285,7 @@ <Compile Include="Exceptions\NzbDroneClientException.cs" /> <Compile Include="Exceptions\StatusCodeToExceptions.cs" /> <Compile Include="HealthCheck\CheckHealthCommand.cs" /> + <Compile Include="HealthCheck\Checks\AppDataLocationCheck.cs" /> <Compile Include="HealthCheck\Checks\DownloadClientCheck.cs" /> <Compile Include="HealthCheck\Checks\ImportMechanismCheck.cs" /> <Compile Include="HealthCheck\Checks\MonoVersionCheck.cs" /> diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 17d32991b..0e89f3116 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -60,6 +60,8 @@ namespace NzbDrone.Core.Update { try { + EnsureAppDataSafety(); + var updateSandboxFolder = _appFolderInfo.GetUpdateSandboxFolder(); var packageDestination = Path.Combine(updateSandboxFolder, updatePackage.FileName); @@ -139,6 +141,15 @@ namespace NzbDrone.Core.Update return String.Join(" ", processId, updateSandboxFolder.TrimEnd(Path.DirectorySeparatorChar).WrapInQuotes(), executingApplication.WrapInQuotes()); } + private void EnsureAppDataSafety() + { + if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) || + _appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder)) + { + throw new NotSupportedException("Update will cause AppData to be deleted, correct you configuration before proceeding"); + } + } + public void Execute(ApplicationUpdateCommand message) { _logger.ProgressDebug("Checking for updates"); From d2615cd8319120e2e50b72d5e1d832bf60c05fcc Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sat, 7 Jun 2014 12:31:24 -0700 Subject: [PATCH 35/59] Fixed update tests --- .../UpdateTests/UpdateServiceFixture.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index e01a08761..e08094aa2 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -49,6 +49,9 @@ namespace NzbDrone.Core.Test.UpdateTests } Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.TempFolder).Returns(TempFolder); + Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\ProgramData\NzbDrone".AsOsAgnostic); + Mocker.GetMock<ICheckUpdateService>().Setup(c => c.AvailableUpdate()).Returns(_updatePackage); Mocker.GetMock<IVerifyUpdates>().Setup(c => c.Verify(It.IsAny<UpdatePackage>(), It.IsAny<String>())).Returns(true); @@ -101,7 +104,6 @@ namespace NzbDrone.Core.Test.UpdateTests Subject.Execute(new ApplicationUpdateCommand()); - Mocker.GetMock<IHttpProvider>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive)); } @@ -112,7 +114,6 @@ namespace NzbDrone.Core.Test.UpdateTests Subject.Execute(new ApplicationUpdateCommand()); - Mocker.GetMock<IArchiveService>().Verify(c => c.Extract(updateArchive, _sandboxFolder)); } @@ -239,6 +240,26 @@ namespace NzbDrone.Core.Test.UpdateTests updateSubFolder.GetFiles().Should().NotBeEmpty(); } + [Test] + public void should_log_error_when_app_data_is_child_of_startup_folder() + { + Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone\AppData".AsOsAgnostic); + + Subject.Execute(new ApplicationUpdateCommand()); + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_log_error_when_app_data_is_same_as_startup_folder() + { + Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); + + Subject.Execute(new ApplicationUpdateCommand()); + ExceptionVerification.ExpectedErrors(1); + } + [TearDown] public void TearDown() { From 17482cb6c1b3c310162cf1399ca8e4ca7a899e0f Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 7 Jun 2014 12:40:33 +0200 Subject: [PATCH 36/59] Fixed: Processing more than 3 concurrent Automatic Searches should no longer freeze on mono. --- src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index 52534d5a5..6fb79379a 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -77,8 +77,12 @@ namespace NzbDrone.Core.Messaging.Commands _trackCommands.Store(command); + // TODO: We should use async await (once we get 4.5) or normal Task Continuations on Command processing to prevent blocking the TaskScheduler. + // For now we use TaskCreationOptions 0x10, which is actually .net 4.5 HideScheduler. + // This will detach the scheduler from the thread, causing new Task creating in the command to be executed on the ThreadPool, avoiding a deadlock. + // Please note that the issue only shows itself on mono because since Microsoft .net implementation supports Task inlining on WaitAll. _taskFactory.StartNew(() => ExecuteCommand<TCommand>(command) - , TaskCreationOptions.PreferFairness) + , TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10) .LogExceptions(); return command; From b72c9b338cd0723f013d272ad4049652e57832cf Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Fri, 6 Jun 2014 07:55:38 +0200 Subject: [PATCH 37/59] Changed health check to send user to appropriate scenario on Wiki. Also added support to Nzbget to get the Category config. --- .../Download/Clients/Nzbget/Nzbget.cs | 58 ++++++++++++++++++- .../Download/Clients/Nzbget/NzbgetCategory.cs | 16 +++++ .../Clients/Nzbget/NzbgetConfigItem.cs | 13 +++++ .../Download/Clients/Nzbget/NzbgetProxy.cs | 9 +++ .../Download/Clients/Pneumatic/Pneumatic.cs | 10 ++++ .../Download/Clients/Sabnzbd/Sabnzbd.cs | 10 ++++ .../UsenetBlackhole/UsenetBlackhole.cs | 9 +++ .../Download/DownloadClientBase.cs | 1 + .../Download/DownloadClientStatus.cs | 13 +++++ src/NzbDrone.Core/Download/IDownloadClient.cs | 2 + .../Checks/ImportMechanismCheck.cs | 49 ++++++++++++++-- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 + 12 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetCategory.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetConfigItem.cs create mode 100644 src/NzbDrone.Core/Download/DownloadClientStatus.cs diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 2fb35f2a0..11b84cf4e 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -1,6 +1,7 @@ using System; -using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Collections.Generic; using NLog; using NzbDrone.Common; using NzbDrone.Common.Http; @@ -213,9 +214,55 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _proxy.RetryDownload(id, Settings); } - public override void Test() + public override DownloadClientStatus GetStatus() { - _proxy.GetVersion(Settings); + var config = _proxy.GetConfig(Settings); + + var category = GetCategories(config).FirstOrDefault(v => v.Name == Settings.TvCategory); + + var status = new DownloadClientStatus + { + IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" + }; + + if (category != null) + { + status.OutputRootFolders = new List<string> { category.DestDir }; + } + + return status; + } + + protected IEnumerable<NzbgetCategory> GetCategories(Dictionary<String, String> config) + { + for (int i = 1; i < 100; i++) + { + var name = config.GetValueOrDefault("Category" + i + ".Name"); + + if (name == null) yield break; + + var destDir = config.GetValueOrDefault("Category" + i + ".DestDir"); + + if (destDir.IsNullOrWhiteSpace()) + { + var mainDir = config.GetValueOrDefault("MainDir"); + destDir = config.GetValueOrDefault("DestDir", String.Empty).Replace("${MainDir}", mainDir); + + if (config.GetValueOrDefault("AppendCategoryDir", "yes") == "yes") + { + destDir = Path.Combine(destDir, name); + } + } + + yield return new NzbgetCategory + { + Name = name, + DestDir = destDir, + Unpack = config.GetValueOrDefault("Category" + i + ".Unpack") == "yes", + DefScript = config.GetValueOrDefault("Category" + i + ".DefScript"), + Aliases = config.GetValueOrDefault("Category" + i + ".Aliases"), + }; + } } private String GetVersion(string host = null, int port = 0, string username = null, string password = null) @@ -223,6 +270,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return _proxy.GetVersion(Settings); } + public override void Test() + { + _proxy.GetVersion(Settings); + } + public void Execute(TestNzbgetCommand message) { var settings = new NzbgetSettings(); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetCategory.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetCategory.cs new file mode 100644 index 000000000..8a0615ad7 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetCategory.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetCategory + { + public String Name { get; set; } + public String DestDir { get; set; } + public Boolean Unpack { get; set; } + public String DefScript { get; set; } + public String Aliases { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetConfigItem.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetConfigItem.cs new file mode 100644 index 000000000..e04aad77f --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetConfigItem.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download.Clients.Nzbget +{ + public class NzbgetConfigItem + { + public String Name { get; set; } + public String Value { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 5d6e34bb6..714f3eb81 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget List<NzbgetPostQueueItem> GetPostQueue(NzbgetSettings settings); List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings); String GetVersion(NzbgetSettings settings); + Dictionary<String, String> GetConfig(NzbgetSettings settings); void RemoveFromHistory(string id, NzbgetSettings settings); void RetryDownload(string id, NzbgetSettings settings); } @@ -98,6 +99,14 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return Json.Deserialize<NzbgetResponse<String>>(ProcessRequest(request, settings)).Version; } + public Dictionary<String, String> GetConfig(NzbgetSettings settings) + { + var request = BuildRequest(new JsonRequest("config")); + + return Json.Deserialize<NzbgetResponse<List<NzbgetConfigItem>>>(ProcessRequest(request, settings)).Result.ToDictionary(v => v.Name, v => v.Value); + } + + public void RemoveFromHistory(string id, NzbgetSettings settings) { var history = GetHistory(settings); diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 2753d9056..489acc1d3 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -90,6 +90,16 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic throw new NotSupportedException(); } + public override DownloadClientStatus GetStatus() + { + var status = new DownloadClientStatus + { + IsLocalhost = true + }; + + return status; + } + public override void Test() { PerformTest(Settings.NzbFolder); diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 86c690dd2..31dec7669 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -208,6 +208,16 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd _proxy.RetryDownload(id, Settings); } + public override DownloadClientStatus GetStatus() + { + var status = new DownloadClientStatus + { + IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" + }; + + return status; + } + public override void Test() { _proxy.GetCategories(Settings); diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index 036be4338..0bddc8b2d 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -142,6 +142,15 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole PerformTest(Settings.WatchFolder); } + public override DownloadClientStatus GetStatus() + { + return new DownloadClientStatus + { + IsLocalhost = true, + OutputRootFolders = new List<string> { Settings.WatchFolder } + }; + } + private void PerformTest(string folder) { var testPath = Path.Combine(folder, "drone_test.txt"); diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 453dcc479..0b4da9036 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -67,6 +67,7 @@ namespace NzbDrone.Core.Download public abstract void RemoveItem(string id); public abstract void RetryDownload(string id); public abstract void Test(); + public abstract DownloadClientStatus GetStatus(); protected RemoteEpisode GetRemoteEpisode(String title) { diff --git a/src/NzbDrone.Core/Download/DownloadClientStatus.cs b/src/NzbDrone.Core/Download/DownloadClientStatus.cs new file mode 100644 index 000000000..ef4f71b38 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientStatus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Download +{ + public class DownloadClientStatus + { + public Boolean IsLocalhost { get; set; } + public List<String> OutputRootFolders { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index aab29cde8..7850ca90b 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -14,5 +14,7 @@ namespace NzbDrone.Core.Download void RemoveItem(string id); void RetryDownload(string id); void Test(); + + DownloadClientStatus GetStatus(); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs index d78b97d93..1234b8730 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs @@ -5,37 +5,74 @@ using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Core.Configuration; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Sabnzbd; +using NzbDrone.Core.Download.Clients.Nzbget; namespace NzbDrone.Core.HealthCheck.Checks { public class ImportMechanismCheck : HealthCheckBase { private readonly IConfigService _configService; + private readonly IProvideDownloadClient _provideDownloadClient; private readonly IDownloadTrackingService _downloadTrackingService; - public ImportMechanismCheck(IConfigService configService, IDownloadTrackingService downloadTrackingService) + public ImportMechanismCheck(IConfigService configService, IProvideDownloadClient provideDownloadClient, IDownloadTrackingService downloadTrackingService) { _configService = configService; + _provideDownloadClient = provideDownloadClient; _downloadTrackingService = downloadTrackingService; } public override HealthCheck Check() { + var droneFactoryFolder = _configService.DownloadedEpisodesFolder; + var downloadClients = _provideDownloadClient.GetDownloadClients().Select(v => new { downloadClient = v, status = v.GetStatus() }).ToList(); + + var downloadClientIsLocalHost = downloadClients.All(v => v.status.IsLocalhost); + var downloadClientOutputInDroneFactory = !droneFactoryFolder.IsNullOrWhiteSpace() + && downloadClients.Any(v => v.status.OutputRootFolders != null && v.status.OutputRootFolders.Contains(droneFactoryFolder, PathEqualityComparer.Instance)); + if (!_configService.IsDefined("EnableCompletedDownloadHandling")) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Completed Download Handling is disabled"); - } + // Migration helper logic + if (!downloadClientIsLocalHost) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Multi-Computer unsupported)", "Migrating-to-Completed-Download-Handling#Unsupported-download-client-on-different-computer"); + } - var droneFactoryFolder = _configService.DownloadedEpisodesFolder; + if (downloadClients.All(v => v.downloadClient is Sabnzbd)) + { + // With Sabnzbd we cannot check the category settings. + + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Sabnzbd)", "Migrating-to-Completed-Download-Handling#sabnzbd-enable-completed-download-handling"); + } + else if (downloadClients.All(v => v.downloadClient is Nzbget)) + { + // With Nzbget we can check if the category should be changed. + if (downloadClientOutputInDroneFactory) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget - Conflicting Category)", "Migrating-to-Completed-Download-Handling#nzbget-conflicting-download-client-category"); + } + + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible (Nzbget)", "Migrating-to-Completed-Download-Handling#nzbget-enable-completed-download-handling"); + } + else + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling if possible", "Migrating-to-Completed-Download-Handling"); + } + } 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))) + if (_configService.EnableCompletedDownloadHandling && !droneFactoryFolder.IsNullOrWhiteSpace()) { - return new HealthCheck(GetType(), HealthCheckResult.Warning, "Download Client has history items in Drone Factory conflicting with Completed Download Handling"); + if (_downloadTrackingService.GetCompletedDownloads().Any(v => droneFactoryFolder.PathEquals(v.DownloadItem.OutputPath) || droneFactoryFolder.IsParentPath(v.DownloadItem.OutputPath))) + { + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Completed Download Handling conflict with Drone Factory (Conflicting History Item)", "Migrating-to-Completed-Download-Handling#conflicting-download-client-category"); + } } return new HealthCheck(GetType()); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 7dcfdebe4..63c970219 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -240,6 +240,8 @@ <Compile Include="DecisionEngine\Specifications\RssSync\HistorySpecification.cs" /> <Compile Include="DiskSpace\DiskSpace.cs" /> <Compile Include="DiskSpace\DiskSpaceService.cs" /> + <Compile Include="Download\Clients\Nzbget\NzbgetCategory.cs" /> + <Compile Include="Download\Clients\Nzbget\NzbgetConfigItem.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetGlobalStatus.cs" /> <Compile Include="Download\Clients\Nzbget\NzbgetPostQueueItem.cs" /> <Compile Include="Download\Clients\Sabnzbd\SabnzbdDownloadStatus.cs" /> @@ -271,6 +273,7 @@ <Compile Include="Download\Clients\Sabnzbd\SabnzbdProxy.cs" /> <Compile Include="Download\CheckForFinishedDownloadCommand.cs" /> <Compile Include="Download\DownloadClientItem.cs" /> + <Compile Include="Download\DownloadClientStatus.cs" /> <Compile Include="Download\FailedDownloadService.cs" /> <Compile Include="Download\DownloadItemStatus.cs" /> <Compile Include="Download\TrackedDownload.cs" /> From 231128e01c31e0de67e3d0ee0a70b95b37695b96 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sat, 7 Jun 2014 22:18:45 +0200 Subject: [PATCH 38/59] Changed code to avoid duplicates making their way into the TrackedDownload cache. --- src/NzbDrone.Core/Download/DownloadTrackingService.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Download/DownloadTrackingService.cs b/src/NzbDrone.Core/Download/DownloadTrackingService.cs index 78bb5ec5f..463c06d97 100644 --- a/src/NzbDrone.Core/Download/DownloadTrackingService.cs +++ b/src/NzbDrone.Core/Download/DownloadTrackingService.cs @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Download var downloadClients = _downloadClientProvider.GetDownloadClients(); var oldTrackedDownloads = GetTrackedDownloads().ToDictionary(v => v.TrackingId); - var newTrackedDownloads = new List<TrackedDownload>(); + var newTrackedDownloads = new Dictionary<String, TrackedDownload>(); var stateChanged = false; @@ -117,6 +117,8 @@ namespace NzbDrone.Core.Download var trackingId = String.Format("{0}-{1}", downloadClient.Definition.Id, downloadItem.DownloadClientId); TrackedDownload trackedDownload; + if (newTrackedDownloads.ContainsKey(trackingId)) continue; + if (!oldTrackedDownloads.TryGetValue(trackingId, out trackedDownload)) { trackedDownload = new TrackedDownload @@ -133,11 +135,11 @@ namespace NzbDrone.Core.Download trackedDownload.DownloadItem = downloadItem; - newTrackedDownloads.Add(trackedDownload); + newTrackedDownloads[trackingId] = trackedDownload; } } - foreach (var downloadItem in oldTrackedDownloads.Values.Except(newTrackedDownloads)) + foreach (var downloadItem in oldTrackedDownloads.Values.Where(v => !newTrackedDownloads.ContainsKey(v.TrackingId))) { if (downloadItem.State != TrackedDownloadState.Removed) { @@ -150,7 +152,7 @@ namespace NzbDrone.Core.Download _logger.Trace("Stopped tracking download: {0}: {1}", downloadItem.TrackingId, downloadItem.DownloadItem.Title); } - _trackedDownloadCache.Set("tracked", newTrackedDownloads.ToArray()); + _trackedDownloadCache.Set("tracked", newTrackedDownloads.Values.ToArray()); return stateChanged; } From 5b5cc485498b17efa9a1ad7303ae14f328e2d51a Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sat, 7 Jun 2014 20:34:06 -0700 Subject: [PATCH 39/59] Reverted mono fix because it breaks on .net 4 --- src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index 6fb79379a..4ad89fe5e 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -82,7 +82,9 @@ namespace NzbDrone.Core.Messaging.Commands // This will detach the scheduler from the thread, causing new Task creating in the command to be executed on the ThreadPool, avoiding a deadlock. // Please note that the issue only shows itself on mono because since Microsoft .net implementation supports Task inlining on WaitAll. _taskFactory.StartNew(() => ExecuteCommand<TCommand>(command) - , TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10) + , TaskCreationOptions.PreferFairness) +// This breaks on systems that don't have .Net 4.5 installed (but works fine when it does, even though we are targetting 4.0) +// , TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10) .LogExceptions(); return command; From 052a3bf47ee722d67cf48392bacc29865e36bc72 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Sun, 8 Jun 2014 09:50:29 +0200 Subject: [PATCH 40/59] Fixed mono fix by checking if the Enum value exists in the runtime. --- .../Messaging/Commands/CommandExecutor.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index 4ad89fe5e..2f3c72ee0 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -81,11 +81,18 @@ namespace NzbDrone.Core.Messaging.Commands // For now we use TaskCreationOptions 0x10, which is actually .net 4.5 HideScheduler. // This will detach the scheduler from the thread, causing new Task creating in the command to be executed on the ThreadPool, avoiding a deadlock. // Please note that the issue only shows itself on mono because since Microsoft .net implementation supports Task inlining on WaitAll. - _taskFactory.StartNew(() => ExecuteCommand<TCommand>(command) - , TaskCreationOptions.PreferFairness) -// This breaks on systems that don't have .Net 4.5 installed (but works fine when it does, even though we are targetting 4.0) -// , TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10) - .LogExceptions(); + if (Enum.IsDefined(typeof(TaskCreationOptions), (TaskCreationOptions)0x10)) + { + _taskFactory.StartNew(() => ExecuteCommand<TCommand>(command) + , TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10) + .LogExceptions(); + } + else + { + _taskFactory.StartNew(() => ExecuteCommand<TCommand>(command) + , TaskCreationOptions.PreferFairness) + .LogExceptions(); + } return command; } From 61ec9aff27dd70e2d7e1d497847af9527957ff67 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 8 Jun 2014 23:03:23 -0700 Subject: [PATCH 41/59] Use sane IDs for XBMC JSON RPC calls --- src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs | 2 +- src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs index ef39eb353..0f933b570 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/JsonApiProvider.cs @@ -215,7 +215,7 @@ namespace NzbDrone.Core.Notifications.Xbmc postJson.Add(new JProperty("params", parameters)); } - postJson.Add(new JProperty("id", DateTime.Now.Ticks)); + postJson.Add(new JProperty("id", 2)); return postJson; } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs index 7e3c236e7..ce94da593 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcService.cs @@ -58,7 +58,7 @@ namespace NzbDrone.Core.Notifications.Xbmc var postJson = new JObject(); postJson.Add(new JProperty("jsonrpc", "2.0")); postJson.Add(new JProperty("method", "JSONRPC.Version")); - postJson.Add(new JProperty("id", DateTime.Now.Ticks)); + postJson.Add(new JProperty("id", 1)); var response = _httpProvider.PostCommand(settings.Address, settings.Username, settings.Password, postJson.ToString()); From d489dff152b77d2869d369b3a49fa2f77212b93d Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 8 Jun 2014 23:49:56 -0700 Subject: [PATCH 42/59] Trying to fix XBMC timeout errors --- src/NzbDrone.Common/Http/NzbDroneWebClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Common/Http/NzbDroneWebClient.cs b/src/NzbDrone.Common/Http/NzbDroneWebClient.cs index bc39ac4cb..ccd369bb7 100644 --- a/src/NzbDrone.Common/Http/NzbDroneWebClient.cs +++ b/src/NzbDrone.Common/Http/NzbDroneWebClient.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Common.Http if (request is HttpWebRequest) { ((HttpWebRequest)request).KeepAlive = false; + ((HttpWebRequest)request).ServicePoint.Expect100Continue = false; } return request; From cdca754b0f1c4a14c15b4af04b445db5a2fe5c0e Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Mon, 9 Jun 2014 12:55:45 +0200 Subject: [PATCH 43/59] Fixed issue with Nzbget client detecting completed downloads when no intermediate directory was used. --- src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 11b84cf4e..6ae144000 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -180,10 +180,6 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { historyItem.Status = DownloadItemStatus.Failed; } - else if (item.MoveStatus != "SUCCESS") - { - historyItem.Status = DownloadItemStatus.Queued; - } historyItems.Add(historyItem); } From 95f4d71c683b025781fa234761e8409319f88f48 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Mon, 9 Jun 2014 18:26:49 -0700 Subject: [PATCH 44/59] Fixed issue trying getting parent of drive --- .../SabnzbdTests/SabnzbdFixture.cs | 13 +++++++++++++ .../Download/Clients/Sabnzbd/Sabnzbd.cs | 2 +- .../Download/Clients/Sabnzbd/SabnzbdProxy.cs | 1 - 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index 9a5760d62..3fd83618e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -268,5 +268,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests result.OutputPath.Should().Be(@"C:\sorted\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); } + + [Test] + public void should_not_blow_up_if_storage_is_drive_root() + { + _completed.Items.First().Storage = @"C:\".AsOsAgnostic(); + + WithQueue(null); + WithHistory(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"C:\".AsOsAgnostic()); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 31dec7669..687752b2c 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -162,7 +162,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (!sabHistoryItem.Storage.IsNullOrWhiteSpace()) { var parent = Directory.GetParent(sabHistoryItem.Storage); - if (parent.Name == sabHistoryItem.Title) + if (parent != null && parent.Name == sabHistoryItem.Title) { historyItem.OutputPath = parent.FullName; } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 52259fd7a..1fe9ce8ec 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -6,7 +6,6 @@ using NzbDrone.Common; using NzbDrone.Common.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; -using NzbDrone.Core.Instrumentation.Extensions; using RestSharp; namespace NzbDrone.Core.Download.Clients.Sabnzbd From e3d0d25da7a1c421afcd7ddb8627309115480393 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Mon, 9 Jun 2014 18:43:20 -0700 Subject: [PATCH 45/59] Fixed: Do not create XBMC Episode Metadata files when setting is off --- src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs index ee3c13e9d..3547fdf52 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Xbmc/XbmcMetadata.cs @@ -210,7 +210,12 @@ namespace NzbDrone.Core.Metadata.Consumers.Xbmc } public override MetadataFileResult EpisodeMetadata(Series series, EpisodeFile episodeFile) - { + { + if (!Settings.EpisodeMetadata) + { + return null; + } + _logger.Debug("Generating Episode Metadata for: {0}", episodeFile.Path); var xmlResult = String.Empty; From 8967f59f35237a1ebe067cbf76be5a2bab3c5720 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Thu, 12 Jun 2014 23:46:08 +0200 Subject: [PATCH 46/59] Fixed: Sabnzbd/Nzbget settings will now fail to save if you entered a non-existing category. --- .../Download/Clients/Nzbget/Nzbget.cs | 15 ++++++++++--- .../Download/Clients/Nzbget/NzbgetProxy.cs | 6 ++++-- .../Download/Clients/Pneumatic/Pneumatic.cs | 12 +++++++---- .../Download/Clients/Sabnzbd/Sabnzbd.cs | 11 +++++++--- .../Download/Clients/Sabnzbd/SabnzbdProxy.cs | 10 +++++---- .../UsenetBlackhole/UsenetBlackhole.cs | 21 +++++++++++-------- .../Download/DownloadClientBase.cs | 3 ++- src/NzbDrone.Core/Download/IDownloadClient.cs | 1 - 8 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 6ae144000..e99382894 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -266,9 +266,18 @@ namespace NzbDrone.Core.Download.Clients.Nzbget return _proxy.GetVersion(Settings); } - public override void Test() + public override void Test(NzbgetSettings settings) { - _proxy.GetVersion(Settings); + _proxy.GetVersion(settings); + + var config = _proxy.GetConfig(settings); + + var categories = GetCategories(config); + + if (!categories.Any(v => v.Name == settings.TvCategory)) + { + throw new ApplicationException("Category does not exist"); + } } public void Execute(TestNzbgetCommand message) @@ -276,7 +285,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var settings = new NzbgetSettings(); settings.InjectFrom(message); - _proxy.GetVersion(settings); + Test(settings); } // Javascript doesn't support 64 bit integers natively so json officially doesn't either. diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 714f3eb81..6f7f50a6f 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget var request = BuildRequest(new JsonRequest("append", parameters)); var response = Json.Deserialize<NzbgetResponse<Boolean>>(ProcessRequest(request, settings)); - _logger.Debug("Queue Response: [{0}]", response.Result); + _logger.Trace("Response: [{0}]", response.Result); if (!response.Result) { @@ -154,7 +154,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget { var client = BuildClient(settings); var response = client.Execute(restRequest); - _logger.Debug("Response: {0}", response.Content); + _logger.Trace("Response: {0}", response.Content); CheckForError(response); @@ -170,6 +170,8 @@ namespace NzbDrone.Core.Download.Clients.Nzbget settings.Host, settings.Port); + _logger.Debug("Url: " + url); + var client = new RestClient(url); client.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password); diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 489acc1d3..e8f0b7a09 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using Omu.ValueInjecter; namespace NzbDrone.Core.Download.Clients.Pneumatic { @@ -100,12 +101,12 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic return status; } - public override void Test() + public override void Test(PneumaticSettings settings) { - PerformTest(Settings.NzbFolder); + PerformWriteTest(settings.NzbFolder); } - private void PerformTest(string folder) + private void PerformWriteTest(string folder) { var testPath = Path.Combine(folder, "drone_test.txt"); _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); @@ -114,7 +115,10 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic public void Execute(TestPneumaticCommand message) { - PerformTest(message.Folder); + var settings = new PneumaticSettings(); + settings.InjectFrom(message); + + Test(settings); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 687752b2c..e14c15336 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -218,9 +218,14 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return status; } - public override void Test() + public override void Test(SabnzbdSettings settings) { - _proxy.GetCategories(Settings); + var categories = _proxy.GetCategories(settings); + + if (!categories.Any(v => v == settings.TvCategory)) + { + throw new ApplicationException("Category does not exist"); + } } public void Execute(TestSabnzbdCommand message) @@ -228,7 +233,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var settings = new SabnzbdSettings(); settings.InjectFrom(message); - _proxy.GetCategories(settings); + Test(settings); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 1fe9ce8ec..00e943721 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Linq; +using System.Collections.Generic; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common; @@ -16,7 +18,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd void RemoveFrom(string source, string id, SabnzbdSettings settings); string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings); SabnzbdVersionResponse GetVersion(SabnzbdSettings settings); - SabnzbdCategoryResponse GetCategories(SabnzbdSettings settings); + List<String> GetCategories(SabnzbdSettings settings); SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings); SabnzbdHistory GetHistory(int start, int limit, SabnzbdSettings settings); void RetryDownload(string id, SabnzbdSettings settings); @@ -83,12 +85,12 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return response; } - public SabnzbdCategoryResponse GetCategories(SabnzbdSettings settings) + public List<String> GetCategories(SabnzbdSettings settings) { var request = new RestRequest(); var action = "mode=get_cats"; - var response = Json.Deserialize<SabnzbdCategoryResponse>(ProcessRequest(request, action, settings)); + var response = Json.Deserialize<SabnzbdCategoryResponse>(ProcessRequest(request, action, settings)).Categories; return response; } @@ -134,7 +136,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd action, authentication); - _logger.Debug(url); + _logger.Debug("Url: " + url); return new RestClient(url); } diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index 0bddc8b2d..290eb8564 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.MediaFiles; +using Omu.ValueInjecter; namespace NzbDrone.Core.Download.Clients.UsenetBlackhole { @@ -136,12 +137,6 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole throw new NotSupportedException(); } - public override void Test() - { - PerformTest(Settings.NzbFolder); - PerformTest(Settings.WatchFolder); - } - public override DownloadClientStatus GetStatus() { return new DownloadClientStatus @@ -151,7 +146,13 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole }; } - private void PerformTest(string folder) + public override void Test(UsenetBlackholeSettings settings) + { + PerformWriteTest(settings.NzbFolder); + PerformWriteTest(settings.WatchFolder); + } + + private void PerformWriteTest(string folder) { var testPath = Path.Combine(folder, "drone_test.txt"); _diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); @@ -160,8 +161,10 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole public void Execute(TestUsenetBlackholeCommand message) { - PerformTest(message.NzbFolder); - PerformTest(message.WatchFolder); + var settings = new UsenetBlackholeSettings(); + settings.InjectFrom(message); + + Test(settings); } } } diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 0b4da9036..2176ba21f 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -66,9 +66,10 @@ namespace NzbDrone.Core.Download public abstract IEnumerable<DownloadClientItem> GetItems(); public abstract void RemoveItem(string id); public abstract void RetryDownload(string id); - public abstract void Test(); public abstract DownloadClientStatus GetStatus(); + public abstract void Test(TSettings settings); + protected RemoteEpisode GetRemoteEpisode(String title) { var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index 7850ca90b..b0b3d5fb9 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -13,7 +13,6 @@ namespace NzbDrone.Core.Download IEnumerable<DownloadClientItem> GetItems(); void RemoveItem(string id); void RetryDownload(string id); - void Test(); DownloadClientStatus GetStatus(); } From fbdc84b6cd62e5b70d5542278b1fb5d5b3210e60 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Tue, 10 Jun 2014 17:07:30 -0700 Subject: [PATCH 47/59] Removed edit button from metadata (click to edit now) --- src/UI/Settings/Metadata/MetadataItemView.js | 2 +- src/UI/Settings/Metadata/MetadataItemViewTemplate.html | 5 +---- src/UI/Settings/Metadata/metadata.less | 3 ++- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/UI/Settings/Metadata/MetadataItemView.js b/src/UI/Settings/Metadata/MetadataItemView.js index 3fa9f43f4..95f6bdd4a 100644 --- a/src/UI/Settings/Metadata/MetadataItemView.js +++ b/src/UI/Settings/Metadata/MetadataItemView.js @@ -13,7 +13,7 @@ define( tagName : 'li', events: { - 'click .x-edit' : '_edit' + 'click' : '_edit' }, initialize: function () { diff --git a/src/UI/Settings/Metadata/MetadataItemViewTemplate.html b/src/UI/Settings/Metadata/MetadataItemViewTemplate.html index f461ea4fb..544985ff0 100644 --- a/src/UI/Settings/Metadata/MetadataItemViewTemplate.html +++ b/src/UI/Settings/Metadata/MetadataItemViewTemplate.html @@ -1,9 +1,6 @@ -<div class="metadata-item"> +<div class="metadata-item" title="Click to edit"> <div> <h3>{{name}}</h3> - <span class="btn-group pull-right"> - <button class="btn btn-xs btn-icon-only x-edit"><i class="icon-nd-edit"/></button> - </span> </div> <div class="settings"> diff --git a/src/UI/Settings/Metadata/metadata.less b/src/UI/Settings/Metadata/metadata.less index b90674017..566114a39 100644 --- a/src/UI/Settings/Metadata/metadata.less +++ b/src/UI/Settings/Metadata/metadata.less @@ -10,6 +10,7 @@ .metadata-item { .card; + .clickable; width: 200px; height: 230px; @@ -18,7 +19,7 @@ h3 { margin-top: 0px; display: inline-block; - width: 140px; + width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; From 14f57d655a2915c932182cb65643ab5e4ebb593a Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Fri, 13 Jun 2014 08:28:52 -0700 Subject: [PATCH 48/59] Fixed: Don't show an error if filesize cannot be formatted --- src/UI/Cells/EpisodeStatusCell.js | 8 ++++---- src/UI/Shared/FormatHelpers.js | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js index 167ac8671..c8cd57510 100644 --- a/src/UI/Cells/EpisodeStatusCell.js +++ b/src/UI/Cells/EpisodeStatusCell.js @@ -50,15 +50,15 @@ define( var title = 'Episode downloaded'; if (quality.proper) { - title += ' [PROPER] - {0}'.format(size); - this.$el.html('<span class="badge badge-info" title="{0}">{1}</span>'.format(title, quality.quality.name)); + title += ' [PROPER]'; } - else { + if (size !== '') { title += ' - {0}'.format(size); - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); } + this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); + return; } diff --git a/src/UI/Shared/FormatHelpers.js b/src/UI/Shared/FormatHelpers.js index 51e93cf5a..aaf51b1e1 100644 --- a/src/UI/Shared/FormatHelpers.js +++ b/src/UI/Shared/FormatHelpers.js @@ -10,6 +10,11 @@ define( bytes: function (sourceSize) { var size = Number(sourceSize); + + if (isNaN(size)) { + return ''; + } + return Filesize(size, { base: 2, round: 1 }); }, From b185748822e8712c79b27362189953b8aeecee89 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Fri, 13 Jun 2014 08:40:30 -0700 Subject: [PATCH 49/59] Fixed: show no results found when manual search returns 0 results --- src/UI/Episode/Search/EpisodeSearchLayout.js | 14 +++++++++++--- src/UI/Episode/Search/NoResultsView.js | 10 ++++++++++ src/UI/Episode/Search/NoResultsViewTemplate.html | 1 + 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 src/UI/Episode/Search/NoResultsView.js create mode 100644 src/UI/Episode/Search/NoResultsViewTemplate.html diff --git a/src/UI/Episode/Search/EpisodeSearchLayout.js b/src/UI/Episode/Search/EpisodeSearchLayout.js index 20767ebd1..a5d400625 100644 --- a/src/UI/Episode/Search/EpisodeSearchLayout.js +++ b/src/UI/Episode/Search/EpisodeSearchLayout.js @@ -8,8 +8,9 @@ define( 'Release/ReleaseCollection', 'Series/SeriesCollection', 'Commands/CommandController', - 'Shared/LoadingView' - ], function (vent, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection,CommandController, LoadingView) { + 'Shared/LoadingView', + 'Episode/Search/NoResultsView' + ], function (vent, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection,CommandController, LoadingView, NoResultsView) { return Marionette.Layout.extend({ template: 'Episode/Search/EpisodeSearchLayoutTemplate', @@ -73,7 +74,14 @@ define( }, _showSearchResults: function () { - this.mainView = new ManualSearchLayout({ collection: this.releaseCollection }); + if (this.releaseCollection.length === 0) { + this.mainView = new NoResultsView(); + } + + else { + this.mainView = new ManualSearchLayout({ collection: this.releaseCollection }); + } + this._showMainView(); } }); diff --git a/src/UI/Episode/Search/NoResultsView.js b/src/UI/Episode/Search/NoResultsView.js new file mode 100644 index 000000000..54d868438 --- /dev/null +++ b/src/UI/Episode/Search/NoResultsView.js @@ -0,0 +1,10 @@ +'use strict'; + +define( + [ + 'marionette' + ], function (Marionette) { + return Marionette.ItemView.extend({ + template: 'Episode/Search/NoResultsViewTemplate' + }); + }); diff --git a/src/UI/Episode/Search/NoResultsViewTemplate.html b/src/UI/Episode/Search/NoResultsViewTemplate.html new file mode 100644 index 000000000..87201af05 --- /dev/null +++ b/src/UI/Episode/Search/NoResultsViewTemplate.html @@ -0,0 +1 @@ +<div>No results found</div> \ No newline at end of file From 590a39a47e27414c78ceb45336108563ae7d03bc Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sat, 14 Jun 2014 22:52:40 -0700 Subject: [PATCH 50/59] Fixed: Show unlimited when quality max size is set to 0 --- .../Quality/Definition/QualityDefinitionView.js | 10 +++++++++- ...emplate.html => QualityDefinitionViewTemplate.html} | 0 2 files changed, 9 insertions(+), 1 deletion(-) rename src/UI/Settings/Quality/Definition/{QualityDefinitionTemplate.html => QualityDefinitionViewTemplate.html} (100%) diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js index 003d6520d..d5a3f9d1a 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionView.js +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js @@ -9,7 +9,7 @@ define( ], function (Marionette, AsModelBoundView, fileSize) { var view = Marionette.ItemView.extend({ - template: 'Settings/Quality/Definition/QualityDefinitionTemplate', + template: 'Settings/Quality/Definition/QualityDefinitionViewTemplate', className: 'row', ui: { @@ -61,6 +61,14 @@ define( } { + if (maxSize === 0) + { + this.ui.thirtyMinuteMaxSize.html('Unlimited'); + this.ui.sixtyMinuteMaxSize.html('Unlimited'); + + return; + } + var maxBytes = maxSize * 1024 * 1024; var maxThirty = fileSize(maxBytes * 30, 1, false); var maxSixty = fileSize(maxBytes * 60, 1, false); diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html b/src/UI/Settings/Quality/Definition/QualityDefinitionViewTemplate.html similarity index 100% rename from src/UI/Settings/Quality/Definition/QualityDefinitionTemplate.html rename to src/UI/Settings/Quality/Definition/QualityDefinitionViewTemplate.html From df1283c0a548f4e20f5bd3d2a9c181dbeb5105f3 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Mon, 16 Jun 2014 07:57:55 -0700 Subject: [PATCH 51/59] Quality parsing improvements Fixed: Parsing quality in filename from other applications Fixed: Parsing WEB.DL as WEB-DL --- .../ParserTests/QualityParserFixture.cs | 30 ++++++++++++++++++- src/NzbDrone.Core/Parser/QualityParser.cs | 24 +++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index 0b4a71cf2..bc55dd784 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -16,12 +16,26 @@ namespace NzbDrone.Core.Test.ParserTests new object[] { Quality.DVD }, new object[] { Quality.WEBDL480p }, new object[] { Quality.HDTV720p }, + new object[] { Quality.HDTV1080p }, new object[] { Quality.WEBDL720p }, new object[] { Quality.WEBDL1080p }, new object[] { Quality.Bluray720p }, new object[] { Quality.Bluray1080p } }; + public static object[] OtherSourceQualityParserCases = + { + new object[] { "SD TV", Quality.SDTV }, + new object[] { "SD DVD", Quality.DVD }, + new object[] { "480p WEB-DL", Quality.WEBDL480p }, + new object[] { "HD TV", Quality.HDTV720p }, + new object[] { "1080p HD TV", Quality.HDTV1080p }, + new object[] { "720p WEB-DL", Quality.WEBDL720p }, + new object[] { "1080p WEB-DL", Quality.WEBDL1080p }, + new object[] { "720p BluRay", Quality.Bluray720p }, + new object[] { "1080p BluRay", Quality.Bluray1080p } + }; + [TestCase("S07E23 .avi ", false)] [TestCase("The.Shield.S01E13.x264-CtrlSD", false)] [TestCase("Nikita S02E01 HDTV XviD 2HD", false)] @@ -64,7 +78,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Elementary.S01E10.The.Leviathan.480p.WEB-DL.x264-mSD", false)] [TestCase("Glee.S04E10.Glee.Actually.480p.WEB-DL.x264-mSD", false)] - [TestCase("The.Big.Bang.Theory.S06E11.The.Santa.Simulation.480p.WEB-DL.x264-mSD", false)] + [TestCase("The.Big.Bang.Theory.S06E11.The.Santa.Simulation.480p.WEB-DL.x264-mSD", false)] + [TestCase("Da.Vincis.Demons.S02E04.480p.WEB.DL.nSD.x264-NhaNc3", false)] public void should_parse_webdl480p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.WEBDL480p, proper); @@ -105,6 +120,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("S07E23 - [WEBDL].mkv ", false)] [TestCase("Fringe S04E22 720p WEB-DL DD5.1 H264-EbP.mkv", false)] [TestCase("House.S04.720p.Web-Dl.Dd5.1.h264-P2PACK", false)] + [TestCase("Da.Vincis.Demons.S02E04.720p.WEB.DL.nSD.x264-NhaNc3", false)] public void should_parse_webdl720p_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.WEBDL720p, proper); @@ -164,6 +180,18 @@ namespace NzbDrone.Core.Test.ParserTests result.Quality.Should().Be(quality); } + [Test, TestCaseSource("OtherSourceQualityParserCases")] + public void should_parse_quality_from_other_source(string qualityString, Quality quality) + { + foreach (var c in new char[] { '-', '.', ' ', '_' }) + { + var title = String.Format("My series S01E01 {0}", qualityString.Replace(' ', c)); + + ParseAndVerifyQuality(title, quality, false); + } + } + + private void ParseAndVerifyQuality(string title, Quality quality, bool proper) { var result = Parser.QualityParser.ParseQuality(title); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index e8b6e5d5b..66da37c71 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -14,12 +14,12 @@ namespace NzbDrone.Core.Parser private static readonly Regex SourceRegex = new Regex(@"\b(?: (?<bluray>BluRay)| - (?<webdl>WEB-DL|WEBDL|WEB\sDL|WEB\-DL|WebRip)| + (?<webdl>WEB[-_. ]DL|WEBDL|WebRip)| (?<hdtv>HDTV)| (?<bdrip>BDRiP)| (?<brrip>BRRip)| (?<dvd>DVD|DVDRip|NTSC|PAL|xvidvd)| - (?<dsr>WS\sDSR|WS_DSR|WS\.DSR|DSR)| + (?<dsr>WS[-_. ]DSR|DSR)| (?<pdtv>PDTV)| (?<sdtv>SDTV) )\b", @@ -37,6 +37,8 @@ namespace NzbDrone.Core.Parser private static readonly Regex CodecRegex = new Regex(@"\b(?:(?<x264>x264)|(?<h264>h264)|(?<xvidhd>XvidHD)|(?<xvid>Xvid)|(?<divx>divx))\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex OtherSourceRegex = new Regex(@"(?<hdtv>HD[-_. ]TV)|(?<sdtv>SD[-_. ]TV)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static QualityModel ParseQuality(string name) { Logger.Debug("Trying to parse quality for {0}", name); @@ -191,6 +193,13 @@ namespace NzbDrone.Core.Parser result.Quality = Quality.Bluray1080p; } + var otherSourceMatch = OtherSourceMatch(normalizedName); + + if (otherSourceMatch != Quality.Unknown) + { + result.Quality = otherSourceMatch; + } + //Based on extension if (result.Quality == Quality.Unknown && !name.ContainsInvalidPathChars()) { @@ -220,6 +229,17 @@ namespace NzbDrone.Core.Parser return Resolution.Unknown; } + + private static Quality OtherSourceMatch(string name) + { + var match = OtherSourceRegex.Match(name); + + if (!match.Success) return Quality.Unknown; + if (match.Groups["sdtv"].Success) return Quality.SDTV; + if (match.Groups["hdtv"].Success) return Quality.HDTV720p; + + return Quality.Unknown; + } } public enum Resolution From 6a2970fef27b6ae512dd723afc6c4612623d52f8 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 18 Jun 2014 17:15:54 -0700 Subject: [PATCH 52/59] Fixed: Update installed checkmark spacing and tooltip --- .../Instrumentation/LogTargets.cs | 3 ++- .../System/Update/UpdateItemViewTemplate.html | 12 ++++++----- src/UI/System/Update/update.less | 21 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/NzbDrone.Common/Instrumentation/LogTargets.cs b/src/NzbDrone.Common/Instrumentation/LogTargets.cs index 9230a428d..099dd71bc 100644 --- a/src/NzbDrone.Common/Instrumentation/LogTargets.cs +++ b/src/NzbDrone.Common/Instrumentation/LogTargets.cs @@ -22,7 +22,8 @@ namespace NzbDrone.Common.Instrumentation RegisterDebugger(); } - RegisterExceptron(); + //Disabling for now - until its fixed or we yank it out + //RegisterExceptron(); if (updateApp) { diff --git a/src/UI/System/Update/UpdateItemViewTemplate.html b/src/UI/System/Update/UpdateItemViewTemplate.html index 95b56de51..408ec669b 100644 --- a/src/UI/System/Update/UpdateItemViewTemplate.html +++ b/src/UI/System/Update/UpdateItemViewTemplate.html @@ -3,12 +3,14 @@ <legend>{{version}} <span class="date"> - {{ShortDate releaseDate}} - {{#if installed}}<i class="icon-ok" title="Installed"></i>{{/if}} - - {{#if isUpgrade}} - <span class="label label-default install-update x-install-update">Install</span> - {{/if}} </span> + {{#if installed}} + <span class="update-installed"><i class="icon-ok" title="Installed"></i></span> + {{/if}} + + {{#if isUpgrade}} + <span class="label label-default install-update x-install-update">Install</span> + {{/if}} </legend> {{#with changes}} diff --git a/src/UI/System/Update/update.less b/src/UI/System/Update/update.less index 48c138f40..6d4e8e944 100644 --- a/src/UI/System/Update/update.less +++ b/src/UI/System/Update/update.less @@ -4,11 +4,21 @@ margin-bottom: 30px; legend { - margin-bottom: 5px; - line-height: 30px; + cursor : default; + margin-bottom : 5px; + line-height : 30px; .date { - font-size: 16px; + font-size : 16px; + } + + .install-update { + .clickable(); + margin-left : 10px; + } + + .update-installed { + margin-left : 10px; } } @@ -25,11 +35,6 @@ font-size: 13px; } - .install-update { - .clickable(); - margin-left: 10px; - } - a { color: white; text-decoration: none; From 420e6ee533df79cc25053a1fafecdda97905b40e Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 18 Jun 2014 17:23:17 -0700 Subject: [PATCH 53/59] Tooltip fixes Fixed: Tooltips for toolbar buttons Fixed: Tooltips for episode search from series details --- src/UI/Cells/EpisodeActionsCellTemplate.html | 2 +- src/UI/Shared/Toolbar/Radio/RadioButtonView.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/UI/Cells/EpisodeActionsCellTemplate.html b/src/UI/Cells/EpisodeActionsCellTemplate.html index 77e0ef5cd..a30cf3d5f 100644 --- a/src/UI/Cells/EpisodeActionsCellTemplate.html +++ b/src/UI/Cells/EpisodeActionsCellTemplate.html @@ -1,5 +1,5 @@ <div class="btn-group hidden-xs"> - <button class="btn btn-xs x-automatic-search x-automatic-search-icon" title="Automatic Search"><i class="icon-search"></i></button> + <button class="btn btn-xs x-automatic-search x-automatic-search-icon" title="Automatic Search" data-container="body"><i class="icon-search"></i></button> <button class="btn btn-xs dropdown-toggle" data-toggle="dropdown"> <span class="caret"></span> </button> diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index fe67f68cf..a72863d5a 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -34,6 +34,7 @@ define( if (this.model.get('tooltip')) { this.$el.attr('title', this.model.get('tooltip')); + this.$el.attr('data-container', 'body'); } }, From 6c9ea60382d5fe0fd8da4589a872d6ad7bf4dee7 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 18 Jun 2014 23:46:04 -0700 Subject: [PATCH 54/59] Removed delete button from series lists, added refresh button New: Refresh button on series lists (replaces delete) New: Show series title on poster on hover --- src/UI/Cells/SeriesActionsCell.js | 32 +++++++++++---- .../Index/Overview/SeriesOverviewItemView.js | 24 ++--------- .../SeriesOverviewItemViewTemplate.html | 2 +- .../Index/Posters/SeriesPostersItemView.js | 32 +++++---------- .../SeriesPostersItemViewTemplate.html | 7 +++- src/UI/Series/Index/SeriesIndexItemView.js | 41 +++++++++++++++++++ src/UI/Series/series.less | 23 ++++++++++- 7 files changed, 106 insertions(+), 55 deletions(-) create mode 100644 src/UI/Series/Index/SeriesIndexItemView.js diff --git a/src/UI/Cells/SeriesActionsCell.js b/src/UI/Cells/SeriesActionsCell.js index 313f8ef29..066fea0fa 100644 --- a/src/UI/Cells/SeriesActionsCell.js +++ b/src/UI/Cells/SeriesActionsCell.js @@ -3,25 +3,38 @@ define( [ 'vent', - 'Cells/NzbDroneCell' - ], function (vent, NzbDroneCell) { + 'Cells/NzbDroneCell', + 'Commands/CommandController' + ], function (vent, NzbDroneCell, CommandController) { return NzbDroneCell.extend({ className: 'series-actions-cell', + ui: { + refresh: '.x-refresh' + }, + events: { - 'click .x-edit-series' : '_editSeries', - 'click .x-remove-series': '_removeSeries' + 'click .x-edit' : '_editSeries', + 'click .x-refresh' : '_refreshSeries' }, render: function () { this.$el.empty(); this.$el.html( - '<i class="icon-nd-edit x-edit-series" title="" data-original-title="Edit Series"></i> ' + - '<i class="icon-remove x-remove-series hidden-xs" title="" data-original-title="Delete Series"></i>' + '<i class="icon-refresh x-refresh hidden-xs" title="" data-original-title="Update series info and scan disk"></i> ' + + '<i class="icon-nd-edit x-edit" title="" data-original-title="Edit Series"></i>' ); + CommandController.bindToCommand({ + element: this.$el.find('.x-refresh'), + command: { + name : 'refreshSeries', + seriesId : this.model.get('id') + } + }); + this.delegateEvents(); return this; }, @@ -30,8 +43,11 @@ define( vent.trigger(vent.Commands.EditSeriesCommand, {series:this.model}); }, - _removeSeries: function () { - vent.trigger(vent.Commands.DeleteSeriesCommand, {series:this.model}); + _refreshSeries: function () { + CommandController.Execute('refreshSeries', { + name : 'refreshSeries', + seriesId: this.model.id + }); } }); }); diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js index 1c4970a65..729723ab1 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js @@ -3,26 +3,10 @@ define( [ 'vent', - 'marionette' - ], function (vent, Marionette) { - return Marionette.ItemView.extend({ + 'marionette', + 'Series/Index/SeriesIndexItemView' + ], function (vent, Marionette, SeriesIndexItemView) { + return SeriesIndexItemView.extend({ template: 'Series/Index/Overview/SeriesOverviewItemViewTemplate', - - ui: { - 'progressbar': '.progress .bar' - }, - - events: { - 'click .x-edit' : 'editSeries', - 'click .x-remove': 'removeSeries' - }, - - editSeries: function () { - vent.trigger(vent.Commands.EditSeriesCommand, {series: this.model}); - }, - - removeSeries: function () { - vent.trigger(vent.Commands.DeleteSeriesCommand, {series: this.model}); - } }); }); diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html index a43ee6b6e..3b9ddf3c1 100644 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html +++ b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.html @@ -14,8 +14,8 @@ </div> <div class="col-md-2 col-xs-2"> <div class="pull-right series-overview-list-actions"> + <i class="icon-refresh x-refresh" title="Update series info and scan disk"/> <i class="icon-nd-edit x-edit" title="Edit Series"/> - <i class="icon-remove x-remove" title="Delete Series"/> </div> </div> </div> diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemView.js b/src/UI/Series/Index/Posters/SeriesPostersItemView.js index 997c1c9e1..e72cb6a4b 100644 --- a/src/UI/Series/Index/Posters/SeriesPostersItemView.js +++ b/src/UI/Series/Index/Posters/SeriesPostersItemView.js @@ -3,37 +3,25 @@ define( [ 'vent', - 'marionette' - ], function (vent, Marionette) { + 'marionette', + 'Series/Index/SeriesIndexItemView' + ], function (vent, Marionette, SeriesIndexItemView) { - return Marionette.ItemView.extend({ + return SeriesIndexItemView.extend({ tagName : 'li', template: 'Series/Index/Posters/SeriesPostersItemViewTemplate', + initialize: function () { + this.events['mouseenter .x-series-poster'] = 'posterHoverAction'; + this.events['mouseleave .x-series-poster'] = 'posterHoverAction'; - ui: { - 'progressbar': '.progress .bar', - 'controls' : '.series-controls' - }, - - events: { - 'click .x-edit' : 'editSeries', - 'click .x-remove' : 'removeSeries', - 'mouseenter .x-series-poster': 'posterHoverAction', - 'mouseleave .x-series-poster': 'posterHoverAction' - }, - - - editSeries: function () { - vent.trigger(vent.Commands.EditSeriesCommand, {series:this.model}); - }, - - removeSeries: function () { - vent.trigger(vent.Commands.DeleteSeriesCommand, {series:this.model}); + this.ui.controls = '.x-series-controls'; + this.ui.title = '.x-title'; }, posterHoverAction: function () { this.ui.controls.slideToggle(); + this.ui.title.slideToggle(); } }); }); diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html index bf3de36b8..f1b82fc52 100644 --- a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html +++ b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.html @@ -1,9 +1,9 @@ <div class="series-posters-item"> <div class="center"> <div class="series-poster-container x-series-poster"> - <div class="series-controls"> + <div class="series-controls x-series-controls"> + <i class="icon-refresh x-refresh" title="Refresh Series"/> <i class="icon-nd-edit x-edit" title="Edit Series"/> - <i class="icon-remove x-remove" title="Delete Series"/> </div> {{#unless_eq status compare="continuing"}} <div class="ended-banner">Ended</div> @@ -12,6 +12,9 @@ <img class="series-poster" src="{{poster}}" {{defaultImg}}> <div class="center title">{{title}}</div> </a> + <div class="hidden-title x-title"> + {{title}} + </div> </div> </div> diff --git a/src/UI/Series/Index/SeriesIndexItemView.js b/src/UI/Series/Index/SeriesIndexItemView.js new file mode 100644 index 000000000..9513ac588 --- /dev/null +++ b/src/UI/Series/Index/SeriesIndexItemView.js @@ -0,0 +1,41 @@ +'use strict'; + +define( + [ + 'vent', + 'marionette', + 'Commands/CommandController' + ], function (vent, Marionette, CommandController) { + return Marionette.ItemView.extend({ + + ui: { + refresh : '.x-refresh' + }, + + events: { + 'click .x-edit' : '_editSeries', + 'click .x-refresh' : '_refreshSeries' + }, + + onRender: function () { + CommandController.bindToCommand({ + element: this.ui.refresh, + command: { + name : 'refreshSeries', + seriesId : this.model.get('id') + } + }); + }, + + _editSeries: function () { + vent.trigger(vent.Commands.EditSeriesCommand, {series: this.model}); + }, + + _refreshSeries: function () { + CommandController.Execute('refreshSeries', { + name : 'refreshSeries', + seriesId: this.model.id + }); + } + }); + }); diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less index e6b32703c..1420fe73e 100644 --- a/src/UI/Series/series.less +++ b/src/UI/Series/series.less @@ -188,7 +188,22 @@ background-color : #eeeeee; width : 100%; text-align : right; - padding-right : 20px; + padding-right : 10px; + .opacity(0.8); + display : none; + + i { + .clickable(); + } + } + + .hidden-title { + position : absolute;; + bottom : 0px; + overflow : hidden; + background-color : #eeeeee; + width : 100%; + text-align : center; .opacity(0.8); display : none; } @@ -279,7 +294,7 @@ text-transform : none; i { - .clickable; + .clickable(); font-size : 24px; padding-left : 5px; } @@ -340,6 +355,10 @@ .series-overview-list-actions { min-width: 56px; max-width: 56px; + + i { + .clickable(); + } } //Editor From 32e6b7db48ffd9cdec2509f65b73214da19aebbb Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 18 Jun 2014 23:53:07 -0700 Subject: [PATCH 55/59] Fixed: Year on add series will be shown in grey if its not part of the series title --- src/UI/AddSeries/addSeries.less | 5 +++++ src/UI/Handlebars/Helpers/Series.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index 0fa784a19..01a3b19fd 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -44,6 +44,11 @@ margin-left: 15px; vertical-align: middle; } + + .year { + font-style : italic; + color : #aaaaaa; + } } .new-series-overview { diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js index 9618ccf96..7d1276c22 100644 --- a/src/UI/Handlebars/Helpers/Series.js +++ b/src/UI/Handlebars/Helpers/Series.js @@ -69,6 +69,6 @@ define( return this.title; } - return new Handlebars.SafeString('{0} <em>({1})</em>'.format(this.title, this.year)); + return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year)); }); }); From 2fd3c354fd7c36b5ea783b42e2038023b3f8bc8b Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 19 Jun 2014 07:18:50 -0700 Subject: [PATCH 56/59] Added tests for Roksbox and Wdtv metadata Fixed: Detecting metadata files for Roksbox and WDTV inside of Specials folders --- .../Roksbox/FindMetadataFileFixture.cs | 78 +++++++++++++++++++ .../Consumers/Wdtv/FindMetadataFileFixture.cs | 74 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 2 + .../Consumers/Roksbox/RoksboxMetadata.cs | 17 ++-- .../MetaData/Consumers/Wdtv/WdtvMetadata.cs | 17 ++-- 5 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs create mode 100644 src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs new file mode 100644 index 000000000..06cf6fb51 --- /dev/null +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Roksbox/FindMetadataFileFixture.cs @@ -0,0 +1,78 @@ +using System.IO; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Metadata; +using NzbDrone.Core.Metadata.Consumers.Roksbox; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Metadata.Consumers.Roksbox +{ + [TestFixture] + public class FindMetadataFileFixture : CoreTest<RoksboxMetadata> + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder<Series>.CreateNew() + .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) + .Build(); + } + + [Test] + public void should_return_null_if_filename_is_not_handled() + { + var path = Path.Combine(_series.Path, "file.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [TestCase("Specials")] + [TestCase("specials")] + [TestCase("Season 1")] + public void should_return_season_image(string folder) + { + var path = Path.Combine(_series.Path, folder, folder + ".jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + } + + [TestCase(".xml", MetadataType.EpisodeMetadata)] + [TestCase(".jpg", MetadataType.EpisodeImage)] + public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(type); + } + + [TestCase(".xml")] + [TestCase(".jpg")] + public void should_return_null_if_not_valid_file_for_episode(string extension) + { + var path = Path.Combine(_series.Path, "the.series.episode" + extension); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_not_return_metadata_if_image_file_is_a_thumb() + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode-thumb.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_return_series_image_for_folder_jpg_in_series_folder() + { + var path = Path.Combine(_series.Path, new DirectoryInfo(_series.Path).Name + ".jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + } + } +} diff --git a/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs new file mode 100644 index 000000000..4bdbecc1a --- /dev/null +++ b/src/NzbDrone.Core.Test/Metadata/Consumers/Wdtv/FindMetadataFileFixture.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Metadata; +using NzbDrone.Core.Metadata.Consumers.Wdtv; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.Metadata.Consumers.Wdtv +{ + [TestFixture] + public class FindMetadataFileFixture : CoreTest<WdtvMetadata> + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder<Series>.CreateNew() + .With(s => s.Path = @"C:\Test\TV\The.Series".AsOsAgnostic()) + .Build(); + } + + [Test] + public void should_return_null_if_filename_is_not_handled() + { + var path = Path.Combine(_series.Path, "file.jpg"); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [TestCase("Specials")] + [TestCase("specials")] + [TestCase("Season 1")] + public void should_return_season_image(string folder) + { + var path = Path.Combine(_series.Path, folder, "folder.jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeasonImage); + } + + [TestCase(".xml", MetadataType.EpisodeMetadata)] + [TestCase(".metathumb", MetadataType.EpisodeImage)] + public void should_return_metadata_for_episode_if_valid_file_for_episode(string extension, MetadataType type) + { + var path = Path.Combine(_series.Path, "the.series.s01e01.episode" + extension); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(type); + } + + [TestCase(".xml")] + [TestCase(".metathumb")] + public void should_return_null_if_not_valid_file_for_episode(string extension) + { + var path = Path.Combine(_series.Path, "the.series.episode" + extension); + + Subject.FindMetadataFile(_series, path).Should().BeNull(); + } + + [Test] + public void should_return_series_image_for_folder_jpg_in_series_folder() + { + var path = Path.Combine(_series.Path, "folder.jpg"); + + Subject.FindMetadataFile(_series, path).Type.Should().Be(MetadataType.SeriesImage); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 818726309..1305b127d 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -174,6 +174,8 @@ <Compile Include="Messaging\Commands\CommandFixture.cs" /> <Compile Include="Messaging\Events\EventAggregatorFixture.cs" /> <Compile Include="MetadataSourceTests\TraktProxyFixture.cs" /> + <Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" /> + <Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" /> <Compile Include="NotificationTests\Xbmc\GetJsonVersionFixture.cs" /> <Compile Include="NotificationTests\Xbmc\Http\ActivePlayersFixture.cs" /> <Compile Include="NotificationTests\Xbmc\Http\CheckForErrorFixture.cs" /> diff --git a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs index 58237b927..0cc766fa8 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Roksbox/RoksboxMetadata.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -10,11 +9,8 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; @@ -36,7 +32,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox } private static List<string> ValidCertification = new List<string> { "G", "NC-17", "PG", "PG-13", "R", "UR", "UNRATED", "NR", "TV-Y", "TV-Y7", "TV-Y7-FV", "TV-G", "TV-PG", "TV-14", "TV-MA" }; - private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<specials>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles) { @@ -97,26 +93,27 @@ namespace NzbDrone.Core.Metadata.Consumers.Roksbox }; //Series and season images are both named folder.jpg, only season ones sit in season folders - if (String.Compare(filename, parentdir.Name, StringComparison.InvariantCultureIgnoreCase) == 0) + if (Path.GetFileNameWithoutExtension(filename).Equals(parentdir.Name, StringComparison.InvariantCultureIgnoreCase)) { var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); + if (seasonMatch.Success) { metadata.Type = MetadataType.SeasonImage; - var seasonNumber = seasonMatch.Groups["season"].Value; - - if (seasonNumber.Contains("specials")) + if (seasonMatch.Groups["specials"].Success) { metadata.SeasonNumber = 0; } + else { - metadata.SeasonNumber = Convert.ToInt32(seasonNumber); + metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); } return metadata; } + else { metadata.Type = MetadataType.SeriesImage; diff --git a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs index f542eaf39..c48f07d01 100644 --- a/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs +++ b/src/NzbDrone.Core/MetaData/Consumers/Wdtv/WdtvMetadata.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text; using System.Text.RegularExpressions; using System.Xml; @@ -11,11 +9,8 @@ using System.Xml.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; -using NzbDrone.Common.Http; -using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Metadata.Files; using NzbDrone.Core.Tv; @@ -36,7 +31,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv _logger = logger; } - private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<season>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SeasonImagesRegex = new Regex(@"^(season (?<season>\d+))|(?<specials>specials)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public override List<MetadataFile> AfterRename(Series series, List<MetadataFile> existingMetadataFiles, List<EpisodeFile> episodeFiles) { @@ -95,7 +90,7 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv }; //Series and season images are both named folder.jpg, only season ones sit in season folders - if (String.Compare(filename, "folder.jpg", true) == 0) + if (Path.GetFileName(filename).Equals("folder.jpg", StringComparison.InvariantCultureIgnoreCase)) { var parentdir = Directory.GetParent(path); var seasonMatch = SeasonImagesRegex.Match(parentdir.Name); @@ -103,19 +98,19 @@ namespace NzbDrone.Core.Metadata.Consumers.Wdtv { metadata.Type = MetadataType.SeasonImage; - var seasonNumber = seasonMatch.Groups["season"].Value; - - if (seasonNumber.Contains("specials")) + if (seasonMatch.Groups["specials"].Success) { metadata.SeasonNumber = 0; } + else { - metadata.SeasonNumber = Convert.ToInt32(seasonNumber); + metadata.SeasonNumber = Convert.ToInt32(seasonMatch.Groups["season"].Value); } return metadata; } + else { metadata.Type = MetadataType.SeriesImage; From 801b1b4ec4dd4a7f771b36edc25f7bfb3c0080ac Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Fri, 20 Jun 2014 08:18:48 -0700 Subject: [PATCH 57/59] Fixed: Parsing of RAW-HD releases --- src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs | 3 +++ src/NzbDrone.Core/Parser/QualityParser.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs index bc55dd784..dfa5b5989 100644 --- a/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/QualityParserFixture.cs @@ -160,6 +160,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("POI S02E11 1080i HDTV DD5.1 MPEG2-TrollHD", false)] [TestCase("How I Met Your Mother S01E18 Nothing Good Happens After 2 A.M. 720p HDTV DD5.1 MPEG2-TrollHD", false)] [TestCase("The Voice S01E11 The Finals 1080i HDTV DD5.1 MPEG2-TrollHD", false)] + [TestCase("Californication.S07E11.1080i.HDTV.DD5.1.MPEG2-NTb.ts", false)] + [TestCase("Game of Thrones S04E10 1080i HDTV MPEG2 DD5.1-CtrlHD.ts", false)] + [TestCase("VICE.S02E05.1080i.HDTV.DD2.0.MPEG2-NTb.ts", false)] public void should_parse_raw_quality(string title, bool proper) { ParseAndVerifyQuality(title, Quality.RAWHD, proper); diff --git a/src/NzbDrone.Core/Parser/QualityParser.cs b/src/NzbDrone.Core/Parser/QualityParser.cs index 66da37c71..fc87bfc39 100644 --- a/src/NzbDrone.Core/Parser/QualityParser.cs +++ b/src/NzbDrone.Core/Parser/QualityParser.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Parser )\b", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>TrollHD|RawHD)\b", + private static readonly Regex RawHDRegex = new Regex(@"\b(?<rawhd>TrollHD|RawHD|1080i[-_. ]HDTV)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ProperRegex = new Regex(@"\b(?<proper>proper|repack)\b", From 6184105d3c4747b7714aed820fc67283bffaa97c Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 22 Jun 2014 11:01:22 -0700 Subject: [PATCH 58/59] Fixed: Prevent adding a series if the path is the ancestor of another series --- src/NzbDrone.Api/Series/SeriesModule.cs | 24 +++++++++++------- .../PathExtensionFixture.cs | 12 +++++++-- src/NzbDrone.Common/PathExtensions.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Paths/SeriesAncestorValidator.cs | 25 +++++++++++++++++++ 5 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index c69bcd5fc..0439fdb27 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation; +using NzbDrone.Common; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles.Events; @@ -40,7 +41,8 @@ namespace NzbDrone.Api.Series PathExistsValidator pathExistsValidator, SeriesPathValidator seriesPathValidator, SeriesExistsValidator seriesExistsValidator, - DroneFactoryValidator droneFactoryValidator + DroneFactoryValidator droneFactoryValidator, + SeriesAncestorValidator seriesAncestorValidator ) : base(commandExecutor) { @@ -59,17 +61,21 @@ namespace NzbDrone.Api.Series SharedValidator.RuleFor(s => s.QualityProfileId).ValidId(); - PutValidator.RuleFor(s => s.Path) - .Cascade(CascadeMode.StopOnFirstFailure) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator); + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(seriesPathValidator) + .SetValidator(droneFactoryValidator) + .SetValidator(seriesAncestorValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => String.IsNullOrEmpty(s.RootFolderPath)); - PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => String.IsNullOrEmpty(s.Path)); + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Title).NotEmpty(); PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); } private void PopulateAlternativeTitles(List<SeriesResource> resources) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 2564d676a..4b8c355dd 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -12,6 +12,7 @@ namespace NzbDrone.Common.Test [TestFixture] public class PathExtensionFixture : TestBase { + private string _parent = @"C:\Test".AsOsAgnostic(); private IAppFolderInfo GetIAppDirectoryInfo() { @@ -86,8 +87,6 @@ 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() { @@ -137,6 +136,15 @@ namespace NzbDrone.Common.Test parentPath.IsParentPath(childPath).Should().Be(expectedResult); } + [Test] + [Ignore] + public void should_not_be_parent_when_it_is_grandparent() + { + var path = Path.Combine(_parent, "parent", "child"); + + _parent.IsParentPath(path).Should().BeFalse(); + } + [Test] public void normalize_path_exception_empty() { diff --git a/src/NzbDrone.Common/PathExtensions.cs b/src/NzbDrone.Common/PathExtensions.cs index 053a22edb..7598f84f8 100644 --- a/src/NzbDrone.Common/PathExtensions.cs +++ b/src/NzbDrone.Common/PathExtensions.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Common { if (!parentPath.IsParentPath(childPath)) { - throw new NzbDrone.Common.Exceptions.NotParentException("{0} is not a child of {1}", childPath, parentPath); + throw new Exceptions.NotParentException("{0} is not a child of {1}", childPath, parentPath); } return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 63c970219..8b70b8ca6 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -709,6 +709,7 @@ <Compile Include="Update\UpdateCheckService.cs" /> <Compile Include="Update\UpdateVerification.cs" /> <Compile Include="Update\UpdateVerificationFailedException.cs" /> + <Compile Include="Validation\Paths\SeriesAncestorValidator.cs" /> <Compile Include="Validation\Paths\SeriesExistsValidator.cs" /> <Compile Include="Validation\Paths\RootFolderValidator.cs" /> <Compile Include="Validation\Paths\DroneFactoryValidator.cs" /> diff --git a/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs new file mode 100644 index 000000000..c050b9e82 --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs @@ -0,0 +1,25 @@ +using System.Linq; +using FluentValidation.Validators; +using NzbDrone.Common; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Validation.Paths +{ + public class SeriesAncestorValidator : PropertyValidator + { + private readonly ISeriesService _seriesService; + + public SeriesAncestorValidator(ISeriesService seriesService) + : base("Path is an ancestor of an existing path") + { + _seriesService = seriesService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return !_seriesService.GetAllSeries().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); + } + } +} \ No newline at end of file From 9633afc612cdc81adfcabdd438303a4e4ef07375 Mon Sep 17 00:00:00 2001 From: Taloth Saldono <Taloth@users.noreply.github.com> Date: Fri, 20 Jun 2014 00:56:49 +0200 Subject: [PATCH 59/59] New: Added Presets to Indexers to add indexers with default properties. In an older version of NzbDrone these default indexers were added automatically and could not be removed. --- .../ClientSchema/SchemaBuilder.cs | 8 ++- .../DownloadClientSchemaModule.cs | 37 ------------- .../Indexers/IndexerSchemaModule.cs | 38 ------------- .../Notifications/NotificationSchemaModule.cs | 37 ------------- src/NzbDrone.Api/NzbDrone.Api.csproj | 3 -- src/NzbDrone.Api/ProviderModuleBase.cs | 32 ++++++++--- src/NzbDrone.Api/ProviderResource.cs | 3 ++ .../Download/DownloadClientFactory.cs | 4 +- src/NzbDrone.Core/Indexers/IndexerFactory.cs | 4 +- .../ThingiProvider/IProviderFactory.cs | 6 ++- .../ThingiProvider/ProviderFactory.cs | 53 ++++++++++++++----- .../Add/DownloadClientAddItemView.js | 31 ++++++++--- .../DownloadClientAddItemViewTemplate.html | 29 ++++++++-- .../DownloadClient/downloadclient.less | 2 +- .../Indexers/Add/IndexerAddItemView.js | 31 ++++++++--- .../Add/IndexerAddItemViewTemplate.html | 29 ++++++++-- src/UI/Settings/Indexers/indexers.less | 2 +- .../Add/NotificationAddItemView.js | 33 +++++++++--- .../Add/NotificationAddItemViewTemplate.html | 29 ++++++++-- .../Settings/Notifications/notifications.less | 2 +- src/UI/Settings/thingy.less | 18 ++----- 21 files changed, 241 insertions(+), 190 deletions(-) delete mode 100644 src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs delete mode 100644 src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs delete mode 100644 src/NzbDrone.Api/Notifications/NotificationSchemaModule.cs diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs index 8a8a20cef..29abd1468 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs @@ -5,6 +5,7 @@ using NzbDrone.Common; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Reflection; using NzbDrone.Core.Annotations; +using Omu.ValueInjecter; namespace NzbDrone.Api.ClientSchema { @@ -55,7 +56,7 @@ namespace NzbDrone.Api.ClientSchema } - public static object ReadFormSchema(List<Field> fields, Type targetType) + public static object ReadFormSchema(List<Field> fields, Type targetType, object defaults = null) { Ensure.That(targetType, () => targetType).IsNotNull(); @@ -63,6 +64,11 @@ namespace NzbDrone.Api.ClientSchema var target = Activator.CreateInstance(targetType); + if (defaults != null) + { + target.InjectFrom(defaults); + } + foreach (var propertyInfo in properties) { var fieldAttribute = propertyInfo.GetAttribute<FieldDefinitionAttribute>(false); diff --git a/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs b/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs deleted file mode 100644 index 0ea47266b..000000000 --- a/src/NzbDrone.Api/DownloadClient/DownloadClientSchemaModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Core.Download; -using Omu.ValueInjecter; - -namespace NzbDrone.Api.DownloadClient -{ - public class DownloadClientSchemaModule : NzbDroneRestModule<DownloadClientResource> - { - private readonly IDownloadClientFactory _downloadClientFactory; - - public DownloadClientSchemaModule(IDownloadClientFactory downloadClientFactory) - : base("downloadclient/schema") - { - _downloadClientFactory = downloadClientFactory; - GetResourceAll = GetSchema; - } - - private List<DownloadClientResource> GetSchema() - { - var downloadClients = _downloadClientFactory.Templates(); - - var result = new List<DownloadClientResource>(downloadClients.Count); - - foreach (var downloadClient in downloadClients) - { - var downloadClientResource = new DownloadClientResource(); - downloadClientResource.InjectFrom(downloadClient); - downloadClientResource.Fields = SchemaBuilder.ToSchema(downloadClient.Settings); - - result.Add(downloadClientResource); - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs b/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs deleted file mode 100644 index 3de973599..000000000 --- a/src/NzbDrone.Api/Indexers/IndexerSchemaModule.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Core.Indexers; -using Omu.ValueInjecter; - -namespace NzbDrone.Api.Indexers -{ - public class IndexerSchemaModule : NzbDroneRestModule<IndexerResource> - { - private readonly IIndexerFactory _indexerFactory; - - public IndexerSchemaModule(IIndexerFactory indexerFactory) - : base("indexer/schema") - { - _indexerFactory = indexerFactory; - GetResourceAll = GetSchema; - } - - private List<IndexerResource> GetSchema() - { - var indexers = _indexerFactory.Templates(); - - var result = new List<IndexerResource>(indexers.Count()); - - foreach (var indexer in indexers) - { - var indexerResource = new IndexerResource(); - indexerResource.InjectFrom(indexer); - indexerResource.Fields = SchemaBuilder.ToSchema(indexer.Settings); - - result.Add(indexerResource); - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Notifications/NotificationSchemaModule.cs b/src/NzbDrone.Api/Notifications/NotificationSchemaModule.cs deleted file mode 100644 index 920b9bb7b..000000000 --- a/src/NzbDrone.Api/Notifications/NotificationSchemaModule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Core.Notifications; -using Omu.ValueInjecter; - -namespace NzbDrone.Api.Notifications -{ - public class NotificationSchemaModule : NzbDroneRestModule<NotificationResource> - { - private readonly INotificationFactory _notificationFactory; - - public NotificationSchemaModule(INotificationFactory notificationFactory) - : base("notification/schema") - { - _notificationFactory = notificationFactory; - GetResourceAll = GetSchema; - } - - private List<NotificationResource> GetSchema() - { - var notifications = _notificationFactory.Templates(); - - var result = new List<NotificationResource>(notifications.Count); - - foreach (var notification in notifications) - { - var notificationResource = new NotificationResource(); - notificationResource.InjectFrom(notification); - notificationResource.Fields = SchemaBuilder.ToSchema(notification.Settings); - - result.Add(notificationResource); - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index da8ffac05..0c23ee731 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -144,11 +144,9 @@ <Compile Include="MediaCovers\MediaCoverModule.cs" /> <Compile Include="Metadata\MetadataResource.cs" /> <Compile Include="Metadata\MetadataModule.cs" /> - <Compile Include="Notifications\NotificationSchemaModule.cs" /> <Compile Include="NzbDroneFeedModule.cs" /> <Compile Include="ProviderResource.cs" /> <Compile Include="ProviderModuleBase.cs" /> - <Compile Include="Indexers\IndexerSchemaModule.cs" /> <Compile Include="Indexers\IndexerModule.cs" /> <Compile Include="Indexers\IndexerResource.cs" /> <Compile Include="Indexers\ReleaseModule.cs" /> @@ -172,7 +170,6 @@ <Compile Include="Queue\QueueModule.cs" /> <Compile Include="Queue\QueueResource.cs" /> <Compile Include="ResourceChangeMessage.cs" /> - <Compile Include="DownloadClient\DownloadClientSchemaModule.cs" /> <Compile Include="Notifications\NotificationModule.cs" /> <Compile Include="Notifications\NotificationResource.cs" /> <Compile Include="NzbDroneRestModule.cs" /> diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index fb0ca9389..943f27077 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using FluentValidation; using Nancy; @@ -22,7 +23,7 @@ namespace NzbDrone.Api : base(resource) { _providerFactory = providerFactory; - Get["templates"] = x => GetTemplates(); + Get["schema"] = x => GetTemplates(); GetResourceAll = GetAll; GetResourceById = GetProviderById; CreateResource = CreateProvider; @@ -82,8 +83,13 @@ namespace NzbDrone.Api definition.InjectFrom(providerResource); + var preset = _providerFactory.GetPresetDefinitions(definition) + .Where(v => v.Name == definition.Name) + .Select(v => v.Settings) + .FirstOrDefault(); + var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); - definition.Settings = (IProviderConfig)SchemaBuilder.ReadFormSchema(providerResource.Fields, configContract); + definition.Settings = (IProviderConfig)SchemaBuilder.ReadFormSchema(providerResource.Fields, configContract, preset); Validate(definition); @@ -97,15 +103,29 @@ namespace NzbDrone.Api private Response GetTemplates() { - var templates = _providerFactory.Templates(); + var defaultDefinitions = _providerFactory.GetDefaultDefinitions(); - var result = new List<TProviderResource>(templates.Count()); + var result = new List<TProviderResource>(defaultDefinitions.Count()); - foreach (var providerDefinition in templates) + foreach (var providerDefinition in defaultDefinitions) { var providerResource = new TProviderResource(); providerResource.InjectFrom(providerDefinition); providerResource.Fields = SchemaBuilder.ToSchema(providerDefinition.Settings); + providerResource.InfoLink = String.Format("https://github.com/NzbDrone/NzbDrone/wiki/Supported-{0}#{1}", + typeof(TProviderResource).Name.Replace("Resource", "s"), + providerDefinition.Implementation.ToLower()); + + var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); + + providerResource.Presets = presetDefinitions.Select(v => + { + var presetResource = new TProviderResource(); + presetResource.InjectFrom(v); + presetResource.Fields = SchemaBuilder.ToSchema(v.Settings); + + return presetResource as ProviderResource; + }).ToList(); result.Add(providerResource); } diff --git a/src/NzbDrone.Api/ProviderResource.cs b/src/NzbDrone.Api/ProviderResource.cs index f866341e0..60c5ad78d 100644 --- a/src/NzbDrone.Api/ProviderResource.cs +++ b/src/NzbDrone.Api/ProviderResource.cs @@ -11,5 +11,8 @@ namespace NzbDrone.Api public List<Field> Fields { get; set; } public String Implementation { get; set; } public String ConfigContract { get; set; } + public String InfoLink { get; set; } + + public List<ProviderResource> Presets { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadClientFactory.cs b/src/NzbDrone.Core/Download/DownloadClientFactory.cs index c038b5177..d48d6d456 100644 --- a/src/NzbDrone.Core/Download/DownloadClientFactory.cs +++ b/src/NzbDrone.Core/Download/DownloadClientFactory.cs @@ -27,9 +27,9 @@ namespace NzbDrone.Core.Download return base.Active().Where(c => c.Enable).ToList(); } - protected override DownloadClientDefinition GetTemplate(IDownloadClient provider) + protected override DownloadClientDefinition GetProviderCharacteristics(IDownloadClient provider, DownloadClientDefinition definition) { - var definition = base.GetTemplate(provider); + definition = base.GetProviderCharacteristics(provider, definition); definition.Protocol = provider.Protocol; diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index bf3627dfb..2b214ed7c 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -50,9 +50,9 @@ namespace NzbDrone.Core.Indexers return base.Create(definition); } - protected override IndexerDefinition GetTemplate(IIndexer provider) + protected override IndexerDefinition GetProviderCharacteristics(IIndexer provider, IndexerDefinition definition) { - var definition = base.GetTemplate(provider); + definition = base.GetProviderCharacteristics(provider, definition); definition.Protocol = provider.Protocol; diff --git a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs index 54e8315a6..b70b1e06a 100644 --- a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace NzbDrone.Core.ThingiProvider { @@ -12,6 +13,7 @@ namespace NzbDrone.Core.ThingiProvider TProviderDefinition Create(TProviderDefinition indexer); void Update(TProviderDefinition indexer); void Delete(int id); - List<TProviderDefinition> Templates(); + IEnumerable<TProviderDefinition> GetDefaultDefinitions(); + IEnumerable<TProviderDefinition> GetPresetDefinitions(TProviderDefinition providerDefinition); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index 0c7530ac5..fa9cd32c2 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -38,9 +38,41 @@ namespace NzbDrone.Core.ThingiProvider return _providerRepository.All().ToList(); } - public List<TProviderDefinition> Templates() + public IEnumerable<TProviderDefinition> GetDefaultDefinitions() { - return _providers.Select(GetTemplate).ToList(); + foreach (var provider in _providers) + { + var definition = provider.DefaultDefinitions + .OfType<TProviderDefinition>() + .FirstOrDefault(v => v.Name == null || v.Name == provider.GetType().Name); + + if (definition == null) + { + definition = new TProviderDefinition() + { + Name = string.Empty, + ConfigContract = provider.ConfigContract.Name, + Implementation = provider.GetType().Name, + Settings = (IProviderConfig)Activator.CreateInstance(provider.ConfigContract) + }; + } + + definition = GetProviderCharacteristics(provider, definition); + + yield return definition; + } + } + + public IEnumerable<TProviderDefinition> GetPresetDefinitions(TProviderDefinition providerDefinition) + { + var provider = _providers.First(v => v.GetType().Name == providerDefinition.Implementation); + + var definitions = provider.DefaultDefinitions + .OfType<TProviderDefinition>() + .Where(v => v.Name != null && v.Name != provider.GetType().Name) + .ToList(); + + return definitions; } public List<TProvider> GetAvailableProviders() @@ -82,18 +114,6 @@ 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); @@ -112,6 +132,11 @@ namespace NzbDrone.Core.ThingiProvider return All().Where(c => c.Settings.Validate().IsValid).ToList(); } + protected virtual TProviderDefinition GetProviderCharacteristics(TProvider provider, TProviderDefinition definition) + { + return definition; + } + private void RemoveMissingImplementations() { var storedProvider = _providerRepository.All(); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js index 4f8b07c7d..aafbc33f3 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js @@ -1,32 +1,51 @@ 'use strict'; define([ + 'underscore', 'jquery', 'AppLayout', 'marionette', 'Settings/DownloadClient/Edit/DownloadClientEditView' -], function ($, AppLayout, Marionette, EditView) { +], function (_, $, AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', - tagName : 'li', + template : 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', events: { - 'click': '_add' + 'click .x-preset': '_addPreset', + 'click' : '_add' }, initialize: function (options) { this.targetCollection = options.targetCollection; }, + _addPreset: function (e) { + + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + + var presetData = _.where(this.model.get('presets'), {name: presetName})[0]; + + this.model.set(presetData); + + this.model.set({ + id : undefined, + enable : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + }, + _add: function (e) { - if (this.$(e.target).hasClass('icon-info-sign')) { + if ($(e.target).closest('.btn,.btn-group').length !== 0) { return; } this.model.set({ id : undefined, - name : this.model.get('implementation'), enable : true }); diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html index f892a4d01..a2c0295be 100644 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html +++ b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.html @@ -1,6 +1,27 @@ <div class="add-thingy"> - {{implementation}} - {{#if link}} - <a href="{{link}}"><i class="icon-info-sign"/></a> - {{/if}} + <div> + {{implementation}} + </div> + <div class="pull-right"> + {{#if_gt presets.length compare=0}} + <div class="btn-group"> + <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> + Presets + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {{#each presets}} + <li class="x-preset" data-id="{{name}}"> + <a>{{name}}</a> + </li> + {{/each}} + </ul> + </div> + {{/if_gt}} + {{#if infoLink}} + <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> + <i class="icon-info-sign"/> + </a> + {{/if}} + </div> </div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/downloadclient.less b/src/UI/Settings/DownloadClient/downloadclient.less index bd8da872e..9b9f30f10 100644 --- a/src/UI/Settings/DownloadClient/downloadclient.less +++ b/src/UI/Settings/DownloadClient/downloadclient.less @@ -27,7 +27,7 @@ } .add-download-client { - li { + li.add-thingy-item { width: 33%; } } \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js index 217a1c999..c28f40bfb 100644 --- a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js +++ b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js @@ -1,32 +1,51 @@ 'use strict'; define([ + 'underscore', 'jquery', 'AppLayout', 'marionette', 'Settings/Indexers/Edit/IndexerEditView' -], function ($, AppLayout, Marionette, EditView) { +], function (_, $, AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ - template: 'Settings/Indexers/Add/IndexerAddItemViewTemplate', - tagName : 'li', + template : 'Settings/Indexers/Add/IndexerAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', events: { - 'click': '_add' + 'click .x-preset': '_addPreset', + 'click' : '_add' }, initialize: function (options) { this.targetCollection = options.targetCollection; }, + _addPreset: function (e) { + + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + + var presetData = _.where(this.model.get('presets'), {name: presetName})[0]; + + this.model.set(presetData); + + this.model.set({ + id : undefined, + enable : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + }, + _add: function (e) { - if (this.$(e.target).hasClass('icon-info-sign')) { + if ($(e.target).closest('.btn,.btn-group').length !== 0) { return; } this.model.set({ id : undefined, - name : undefined, enable : true }); diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html b/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html index f892a4d01..a2c0295be 100644 --- a/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html +++ b/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.html @@ -1,6 +1,27 @@ <div class="add-thingy"> - {{implementation}} - {{#if link}} - <a href="{{link}}"><i class="icon-info-sign"/></a> - {{/if}} + <div> + {{implementation}} + </div> + <div class="pull-right"> + {{#if_gt presets.length compare=0}} + <div class="btn-group"> + <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> + Presets + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {{#each presets}} + <li class="x-preset" data-id="{{name}}"> + <a>{{name}}</a> + </li> + {{/each}} + </ul> + </div> + {{/if_gt}} + {{#if infoLink}} + <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> + <i class="icon-info-sign"/> + </a> + {{/if}} + </div> </div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/indexers.less b/src/UI/Settings/Indexers/indexers.less index d478f0e64..3fed3ef5f 100644 --- a/src/UI/Settings/Indexers/indexers.less +++ b/src/UI/Settings/Indexers/indexers.less @@ -27,7 +27,7 @@ } .add-indexer { - li { + li.add-thingy-item { width: 33%; } } \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemView.js b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js index 5c8c0ec9f..500f7bca8 100644 --- a/src/UI/Settings/Notifications/Add/NotificationAddItemView.js +++ b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js @@ -1,32 +1,53 @@ 'use strict'; define([ + 'underscore', 'jquery', 'AppLayout', 'marionette', 'Settings/Notifications/Edit/NotificationEditView' -], function ($, AppLayout, Marionette, EditView) { +], function (_, $, AppLayout, Marionette, EditView) { return Marionette.ItemView.extend({ - template: 'Settings/Notifications/Add/NotificationAddItemViewTemplate', - tagName : 'li', + template : 'Settings/Notifications/Add/NotificationAddItemViewTemplate', + tagName : 'li', + className : 'add-thingy-item', events: { - 'click': '_add' + 'click .x-preset': '_addPreset', + 'click' : '_add' }, initialize: function (options) { this.targetCollection = options.targetCollection; }, + _addPreset: function (e) { + + var presetName = $(e.target).closest('.x-preset').attr('data-id'); + + var presetData = _.where(this.model.get('presets'), {name: presetName})[0]; + + this.model.set(presetData); + + this.model.set({ + id : undefined, + onGrab : true, + onDownload : true, + onUpgrade : true + }); + + var editView = new EditView({ model: this.model, targetCollection: this.targetCollection }); + AppLayout.modalRegion.show(editView); + }, + _add: function (e) { - if (this.$(e.target).hasClass('icon-info-sign')) { + if ($(e.target).closest('.btn,.btn-group').length !== 0) { return; } this.model.set({ id : undefined, - name : this.model.get('implementation'), onGrab : true, onDownload : true, onUpgrade : true diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html index f892a4d01..a2c0295be 100644 --- a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html +++ b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.html @@ -1,6 +1,27 @@ <div class="add-thingy"> - {{implementation}} - {{#if link}} - <a href="{{link}}"><i class="icon-info-sign"/></a> - {{/if}} + <div> + {{implementation}} + </div> + <div class="pull-right"> + {{#if_gt presets.length compare=0}} + <div class="btn-group"> + <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> + Presets + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {{#each presets}} + <li class="x-preset" data-id="{{name}}"> + <a>{{name}}</a> + </li> + {{/each}} + </ul> + </div> + {{/if_gt}} + {{#if infoLink}} + <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> + <i class="icon-info-sign"/> + </a> + {{/if}} + </div> </div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/notifications.less b/src/UI/Settings/Notifications/notifications.less index 3a20ab1bf..01fd71567 100644 --- a/src/UI/Settings/Notifications/notifications.less +++ b/src/UI/Settings/Notifications/notifications.less @@ -25,7 +25,7 @@ } .add-notifications { - li { + li.add-thingy-item { width: 40%; } } \ No newline at end of file diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less index 69b8def1d..e2348b8c0 100644 --- a/src/UI/Settings/thingy.less +++ b/src/UI/Settings/thingy.less @@ -7,19 +7,7 @@ font-size: 24px; font-weight: lighter; text-align: center; - - a { - font-size: 16px; - color: #595959; - - i { - .clickable; - } - } - - a:hover { - text-decoration: none; - } + height: 85px; } .add-thingies { @@ -30,12 +18,12 @@ text-transform: capitalize; } - .items { + ul.items { list-style-type: none; margin: 0px; padding: 0px; - li { + li.add-thingy-item { display: inline-block; vertical-align: top; }