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 { - 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 GetSchema() { - var notifications = _notificationFactory.Templates(); + var downloadClients = _downloadClientFactory.Templates(); - var result = new List(notifications.Count); + var result = new List(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 GetSchema() { - var indexers = _indexerFactory.Templates().Where(c => c.Implementation =="Newznab"); + var indexers = _indexerFactory.Templates(); var result = new List(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) - { - if (!IsParent(parentPath, childPath)) - { - throw new NotParentException("{0} is not a child of {1}", childPath, parentPath); - } - - return childPath.Substring(parentPath.Length).Trim(Path.DirectorySeparatorChar); - } - public static bool IsParent(string parentPath, string childPath) + public DateTime FolderGetCreationTimeUtc(string path) { - 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; - } + CheckFolderExists(path); - 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 @@ + 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)) + { + throw new NzbDrone.Common.Exceptions.NotParentException("{0} is not a child of {1}", childPath, parentPath); + } + + 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 (firstPath.Equals(secondPath)) return true; - return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath()); + if (child.Parent.FullName.Equals(parent.FullName, OsInfo.PathStringComparison)) + { + return true; + } + + child = child.Parent; } - if (firstPath.Equals(secondPath, StringComparison.OrdinalIgnoreCase)) return true; - return String.Equals(firstPath.CleanFilePath(), secondPath.CleanFilePath(), StringComparison.OrdinalIgnoreCase); + 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().Setup(c => c.GetBestQualityInHistory(It.IsAny(), 3)).Returns(null); Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); + .Setup(c => c.GetDownloadClients()) + .Returns(new IDownloadClient[] { Mocker.GetMock().Object }); } private void WithFirstReportUpgradable() @@ -83,7 +84,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenSabnzbdDownloadClient() { Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve()); + .Setup(c => c.GetDownloadClients()) + .Returns(new IDownloadClient[] { Mocker.Resolve() }); } 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 _downloadClient; private Series _otherSeries; private Episode _otherEpisode; @@ -50,34 +50,30 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Episodes = new List { _episode }) .With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD)}) .Build(); - - _downloadClient = Mocker.GetMock(); - - Mocker.GetMock() - .Setup(s => s.GetDownloadClient()) - .Returns(_downloadClient.Object); } private void GivenEmptyQueue() { - _downloadClient.Setup(s => s.GetQueue()) - .Returns(new List()); + Mocker.GetMock() + .Setup(s => s.GetQueue()) + .Returns(new List()); } private void GivenQueue(IEnumerable remoteEpisodes) { - var queue = new List(); + var queue = new List(); 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() + .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 + { + private List _completed; + + [SetUp] + public void Setup() + { + _completed = Builder.CreateListOfSize(1) + .All() + .With(h => h.Status = DownloadItemStatus.Completed) + .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .Build() + .ToList(); + + Mocker.GetMock() + .Setup(c => c.GetDownloadClients()) + .Returns( new IDownloadClient[] { Mocker.GetMock().Object }); + + Mocker.GetMock() + .SetupGet(c => c.Definition) + .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); + + Mocker.GetMock() + .SetupGet(s => s.EnableCompletedDownloadHandling) + .Returns(true); + + Mocker.GetMock() + .SetupGet(s => s.RemoveCompletedDownloads) + .Returns(true); + + Mocker.GetMock() + .Setup(s => s.Failed()) + .Returns(new List()); + + Mocker.SetConstant(Mocker.Resolve()); + } + + private void GivenNoGrabbedHistory() + { + Mocker.GetMock() + .Setup(s => s.Grabbed()) + .Returns(new List()); + } + + private void GivenGrabbedHistory(List history) + { + Mocker.GetMock() + .Setup(s => s.Grabbed()) + .Returns(history); + } + + private void GivenNoImportedHistory() + { + Mocker.GetMock() + .Setup(s => s.Imported()) + .Returns(new List()); + } + + private void GivenImportedHistory(List importedHistory) + { + Mocker.GetMock() + .Setup(s => s.Imported()) + .Returns(importedHistory); + } + + private void GivenCompletedDownloadClientHistory(bool hasStorage = true) + { + Mocker.GetMock() + .Setup(s => s.GetItems()) + .Returns(_completed); + + Mocker.GetMock() + .Setup(c => c.FolderExists(It.IsAny())) + .Returns(hasStorage); + } + + private void GivenCompletedImport() + { + Mocker.GetMock() + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Returns(new List() { new Core.MediaFiles.EpisodeImport.ImportDecision(null) }); + } + + private void GivenFailedImport() + { + Mocker.GetMock() + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Returns(new List()); + } + + private void VerifyNoImports() + { + Mocker.GetMock() + .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny()), Times.Never()); + } + + private void VerifyImports() + { + Mocker.GetMock() + .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny()), 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.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.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + GivenGrabbedHistory(historyGrabbed); + + var historyImported = Builder.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.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.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.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.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + + Mocker.GetMock() + .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.CreateListOfSize(1) + .Build() + .ToList(); + + GivenGrabbedHistory(history); + GivenNoImportedHistory(); + GivenCompletedImport(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId); + + Mocker.GetMock() + .SetupGet(s => s.RemoveCompletedDownloads) + .Returns(false); + + Subject.Execute(new CheckForFinishedDownloadCommand()); + + Mocker.GetMock() + .Verify(c => c.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_not_remove_while_readonly() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.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() + .Verify(c => c.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_not_remove_if_imported_failed() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.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() + .Verify(c => c.DeleteFolder(It.IsAny(), true), Times.Never()); + } + + [Test] + public void should_remove_if_imported() + { + GivenCompletedDownloadClientHistory(); + + var history = Builder.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() + .Verify(c => c.DeleteFolder(It.IsAny(), 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 + { + 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() + .Setup(c => c.DownloadFile(It.IsAny(), It.IsAny())) + .Throws(new WebException()); + } + + protected void GivenCompletedItem() + { + var targetDir = Path.Combine(_completedDownloadFolder, _title); + Mocker.GetMock() + .Setup(c => c.GetDirectories(_completedDownloadFolder)) + .Returns(new[] { targetDir }); + + Mocker.GetMock() + .Setup(c => c.GetFiles(targetDir, SearchOption.AllDirectories)) + .Returns(new[] { Path.Combine(_completedDownloadFolder, "somefile.mkv") }); + + Mocker.GetMock() + .Setup(c => c.GetFileSize(It.IsAny())) + .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().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().Verify(c => c.DownloadFile(It.IsAny(), expectedFilename), Times.Once()); + } + + [Test] + public void GetItems_should_considered_locked_files_downloading() + { + GivenCompletedItem(); + + Mocker.GetMock() + .Setup(c => c.IsFileLocked(It.IsAny())) + .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 - { - 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().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 DownloadNzb_should_download_file_if_it_doesnt_exist() - { - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock().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().Verify(c => c.DownloadFile(It.IsAny(), 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 : CoreTest + 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() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), 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(); + + 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 - { - 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.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() - .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns("id"); - - Subject.DownloadNzb(_remoteEpisode); - - Mocker.GetMock() - .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), 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 + { + 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 { 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 { 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 { new NzbgetParameter { Name = "drone", Value = "id" } }, + ParStatus = "SUCCESS", + ScriptStatus = "NONE" + }; + } + + protected void WithFailedDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((String)null); + } + + protected void WithSuccessfulDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Guid.NewGuid().ToString().Replace("-", "")); + } + + protected virtual void WithQueue(NzbgetQueueItem queue) + { + var list = new List(); + + if (queue != null) + { + list.Add(queue); + } + + Mocker.GetMock() + .Setup(s => s.GetQueue(It.IsAny())) + .Returns(list); + } + + protected virtual void WithHistory(NzbgetHistoryItem history) + { + var list = new List(); + + if (history != null) + { + list.Add(history); + } + + Mocker.GetMock() + .Setup(s => s.GetHistory(It.IsAny())) + .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 - { - private List _queue; - - [SetUp] - public void Setup() - { - _queue = Builder.CreateListOfSize(5) - .All() - .With(q => q.NzbName = "30.Rock.S01E01.Pilot.720p.hdtv.nzb") - .With(q => q.Parameters = new List - { - 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() - .Setup(s => s.GetQueue(It.IsAny())) - .Returns(_queue); - } - - private void WithEmptyQueue() - { - Mocker.GetMock() - .Setup(s => s.GetQueue(It.IsAny())) - .Returns(new List()); - } - - [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() - .Setup(s => s.Map(It.IsAny(), 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().Verify(c => c.DownloadFile(_nzbUrl, _nzbPath), Times.Once()); } @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests { WithFailedDownload(); - Assert.Throws(() => Subject.DownloadNzb(_remoteEpisode)); + Assert.Throws(() => 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(() => Subject.DownloadNzb(_remoteEpisode)); + Assert.Throws(() => Subject.Download(_remoteEpisode)); + } + + [Test] + public void should_throw_item_is_removed() + { + Assert.Throws(() => 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().Verify(c => c.DownloadFile(It.IsAny(), 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 + public class SabnzbdFixture : DownloadClientFixtureBase { - 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.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() + { + 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() + { + 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() + { + 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() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((SabnzbdAddResponse)null); + } + + protected void WithSuccessfulDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new SabnzbdAddResponse() + { + Status = true, + Ids = new List { "sabznbd_nzo12345" } + }); + } + + protected virtual void WithQueue(SabnzbdQueue queue) + { + if (queue == null) + { + queue = new SabnzbdQueue() { Items = new List() }; + } + + Mocker.GetMock() + .Setup(s => s.GetQueue(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(queue); + } + + protected virtual void WithHistory(SabnzbdHistory history) + { + if (history == null) + history = new SabnzbdHistory() { Items = new List() }; + + Mocker.GetMock() + .Setup(s => s.GetHistory(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(history); + } + + [Test] + 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 downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_true() + public void Download_should_use_sabRecentTvPriority_when_recentEpisode_is_true() { Mocker.GetMock() .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabnzbdPriority.High, It.IsAny())) .Returns(new SabnzbdAddResponse()); - Subject.DownloadNzb(_remoteEpisode); + var remoteEpisode = CreateRemoteEpisode(); + remoteEpisode.Episodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT)) + .Build() + .ToList(); + + Subject.Download(remoteEpisode); Mocker.GetMock() .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabnzbdPriority.High, It.IsAny()), 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 { private RemoteEpisode _parseResult; - + private List _downloadClients; [SetUp] public void Setup() { + _downloadClients = new List(); + + Mocker.GetMock() + .Setup(v => v.GetDownloadClients()) + .Returns(_downloadClients); + Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); + .Setup(v => v.GetDownloadClient(It.IsAny())) + .Returns(v => _downloadClients.FirstOrDefault(d => d.Protocol == v)); var episodes = Builder.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.CreateNew() + .With(v => v.DownloadProtocol = Indexers.DownloadProtocol.Usenet) + .Build(); + _parseResult = Builder.CreateNew() .With(c => c.Series = Builder.CreateNew().Build()) - .With(c => c.Release = Builder.CreateNew().Build()) + .With(c => c.Release = releaseInfo) .With(c => c.Episodes = episodes) .Build(); } - private void WithSuccessfulAdd() + private Mock WithUsenetClient() { - Mocker.GetMock() - .Setup(s => s.DownloadNzb(It.IsAny())); + var mock = new Mock(Moq.MockBehavior.Default); + _downloadClients.Add(mock.Object); + + mock.SetupGet(v => v.Protocol).Returns(Indexers.DownloadProtocol.Usenet); + + return mock; } - private void WithFailedAdd() + private Mock WithTorrentClient() { - Mocker.GetMock() - .Setup(s => s.DownloadNzb(It.IsAny())) - .Throws(new WebException()); + var mock = new Mock(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())); + Subject.DownloadReport(_parseResult); VerifyEventPublished(); @@ -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())); + Subject.DownloadReport(_parseResult); - Mocker.GetMock() - .Verify(s => s.DownloadNzb(It.IsAny()), Times.Once()); + mock.Verify(s => s.Download(It.IsAny()), 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())) + .Throws(new WebException()); Assert.Throws(() => 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() - .Setup(c => c.GetDownloadClient()).Returns((IDownloadClient)null); - Subject.DownloadReport(_parseResult); - Mocker.GetMock().Verify(c => c.DownloadNzb(It.IsAny()), Times.Never()); + Mocker.GetMock().Verify(c => c.Download(It.IsAny()), Times.Never()); VerifyEventNotPublished(); 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()), Times.Never()); + mockUsenet.Verify(c => c.Download(It.IsAny()), 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()), Times.Once()); + mockUsenet.Verify(c => c.Download(It.IsAny()), 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 + public class FailedDownloadServiceFixture : CoreTest { - private List _completed; - private List _failed; + private List _completed; + private List _failed; [SetUp] public void Setup() { - _completed = Builder.CreateListOfSize(5) + _completed = Builder.CreateListOfSize(5) .All() - .With(h => h.Status = HistoryStatus.Completed) + .With(h => h.Status = DownloadItemStatus.Completed) .Build() .ToList(); - _failed = Builder.CreateListOfSize(1) + _failed = Builder.CreateListOfSize(1) .All() - .With(h => h.Status = HistoryStatus.Failed) + .With(h => h.Status = DownloadItemStatus.Failed) .Build() .ToList(); Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); + .Setup(c => c.GetDownloadClients()) + .Returns( new IDownloadClient[] { Mocker.GetMock().Object }); + + Mocker.GetMock() + .SetupGet(c => c.Definition) + .Returns(new Core.Download.DownloadClientDefinition { Id = 1, Name = "testClient" }); Mocker.GetMock() .SetupGet(s => s.EnableFailedDownloadHandling) .Returns(true); + + Mocker.GetMock() + .Setup(s => s.Imported()) + .Returns(new List()); + + Mocker.SetConstant(Mocker.Resolve()); } private void GivenNoGrabbedHistory() @@ -72,7 +83,7 @@ namespace NzbDrone.Core.Test.Download private void GivenFailedDownloadClientHistory() { Mocker.GetMock() - .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() - .Setup(s => s.GetHistory(0, 20)) - .Returns(new List()); + .Setup(s => s.GetItems()) + .Returns(new List()); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); Mocker.GetMock() .Verify(s => s.BetweenDates(It.IsAny(), It.IsAny(), 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() - .Setup(s => s.GetHistory(0, 20)) + .Setup(s => s.GetItems()) .Returns(_completed); - Subject.Execute(new CheckForFailedDownloadCommand()); + Subject.Execute(new CheckForFinishedDownloadCommand()); Mocker.GetMock() .Verify(s => s.BetweenDates(It.IsAny(), It.IsAny(), 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() - .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(); - downloadClient.Setup(s => s.GetQueue()) + downloadClient.Setup(s => s.GetItems()) .Throws(); Mocker.GetMock() - .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(); - downloadClient.Setup(s => s.GetQueue()) - .Returns(new List()); + downloadClient.Setup(s => s.GetItems()) + .Returns(new List()); Mocker.GetMock() - .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() - .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 + { + private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted"; + + private IList _completed; + + private void GivenCompletedDownloadHandling(bool? enabled = null) + { + if (enabled.HasValue) + { + Mocker.GetMock() + .Setup(s => s.IsDefined("EnableCompletedDownloadHandling")) + .Returns(true); + + Mocker.GetMock() + .SetupGet(s => s.EnableCompletedDownloadHandling) + .Returns(enabled.Value); + } + + _completed = Builder.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() + .Setup(v => v.GetCompletedDownloads()) + .Returns(_completed.ToList()); + } + + private void GivenDroneFactoryFolder(bool exists = false) + { + Mocker.GetMock() + .SetupGet(s => s.DownloadedEpisodesFolder) + .Returns(DRONE_FACTORY_FOLDER.AsOsAgnostic()); + + Mocker.GetMock() + .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>(_indexers); } - [Test] - public void should_create_default_indexer_on_startup() - { - IList storedIndexers = null; - - Mocker.GetMock() - .Setup(c => c.InsertMany(It.IsAny>())) - .Callback>(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(Mocker.Resolve()); - - 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 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(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { "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() - .Setup(s => s.Import(It.IsAny>(), true)) + .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(new List()); } @@ -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().Setup(c => c.IsFileLocked(It.IsAny())) .Returns(true); @@ -122,7 +124,7 @@ namespace NzbDrone.Core.Test.MediaFiles public void should_not_delete_folder_if_no_files_were_imported() { Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), false)) + .Setup(s => s.Import(It.IsAny>(), false, null)) .Returns(new List()); 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() - .Setup(s => s.Import(It.IsAny>(), true)) + .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(imported); Subject.Execute(new DownloadedEpisodesScanCommand()); @@ -172,7 +174,7 @@ namespace NzbDrone.Core.Test.MediaFiles .Returns(imported); Mocker.GetMock() - .Setup(s => s.Import(It.IsAny>(), true)) + .Setup(s => s.Import(It.IsAny>(), true, null)) .Returns(imported); Mocker.GetMock() @@ -211,13 +213,13 @@ namespace NzbDrone.Core.Test.MediaFiles private void VerifyNoImport() { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true), + Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true, null), Times.Never()); } private void VerifyImport() { - Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), true), + Mocker.GetMock().Verify(c => c.Import(It.IsAny>(), 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 - { - private LocalEpisode _localEpisode; - - [SetUp] - public void Setup() - { - _localEpisode = new LocalEpisode - { - Path = @"C:\Test\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(), - Size = 100, - Series = Builder.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() - .Verify(v => v.IsFileLocked(It.IsAny()), Times.Never()); - } - - [Test] - public void should_return_false_if_file_is_in_use() - { - Mocker.GetMock() - .Setup(s => s.IsFileLocked(It.IsAny())) - .Returns(true); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); - } - - [Test] - public void should_return_true_if_file_is_not_in_use() - { - Mocker.GetMock() - .Setup(s => s.IsFileLocked(It.IsAny())) - .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() - .Setup(s => s.UpgradeEpisodeFile(It.IsAny(), It.IsAny())) + .Setup(s => s.UpgradeEpisodeFile(It.IsAny(), It.IsAny(), 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 {_approvedDecisions.First()}, true); Mocker.GetMock() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode), + .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _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 { _approvedDecisions.First() }); + Subject.Import(new List { _approvedDecisions.First() }, false); Mocker.GetMock() - .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _approvedDecisions.First().LocalEpisode), + .Verify(v => v.UpgradeEpisodeFile(It.IsAny(), _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 @@ - - - + + + + + @@ -156,7 +158,6 @@ - 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 @@ + + \ 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 AllWithDefaults(); void SaveConfigDictionary(Dictionary 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().RegisterModel("Config"); Mapper.Entity().RegisterModel("RootFolders").Ignore(r => r.FreeSpace); - Mapper.Entity().RegisterModel("Indexers"); + Mapper.Entity().RegisterModel("Indexers") + .Ignore(s => s.Protocol); Mapper.Entity().RegisterModel("ScheduledTasks"); Mapper.Entity().RegisterModel("Notifications"); Mapper.Entity().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()) { _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, IExecute - { - 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 GetQueue() - { - return new QueueItem[0]; - } - - public override IEnumerable 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 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, IExecute { 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 GetQueue() + private IEnumerable GetQueue() { List queue; @@ -59,36 +65,42 @@ namespace NzbDrone.Core.Download.Clients.Nzbget catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty(); + return Enumerable.Empty(); } - var queueItems = new List(); + var queueItems = new List(); 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"; - - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); - if (parsedEpisodeInfo == null) continue; + queueItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); + queueItem.RemainingSize = MakeInt64(item.RemainingSizeHi, item.RemainingSizeLo); + queueItem.Category = item.Category; + + 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 GetHistory(int start = 0, int limit = 10) + private IEnumerable GetHistory() { List history; @@ -99,10 +111,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget catch (DownloadClientException ex) { _logger.ErrorException(ex.Message, ex); - return Enumerable.Empty(); + return Enumerable.Empty(); } - var historyItems = new List(); + var historyItems = new List(); 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 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 GetQueue() + public override IEnumerable GetItems() { - return new QueueItem[0]; + return new DownloadClientItem[0]; } - - public override IEnumerable 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 { 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, IExecute { private readonly IHttpProvider _httpProvider; - private readonly IParsingService _parsingService; private readonly ISabnzbdProxy _proxy; - private readonly ICached> _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>(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 GetQueue() + private IEnumerable 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(); + } + + var queueItems = new List(); - try + 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.Status = DownloadItemStatus.Queued; } - - var queueItems = new List(); - - foreach (var sabQueueItem in sabQueue.Items) + else { - 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; + queueItem.Status = DownloadItemStatus.Downloading; + } - queueItems.Add(queueItem); + if (queueItem.Title.StartsWith("ENCRYPTED /")) + { + queueItem.Title = queueItem.Title.Substring(11); + queueItem.IsEncrypted = true; } - return queueItems; - }, TimeSpan.FromSeconds(10)); + queueItems.Add(queueItem); + } + + return queueItems; } - public override IEnumerable GetHistory(int start = 0, int limit = 10) + private IEnumerable 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(); + return Enumerable.Empty(); } - var historyItems = new List(); + var historyItems = new List(); 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 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, IExecute + { + 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 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 + { + 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 grabbedHistory, List 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 GetHistoryItems(List 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 grabbedHistory, List 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 : IDownloadClient where TSettings : IProviderConfig, new() + public abstract class DownloadClientBase : 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 GetQueue(); - public abstract IEnumerable 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 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 { - List Enabled(); + } public class DownloadClientFactory : ProviderFactory, IDownloadClientFactory @@ -22,9 +22,18 @@ namespace NzbDrone.Core.Download _providerRepository = providerRepository; } - public List Enabled() + protected override List Active() + { + return base.Active().Where(c => c.Enable).ToList(); + } + + protected override DownloadClientDefinition GetTemplate(IDownloadClient provider) { - return GetAvailableProviders().Where(n => ((DownloadClientDefinition)n.Definition).Enable).ToList(); + 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 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 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 GetTrackedDownloads(); + List GetCompletedDownloads(); + List GetQueuedDownloads(); + } + + public class DownloadTrackingService : IDownloadTrackingService, IExecute, IHandle + { + 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 _trackedDownloads; + private readonly ICached> _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(GetType()); + _queuedDownloads = cacheManager.GetCache>(GetType(), "queued"); + } + + public List GetTrackedDownloads() + { + return _trackedDownloads.Values.ToList(); + } + + public List GetCompletedDownloads() + { + return _trackedDownloads.Values.Where(v => v.State == TrackedDownloadState.Downloading && v.DownloadItem.Status == DownloadItemStatus.Completed).ToList(); + } + + public List 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 GetHistoryItems(List 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(_trackedDownloads.Values); + var newTrackedDownloads = new HashSet(); + + 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 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 grabbedHistory, List failedHistory); } - public class FailedDownloadService : IFailedDownloadService, IExecute + 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 _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(GetType()); } public void MarkAsFailed(int historyId) @@ -51,149 +41,92 @@ namespace NzbDrone.Core.Download PublishDownloadFailedEvent(new List { item }, "Manually marked as failed"); } - private void CheckQueue(List grabbedHistory, List failedHistory) + public void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List grabbedHistory, List failedHistory) { - var downloadClient = GetDownloadClient(); - - if (downloadClient == null) - { - return; - } - - var downloadClientQueue = downloadClient.GetQueue().ToList(); - var failedItems = downloadClientQueue.Where(q => q.Title.StartsWith("ENCRYPTED / ")).ToList(); - - if (!failedItems.Any()) + if (!_configService.EnableFailedDownloadHandling) { - _logger.Debug("Yay! No encrypted downloads"); return; } - foreach (var failedItem in failedItems) + if (trackedDownload.DownloadItem.IsEncrypted && trackedDownload.State == TrackedDownloadState.Downloading) { - var failedLocal = failedItem; - var historyItems = GetHistoryItems(grabbedHistory, failedLocal.Id); + var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId); - 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 grabbedHistory, List 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); } } - } - - private List GetHistoryItems(List grabbedHistory, string downloadClientId) - { - return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID))) - .ToList(); - } - private void PublishDownloadFailedEvent(List 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) + if (_configService.RemoveFailedDownloads && trackedDownload.State == TrackedDownloadState.DownloadFailed) { - _logger.Debug("No download client is configured"); - } + try + { + _logger.Info("Removing failed download from client: {0}", trackedDownload.DownloadItem.Title); + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId); - return downloadClient; + trackedDownload.State = TrackedDownloadState.Removed; + } + catch (NotSupportedException) + { + _logger.Debug("Removing item not supported by your download client"); + } + } } - private bool FailedDownloadForRecentRelease(HistoryItem failedDownloadHistoryItem, List matchingHistoryItems) + private bool FailedDownloadForRecentRelease(IDownloadClient downloadClient, TrackedDownload trackedDownload, List 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 GetHistoryItems(List grabbedHistory, string downloadClientId) { - if (!_configService.EnableFailedDownloadHandling) + return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID))) + .ToList(); + } + + private void PublishDownloadFailedEvent(List 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 GetQueue(); - IEnumerable GetHistory(int start = 0, int limit = 0); - void RemoveFromQueue(string id); - void RemoveFromHistory(string id); + DownloadProtocol Protocol { get; } + + string Download(RemoteEpisode remoteEpisode); + IEnumerable 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 BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); List Grabbed(); + List Imported(); History MostRecentForEpisode(int episodeId); List FindBySourceTitle(string sourceTitle); } @@ -62,6 +63,11 @@ namespace NzbDrone.Core.History return Query.Where(h => h.EventType == HistoryEventType.Grabbed); } + public List 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 BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); List Grabbed(); + List Imported(); History MostRecentForEpisode(int episodeId); History Get(int id); List FindBySourceTitle(string sourceTitle); @@ -62,6 +63,11 @@ namespace NzbDrone.Core.History return _historyRepository.Grabbed(); } + public List 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 - { - 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 RecentFeed - { - get - { - yield return "http://www.ezrss.it/feed/"; - } - } - - public override IEnumerable 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 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 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 GetSearchUrls(string query, int offset) - { - return new List(); - } - } -} \ 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().ToList()); - } } protected override List 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 { + 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 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 { - 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(); } - - 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 { - 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 + public interface IDownloadedEpisodesImportService + { + List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem); + List ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem); + } + + public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService, IExecute { private readonly IDiskProvider _diskProvider; private readonly IDiskScanService _diskScanService; @@ -50,9 +57,53 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } + public List 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(); + } + + 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 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(); + } + + var decisions = _importDecisionMaker.GetImportDecisions(new List() { 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(); + } + } + 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 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 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 Import(List decisions, bool newDownloads = false); + List Import(List decisions, bool newDownload, DownloadClientItem historyItem = null); } public class ImportApprovedEpisodes : IImportApprovedEpisodes @@ -39,14 +40,14 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport _logger = logger; } - public List Import(List decisions, bool newDownload = false) + public List Import(List 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(); @@ -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 @@ ..\packages\valueinjecter.2.3.3\lib\net35\Omu.ValueInjecter.dll + + False + ..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll + @@ -90,9 +94,6 @@ ..\packages\Prowlin.0.9.4456.26422\lib\net40\Prowlin.dll - - ..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll - ..\Libraries\Sqlite\System.Data.SQLite.dll @@ -238,10 +239,12 @@ - - + + + + - + @@ -253,6 +256,7 @@ + @@ -262,15 +266,16 @@ - - - + + + + + - - + @@ -278,6 +283,7 @@ + @@ -307,10 +313,9 @@ - + - @@ -440,7 +445,6 @@ - @@ -754,9 +758,7 @@ Always - - - + 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 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(); - } - - 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(); - } + return MapQueue(queueItems); } - private List MapQueue(IEnumerable queueItems) + private List MapQueue(IEnumerable queueItems) { var queued = new List(); @@ -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 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 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 @@ - + \ 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() + }); } 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 @@ ..\packages\NUnit.2.6.2\lib\nunit.framework.dll + + False + ..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll + @@ -84,9 +88,6 @@ ..\packages\Owin.1.0\lib\net40\Owin.dll - - ..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll - 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 @@ - + \ 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 registeredMocks; + private IUnityContainer _container; + private IDictionary _registeredMocks; public AutoMoqer() { @@ -46,7 +46,7 @@ namespace NzbDrone.Test.Common.AutoMoq public virtual T Resolve() { ResolveType = typeof(T); - var result = container.Resolve(); + var result = _container.Resolve(); 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 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(); + _registeredMocks = new Dictionary(); AddTheAutoMockingContainerExtensionToTheContainer(container); } @@ -149,19 +149,19 @@ namespace NzbDrone.Test.Common.AutoMoq private Mock TheRegisteredMockForThisType(Type type) where T : class { - return (Mock)registeredMocks.Where(x => x.Key == type).First().Value; + return (Mock)_registeredMocks.Where(x => x.Key == type).First().Value; } private void CreateANewMockAndRegisterIt(Type type, MockBehavior behavior) where T : class { var mock = new Mock(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() 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 @@ ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll + + False + ..\packages\RestSharp.104.4.0\lib\net4\RestSharp.dll + @@ -68,9 +72,6 @@ ..\packages\NUnit.2.6.2\lib\nunit.framework.dll - - ..\packages\RestSharp.104.3.3\lib\net4\RestSharp.dll - 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 @@ - + \ 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(''.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 @@ Download Clients
-
    +
    • 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 @@ 
      -
      -
      +
      +
      - 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 @@ -
      - Failed Download Handling +
      + Download Client Import Handling +
      + + +
      +
      +
      +
      + +
      +
      + + +
      +
      +
      +
      +
      +
      + +
      + Download Client Failed Handling +
      @@ -40,7 +90,7 @@ - +
      @@ -62,7 +112,7 @@ - +
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 @@ 
- Options + Drone Factory Options
- +
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, _) { - - var view = Marionette.ItemView.extend({ - template: 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', - - 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' - }, - - initialize: function (options) { - this.downloadClientCollection = options.downloadClientCollection; - }, - - 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; +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', + + 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' + }, + + 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.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 }); - CommandController.Execute(testCommand, properties); + 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); + }, - AsModelBoundView.call(view); - AsValidatedView.call(view); + _test: function () { + var testCommand = 'test{0}'.format(this.model.get('implementation')); + var properties = {}; - return view; + _.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 @@ + 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 @@