diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
index 20c5a8d66..695e3e405 100644
--- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
+++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
@@ -118,6 +118,7 @@
+
diff --git a/NzbDrone.Core.Test/ProviderTests/DownloadClientTests/PneumaticProviderFixture.cs b/NzbDrone.Core.Test/ProviderTests/DownloadClientTests/PneumaticProviderFixture.cs
new file mode 100644
index 000000000..7949c5ca5
--- /dev/null
+++ b/NzbDrone.Core.Test/ProviderTests/DownloadClientTests/PneumaticProviderFixture.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Common;
+using NzbDrone.Core.Providers.Core;
+using NzbDrone.Core.Providers.DownloadClients;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.ProviderTests.DownloadClientTests
+{
+ [TestFixture]
+ public class PneumaticProviderFixture : CoreTest
+ {
+ private const string nzbUrl = "http://www.nzbs.com/url";
+ private const string title = "some_nzb_title";
+ private const string pneumaticFolder = @"d:\nzb\pneumatic\";
+ private const string nzbPath = @"d:\nzb\blackhole\some_nzb_title.nzb";
+
+ [SetUp]
+ public void Setup()
+ {
+ Mocker.GetMock().SetupGet(c => c.BlackholeDirectory).Returns(pneumaticFolder);
+ }
+
+ private void WithExistingFile()
+ {
+ Mocker.GetMock().Setup(c => c.FileExists(nzbPath)).Returns(true);
+ }
+
+ private void WithFailedDownload()
+ {
+ Mocker.GetMock().Setup(c => c.DownloadFile(It.IsAny(), It.IsAny())).Throws(new WebException());
+ }
+
+ [Test]
+ public void should_download_file_if_it_doesnt_exist()
+ {
+ Mocker.Resolve().DownloadNzb(nzbUrl, title).Should().BeTrue();
+
+ Mocker.GetMock().Verify(c => c.DownloadFile(nzbUrl, nzbPath),Times.Once());
+ }
+
+ [Test]
+ public void should_not_download_file_if_it_doesn_exist()
+ {
+ WithExistingFile();
+
+ Mocker.Resolve().DownloadNzb(nzbUrl, title).Should().BeTrue();
+
+ Mocker.GetMock().Verify(c => c.DownloadFile(It.IsAny(), It.IsAny()), Times.Never());
+ }
+
+ [Test]
+ public void should_return_false_on_failed_download()
+ {
+ WithFailedDownload();
+
+ Mocker.Resolve().DownloadNzb(nzbUrl, title).Should().BeFalse();
+
+ ExceptionVerification.ExpectedWarns(1);
+ }
+
+ [Test]
+ public void should_skip_if_full_season_download()
+ {
+ Mocker.Resolve().DownloadNzb(nzbUrl, "30 Rock - Season 1").Should().BeFalse();
+ ExceptionVerification.ExpectedErrors(1);
+ }
+
+
+ }
+}
diff --git a/NzbDrone.Core/Providers/Core/ConfigProvider.cs b/NzbDrone.Core/Providers/Core/ConfigProvider.cs
index 605af206b..06e4770f5 100644
--- a/NzbDrone.Core/Providers/Core/ConfigProvider.cs
+++ b/NzbDrone.Core/Providers/Core/ConfigProvider.cs
@@ -520,6 +520,12 @@ namespace NzbDrone.Core.Providers.Core
set { SetValue("AllowedReleaseGroups", value); }
}
+ public virtual string PneumaticDirectory
+ {
+ get { return GetValue("PneumaticDirectory", String.Empty); }
+ set { SetValue("PneumaticDirectory", value); }
+ }
+
private string GetValue(string key)
{
return GetValue(key, String.Empty);
diff --git a/NzbDrone.Core/Providers/DownloadClients/PneumaticProvider.cs b/NzbDrone.Core/Providers/DownloadClients/PneumaticProvider.cs
new file mode 100644
index 000000000..dc323d2e7
--- /dev/null
+++ b/NzbDrone.Core/Providers/DownloadClients/PneumaticProvider.cs
@@ -0,0 +1,79 @@
+using System;
+using System.IO;
+using System.Linq;
+using NLog;
+using Ninject;
+using NzbDrone.Common;
+using NzbDrone.Core.Model;
+using NzbDrone.Core.Providers.Core;
+using NzbDrone.Core.Providers.DecisionEngine;
+
+namespace NzbDrone.Core.Providers.DownloadClients
+{
+ public class PneumaticProvider : IDownloadClient
+ {
+ private readonly ConfigProvider _configProvider;
+ private readonly HttpProvider _httpProvider;
+ private readonly DiskProvider _diskProvider;
+ private readonly UpgradeHistorySpecification _upgradeHistorySpecification;
+
+ private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+ [Inject]
+ public PneumaticProvider(ConfigProvider configProvider, HttpProvider httpProvider,
+ DiskProvider diskProvider, UpgradeHistorySpecification upgradeHistorySpecification)
+ {
+ _configProvider = configProvider;
+ _httpProvider = httpProvider;
+ _diskProvider = diskProvider;
+ _upgradeHistorySpecification = upgradeHistorySpecification;
+ }
+
+ public PneumaticProvider()
+ {
+ }
+
+ public virtual bool DownloadNzb(string url, string title)
+ {
+ try
+ {
+ //Todo: Allow full season releases
+ if (Parser.ParseTitle(title).FullSeason)
+ {
+ logger.Warn("Skipping Full Season Release: {0}", title);
+ return false;
+ }
+
+ //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC)
+ var filename = Path.Combine(_configProvider.PneumaticDirectory, title + ".nzb");
+
+ if (_diskProvider.FileExists(filename))
+ {
+ //Return true so a lesser quality is not returned.
+ logger.Info("NZB already exists on disk: {0}", filename);
+ return true;
+ }
+
+ logger.Trace("Downloading NZB from: {0} to: {1}", url, filename);
+ _httpProvider.DownloadFile(url, filename);
+
+ logger.Trace("NZB Download succeeded, saved to: {0}", filename);
+
+ var contents = String.Format("plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb={0}&nzbname={1}", filename, title);
+ _diskProvider.WriteAllText(title + ".strm", contents);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.WarnException("Failed to download NZB: " + url, ex);
+ return false;
+ }
+ }
+
+ public virtual bool IsInQueue(EpisodeParseResult newParseResult)
+ {
+ return !_upgradeHistorySpecification.IsSatisfiedBy(newParseResult);
+ }
+ }
+}