New: Rebuilt Completed/Failed download handling from scratch

pull/4/head
Keivan Beigi 10 years ago
parent 264bb66c16
commit a6d34caf2c

@ -17,14 +17,6 @@ namespace NzbDrone.Api.Config
.SetValidator(pathExistsValidator)
.When(c => !String.IsNullOrWhiteSpace(c.DownloadedEpisodesFolder));
SharedValidator.RuleFor(c => c.BlacklistGracePeriod)
.InclusiveBetween(1, 24);
SharedValidator.RuleFor(c => c.BlacklistRetryInterval)
.InclusiveBetween(5, 120);
SharedValidator.RuleFor(c => c.BlacklistRetryLimit)
.InclusiveBetween(0, 10);
}
}
}

@ -12,11 +12,7 @@ namespace NzbDrone.Api.Config
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 Int32 BlacklistGracePeriod { get; set; }
public Int32 BlacklistRetryInterval { get; set; }
public Int32 BlacklistRetryLimit { get; set; }
}
}

@ -12,15 +12,15 @@ namespace NzbDrone.Api.History
{
private readonly IHistoryService _historyService;
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
private readonly IDownloadTrackingService _downloadTrackingService;
private readonly IFailedDownloadService _failedDownloadService;
public HistoryModule(IHistoryService historyService,
IQualityUpgradableSpecification qualityUpgradableSpecification,
IDownloadTrackingService downloadTrackingService)
IFailedDownloadService failedDownloadService)
{
_historyService = historyService;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
_downloadTrackingService = downloadTrackingService;
_failedDownloadService = failedDownloadService;
GetResourcePaged = GetHistory;
Post["/failed"] = x => MarkAsFailed();
@ -28,9 +28,9 @@ namespace NzbDrone.Api.History
protected override HistoryResource ToResource<TModel>(TModel model)
{
var resource = base.ToResource<TModel>(model);
var resource = base.ToResource(model);
var history = model as NzbDrone.Core.History.History;
var history = model as Core.History.History;
if (history != null && history.Series != null)
{
@ -70,7 +70,7 @@ namespace NzbDrone.Api.History
private Response MarkAsFailed()
{
var id = (int)Request.Form.Id;
_downloadTrackingService.MarkAsFailed(id);
_failedDownloadService.MarkAsFailed(id);
return new Object().AsResponse();
}
}

@ -20,6 +20,7 @@ namespace NzbDrone.Api.History
public string Indexer { get; set; }
public string NzbInfoUrl { get; set; }
public string ReleaseGroup { get; set; }
public string DownloadId { get; set; }
public HistoryEventType EventType { get; set; }

@ -63,10 +63,6 @@
<Reference Include="DDay.iCal">
<HintPath>..\packages\DDay.iCal.1.0.2.575\lib\DDay.iCal.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AspNet.SignalR.Core, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Microsoft.AspNet.SignalR.Core.1.1.3\lib\net40\Microsoft.AspNet.SignalR.Core.dll</HintPath>
</Reference>
<Reference Include="NLog">
<HintPath>..\packages\NLog.2.1.0\lib\net40\NLog.dll</HintPath>
</Reference>

@ -1,10 +1,10 @@
using System.Linq;
using Nancy;
using Nancy;
using Nancy.Responses;
using NzbDrone.Api.Extensions;
using NzbDrone.Api.REST;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Queue;
namespace NzbDrone.Api.Queue
@ -12,21 +12,21 @@ namespace NzbDrone.Api.Queue
public class QueueActionModule : NzbDroneRestModule<QueueResource>
{
private readonly IQueueService _queueService;
private readonly IDownloadTrackingService _downloadTrackingService;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly ICompletedDownloadService _completedDownloadService;
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IPendingReleaseService _pendingReleaseService;
private readonly IDownloadService _downloadService;
public QueueActionModule(IQueueService queueService,
IDownloadTrackingService downloadTrackingService,
ITrackedDownloadService trackedDownloadService,
ICompletedDownloadService completedDownloadService,
IProvideDownloadClient downloadClientProvider,
IPendingReleaseService pendingReleaseService,
IDownloadService downloadService)
{
_queueService = queueService;
_downloadTrackingService = downloadTrackingService;
_trackedDownloadService = trackedDownloadService;
_completedDownloadService = completedDownloadService;
_downloadClientProvider = downloadClientProvider;
_pendingReleaseService = pendingReleaseService;
@ -60,7 +60,7 @@ namespace NzbDrone.Api.Queue
throw new BadRequestException();
}
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId);
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId);
return new object().AsResponse();
}
@ -70,7 +70,7 @@ namespace NzbDrone.Api.Queue
var resource = Request.Body.FromJson<QueueResource>();
var trackedDownload = GetTrackedDownload(resource.Id);
_completedDownloadService.Import(trackedDownload);
_completedDownloadService.Process(trackedDownload);
return resource.AsResponse();
}
@ -100,7 +100,7 @@ namespace NzbDrone.Api.Queue
throw new NotFoundException();
}
var trackedDownload = _downloadTrackingService.Find(queueItem.TrackingId);
var trackedDownload = _trackedDownloadService.Find(queueItem.TrackingId);
if (trackedDownload == null)
{

@ -9,7 +9,7 @@ using NzbDrone.SignalR;
namespace NzbDrone.Api.Queue
{
public class QueueModule : NzbDroneRestModuleWithSignalR<QueueResource, Core.Queue.Queue>,
IHandle<UpdateQueueEvent>, IHandle<PendingReleasesUpdatedEvent>
IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent>
{
private readonly IQueueService _queueService;
private readonly IPendingReleaseService _pendingReleaseService;
@ -35,7 +35,7 @@ namespace NzbDrone.Api.Queue
return queue.Concat(pending);
}
public void Handle(UpdateQueueEvent message)
public void Handle(QueueUpdatedEvent message)
{
BroadcastResourceChange(ModelAction.Sync);
}

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using NzbDrone.Api.REST;
using NzbDrone.Core.Download;
using NzbDrone.Core.Qualities;
using NzbDrone.Api.Series;
using NzbDrone.Api.Episodes;
using NzbDrone.Core.Download.TrackedDownloads;
namespace NzbDrone.Api.Queue
{

@ -88,6 +88,7 @@
<Compile Include="ReflectionTests\ReflectionExtensionFixture.cs" />
<Compile Include="ServiceFactoryFixture.cs" />
<Compile Include="ServiceProviderTests.cs" />
<Compile Include="TPLTests\DebouncerFixture.cs" />
<Compile Include="WebClientTests.cs" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,69 @@
using System;
using System.Threading;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.TPL;
namespace NzbDrone.Common.Test.TPLTests
{
[TestFixture]
public class DebouncerFixture
{
public class Counter
{
public int Count { get; private set; }
public void Hit()
{
Count++;
}
}
[Test]
public void should_hold_the_call_for_debounce_duration()
{
var counter = new Counter();
var debounceFunction = new Debouncer(counter.Hit, TimeSpan.FromMilliseconds(50));
debounceFunction.Execute();
debounceFunction.Execute();
debounceFunction.Execute();
counter.Count.Should().Be(0);
Thread.Sleep(100);
counter.Count.Should().Be(1);
}
[Test]
public void should_throttle_cals()
{
var counter = new Counter();
var debounceFunction = new Debouncer(counter.Hit, TimeSpan.FromMilliseconds(50));
debounceFunction.Execute();
debounceFunction.Execute();
debounceFunction.Execute();
counter.Count.Should().Be(0);
Thread.Sleep(200);
debounceFunction.Execute();
debounceFunction.Execute();
debounceFunction.Execute();
Thread.Sleep(200);
counter.Count.Should().Be(2);
}
}
}

@ -182,6 +182,7 @@
<Compile Include="ServiceProvider.cs" />
<Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="TinyIoC.cs" />
<Compile Include="TPL\Debouncer.cs" />
<Compile Include="TPL\LimitedConcurrencyLevelTaskScheduler.cs" />
<Compile Include="TPL\TaskExtensions.cs" />
<Compile Include="Extensions\TryParseExtensions.cs" />

@ -0,0 +1,28 @@
using System;
namespace NzbDrone.Common.TPL
{
public class Debouncer
{
private readonly Action _action;
private readonly System.Timers.Timer _timer;
public Debouncer(Action action, TimeSpan debounceDuration)
{
_action = action;
_timer = new System.Timers.Timer(debounceDuration.TotalMilliseconds);
_timer.Elapsed += timer_Elapsed;
}
void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
_timer.Stop();
_action();
}
public void Execute()
{
_timer.Start();
}
}
}

@ -24,20 +24,12 @@ namespace NzbDrone.Core.Test.Blacklisting
Quality = new QualityModel(Quality.Bluray720p),
SourceTitle = "series.title.s01e01",
DownloadClient = "SabnzbdClient",
DownloadClientId = "Sabnzbd_nzo_2dfh73k"
DownloadId = "Sabnzbd_nzo_2dfh73k"
};
_event.Data.Add("publishedDate", DateTime.UtcNow.ToString("s") + "Z");
}
[Test]
public void should_trigger_redownload()
{
Subject.Handle(_event);
Mocker.GetMock<IRedownloadFailedDownloads>()
.Verify(v => v.Redownload(_event.SeriesId, _event.EpisodeIds), Times.Once());
}
[Test]
public void should_add_to_repository()

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using FluentMigrator;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.History;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class history_downloadIdFixture : MigrationTest<history_downloadId>
{
[Test]
public void should_move_grab_id_from_date_to_columns()
{
WithTestDb(c =>
{
InsertHistory(c, new Dictionary<string, string>
{
{"indexer","test"},
{"downloadClientId","123"}
});
InsertHistory(c, new Dictionary<string, string>
{
{"indexer","test"},
{"downloadClientId","abc"}
});
});
var allProfiles = Mocker.Resolve<HistoryRepository>().All().ToList();
allProfiles.Should().HaveCount(2);
allProfiles.Should().NotContain(c => c.Data.ContainsKey("downloadClientId"));
allProfiles.Should().Contain(c => c.DownloadId == "123");
allProfiles.Should().Contain(c => c.DownloadId == "abc");
}
[Test]
public void should_leave_items_with_no_grabid()
{
WithTestDb(c =>
{
InsertHistory(c, new Dictionary<string, string>
{
{"indexer","test"},
{"downloadClientId","123"}
});
InsertHistory(c, new Dictionary<string, string>
{
{"indexer","test"}
});
});
var allProfiles = Mocker.Resolve<HistoryRepository>().All().ToList();
allProfiles.Should().HaveCount(2);
allProfiles.Should().NotContain(c => c.Data.ContainsKey("downloadClientId"));
allProfiles.Should().Contain(c => c.DownloadId == "123");
allProfiles.Should().Contain(c => c.DownloadId == null);
}
[Test]
public void should_leave_other_data()
{
WithTestDb(c =>
{
InsertHistory(c, new Dictionary<string, string>
{
{"indexer","test"},
{"group","test2"},
{"downloadClientId","123"}
});
});
var allProfiles = Mocker.Resolve<HistoryRepository>().All().Single();
allProfiles.Data.Should().NotContainKey("downloadClientId");
allProfiles.Data.Should().Contain(new KeyValuePair<string, string>("indexer", "test"));
allProfiles.Data.Should().Contain(new KeyValuePair<string, string>("group", "test2"));
allProfiles.DownloadId.Should().Be("123");
}
private void InsertHistory(MigrationBase migrationBase, Dictionary<string, string> data)
{
migrationBase.Insert.IntoTable("History").Row(new
{
EpisodeId = 1,
SeriesId = 1,
SourceTitle = "Test",
Date = DateTime.Now,
Quality = "{}",
Data = data.ToJson(),
EventType = 1
});
}
}
}

@ -1,12 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Queue;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Test.Framework;
@ -47,33 +49,27 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_remoteEpisode = Builder<RemoteEpisode>.CreateNew()
.With(r => r.Series = _series)
.With(r => r.Episodes = new List<Episode> { _episode })
.With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD)})
.With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD) })
.Build();
}
private void GivenEmptyQueue()
{
Mocker.GetMock<IDownloadTrackingService>()
.Setup(s => s.GetQueuedDownloads())
.Returns(new TrackedDownload[0]);
Mocker.GetMock<IQueueService>()
.Setup(s => s.GetQueue())
.Returns(new List<Queue.Queue>());
}
private void GivenQueue(IEnumerable<RemoteEpisode> remoteEpisodes, TrackedDownloadState state = TrackedDownloadState.Downloading)
private void GivenQueue(IEnumerable<RemoteEpisode> remoteEpisodes)
{
var queue = new List<TrackedDownload>();
foreach (var remoteEpisode in remoteEpisodes)
var queue = remoteEpisodes.Select(remoteEpisode => new Queue.Queue
{
queue.Add(new TrackedDownload
{
State = state,
RemoteEpisode = remoteEpisode
});
}
Mocker.GetMock<IDownloadTrackingService>()
.Setup(s => s.GetQueuedDownloads())
.Returns(queue.ToArray());
RemoteEpisode = remoteEpisode
});
Mocker.GetMock<IQueueService>()
.Setup(s => s.GetQueue())
.Returns(queue.ToList());
}
[Test]
@ -95,22 +91,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_when_download_is_failed()
{
var remoteEpisode = Builder<RemoteEpisode>.CreateNew()
.With(r => r.Series = _series)
.With(r => r.Episodes = new List<Episode> { _episode })
.With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo
{
Quality = new QualityModel(Quality.DVD)
})
.Build();
GivenQueue(new List<RemoteEpisode> { remoteEpisode }, TrackedDownloadState.DownloadFailed);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
public void should_return_true_when_quality_in_queue_is_lower()
@ -241,9 +221,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality.HDTV720p)
})
.TheFirst(1)
.With(r => r.Episodes = new List<Episode> {_episode})
.With(r => r.Episodes = new List<Episode> { _episode })
.TheNext(1)
.With(r => r.Episodes = new List<Episode> {_otherEpisode})
.With(r => r.Episodes = new List<Episode> { _otherEpisode })
.Build();
_remoteEpisode.Episodes.Add(_otherEpisode);

@ -1,560 +1,225 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Test.Download
{
[TestFixture]
public class CompletedDownloadServiceFixture : CoreTest<DownloadTrackingService>
public class CompletedDownloadServiceFixture : CoreTest<CompletedDownloadService>
{
private List<DownloadClientItem> _completed;
private TrackedDownload _trackedDownload;
[SetUp]
public void Setup()
{
_completed = Builder<DownloadClientItem>.CreateListOfSize(1)
.All()
var completed = Builder<DownloadClientItem>.CreateNew()
.With(h => h.Status = DownloadItemStatus.Completed)
.With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic()))
.With(h => h.Title = "Drone.S01E01.HDTV")
.Build()
.ToList();
.Build();
var remoteEpisode = new RemoteEpisode
{
Series = new Series(),
Episodes = new List<Episode> {new Episode {Id = 1}}
Episodes = new List<Episode> { new Episode { Id = 1 } }
};
Mocker.GetMock<IProvideDownloadClient>()
.Setup(c => c.GetDownloadClients())
.Returns( new[] { Mocker.GetMock<IDownloadClient>().Object });
Mocker.GetMock<IDownloadClient>()
.SetupGet(c => c.Definition)
.Returns(new DownloadClientDefinition { Id = 1, Name = "testClient" });
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableCompletedDownloadHandling)
.Returns(true);
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.RemoveCompletedDownloads)
.Returns(true);
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Failed())
.Returns(new List<History.History>());
_trackedDownload = Builder<TrackedDownload>.CreateNew()
.With(c => c.State = TrackedDownloadStage.Downloading)
.With(c => c.DownloadItem = completed)
.With(c => c.RemoteEpisode = remoteEpisode)
.Build();
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Int32>(), It.IsAny<IEnumerable<Int32>>()))
.Returns(remoteEpisode);
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Int32>(), (SearchCriteriaBase)null))
.Returns(remoteEpisode);
Mocker.SetConstant<ICompletedDownloadService>(Mocker.Resolve<CompletedDownloadService>());
}
Mocker.GetMock<IDownloadClient>()
.SetupGet(c => c.Definition)
.Returns(new DownloadClientDefinition { Id = 1, Name = "testClient" });
private void GivenNoGrabbedHistory()
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Grabbed())
.Returns(new List<History.History>());
}
Mocker.GetMock<IProvideDownloadClient>()
.Setup(c => c.Get(It.IsAny<int>()))
.Returns(Mocker.GetMock<IDownloadClient>().Object);
private void GivenGrabbedHistory(List<History.History> history)
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Grabbed())
.Returns(history);
}
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
.Returns(new History.History());
private void GivenNoImportedHistory()
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Imported())
.Returns(new List<History.History>());
}
private void GivenImportedHistory(List<History.History> importedHistory)
private void GivenNoGrabbedHistory()
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Imported())
.Returns(importedHistory);
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
.Returns((History.History)null);
}
private void GivenCompletedDownloadClientHistory(bool hasStorage = true)
{
Mocker.GetMock<IDownloadClient>()
.Setup(s => s.GetItems())
.Returns(_completed);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderExists(It.IsAny<string>()))
.Returns(hasStorage);
}
private void GivenCompletedImport()
private void GivenSuccessfulImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }))
});
}
private void GivenFailedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>()
{
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }, "Test Failure"))
});
}
private void VerifyNoImports()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Never());
}
private void VerifyImports()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Once());
}
[Test]
public void should_process_if_matching_history_is_not_found_but_category_specified()
[TestCase(DownloadItemStatus.Downloading)]
[TestCase(DownloadItemStatus.Failed)]
[TestCase(DownloadItemStatus.Queued)]
[TestCase(DownloadItemStatus.Paused)]
[TestCase(DownloadItemStatus.Warning)]
public void should_not_process_if_download_status_isnt_completed(DownloadItemStatus status)
{
_completed.First().Category = "tv";
_trackedDownload.DownloadItem.Status = status;
GivenCompletedDownloadClientHistory();
GivenNoGrabbedHistory();
GivenNoImportedHistory();
GivenCompletedImport();
Subject.Execute(new CheckForFinishedDownloadCommand());
Subject.Process(_trackedDownload);
VerifyImports();
AssertNoAttemptedImport();
}
[Test]
public void should_not_process_if_matching_history_is_not_found_and_no_category_specified()
{
_completed.First().Category = null;
GivenCompletedDownloadClientHistory();
_trackedDownload.DownloadItem.Category = null;
GivenNoGrabbedHistory();
GivenNoImportedHistory();
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoImports();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_process_if_grabbed_history_contains_null_downloadclient_id()
{
_completed.First().Category = null;
GivenCompletedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", null);
GivenGrabbedHistory(historyGrabbed);
GivenNoImportedHistory();
GivenFailedImport();
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoImports();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_process_if_failed_history_contains_null_downloadclient_id()
{
GivenCompletedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
GivenGrabbedHistory(historyGrabbed);
var historyImported = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyImported.First().Data.Add("downloadClient", "SabnzbdClient");
historyImported.First().Data.Add("downloadClientId", null);
GivenImportedHistory(historyImported);
GivenCompletedImport();
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyImports();
}
[Test]
public void should_not_process_if_already_added_to_history_as_imported()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenImportedHistory(history);
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
Subject.Process(_trackedDownload);
VerifyNoImports();
AssertNoAttemptedImport();
}
[Test]
public void should_process_if_not_already_in_imported_history()
public void should_process_if_matching_history_is_not_found_but_category_specified()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
GivenCompletedImport();
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
_trackedDownload.DownloadItem.Category = "tv";
GivenNoGrabbedHistory();
GivenSuccessfulImport();
Subject.Execute(new CheckForFinishedDownloadCommand());
Subject.Process(_trackedDownload);
VerifyImports();
AssertCompletedDownload();
}
[Test]
public void should_not_process_if_storage_directory_does_not_exist()
{
GivenCompletedDownloadClientHistory(false);
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoImports();
ExceptionVerification.IgnoreErrors();
}
[Test]
public void should_not_process_if_storage_directory_in_drone_factory()
{
GivenCompletedDownloadClientHistory(true);
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
Mocker.GetMock<IConfigService>()
.SetupGet(v => v.DownloadedEpisodesFolder)
.Returns(@"C:\DropFolder".AsOsAgnostic());
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoImports();
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_process_as_already_imported_if_drone_factory_import_history_exists()
{
GivenCompletedDownloadClientHistory(false);
_completed.Clear();
_completed.AddRange(Builder<DownloadClientItem>.CreateListOfSize(2)
.All()
.With(h => h.Status = DownloadItemStatus.Completed)
.With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic()))
.With(h => h.Title = "Drone.S01E01.HDTV")
.Build());
var grabbedHistory = Builder<History.History>.CreateListOfSize(2)
.All()
.With(d => d.Data["downloadClient"] = "SabnzbdClient")
.TheFirst(1)
.With(d => d.Data["downloadClientId"] = _completed.First().DownloadClientId)
.With(d => d.SourceTitle = "Droned.S01E01.720p-LAZY")
.TheLast(1)
.With(d => d.Data["downloadClientId"] = _completed.Last().DownloadClientId)
.With(d => d.SourceTitle = "Droned.S01E01.Proper.720p-LAZY")
.Build()
.ToList();
var importedHistory = Builder<History.History>.CreateListOfSize(2)
.All()
.With(d => d.EpisodeId = 1)
.TheFirst(1)
.With(d => d.Data["droppedPath"] = @"C:\mydownload\Droned.S01E01.720p-LAZY\lzy-dr101.mkv".AsOsAgnostic())
.TheLast(1)
.With(d => d.Data["droppedPath"] = @"C:\mydownload\Droned.S01E01.Proper.720p-LAZY\lzy-dr101.mkv".AsOsAgnostic())
.Build()
.ToList();
GivenGrabbedHistory(grabbedHistory);
GivenImportedHistory(importedHistory);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoImports();
Mocker.GetMock<IHistoryService>()
.Verify(v => v.UpdateHistoryData(It.IsAny<int>(), It.IsAny<Dictionary<String, String>>()), Times.Exactly(2));
}
[Test]
public void should_not_remove_if_config_disabled()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
_trackedDownload.DownloadItem.OutputPath = new OsPath(@"C:\DropFolder\SomeOtherFolder".AsOsAgnostic());
GivenGrabbedHistory(history);
GivenNoImportedHistory();
GivenCompletedImport();
Subject.Process(_trackedDownload);
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.RemoveCompletedDownloads)
.Returns(false);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
AssertNoAttemptedImport();
}
[Test]
public void should_not_remove_while_readonly()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
GivenCompletedImport();
_completed.First().IsReadOnly = true;
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
}
[Test]
public void should_not_remove_if_imported_failed()
public void should_not_process_if_output_path_is_empty()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
GivenFailedImport();
_trackedDownload.DownloadItem.OutputPath = new OsPath();
_completed.First().IsReadOnly = true;
Subject.Process(_trackedDownload);
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
ExceptionVerification.IgnoreErrors();
AssertNoAttemptedImport();
}
[Test]
public void should_remove_if_imported()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
GivenCompletedImport();
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Once());
}
[Test]
public void should_not_mark_as_imported_if_all_files_were_rejected()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(new LocalEpisode() {Path = @"C:\TestPath\Droned.S01E01.mkv"}, "Rejected!"),
"Test Failure")
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}, "Rejected!"),"Test Failure"),
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv"}, "Rejected!"),"Test Failure")
});
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Process(_trackedDownload);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
_trackedDownload.State.Should().NotBe(TrackedDownloadStage.Imported);
ExceptionVerification.ExpectedErrors(1);
AssertNoCompletedDownload();
}
[Test]
public void should_not_mark_as_imported_if_all_files_were_skipped()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(new LocalEpisode() {Path = @"C:\TestPath\Droned.S01E01.mkv"}),
"Test Failure")
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure"),
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure")
});
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
Subject.Process(_trackedDownload);
ExceptionVerification.ExpectedErrors(1);
AssertNoCompletedDownload();
}
[Test]
public void should_not_mark_as_imported_if_some_files_were_skipped()
{
GivenCompletedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode() {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
new ImportResult(
new ImportDecision(new LocalEpisode() {Path = @"C:\TestPath\Droned.S01E01.mkv"}),
"Test Failure")
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"})),
new ImportResult(new ImportDecision(new LocalEpisode{Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure")
});
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _completed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
Subject.Process(_trackedDownload);
AssertNoCompletedDownload();
}
private void AssertNoAttemptedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<DownloadClientItem>()), Times.Never());
AssertNoCompletedDownload();
}
private void AssertNoCompletedDownload()
{
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never());
Mocker.GetMock<IDiskProvider>()
.Verify(c => c.DeleteFolder(It.IsAny<string>(), true), Times.Never());
_trackedDownload.State.Should().NotBe(TrackedDownloadStage.Imported);
}
ExceptionVerification.ExpectedErrors(1);
private void AssertCompletedDownload()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, _trackedDownload.DownloadItem), Times.Once());
_trackedDownload.State.Should().Be(TrackedDownloadStage.Imported);
}
}
}

@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests
protected void VerifyIdentifiable(DownloadClientItem downloadClientItem)
{
downloadClientItem.DownloadClient.Should().Be(Subject.Definition.Name);
downloadClientItem.DownloadClientId.Should().NotBeNullOrEmpty();
downloadClientItem.DownloadId.Should().NotBeNullOrEmpty();
downloadClientItem.Title.Should().NotBeNullOrEmpty();
}

@ -1,15 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
@ -18,28 +16,21 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download
{
[TestFixture]
public class FailedDownloadServiceFixture : CoreTest<DownloadTrackingService>
public class FailedDownloadServiceFixture : CoreTest<FailedDownloadService>
{
private List<DownloadClientItem> _completed;
private List<DownloadClientItem> _failed;
private TrackedDownload _trackedDownload;
private List<History.History> _grabHistory;
[SetUp]
public void Setup()
{
_completed = Builder<DownloadClientItem>.CreateListOfSize(5)
.All()
var completed = Builder<DownloadClientItem>.CreateNew()
.With(h => h.Status = DownloadItemStatus.Completed)
.With(h => h.IsEncrypted = false)
.With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic()))
.With(h => h.Title = "Drone.S01E01.HDTV")
.Build()
.ToList();
.Build();
_failed = Builder<DownloadClientItem>.CreateListOfSize(1)
.All()
.With(h => h.Status = DownloadItemStatus.Failed)
.With(h => h.Title = "Drone.S01E01.HDTV")
.Build()
.ToList();
_grabHistory = Builder<History.History>.CreateListOfSize(2).BuildList();
var remoteEpisode = new RemoteEpisode
{
@ -47,410 +38,74 @@ namespace NzbDrone.Core.Test.Download
Episodes = new List<Episode> { new Episode { Id = 1 } }
};
Mocker.GetMock<IProvideDownloadClient>()
.Setup(c => c.GetDownloadClients())
.Returns( new IDownloadClient[] { Mocker.GetMock<IDownloadClient>().Object });
_trackedDownload = Builder<TrackedDownload>.CreateNew()
.With(c => c.State = TrackedDownloadStage.Downloading)
.With(c => c.DownloadItem = completed)
.With(c => c.RemoteEpisode = remoteEpisode)
.Build();
Mocker.GetMock<IDownloadClient>()
.SetupGet(c => c.Definition)
.Returns(new DownloadClientDefinition { Id = 1, Name = "testClient" });
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableFailedDownloadHandling)
.Returns(true);
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Imported())
.Returns(new List<History.History>());
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Int32>(), It.IsAny<IEnumerable<Int32>>()))
.Returns(remoteEpisode);
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Int32>(), (SearchCriteriaBase)null))
.Returns(remoteEpisode);
.Setup(s => s.Find(_trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed))
.Returns(_grabHistory);
Mocker.SetConstant<IFailedDownloadService>(Mocker.Resolve<FailedDownloadService>());
}
private void GivenNoGrabbedHistory()
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Grabbed())
.Returns(new List<History.History>());
}
private void GivenGrabbedHistory(List<History.History> history)
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Grabbed())
.Returns(history);
}
private void GivenNoFailedHistory()
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Failed())
.Returns(new List<History.History>());
}
private void GivenFailedHistory(List<History.History> failedHistory)
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Failed())
.Returns(failedHistory);
}
private void GivenFailedDownloadClientHistory()
{
Mocker.GetMock<IDownloadClient>()
.Setup(s => s.GetItems())
.Returns(_failed);
}
private void GivenGracePeriod(int hours)
{
Mocker.GetMock<IConfigService>().SetupGet(s => s.BlacklistGracePeriod).Returns(hours);
}
private void GivenRetryLimit(int count, int interval = 5)
{
Mocker.GetMock<IConfigService>().SetupGet(s => s.BlacklistRetryLimit).Returns(count);
Mocker.GetMock<IConfigService>().SetupGet(s => s.BlacklistRetryInterval).Returns(interval);
}
private void VerifyNoFailedDownloads()
{
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Never());
}
private void VerifyFailedDownloads(int count = 1)
{
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.Is<DownloadFailedEvent>(d => d.EpisodeIds.Count == count)), Times.Once());
}
private void VerifyRetryDownload()
{
Mocker.GetMock<IDownloadClient>()
.Verify(v => v.RetryDownload(It.IsAny<String>()), Times.Once());
}
private void VerifyNoRetryDownload()
{
Mocker.GetMock<IDownloadClient>()
.Verify(v => v.RetryDownload(It.IsAny<String>()), Times.Never());
}
[Test]
public void should_not_process_if_no_download_client_history()
{
Mocker.GetMock<IDownloadClient>()
.Setup(s => s.GetItems())
.Returns(new List<DownloadClientItem>());
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IHistoryService>()
.Verify(s => s.BetweenDates(It.IsAny<DateTime>(), It.IsAny<DateTime>(), HistoryEventType.Grabbed),
Times.Never());
VerifyNoFailedDownloads();
.Setup(s => s.Find(_trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed))
.Returns(new List<History.History>());
}
[Test]
public void should_not_process_if_no_failed_items_in_download_client_history()
public void should_not_fail_if_matching_history_is_not_found()
{
GivenNoGrabbedHistory();
GivenNoFailedHistory();
Mocker.GetMock<IDownloadClient>()
.Setup(s => s.GetItems())
.Returns(_completed);
Subject.Execute(new CheckForFinishedDownloadCommand());
Subject.Process(_trackedDownload);
Mocker.GetMock<IHistoryService>()
.Verify(s => s.BetweenDates(It.IsAny<DateTime>(), It.IsAny<DateTime>(), HistoryEventType.Grabbed),
Times.Never());
VerifyNoFailedDownloads();
AssertDownloadNotFailed();
}
[Test]
public void should_not_process_if_matching_history_is_not_found()
{
GivenNoGrabbedHistory();
GivenFailedDownloadClientHistory();
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoFailedDownloads();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_process_if_grabbed_history_contains_null_downloadclient_id()
public void should_mark_failed_if_encrypted()
{
GivenFailedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", null);
_trackedDownload.DownloadItem.IsEncrypted = true;
GivenGrabbedHistory(historyGrabbed);
GivenNoFailedHistory();
Subject.Process(_trackedDownload);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoFailedDownloads();
ExceptionVerification.ExpectedWarns(1);
AssertDownloadFailed();
}
[Test]
public void should_process_if_failed_history_contains_null_downloadclient_id()
{
GivenFailedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
GivenGrabbedHistory(historyGrabbed);
var historyFailed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyFailed.First().Data.Add("downloadClient", "SabnzbdClient");
historyFailed.First().Data.Add("downloadClientId", null);
GivenFailedHistory(historyFailed);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyFailedDownloads();
}
[Test]
public void should_not_process_if_already_added_to_history_as_failed()
public void should_mark_failed_if_download_item_is_failed()
{
GivenFailedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenFailedHistory(history);
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
Subject.Execute(new CheckForFinishedDownloadCommand());
Subject.Process(_trackedDownload);
VerifyNoFailedDownloads();
AssertDownloadFailed();
}
[Test]
public void should_process_if_not_already_in_failed_history()
{
GivenFailedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoFailedHistory();
history.First().Data.Add("downloadClient", "SabnzbdClient");
history.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyFailedDownloads();
}
[Test]
public void should_have_multiple_episode_ids_when_multi_episode_release_fails()
{
GivenFailedDownloadClientHistory();
var history = Builder<History.History>.CreateListOfSize(2)
.Build()
.ToList();
GivenGrabbedHistory(history);
GivenNoFailedHistory();
history.ForEach(h =>
{
h.Data.Add("downloadClient", "SabnzbdClient");
h.Data.Add("downloadClientId", _failed.First().DownloadClientId);
});
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyFailedDownloads(2);
}
[Test]
public void should_skip_if_enable_failed_download_handling_is_off()
{
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableFailedDownloadHandling)
.Returns(false);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoFailedDownloads();
}
[Test]
public void should_process_if_ageHours_is_not_set()
{
GivenFailedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
GivenGrabbedHistory(historyGrabbed);
GivenNoFailedHistory();
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyFailedDownloads();
VerifyNoRetryDownload();
}
[Test]
public void should_process_if_age_is_greater_than_grace_period()
{
GivenFailedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
historyGrabbed.First().Data.Add("ageHours", "48");
GivenGrabbedHistory(historyGrabbed);
GivenNoFailedHistory();
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyFailedDownloads();
VerifyNoRetryDownload();
}
[Test]
public void should_not_retry_if_already_failed()
{
GivenFailedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
historyGrabbed.First().Data.Add("ageHours", "1");
GivenGrabbedHistory(historyGrabbed);
GivenFailedHistory(historyGrabbed);
GivenGracePeriod(6);
GivenRetryLimit(1);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoFailedDownloads();
VerifyNoRetryDownload();
}
[Test]
public void should_process_if_retry_count_is_greater_than_grace_period()
private void AssertDownloadNotFailed()
{
GivenFailedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
historyGrabbed.First().Data.Add("ageHours", "48");
GivenGrabbedHistory(historyGrabbed);
GivenNoFailedHistory();
GivenGracePeriod(6);
Subject.Execute(new CheckForFinishedDownloadCommand());
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Never());
VerifyFailedDownloads();
VerifyNoRetryDownload();
_trackedDownload.State.Should().NotBe(TrackedDownloadStage.DownloadFailed);
}
[Test]
public void should_not_process_if_age_is_less_than_grace_period()
{
GivenFailedDownloadClientHistory();
var historyGrabbed = Builder<History.History>.CreateListOfSize(1)
.Build()
.ToList();
historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient");
historyGrabbed.First().Data.Add("downloadClientId", _failed.First().DownloadClientId);
historyGrabbed.First().Data.Add("ageHours", "1");
GivenGrabbedHistory(historyGrabbed);
GivenNoFailedHistory();
GivenGracePeriod(6);
GivenRetryLimit(1);
Subject.Execute(new CheckForFinishedDownloadCommand());
VerifyNoFailedDownloads();
VerifyNoRetryDownload();
ExceptionVerification.IgnoreWarns();
}
[Test]
public void should_manual_mark_all_episodes_of_release_as_failed()
private void AssertDownloadFailed()
{
var historyFailed = Builder<History.History>.CreateListOfSize(2)
.All()
.With(v => v.EventType == HistoryEventType.Grabbed)
.Do(v => v.Data.Add("downloadClient", "SabnzbdClient"))
.Do(v => v.Data.Add("downloadClientId", "test"))
.Build()
.ToList();
GivenGrabbedHistory(historyFailed);
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Get(It.IsAny<Int32>()))
.Returns<Int32>(i => historyFailed.FirstOrDefault(v => v.Id == i));
Subject.MarkAsFailed(1);
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Once());
VerifyFailedDownloads(2);
_trackedDownload.State.Should().Be(TrackedDownloadStage.DownloadFailed);
}
}
}

@ -1,13 +1,9 @@
using System.Linq;
using System.Collections.Generic;
using FizzWare.NBuilder;
using NUnit.Framework;
using NzbDrone.Test.Common;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
@ -16,7 +12,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{
private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted";
private IList<TrackedDownload> _completed;
private void GivenCompletedDownloadHandling(bool? enabled = null)
{
@ -30,18 +25,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.SetupGet(s => s.EnableCompletedDownloadHandling)
.Returns(enabled.Value);
}
_completed = Builder<TrackedDownload>.CreateListOfSize(1)
.All()
.With(v => v.State == TrackedDownloadState.Downloading)
.With(v => v.DownloadItem = new DownloadClientItem())
.With(v => v.DownloadItem.Status = DownloadItemStatus.Completed)
.With(v => v.DownloadItem.OutputPath = new OsPath(@"C:\Test\DropFolder\myfile.mkv".AsOsAgnostic()))
.Build();
Mocker.GetMock<IDownloadTrackingService>()
.Setup(v => v.GetCompletedDownloads())
.Returns(_completed.ToArray());
}
private void GivenDroneFactoryFolder(bool exists = false)
@ -68,17 +51,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_warning_when_downloadclient_drops_in_dronefactory_folder()
{
GivenCompletedDownloadHandling(true);
GivenDroneFactoryFolder(true);
_completed.First().DownloadItem.OutputPath = new OsPath((DRONE_FACTORY_FOLDER + @"\myfile.mkv").AsOsAgnostic());
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_ok_when_no_issues_found()

@ -1,5 +1,4 @@
using System;
using FizzWare.NBuilder;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.History;
@ -11,25 +10,6 @@ namespace NzbDrone.Core.Test.HistoryTests
[TestFixture]
public class HistoryRepositoryFixture : DbTest<HistoryRepository, History.History>
{
[Test]
public void Trim_Items()
{
var historyItem = Builder<History.History>.CreateListOfSize(30)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality = new QualityModel())
.TheFirst(10).With(c => c.Date = DateTime.Now)
.TheNext(20).With(c => c.Date = DateTime.Now.AddDays(-31))
.Build();
Db.InsertMany(historyItem);
AllStoredModels.Should().HaveCount(30);
Subject.Trim();
AllStoredModels.Should().HaveCount(10);
AllStoredModels.Should().OnlyContain(s => s.Date > DateTime.Now.AddDays(-30));
}
[Test]
public void should_read_write_dictionary()
@ -38,29 +18,37 @@ namespace NzbDrone.Core.Test.HistoryTests
.With(c => c.Quality = new QualityModel())
.BuildNew();
history.Data.Add("key1","value1");
history.Data.Add("key2","value2");
history.Data.Add("key1", "value1");
history.Data.Add("key2", "value2");
Subject.Insert(history);
StoredModel.Data.Should().HaveCount(2);
}
[Test]
public void grabbed_should_return_grabbed_items()
public void should_get_download_history()
{
var history = Builder<History.History>
.CreateListOfSize(5)
.All()
.With(c => c.Quality = new QualityModel())
.With(c => c.EventType = HistoryEventType.Unknown)
.Random(3)
var historyBluray = Builder<History.History>.CreateNew()
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
.With(c => c.SeriesId = 12)
.With(c => c.EventType = HistoryEventType.Grabbed)
.BuildListOfNew();
.BuildNew();
Subject.InsertMany(history);
var historyDvd = Builder<History.History>.CreateNew()
.With(c => c.Quality = new QualityModel(Quality.DVD))
.With(c => c.SeriesId = 12)
.With(c => c.EventType = HistoryEventType.Grabbed)
.BuildNew();
Subject.Grabbed().Should().HaveCount(3);
Subject.Insert(historyBluray);
Subject.Insert(historyDvd);
var downloadHistory = Subject.FindDownloadHistory(12, new QualityModel(Quality.Bluray1080p));
downloadHistory.Should().HaveCount(1);
}
}
}

@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.HistoryTests
Path = @"C:\Test\Unsorted\Series.s01e01.mkv"
};
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true));
Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true, "sab","abcd"));
Mocker.GetMock<IHistoryRepository>()
.Verify(v => v.Insert(It.Is<History.History>(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path))));

@ -9,7 +9,6 @@ using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
@ -19,9 +18,7 @@ namespace NzbDrone.Core.Test.MediaFiles
public class DownloadedEpisodesCommandServiceFixture : CoreTest<DownloadedEpisodesCommandService>
{
private string _droneFactory = "c:\\drop\\".AsOsAgnostic();
private string _downloadFolder = "c:\\drop_other\\Show.S01E01\\".AsOsAgnostic();
private TrackedDownload _trackedDownload;
[SetUp]
public void Setup()
@ -39,56 +36,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>());
var downloadItem = Builder<DownloadClientItem>.CreateNew()
.With(v => v.DownloadClientId = "sab1")
.With(v => v.Status = DownloadItemStatus.Downloading)
.Build();
_trackedDownload = new TrackedDownload
{
DownloadItem = downloadItem,
State = TrackedDownloadState.Downloading
};
}
private void GivenValidQueueItem()
{
var downloadItem = Builder<DownloadClientItem>.CreateNew()
.With(v => v.DownloadClientId = "sab1")
.With(v => v.Status = DownloadItemStatus.Downloading)
.Build();
Mocker.GetMock<IDownloadTrackingService>()
.Setup(s => s.GetQueuedDownloads())
.Returns(new [] { _trackedDownload });
}
private void GivenSuccessfulImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>() {
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }))
});
}
private void GivenRejectedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>() {
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }, "Some Rejection"), "I was rejected")
});
}
private void GivenSkippedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>() {
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }), "I was skipped")
});
}
[Test]
@ -110,41 +58,6 @@ namespace NzbDrone.Core.Test.MediaFiles
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_ignore_downloadclientid_if_path_is_not_specified()
{
Subject.Execute(new DownloadedEpisodesScanCommand() { DownloadClientId = "sab1" });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessRootFolder(It.IsAny<DirectoryInfo>()), Times.Once());
}
[Test]
public void should_process_folder_if_downloadclientid_is_not_specified()
{
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessFolder(It.IsAny<DirectoryInfo>(), null), Times.Once());
}
[Test]
public void should_process_folder_with_downloadclientitem_if_available()
{
GivenValidQueueItem();
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" });
Mocker.GetMock<ICompletedDownloadService>().Verify(c => c.Import(It.Is<TrackedDownload>(v => v != null), _downloadFolder), Times.Once());
}
[Test]
public void should_process_folder_without_downloadclientitem_if_not_available()
{
Subject.Execute(new DownloadedEpisodesScanCommand() { Path = _downloadFolder, DownloadClientId = "sab1" });
Mocker.GetMock<IDownloadedEpisodesImportService>().Verify(c => c.ProcessFolder(It.IsAny<DirectoryInfo>(), null), Times.Once());
ExceptionVerification.ExpectedWarns(1);
}
}
}

@ -117,6 +117,7 @@
<Compile Include="Datastore\MappingExtentionFixture.cs" />
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" />
<Compile Include="Datastore\Migration\071_unknown_quality_in_profileFixture.cs" />
<Compile Include="Datastore\Migration\072_history_grabIdFixture.cs" />
<Compile Include="Datastore\Migration\070_delay_profileFixture.cs" />
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />

@ -11,7 +11,7 @@ namespace NzbDrone.Core.Blacklisting
{
public interface IBlacklistService
{
bool Blacklisted(int seriesId,string sourceTitle, DateTime publishedDate);
bool Blacklisted(int seriesId, string sourceTitle, DateTime publishedDate);
PagingSpec<Blacklist> Paged(PagingSpec<Blacklist> pagingSpec);
void Delete(int id);
}
@ -19,16 +19,13 @@ namespace NzbDrone.Core.Blacklisting
public class BlacklistService : IBlacklistService,
IExecute<ClearBlacklistCommand>,
IHandle<DownloadFailedEvent>,
IHandle<SeriesDeletedEvent>
IHandleAsync<SeriesDeletedEvent>
{
private readonly IBlacklistRepository _blacklistRepository;
private readonly IRedownloadFailedDownloads _redownloadFailedDownloadService;
public BlacklistService(IBlacklistRepository blacklistRepository,
IRedownloadFailedDownloads redownloadFailedDownloadService)
public BlacklistService(IBlacklistRepository blacklistRepository)
{
_blacklistRepository = blacklistRepository;
_redownloadFailedDownloadService = redownloadFailedDownloadService;
}
public bool Blacklisted(int seriesId, string sourceTitle, DateTime publishedDate)
@ -48,7 +45,7 @@ namespace NzbDrone.Core.Blacklisting
_blacklistRepository.Delete(id);
}
private bool HasSamePublishedDate(Blacklist item, DateTime publishedDate)
private static bool HasSamePublishedDate(Blacklist item, DateTime publishedDate)
{
if (!item.PublishedDate.HasValue) return true;
@ -70,15 +67,13 @@ namespace NzbDrone.Core.Blacklisting
SourceTitle = message.SourceTitle,
Quality = message.Quality,
Date = DateTime.UtcNow,
PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate", null))
PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate"))
};
_blacklistRepository.Insert(blacklist);
_redownloadFailedDownloadService.Redownload(message.SeriesId, message.EpisodeIds);
}
public void Handle(SeriesDeletedEvent message)
public void HandleAsync(SeriesDeletedEvent message)
{
var blacklisted = _blacklistRepository.BlacklistedBySeries(message.Series.Id);

@ -124,7 +124,7 @@ namespace NzbDrone.Core.Configuration
public Boolean EnableCompletedDownloadHandling
{
get { return GetValueBoolean("EnableCompletedDownloadHandling", false); }
get { return GetValueBoolean("EnableCompletedDownloadHandling", true); }
set { SetValue("EnableCompletedDownloadHandling", value); }
}
@ -136,13 +136,6 @@ namespace NzbDrone.Core.Configuration
set { SetValue("RemoveCompletedDownloads", value); }
}
public Boolean EnableFailedDownloadHandling
{
get { return GetValueBoolean("EnableFailedDownloadHandling", true); }
set { SetValue("EnableFailedDownloadHandling", value); }
}
public Boolean AutoRedownloadFailed
{
get { return GetValueBoolean("AutoRedownloadFailed", true); }
@ -157,27 +150,6 @@ namespace NzbDrone.Core.Configuration
set { SetValue("RemoveFailedDownloads", value); }
}
public Int32 BlacklistGracePeriod
{
get { return GetValueInt("BlacklistGracePeriod", 2); }
set { SetValue("BlacklistGracePeriod", value); }
}
public Int32 BlacklistRetryInterval
{
get { return GetValueInt("BlacklistRetryInterval", 60); }
set { SetValue("BlacklistRetryInterval", value); }
}
public Int32 BlacklistRetryLimit
{
get { return GetValueInt("BlacklistRetryLimit", 1); }
set { SetValue("BlacklistRetryLimit", value); }
}
public Boolean CreateEmptySeriesFolders
{
get { return GetValueBoolean("CreateEmptySeriesFolders", false); }

@ -22,12 +22,8 @@ namespace NzbDrone.Core.Configuration
Boolean EnableCompletedDownloadHandling { get; set; }
Boolean RemoveCompletedDownloads { get; set; }
Boolean EnableFailedDownloadHandling { get; set; }
Boolean AutoRedownloadFailed { get; set; }
Boolean RemoveFailedDownloads { get; set; }
Int32 BlacklistGracePeriod { get; set; }
Int32 BlacklistRetryInterval { get; set; }
Int32 BlacklistRetryLimit { get; set; }
//Media Management
Boolean AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; }

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using FluentMigrator;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(72)]
public class history_downloadId : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("History")
.AddColumn("DownloadId").AsString()
.Nullable()
.Indexed();
Execute.WithConnection(MoveToColumn);
}
private void MoveToColumn(IDbConnection conn, IDbTransaction tran)
{
using (IDbCommand getHistory = conn.CreateCommand())
{
getHistory.Transaction = tran;
getHistory.CommandText = @"SELECT Id, Data FROM History WHERE Data LIKE '%downloadClientId%'";
using (var historyReader = getHistory.ExecuteReader())
{
while (historyReader.Read())
{
var id = historyReader.GetInt32(0);
var data = historyReader.GetString(1);
UpdateHistory(tran, conn, id, data);
}
}
}
}
private void UpdateHistory(IDbTransaction tran, IDbConnection conn, int id, string data)
{
var dic = Json.Deserialize<Dictionary<string, string>>(data);
var downloadId = dic["downloadClientId"];
dic.Remove("downloadClientId");
using (var updateHistoryCmd = conn.CreateCommand())
{
updateHistoryCmd.Transaction = tran;
updateHistoryCmd.CommandText = @"UPDATE History SET DownloadId = ? , Data = ? WHERE Id = ?";
updateHistoryCmd.AddParameter(downloadId);
updateHistoryCmd.AddParameter(dic.ToJson());
updateHistoryCmd.AddParameter(id);
updateHistoryCmd.ExecuteNonQuery();
}
}
}
}

@ -7,12 +7,14 @@ namespace NzbDrone.Core.DecisionEngine
public Boolean Accepted { get; private set; }
public String Reason { get; private set; }
private static readonly Decision AcceptDecision = new Decision { Accepted = true };
private Decision()
{
}
public static Decision Accept()
{
return new Decision
{
Accepted = true
};
return AcceptDecision;
}
public static Decision Reject(String reason, params object[] args)

@ -1,6 +1,6 @@
using NLog;
using NzbDrone.Core.Blacklisting;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
@ -9,13 +9,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public class BlacklistSpecification : IDecisionEngineSpecification
{
private readonly IBlacklistService _blacklistService;
private readonly IConfigService _configService;
private readonly Logger _logger;
public BlacklistSpecification(IBlacklistService blacklistService, IConfigService configService, Logger logger)
public BlacklistSpecification(IBlacklistService blacklistService, Logger logger)
{
_blacklistService = blacklistService;
_configService = configService;
_logger = logger;
}
@ -23,12 +21,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
{
if (!_configService.EnableFailedDownloadHandling)
if (subject.Release.DownloadProtocol == DownloadProtocol.Torrent)
{
_logger.Debug("Failed Download Handling is not enabled");
return Decision.Accept();
}
if (_blacklistService.Blacklisted(subject.Series.Id, subject.Release.Title, subject.Release.PublishDate))
{
_logger.Debug("{0} is blacklisted, rejecting.", subject.Release.Title);

@ -1,21 +1,21 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
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 IDownloadTrackingService _downloadTrackingService;
private readonly IQueueService _queueService;
private readonly Logger _logger;
public NotInQueueSpecification(IDownloadTrackingService downloadTrackingService, Logger logger)
public NotInQueueSpecification(IQueueService queueService, Logger logger)
{
_downloadTrackingService = downloadTrackingService;
_queueService = queueService;
_logger = logger;
}
@ -23,8 +23,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
{
var queue = _downloadTrackingService.GetQueuedDownloads()
.Where(v => v.State == TrackedDownloadState.Downloading)
var queue = _queueService.GetQueue()
.Select(q => q.RemoteEpisode).ToList();
if (IsInQueue(subject, queue))
@ -36,9 +35,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return Decision.Accept();
}
private bool IsInQueue(RemoteEpisode newEpisode, IEnumerable<RemoteEpisode> queue)
private bool IsInQueue(RemoteEpisode newEpisode, IEnumerable<RemoteEpisode> episodesInQueue)
{
var matchingSeries = queue.Where(q => q.Series.Id == newEpisode.Series.Id);
var matchingSeries = episodesInQueue.Where(q => q.Series.Id == newEpisode.Series.Id);
var matchingSeriesAndQuality = matchingSeries.Where(q => new QualityModelComparer(q.Series.Profile).Compare(q.ParsedEpisodeInfo.Quality, newEpisode.ParsedEpisodeInfo.Quality) >= 0);
return matchingSeriesAndQuality.Any(q => q.Episodes.Select(e => e.Id).Intersect(newEpisode.Episodes.Select(e => e.Id)).Any());

@ -1,57 +0,0 @@
using System;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.History;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class RetrySpecification : IDecisionEngineSpecification
{
private readonly IHistoryService _historyService;
private readonly IConfigService _configService;
private readonly Logger _logger;
public RetrySpecification(IHistoryService historyService, IConfigService configService, Logger logger)
{
_historyService = historyService;
_configService = configService;
_logger = logger;
}
public RejectionType Type { get { return RejectionType.Permanent; } }
public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
{
if (!_configService.EnableFailedDownloadHandling)
{
_logger.Debug("Failed Download Handling is not enabled");
return Decision.Accept();
}
var history = _historyService.FindBySourceTitle(subject.Release.Title);
if (history.Count(h => h.EventType == HistoryEventType.Grabbed &&
HasSamePublishedDate(h, subject.Release.PublishDate)) >
_configService.BlacklistRetryLimit)
{
_logger.Debug("Release has been attempted more times than allowed, rejecting");
return Decision.Reject("Retried too many times");
}
return Decision.Accept();
}
private bool HasSamePublishedDate(History.History item, DateTime publishedDate)
{
DateTime itemsPublishedDate;
if (!DateTime.TryParse(item.Data.GetValueOrDefault("PublishedDate", null), out itemsPublishedDate)) return true;
return itemsPublishedDate.AddDays(-2) <= publishedDate && itemsPublishedDate.AddDays(2) >= publishedDate;
}
}
}

@ -103,12 +103,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
foreach (var torrent in torrents)
{
var item = new DownloadClientItem();
item.DownloadClientId = torrent.Hash.ToUpper();
item.DownloadId = torrent.Hash.ToUpper();
item.Title = torrent.Name;
item.Category = Settings.TvCategory;
item.DownloadClient = Definition.Name;
item.DownloadTime = TimeSpan.FromSeconds(torrent.SecondsDownloading);
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath));
item.OutputPath = outputPath + torrent.Name;

@ -283,7 +283,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
if (resultHosts.Result != null)
{
// The returned list contains the id, ip, port and status of each available connection. We want the 127.0.0.1
var connection = resultHosts.Result.Where(v => "127.0.0.1" == (v[1] as String)).FirstOrDefault();
var connection = resultHosts.Result.FirstOrDefault(v => "127.0.0.1" == (v[1] as String));
if (connection != null)
{

@ -69,10 +69,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var droneParameter = item.Parameters.SingleOrDefault(p => p.Name == "drone");
var queueItem = new DownloadClientItem();
queueItem.DownloadClientId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString();
queueItem.DownloadId = droneParameter == null ? item.NzbId.ToString() : droneParameter.Value.ToString();
queueItem.Title = item.NzbName;
queueItem.TotalSize = totalSize;
queueItem.Category = item.Category;
queueItem.DownloadClient = Definition.Name;
if (globalStatus.DownloadPaused || remainingSize == pausedSize)
{
@ -128,7 +129,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var historyItem = new DownloadClientItem();
historyItem.DownloadClient = Definition.Name;
historyItem.DownloadClientId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
historyItem.DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
historyItem.Title = item.Name;
historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo);
historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(item.DestDir));
@ -181,13 +182,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
{
MigrateLocalCategoryPath();
foreach (var downloadClientItem in GetQueue().Concat(GetHistory()))
{
if (downloadClientItem.Category == Settings.TvCategory)
{
yield return downloadClientItem;
}
}
return GetQueue().Concat(GetHistory()).Where(downloadClientItem => downloadClientItem.Category == Settings.TvCategory);
}
public override void RemoveItem(String id)

@ -84,7 +84,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = GetDownloadClientId(file),
DownloadId = GetDownloadClientId(file),
Title = title,
TotalSize = _diskProvider.GetFileSize(file),

@ -67,7 +67,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
{
var queueItem = new DownloadClientItem();
queueItem.DownloadClient = Definition.Name;
queueItem.DownloadClientId = sabQueueItem.Id;
queueItem.DownloadId = sabQueueItem.Id;
queueItem.Category = sabQueueItem.Category;
queueItem.Title = sabQueueItem.Title;
queueItem.TotalSize = (long)(sabQueueItem.Size * 1024 * 1024);
@ -122,13 +122,12 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = sabHistoryItem.Id,
DownloadId = sabHistoryItem.Id,
Category = sabHistoryItem.Category,
Title = sabHistoryItem.Title,
TotalSize = sabHistoryItem.Size,
RemainingSize = 0,
DownloadTime = TimeSpan.FromSeconds(sabHistoryItem.DownloadTime),
RemainingTime = TimeSpan.Zero,
Message = sabHistoryItem.FailMessage
@ -193,7 +192,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
public override void RemoveItem(String id)
{
if (GetQueue().Any(v => v.DownloadClientId == id))
if (GetQueue().Any(v => v.DownloadId == id))
{
_proxy.RemoveFrom("queue", id, Settings);
}
@ -209,7 +208,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
// Check both the queue and history because sometimes SAB keeps item in history to retry post processing (depends on failure reason)
var currentHistory = GetHistory().ToList();
var currentHistoryItems = currentHistory.Where(v => v.DownloadClientId == id).ToList();
var currentHistoryItems = currentHistory.Where(v => v.DownloadId == id).ToList();
if (currentHistoryItems.Count != 1)
{
@ -219,7 +218,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
var currentHistoryItem = currentHistoryItems.First();
var otherItemsWithSameTitle = currentHistory.Where(h => h.Title == currentHistoryItem.Title &&
h.DownloadClientId != currentHistoryItem.DownloadClientId).ToList();
h.DownloadId != currentHistoryItem.DownloadId).ToList();
var newId = _proxy.RetryDownload(id, Settings);
@ -235,17 +234,17 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
var history = GetHistory().Where(v => v.Category == currentHistoryItem.Category &&
v.Title == currentHistoryItem.Title &&
!otherItemsWithSameTitle.Select(h => h.DownloadClientId)
.Contains(v.DownloadClientId)).ToList();
!otherItemsWithSameTitle.Select(h => h.DownloadId)
.Contains(v.DownloadId)).ToList();
if (queue.Count == 1)
{
return queue.First().DownloadClientId;
return queue.First().DownloadId;
}
if (history.Count == 1)
{
return history.First().DownloadClientId;
return history.First().DownloadId;
}
if (queue.Count > 1 || history.Count > 1)

@ -70,7 +70,7 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTime(folder).Ticks,
DownloadId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTime(folder).Ticks,
Category = "nzbdrone",
Title = title,
@ -100,7 +100,7 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWrite(videoFile).Ticks,
DownloadId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWrite(videoFile).Ticks,
Category = "nzbdrone",
Title = title,

@ -102,12 +102,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission
}
var item = new DownloadClientItem();
item.DownloadClientId = torrent.HashString.ToUpper();
item.DownloadId = torrent.HashString.ToUpper();
item.Category = Settings.TvCategory;
item.Title = torrent.Name;
item.DownloadClient = Definition.Name;
item.DownloadTime = TimeSpan.FromSeconds(torrent.SecondsDownloading);
item.OutputPath = outputPath + torrent.Name;
item.RemainingSize = torrent.LeftUntilDone;

@ -68,7 +68,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTime(folder).Ticks,
DownloadId = Definition.Name + "_" + Path.GetFileName(folder) + "_" + _diskProvider.FolderGetCreationTime(folder).Ticks,
Category = "nzbdrone",
Title = title,
@ -98,7 +98,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole
var historyItem = new DownloadClientItem
{
DownloadClient = Definition.Name,
DownloadClientId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWrite(videoFile).Ticks,
DownloadId = Definition.Name + "_" + Path.GetFileName(videoFile) + "_" + _diskProvider.FileGetLastWrite(videoFile).Ticks,
Category = "nzbdrone",
Title = title,

@ -89,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
}
var item = new DownloadClientItem();
item.DownloadClientId = torrent.Hash;
item.DownloadId = torrent.Hash;
item.Title = torrent.Name;
item.TotalSize = torrent.Size;
item.Category = torrent.Label;

@ -1,244 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using System.IO;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Download
{
public interface ICompletedDownloadService
{
void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> importedHistory);
List<ImportResult> Import(TrackedDownload trackedDownload, String overrideOutputPath = null);
void Process(TrackedDownload trackedDownload);
}
public class CompletedDownloadService : ICompletedDownloadService
{
private readonly IConfigService _configService;
private readonly IDiskProvider _diskProvider;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
private readonly IEventAggregator _eventAggregator;
private readonly IHistoryService _historyService;
private readonly Logger _logger;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
public CompletedDownloadService(IConfigService configService,
IDiskProvider diskProvider,
IDownloadedEpisodesImportService downloadedEpisodesImportService,
IEventAggregator eventAggregator,
IHistoryService historyService,
Logger logger)
IDownloadedEpisodesImportService downloadedEpisodesImportService)
{
_configService = configService;
_diskProvider = diskProvider;
_downloadedEpisodesImportService = downloadedEpisodesImportService;
_eventAggregator = eventAggregator;
_historyService = historyService;
_logger = logger;
}
private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId)
{
return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID)))
.ToList();
_downloadedEpisodesImportService = downloadedEpisodesImportService;
}
public void CheckForCompletedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> importedHistory)
public void Process(TrackedDownload trackedDownload)
{
if (!_configService.EnableCompletedDownloadHandling)
if (trackedDownload.DownloadItem.Status != DownloadItemStatus.Completed)
{
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())
{
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download wasn't grabbed by drone or not in a category, ignoring download.");
return;
}
var importedItems = GetHistoryItems(importedHistory, trackedDownload.DownloadItem.DownloadClientId);
if (importedItems.Any())
{
trackedDownload.State = TrackedDownloadState.Imported;
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Already added to history as imported.");
}
else if (trackedDownload.Status != TrackedDownloadStatus.Ok)
{
_logger.Debug("Tracked download status is: {0}, skipping import. {1}", trackedDownload.Status,
String.Join(". ", trackedDownload.StatusMessages));
return;
}
else
{
var downloadedEpisodesFolder = new OsPath(_configService.DownloadedEpisodesFolder);
var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
if (downloadItemOutputPath.IsEmpty)
{
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download doesn't contain intermediate path, ignoring download.");
return;
}
if (!downloadedEpisodesFolder.IsEmpty && downloadedEpisodesFolder.Contains(downloadItemOutputPath))
{
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Intermediate Download path inside drone factory, ignoring download.");
return;
}
var importResults = Import(trackedDownload);
//Only attempt to associate it with a previous import if its still in the downloading state
if (trackedDownload.State == TrackedDownloadState.Downloading && importResults.Empty())
{
AssociateWithPreviouslyImported(trackedDownload, grabbedItems, importedHistory);
}
}
}
var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId);
if (_configService.RemoveCompletedDownloads)
if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace())
{
RemoveCompleted(trackedDownload, downloadClient);
trackedDownload.Warn("Download wasn't grabbed by Sonarr and not in a category, Skipping.");
return;
}
}
public List<ImportResult> Import(TrackedDownload trackedDownload, String overrideOutputPath = null)
{
var importResults = new List<ImportResult>();
var outputPath = overrideOutputPath ?? trackedDownload.DownloadItem.OutputPath.FullPath;
var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
if (_diskProvider.FolderExists(outputPath))
if (downloadItemOutputPath.IsEmpty)
{
importResults = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(outputPath), trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
ProcessImportResults(trackedDownload, outputPath, importResults);
trackedDownload.Warn("Download doesn't contain intermediate path, Skipping.");
return;
}
else if (_diskProvider.FileExists(outputPath))
var downloadedEpisodesFolder = new OsPath(_configService.DownloadedEpisodesFolder);
if (downloadedEpisodesFolder.Contains(downloadItemOutputPath))
{
importResults = _downloadedEpisodesImportService.ProcessFile(new FileInfo(outputPath), trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
ProcessImportResults(trackedDownload, outputPath, importResults);
trackedDownload.Warn("Intermediate Download path inside drone factory, Skipping.");
return;
}
return importResults;
Import(trackedDownload);
}
private void UpdateStatusMessage(TrackedDownload trackedDownload, LogLevel logLevel, String message, params object[] args)
private void Import(TrackedDownload trackedDownload)
{
var statusMessage = String.Format(message, args);
var logMessage = String.Format("[{0}] {1}", trackedDownload.DownloadItem.Title, statusMessage);
if (trackedDownload.StatusMessage != statusMessage)
{
trackedDownload.SetStatusLevel(logLevel);
trackedDownload.StatusMessage = statusMessage;
_logger.Log(logLevel, logMessage);
}
else
{
_logger.Debug(logMessage);
}
}
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, trackedDownload.DownloadItem);
private void ProcessImportResults(TrackedDownload trackedDownload, String outputPath, List<ImportResult> importResults)
{
if (importResults.Empty())
{
UpdateStatusMessage(trackedDownload, LogLevel.Error, "No files found are eligible for import in {0}", outputPath);
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
return;
}
else if (importResults.Any(v => v.Result == ImportResultType.Imported) && importResults.All(v => v.Result == ImportResultType.Imported || v.Result == ImportResultType.Rejected))
{
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Imported {0} files.", importResults.Count(v => v.Result == ImportResultType.Imported));
trackedDownload.State = TrackedDownloadState.Imported;
}
else
if (importResults.Any(c => c.Result != ImportResultType.Imported))
{
var errors = importResults
.Where(v => v.Result == ImportResultType.Skipped || v.Result == ImportResultType.Rejected)
.Select(v => v.Errors.Aggregate(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), (a, r) => a + "\r\n- " + r))
.Aggregate("Failed to import:", (a, r) => a + "\r\n" + r);
trackedDownload.StatusMessages = importResults.Where(v => v.Result == ImportResultType.Skipped || v.Result == ImportResultType.Rejected)
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors)).ToList();
var statusMessages = importResults
.Where(v => v.Result != ImportResultType.Imported)
.Select(v => new TrackedDownloadStatusMessage(Path.GetFileName(v.ImportDecision.LocalEpisode.Path), v.Errors))
.ToArray();
UpdateStatusMessage(trackedDownload, LogLevel.Error, errors);
}
}
private void AssociateWithPreviouslyImported(TrackedDownload trackedDownload, List<History.History> grabbedItems, List<History.History> importedHistory)
{
if (grabbedItems.Any())
{
var episodeIds = trackedDownload.RemoteEpisode.Episodes.Select(v => v.Id).ToList();
// Check if we can associate it with a previous drone factory import.
var importedItems = importedHistory.Where(v => v.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID) == null &&
episodeIds.Contains(v.EpisodeId) &&
v.Data.GetValueOrDefault("droppedPath") != null &&
new FileInfo(v.Data["droppedPath"]).Directory.Name == grabbedItems.First().SourceTitle
).ToList();
if (importedItems.Count == 1)
{
var importedFile = new FileInfo(importedItems.First().Data["droppedPath"]);
if (importedFile.Directory.Name == grabbedItems.First().SourceTitle)
{
trackedDownload.State = TrackedDownloadState.Imported;
importedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT] = grabbedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT];
importedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID] = grabbedItems.First().Data[DownloadTrackingService.DOWNLOAD_CLIENT_ID];
_historyService.UpdateHistoryData(importedItems.First().Id, importedItems.First().Data);
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Intermediate Download path does not exist, but found probable drone factory ImportEvent.");
return;
}
}
trackedDownload.Warn(statusMessages);
return;
}
UpdateStatusMessage(trackedDownload, LogLevel.Error, "Intermediate Download path does not exist: {0}", trackedDownload.DownloadItem.OutputPath);
}
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
private void RemoveCompleted(TrackedDownload trackedDownload, IDownloadClient downloadClient)
{
if (trackedDownload.State == TrackedDownloadState.Imported && !trackedDownload.DownloadItem.IsReadOnly)
{
try
{
_logger.Debug("[{0}] Removing completed download from history.", trackedDownload.DownloadItem.Title);
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId);
if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath))
{
_logger.Debug("Removing completed download directory: {0}",
trackedDownload.DownloadItem.OutputPath);
_diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true);
}
else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath.FullPath))
{
_logger.Debug("Removing completed download file: {0}", trackedDownload.DownloadItem.OutputPath);
_diskProvider.DeleteFile(trackedDownload.DownloadItem.OutputPath.FullPath);
}
trackedDownload.State = TrackedDownloadState.Removed;
}
catch (NotSupportedException)
{
UpdateStatusMessage(trackedDownload, LogLevel.Debug,
"Removing item not supported by your download client.");
}
}
}
}
}

@ -1,18 +1,19 @@
using System;
using System.Diagnostics;
using NzbDrone.Common.Disk;
namespace NzbDrone.Core.Download
{
[DebuggerDisplay("{DownloadClient}:{Title}")]
public class DownloadClientItem
{
public String DownloadClient { get; set; }
public String DownloadClientId { get; set; }
public String DownloadId { get; set; }
public String Category { get; set; }
public String Title { get; set; }
public Int64 TotalSize { get; set; }
public Int64 RemainingSize { get; set; }
public TimeSpan? DownloadTime { get; set; }
public TimeSpan? RemainingTime { get; set; }
public OsPath OutputPath { get; set; }
@ -21,5 +22,7 @@ namespace NzbDrone.Core.Download
public DownloadItemStatus Status { get; set; }
public Boolean IsEncrypted { get; set; }
public Boolean IsReadOnly { get; set; }
public bool Removed { get; set; }
}
}

@ -29,10 +29,9 @@ namespace NzbDrone.Core.Download
{
return _downloadClientFactory.GetAvailableProviders();
}
public IDownloadClient Get(int id)
{
return _downloadClientFactory.GetAvailableProviders().Single(d => d.Definition.Id == id);
}
}
}
}

@ -2,7 +2,6 @@
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download
{
public interface IDownloadClientRepository : IProviderRepository<DownloadClientDefinition>

@ -0,0 +1,83 @@
using System;
using NLog;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Download
{
public class DownloadCompletedEvent : IEvent
{
public TrackedDownload TrackedDownload { get; private set; }
public DownloadCompletedEvent(TrackedDownload trackedDownload)
{
TrackedDownload = trackedDownload;
}
}
public class DownloadEventHub : IHandle<DownloadFailedEvent>,
IHandle<DownloadCompletedEvent>
{
private readonly IConfigService _configService;
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly Logger _logger;
public DownloadEventHub(IConfigService configService,
IProvideDownloadClient downloadClientProvider,
ITrackedDownloadService trackedDownloadService,
Logger logger)
{
_configService = configService;
_downloadClientProvider = downloadClientProvider;
_trackedDownloadService = trackedDownloadService;
_logger = logger;
}
public void Handle(DownloadCompletedEvent message)
{
if (message.TrackedDownload.DownloadItem.Removed || message.TrackedDownload.DownloadItem.IsReadOnly || !_configService.RemoveCompletedDownloads)
{
return;
}
RemoveFromDownloadClient(message.TrackedDownload);
}
public void Handle(DownloadFailedEvent message)
{
var trackedDownload = _trackedDownloadService.Find(message.DownloadId);
if (trackedDownload == null || trackedDownload.DownloadItem.IsReadOnly || !_configService.RemoveFailedDownloads)
{
return;
}
RemoveFromDownloadClient(trackedDownload);
}
private void RemoveFromDownloadClient(TrackedDownload trackedDownload)
{
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
try
{
_logger.Debug("[{0}] Removing download from {1} history", trackedDownload.DownloadItem.DownloadClient);
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId);
trackedDownload.DownloadItem.Removed = true;
}
catch (NotSupportedException)
{
_logger.Warn("Removing item not supported by your download client ({0}).", downloadClient.Definition.Name);
}
catch (Exception e)
{
_logger.ErrorException("Couldn't remove item from client " + trackedDownload.DownloadItem.Title, e);
}
}
}
}

@ -17,7 +17,7 @@ namespace NzbDrone.Core.Download
public QualityModel Quality { get; set; }
public String SourceTitle { get; set; }
public String DownloadClient { get; set; }
public String DownloadClientId { get; set; }
public String DownloadId { get; set; }
public String Message { get; set; }
public Dictionary<string, string> Data { get; set; }
}

@ -47,7 +47,7 @@ namespace NzbDrone.Core.Download
if (!String.IsNullOrWhiteSpace(downloadClientId))
{
episodeGrabbedEvent.DownloadClientId = downloadClientId;
episodeGrabbedEvent.DownloadId = downloadClientId;
}
_logger.ProgressInfo("Report sent to download client. {0}", downloadTitle);

@ -1,320 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Queue;
namespace NzbDrone.Core.Download
{
public interface IDownloadTrackingService
{
TrackedDownload[] GetCompletedDownloads();
TrackedDownload[] GetQueuedDownloads();
TrackedDownload Find(string trackingId);
void MarkAsFailed(Int32 historyId);
}
public class DownloadTrackingService : IDownloadTrackingService,
IExecute<CheckForFinishedDownloadCommand>,
IHandleAsync<ApplicationStartedEvent>,
IHandle<EpisodeGrabbedEvent>,
IHandle<SceneMappingsUpdatedEvent>
{
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 IParsingService _parsingService;
private readonly Logger _logger;
private readonly ICached<TrackedDownload[]> _trackedDownloadCache;
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,
IParsingService parsingService,
Logger logger)
{
_downloadClientProvider = downloadClientProvider;
_historyService = historyService;
_eventAggregator = eventAggregator;
_configService = configService;
_failedDownloadService = failedDownloadService;
_completedDownloadService = completedDownloadService;
_parsingService = parsingService;
_logger = logger;
_trackedDownloadCache = cacheManager.GetCache<TrackedDownload[]>(GetType());
}
private TrackedDownload[] GetTrackedDownloads()
{
return _trackedDownloadCache.Get("tracked", () => new TrackedDownload[0]);
}
public TrackedDownload[] GetCompletedDownloads()
{
return GetTrackedDownloads()
.Where(v => v.State == TrackedDownloadState.Downloading && v.DownloadItem.Status == DownloadItemStatus.Completed)
.ToArray();
}
public TrackedDownload[] GetQueuedDownloads()
{
return _trackedDownloadCache.Get("queued", () =>
{
UpdateTrackedDownloads(_historyService.Grabbed());
return FilterQueuedDownloads(GetTrackedDownloads());
}, TimeSpan.FromSeconds(5.0));
}
public TrackedDownload Find(string trackingId)
{
return GetQueuedDownloads().SingleOrDefault(t => t.TrackingId == trackingId);
}
public void MarkAsFailed(Int32 historyId)
{
var item = _historyService.Get(historyId);
var trackedDownload = GetTrackedDownloads()
.FirstOrDefault(h => h.DownloadItem.DownloadClientId.Equals(item.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID)));
if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Unknown)
{
ProcessTrackedDownloads();
}
_failedDownloadService.MarkAsFailed(trackedDownload, item);
}
private TrackedDownload[] FilterQueuedDownloads(IEnumerable<TrackedDownload> trackedDownloads)
{
var enabledFailedDownloadHandling = _configService.EnableFailedDownloadHandling;
var enabledCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling;
return trackedDownloads
.Where(v => v.State == TrackedDownloadState.Downloading)
.Where(v =>
v.DownloadItem.Status == DownloadItemStatus.Queued ||
v.DownloadItem.Status == DownloadItemStatus.Paused ||
v.DownloadItem.Status == DownloadItemStatus.Downloading ||
v.DownloadItem.Status == DownloadItemStatus.Warning ||
v.DownloadItem.Status == DownloadItemStatus.Failed && enabledFailedDownloadHandling ||
v.DownloadItem.Status == DownloadItemStatus.Completed && enabledCompletedDownloadHandling)
.ToArray();
}
private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId)
{
return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID)))
.ToList();
}
private Boolean UpdateTrackedDownloads(List<History.History> grabbedHistory)
{
var downloadClients = _downloadClientProvider.GetDownloadClients();
var oldTrackedDownloads = GetTrackedDownloads().ToDictionary(v => v.TrackingId);
var newTrackedDownloads = new Dictionary<String, TrackedDownload>();
var stateChanged = false;
foreach (var downloadClient in downloadClients)
{
List<DownloadClientItem> downloadClientHistory;
try
{
downloadClientHistory = downloadClient.GetItems().ToList();
}
catch (Exception ex)
{
_logger.WarnException("Unable to retrieve queue and history items from " + downloadClient.Definition.Name, ex);
continue;
}
foreach (var downloadItem in downloadClientHistory)
{
var trackingId = String.Format("{0}-{1}", downloadClient.Definition.Id, downloadItem.DownloadClientId);
TrackedDownload trackedDownload;
if (newTrackedDownloads.ContainsKey(trackingId)) continue;
if (!oldTrackedDownloads.TryGetValue(trackingId, out trackedDownload))
{
trackedDownload = GetTrackedDownload(trackingId, downloadClient.Definition.Id, downloadItem, grabbedHistory);
if (trackedDownload == null) continue;
_logger.Debug("[{0}] Started tracking download with id {1}.", downloadItem.Title, trackingId);
stateChanged = true;
}
trackedDownload.DownloadItem = downloadItem;
newTrackedDownloads[trackingId] = trackedDownload;
}
}
foreach (var trackedDownload in oldTrackedDownloads.Values.Where(v => !newTrackedDownloads.ContainsKey(v.TrackingId)))
{
if (trackedDownload.State != TrackedDownloadState.Removed)
{
trackedDownload.State = TrackedDownloadState.Removed;
stateChanged = true;
_logger.Debug("[{0}] Item with id {1} removed from download client directly (possibly by user).", trackedDownload.DownloadItem.Title, trackedDownload.TrackingId);
}
_logger.Debug("[{0}] Stopped tracking download with id {1}.", trackedDownload.DownloadItem.Title, trackedDownload.TrackingId);
}
_trackedDownloadCache.Set("tracked", newTrackedDownloads.Values.ToArray());
return stateChanged;
}
private void ProcessTrackedDownloads()
{
var grabbedHistory = _historyService.Grabbed();
var failedHistory = _historyService.Failed();
var importedHistory = _historyService.Imported();
var stateChanged = UpdateTrackedDownloads(grabbedHistory);
var downloadClients = _downloadClientProvider.GetDownloadClients().ToList();
var trackedDownloads = GetTrackedDownloads();
foreach (var trackedDownload in trackedDownloads)
{
var downloadClient = downloadClients.SingleOrDefault(v => v.Definition.Id == trackedDownload.DownloadClient);
if (downloadClient == null)
{
_logger.Debug("TrackedDownload for unknown download client, download client was probably removed or disabled between scans.");
continue;
}
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;
}
}
_trackedDownloadCache.Set("queued", FilterQueuedDownloads(trackedDownloads), TimeSpan.FromSeconds(5.0));
if (stateChanged)
{
_eventAggregator.PublishEvent(new UpdateQueueEvent());
}
}
private TrackedDownload GetTrackedDownload(String trackingId, Int32 downloadClient, DownloadClientItem downloadItem, List<History.History> grabbedHistory)
{
var trackedDownload = new TrackedDownload
{
TrackingId = trackingId,
DownloadClient = downloadClient,
DownloadItem = downloadItem,
StartedTracking = DateTime.UtcNow,
State = TrackedDownloadState.Unknown,
Status = TrackedDownloadStatus.Ok,
};
try
{
var historyItems = grabbedHistory.Where(h =>
{
var downloadClientId = h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID);
if (downloadClientId == null) return false;
return downloadClientId.Equals(trackedDownload.DownloadItem.DownloadClientId);
}).ToList();
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
if (parsedEpisodeInfo == null) return null;
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0);
if (remoteEpisode.Series == null)
{
if (historyItems.Empty()) return null;
trackedDownload.Status = TrackedDownloadStatus.Warning;
trackedDownload.StatusMessages.Add(new TrackedDownloadStatusMessage(
trackedDownload.DownloadItem.Title,
"Series title mismatch, automatic import is not possible")
);
remoteEpisode = _parsingService.Map(parsedEpisodeInfo, historyItems.First().SeriesId, historyItems.Select(h => h.EpisodeId));
}
trackedDownload.RemoteEpisode = remoteEpisode;
}
catch (Exception e)
{
_logger.DebugException("Failed to find episode for " + downloadItem.Title, e);
return null;
}
return trackedDownload;
}
public void Execute(CheckForFinishedDownloadCommand message)
{
ProcessTrackedDownloads();
}
public void HandleAsync(ApplicationStartedEvent message)
{
ProcessTrackedDownloads();
}
public void Handle(EpisodeGrabbedEvent message)
{
ProcessTrackedDownloads();
}
public void Handle(SceneMappingsUpdatedEvent message)
{
var grabbedHistory = _historyService.Grabbed();
foreach (var trackedDownload in GetTrackedDownloads().Where(t => t.Status == TrackedDownloadStatus.Warning))
{
var newTrackedDownload = GetTrackedDownload(trackedDownload.TrackingId, trackedDownload.DownloadClient, trackedDownload.DownloadItem, grabbedHistory);
trackedDownload.Status = newTrackedDownload.Status;
trackedDownload.StatusMessages = newTrackedDownload.StatusMessages;
}
}
}
}

@ -8,7 +8,7 @@ namespace NzbDrone.Core.Download
{
public RemoteEpisode Episode { get; private set; }
public String DownloadClient { get; set; }
public String DownloadClientId { get; set; }
public String DownloadId { get; set; }
public EpisodeGrabbedEvent(RemoteEpisode episode)
{

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events;
@ -11,208 +9,59 @@ namespace NzbDrone.Core.Download
{
public interface IFailedDownloadService
{
void MarkAsFailed(TrackedDownload trackedDownload, History.History grabbedHistory);
void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> failedHistory);
void MarkAsFailed(int historyId);
void Process(TrackedDownload trackedDownload);
}
public class FailedDownloadService : IFailedDownloadService
{
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator;
private readonly IConfigService _configService;
private readonly Logger _logger;
public FailedDownloadService(IHistoryService historyService,
IEventAggregator eventAggregator,
IConfigService configService,
Logger logger)
IEventAggregator eventAggregator)
{
_historyService = historyService;
_eventAggregator = eventAggregator;
_configService = configService;
_logger = logger;
}
public void MarkAsFailed(TrackedDownload trackedDownload, History.History history)
public void MarkAsFailed(int historyId)
{
if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading)
{
trackedDownload.State = TrackedDownloadState.DownloadFailed;
}
var history = _historyService.Get(historyId);
var downloadClientId = history.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID);
if (downloadClientId.IsNullOrWhiteSpace())
var downloadId = history.DownloadId;
if (downloadId.IsNullOrWhiteSpace())
{
PublishDownloadFailedEvent(new List<History.History> { history }, "Manually marked as failed");
}
else
{
var grabbedHistory = GetHistoryItems(_historyService.Grabbed(), downloadClientId);
var grabbedHistory = _historyService.Find(downloadId, HistoryEventType.Grabbed).ToList();
PublishDownloadFailedEvent(grabbedHistory, "Manually marked as failed");
}
}
public void CheckForFailedItem(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> grabbedHistory, List<History.History> failedHistory)
{
if (!_configService.EnableFailedDownloadHandling)
{
return;
}
if (trackedDownload.DownloadItem.IsEncrypted && trackedDownload.State == TrackedDownloadState.Downloading)
{
var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId);
if (!grabbedItems.Any())
{
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download wasn't grabbed by drone, ignoring download");
return;
}
trackedDownload.State = TrackedDownloadState.DownloadFailed;
var failedItems = GetHistoryItems(failedHistory, trackedDownload.DownloadItem.DownloadClientId);
if (failedItems.Any())
{
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Already added to history as failed.");
}
else
{
PublishDownloadFailedEvent(grabbedItems, "Encrypted download detected");
}
}
if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed && trackedDownload.State == TrackedDownloadState.Downloading)
{
var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId);
if (!grabbedItems.Any())
{
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download wasn't grabbed by drone, ignoring download");
return;
}
var failedItems = GetHistoryItems(failedHistory, trackedDownload.DownloadItem.DownloadClientId);
if (failedItems.Any())
{
trackedDownload.State = TrackedDownloadState.DownloadFailed;
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Already added to history as failed.");
}
else
{
if (FailedDownloadForRecentRelease(downloadClient, trackedDownload, grabbedItems))
{
_logger.Debug("[{0}] Recent release Failed, do not blacklist.", trackedDownload.DownloadItem.Title);
return;
}
trackedDownload.State = TrackedDownloadState.DownloadFailed;
PublishDownloadFailedEvent(grabbedItems, trackedDownload.DownloadItem.Message);
}
}
if (trackedDownload.DownloadItem.Status != DownloadItemStatus.Failed && trackedDownload.State == TrackedDownloadState.Downloading)
{
var grabbedItems = GetHistoryItems(grabbedHistory, trackedDownload.DownloadItem.DownloadClientId);
var failedItems = GetHistoryItems(failedHistory, trackedDownload.DownloadItem.DownloadClientId);
if (grabbedItems.Any() && failedItems.Any())
{
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Already added to history as failed, updating tracked state.");
trackedDownload.State = TrackedDownloadState.DownloadFailed;
}
}
if (_configService.RemoveFailedDownloads && trackedDownload.State == TrackedDownloadState.DownloadFailed)
{
try
{
_logger.Debug("[{0}] Removing failed download from client.", trackedDownload.DownloadItem.Title);
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId);
trackedDownload.State = TrackedDownloadState.Removed;
}
catch (NotSupportedException)
{
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Removing item not supported by your download client.");
}
}
}
private bool FailedDownloadForRecentRelease(IDownloadClient downloadClient, TrackedDownload trackedDownload, List<History.History> matchingHistoryItems)
public void Process(TrackedDownload trackedDownload)
{
double ageHours;
if (!Double.TryParse(matchingHistoryItems.First().Data.GetValueOrDefault("ageHours"), out ageHours))
{
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Unable to determine age of failed download.");
return false;
}
if (ageHours > _configService.BlacklistGracePeriod)
{
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Download Failed, Failed download is older than the grace period.");
return false;
}
var grabbedItems = _historyService.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed)
.ToList();
if (trackedDownload.RetryCount >= _configService.BlacklistRetryLimit)
if (grabbedItems.Empty())
{
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Download Failed, Retry limit reached.");
return false;
}
if (trackedDownload.LastRetry == new DateTime())
{
trackedDownload.LastRetry = DateTime.UtcNow;
trackedDownload.Warn("Download wasn't grabbed by sonarr, skipping");
return;
}
if (trackedDownload.LastRetry.AddMinutes(_configService.BlacklistRetryInterval) < DateTime.UtcNow)
if (trackedDownload.DownloadItem.IsEncrypted)
{
trackedDownload.LastRetry = DateTime.UtcNow;
trackedDownload.RetryCount++;
UpdateStatusMessage(trackedDownload, LogLevel.Info, "Download Failed, initiating retry attempt {0}/{1}.", trackedDownload.RetryCount, _configService.BlacklistRetryLimit);
try
{
var newDownloadClientId = downloadClient.RetryDownload(trackedDownload.DownloadItem.DownloadClientId);
if (newDownloadClientId != trackedDownload.DownloadItem.DownloadClientId)
{
var oldTrackingId = trackedDownload.TrackingId;
var newTrackingId = String.Format("{0}-{1}", downloadClient.Definition.Id, newDownloadClientId);
trackedDownload.TrackingId = newTrackingId;
trackedDownload.DownloadItem.DownloadClientId = newDownloadClientId;
_logger.Debug("[{0}] Changed id from {1} to {2}.", trackedDownload.DownloadItem.Title, oldTrackingId, newTrackingId);
var newHistoryData = new Dictionary<String, String>(matchingHistoryItems.First().Data);
newHistoryData[DownloadTrackingService.DOWNLOAD_CLIENT_ID] = newDownloadClientId;
_historyService.UpdateHistoryData(matchingHistoryItems.First().Id, newHistoryData);
}
}
catch (NotSupportedException)
{
UpdateStatusMessage(trackedDownload, LogLevel.Debug, "Retrying failed downloads is not supported by your download client.");
return false;
}
trackedDownload.State = TrackedDownloadStage.DownloadFailed;
PublishDownloadFailedEvent(grabbedItems, "Encrypted download detected");
}
else
else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
{
UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download Failed, waiting for retry interval to expire.");
trackedDownload.State = TrackedDownloadStage.DownloadFailed;
PublishDownloadFailedEvent(grabbedItems, trackedDownload.DownloadItem.Message);
}
return true;
}
private List<History.History> GetHistoryItems(List<History.History> grabbedHistory, string downloadClientId)
{
return grabbedHistory.Where(h => downloadClientId.Equals(h.Data.GetValueOrDefault(DownloadTrackingService.DOWNLOAD_CLIENT_ID)))
.ToList();
}
private void PublishDownloadFailedEvent(List<History.History> historyItems, string message)
@ -225,35 +74,15 @@ namespace NzbDrone.Core.Download
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
DownloadClient = historyItem.Data.GetValueOrDefault(History.History.DOWNLOAD_CLIENT),
DownloadId = historyItem.DownloadId,
Message = message,
Data = historyItem.Data
};
downloadFailedEvent.Data = downloadFailedEvent.Data.Merge(historyItem.Data);
_eventAggregator.PublishEvent(downloadFailedEvent);
}
private void UpdateStatusMessage(TrackedDownload trackedDownload, LogLevel logLevel, String message, params object[] args)
{
var statusMessage = String.Format(message, args);
var logMessage = String.Format("[{0}] {1}", trackedDownload.DownloadItem.Title, statusMessage);
if (trackedDownload.StatusMessage != statusMessage)
{
trackedDownload.SetStatusLevel(logLevel);
trackedDownload.StatusMessage = statusMessage;
trackedDownload.StatusMessages = new List<TrackedDownloadStatusMessage>
{
new TrackedDownloadStatusMessage(trackedDownload.DownloadItem.Title, statusMessage)
};
_logger.Log(logLevel, logMessage);
}
else
{
_logger.Debug(logMessage);
}
}
}
}

@ -108,10 +108,10 @@ namespace NzbDrone.Core.Download.Pending
Title = pendingRelease.Title,
Size = pendingRelease.RemoteEpisode.Release.Size,
Sizeleft = pendingRelease.RemoteEpisode.Release.Size,
RemoteEpisode = pendingRelease.RemoteEpisode,
Timeleft = ect.Subtract(DateTime.UtcNow),
EstimatedCompletionTime = ect,
Status = "Pending",
RemoteEpisode = pendingRelease.RemoteEpisode
Status = "Pending"
};
queued.Add(queue);
}

@ -1,19 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using NLog;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Download
{
public interface IRedownloadFailedDownloads
{
void Redownload(int seriesId, List<int> episodeIds);
}
public class RedownloadFailedDownloadService : IRedownloadFailedDownloads
public class RedownloadFailedDownloadService : IHandleAsync<DownloadFailedEvent>
{
private readonly IConfigService _configService;
private readonly IEpisodeService _episodeService;
@ -28,7 +23,7 @@ namespace NzbDrone.Core.Download
_logger = logger;
}
public void Redownload(int seriesId, List<int> episodeIds)
public void HandleAsync(DownloadFailedEvent message)
{
if (!_configService.AutoRedownloadFailed)
{
@ -36,34 +31,34 @@ namespace NzbDrone.Core.Download
return;
}
if (episodeIds.Count == 1)
if (message.EpisodeIds.Count == 1)
{
_logger.Debug("Failed download only contains one episode, searching again");
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(episodeIds));
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(message.EpisodeIds));
return;
}
var seasonNumber = _episodeService.GetEpisode(episodeIds.First()).SeasonNumber;
var episodesInSeason = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber);
var seasonNumber = _episodeService.GetEpisode(message.EpisodeIds.First()).SeasonNumber;
var episodesInSeason = _episodeService.GetEpisodesBySeason(message.SeriesId, seasonNumber);
if (episodeIds.Count == episodesInSeason.Count)
if (message.EpisodeIds.Count == episodesInSeason.Count)
{
_logger.Debug("Failed download was entire season, searching again");
_commandExecutor.PublishCommandAsync(new SeasonSearchCommand
{
SeriesId = seriesId,
SeasonNumber = seasonNumber
});
{
SeriesId = message.SeriesId,
SeasonNumber = seasonNumber
});
return;
}
_logger.Debug("Failed download contains multiple episodes, probably a double episode, searching again");
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(episodeIds));
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(message.EpisodeIds));
}
}
}

@ -1,58 +0,0 @@
using System;
using System.Collections.Generic;
using NLog;
using NzbDrone.Core.Parser.Model;
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 TrackedDownloadStatus Status { get; set; }
public DateTime StartedTracking { get; set; }
public DateTime LastRetry { get; set; }
public Int32 RetryCount { get; set; }
public String StatusMessage { get; set; }
public RemoteEpisode RemoteEpisode { get; set; }
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
public TrackedDownload()
{
StatusMessages = new List<TrackedDownloadStatusMessage>();
}
public void SetStatusLevel(LogLevel logLevel)
{
if (logLevel == LogLevel.Warn)
{
Status = TrackedDownloadStatus.Warning;
}
if (logLevel >= LogLevel.Error)
{
Status = TrackedDownloadStatus.Error;
}
else Status = TrackedDownloadStatus.Ok;
}
}
public enum TrackedDownloadState
{
Unknown,
Downloading,
Imported,
DownloadFailed,
Removed
}
public enum TrackedDownloadStatus
{
Ok,
Warning,
Error
}
}

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Download.TrackedDownloads
{
public class DownloadMonitoringService : IExecute<CheckForFinishedDownloadCommand>,
IHandleAsync<EpisodeGrabbedEvent>,
IHandleAsync<EpisodeImportedEvent>
{
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IEventAggregator _eventAggregator;
private readonly IConfigService _configService;
private readonly IFailedDownloadService _failedDownloadService;
private readonly ICompletedDownloadService _completedDownloadService;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly Logger _logger;
private readonly Debouncer _refreshDebounce;
public DownloadMonitoringService(IProvideDownloadClient downloadClientProvider,
IEventAggregator eventAggregator,
IConfigService configService,
IFailedDownloadService failedDownloadService,
ICompletedDownloadService completedDownloadService,
ITrackedDownloadService trackedDownloadService,
Logger logger)
{
_downloadClientProvider = downloadClientProvider;
_eventAggregator = eventAggregator;
_configService = configService;
_failedDownloadService = failedDownloadService;
_completedDownloadService = completedDownloadService;
_trackedDownloadService = trackedDownloadService;
_logger = logger;
_refreshDebounce = new Debouncer(Refresh, TimeSpan.FromSeconds(5));
}
private void Refresh()
{
var downloadClients = _downloadClientProvider.GetDownloadClients();
var trackedDownload = new List<TrackedDownload>();
foreach (var downloadClient in downloadClients)
{
var clientTrackedDowmloads = ProcessClientDownloads(downloadClient);
trackedDownload.AddRange(clientTrackedDowmloads.Where(c => c.State == TrackedDownloadStage.Downloading));
}
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownload));
}
private List<TrackedDownload> ProcessClientDownloads(IDownloadClient downloadClient)
{
List<DownloadClientItem> downloadClientHistory = new List<DownloadClientItem>();
var trackedDownloads = new List<TrackedDownload>();
try
{
downloadClientHistory = downloadClient.GetItems().ToList();
}
catch (Exception ex)
{
_logger.WarnException("Unable to retrieve queue and history items from " + downloadClient.Definition.Name, ex);
}
foreach (var downloadItem in downloadClientHistory)
{
trackedDownloads.AddRange(ProcessClientItems(downloadClient, downloadItem));
}
if (_configService.RemoveCompletedDownloads)
{
RemoveCompletedDownloads(trackedDownloads);
}
return trackedDownloads;
}
private void RemoveCompletedDownloads(List<TrackedDownload> trackedDownloads)
{
foreach (var trakedDownload in trackedDownloads.Where(c => !c.DownloadItem.IsReadOnly && c.State == TrackedDownloadStage.Imported))
{
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trakedDownload));
}
}
private List<TrackedDownload> ProcessClientItems(IDownloadClient downloadClient, DownloadClientItem downloadItem)
{
var trackedDownloads = new List<TrackedDownload>();
try
{
var trackedDownload = _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, downloadItem);
if (trackedDownload != null && trackedDownload.State == TrackedDownloadStage.Downloading)
{
_failedDownloadService.Process(trackedDownload);
if (_configService.EnableCompletedDownloadHandling)
{
_completedDownloadService.Process(trackedDownload);
}
trackedDownloads.Add(trackedDownload);
}
}
catch (Exception e)
{
_logger.ErrorException("Couldn't process tracked download " + downloadItem.Title, e);
}
return trackedDownloads;
}
public void Execute(CheckForFinishedDownloadCommand message)
{
Refresh();
}
public void HandleAsync(EpisodeGrabbedEvent message)
{
_refreshDebounce.Execute();
}
public void HandleAsync(EpisodeImportedEvent message)
{
_refreshDebounce.Execute();
}
}
}

@ -0,0 +1,48 @@
using System;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download.TrackedDownloads
{
public class TrackedDownload
{
public String TrackingId { get; set; }
public Int32 DownloadClient { get; set; }
public DownloadClientItem DownloadItem { get; set; }
public TrackedDownloadStage State { get; set; }
public TrackedDownloadStatus Status { get; private set; }
public RemoteEpisode RemoteEpisode { get; set; }
public TrackedDownloadStatusMessage[] StatusMessages { get; private set; }
public DownloadProtocol Protocol { get; set; }
public TrackedDownload()
{
StatusMessages = new TrackedDownloadStatusMessage[] {};
}
public void Warn(String message, params object[] args)
{
var statusMessage = String.Format(message, args);
Warn(new TrackedDownloadStatusMessage(DownloadItem.Title, statusMessage));
}
public void Warn(params TrackedDownloadStatusMessage[] statusMessages)
{
Status = TrackedDownloadStatus.Warning;
StatusMessages = statusMessages;
}
}
public enum TrackedDownloadStage
{
Downloading,
Imported,
DownloadFailed
}
public enum TrackedDownloadStatus
{
Ok,
Warning
}
}

@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Download.TrackedDownloads
{
public class TrackedDownloadRefreshedEvent : IEvent
{
public List<TrackedDownload> TrackedDownloads { get; private set; }
public TrackedDownloadRefreshedEvent(List<TrackedDownload> trackedDownloads)
{
TrackedDownloads = trackedDownloads;
}
}
}

@ -0,0 +1,110 @@
using System;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Download.TrackedDownloads
{
public interface ITrackedDownloadService
{
TrackedDownload Find(string downloadId);
TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem);
}
public class TrackedDownloadService : ITrackedDownloadService,
IHandle<SceneMappingsUpdatedEvent>
{
private readonly IParsingService _parsingService;
private readonly IHistoryService _historyService;
private readonly Logger _logger;
private readonly ICached<TrackedDownload> _cache;
public TrackedDownloadService(IParsingService parsingService,
ICacheManager cacheManager,
IHistoryService historyService,
Logger logger)
{
_parsingService = parsingService;
_historyService = historyService;
_cache = cacheManager.GetCache<TrackedDownload>(GetType());
_logger = logger;
}
public TrackedDownload Find(string downloadId)
{
return _cache.Find(downloadId);
}
public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem)
{
var existingItem = Find(downloadItem.DownloadId);
if (existingItem != null)
{
existingItem.DownloadItem = downloadItem;
return existingItem;
}
var trackedDownload = new TrackedDownload
{
TrackingId = downloadClient.Id + "-" + downloadItem.DownloadId,
DownloadClient = downloadClient.Id,
DownloadItem = downloadItem,
Protocol = downloadClient.Protocol
};
var historyItem = _historyService.MostRecentForDownloadId(downloadItem.DownloadId);
if (historyItem != null)
{
trackedDownload.State = GetStateFromHistory(historyItem.EventType);
}
try
{
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
if (parsedEpisodeInfo == null) return null;
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo);
if (remoteEpisode.Series == null)
{
return null;
}
trackedDownload.RemoteEpisode = remoteEpisode;
}
catch (Exception e)
{
_logger.DebugException("Failed to find episode for " + downloadItem.Title, e);
return null;
}
if (trackedDownload.State != TrackedDownloadStage.Downloading)
{
_cache.Set(downloadItem.DownloadId, trackedDownload);
}
return trackedDownload;
}
private static TrackedDownloadStage GetStateFromHistory(HistoryEventType eventType)
{
switch (eventType)
{
case HistoryEventType.DownloadFolderImported:
return TrackedDownloadStage.Imported;
case HistoryEventType.DownloadFailed:
return TrackedDownloadStage.DownloadFailed;
default:
return TrackedDownloadStage.Downloading;
}
}
public void Handle(SceneMappingsUpdatedEvent message)
{
_cache.Clear();
}
}
}

@ -1,16 +1,13 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Core.Download
namespace NzbDrone.Core.Download.TrackedDownloads
{
public class TrackedDownloadStatusMessage
{
public String Title { get; set; }
public List<String> Messages { get; set; }
private TrackedDownloadStatusMessage()
{
}
public TrackedDownloadStatusMessage(String title, List<String> messages)
{

@ -2,8 +2,8 @@
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.Sabnzbd;
using NzbDrone.Core.Download.Clients.Nzbget;
using NzbDrone.Core.Download.Clients.Sabnzbd;
namespace NzbDrone.Core.HealthCheck.Checks
{
@ -11,13 +11,12 @@ namespace NzbDrone.Core.HealthCheck.Checks
{
private readonly IConfigService _configService;
private readonly IProvideDownloadClient _provideDownloadClient;
private readonly IDownloadTrackingService _downloadTrackingService;
public ImportMechanismCheck(IConfigService configService, IProvideDownloadClient provideDownloadClient, IDownloadTrackingService downloadTrackingService)
public ImportMechanismCheck(IConfigService configService, IProvideDownloadClient provideDownloadClient)
{
_configService = configService;
_provideDownloadClient = provideDownloadClient;
_downloadTrackingService = downloadTrackingService;
}
public override HealthCheck Check()
@ -65,13 +64,6 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling or configure Drone factory");
}
if (_configService.EnableCompletedDownloadHandling && !droneFactoryFolder.IsEmpty)
{
if (_downloadTrackingService.GetCompletedDownloads().Any(v => droneFactoryFolder.Contains(v.DownloadItem.OutputPath)))
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Completed Download Handling conflict with Drone Factory (Conflicting History Item)", "Migrating-to-Completed-Download-Handling#conflicting-download-client-category");
}
}
return new HealthCheck(GetType());
}

@ -8,6 +8,8 @@ namespace NzbDrone.Core.History
{
public class History : ModelBase
{
public const string DOWNLOAD_CLIENT = "downloadClient";
public History()
{
Data = new Dictionary<string, string>();
@ -22,6 +24,9 @@ namespace NzbDrone.Core.History
public Series Series { get; set; }
public HistoryEventType EventType { get; set; }
public Dictionary<string, string> Data { get; set; }
public string DownloadId { get; set; }
}
public enum HistoryEventType

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Marr.Data.QGen;
using NzbDrone.Core.Datastore;
@ -11,31 +10,21 @@ namespace NzbDrone.Core.History
{
public interface IHistoryRepository : IBasicRepository<History>
{
void Trim();
List<QualityModel> GetBestQualityInHistory(int episodeId);
List<History> BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType);
List<History> Failed();
List<History> Grabbed();
List<History> Imported();
History MostRecentForEpisode(int episodeId);
List<History> FindBySourceTitle(string sourceTitle);
History MostRecentForDownloadId(string downloadId);
List<History> FindByDownloadId(string downloadId);
List<History> FindDownloadHistory(int idSeriesId, QualityModel quality);
}
public class HistoryRepository : BasicRepository<History>, IHistoryRepository
{
private readonly IDatabase _database;
public HistoryRepository(IDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
_database = database;
}
public void Trim()
{
var cutoff = DateTime.UtcNow.AddDays(-30).Date;
Delete(c=> c.Date < cutoff);
}
public List<QualityModel> GetBestQualityInHistory(int episodeId)
{
@ -44,30 +33,6 @@ namespace NzbDrone.Core.History
return history.Select(h => h.Quality).ToList();
}
public List<History> BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType)
{
return Query.Join<History, Series>(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id)
.Join<History, Episode>(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id)
.Where(h => h.Date >= startDate)
.AndWhere(h => h.Date <= endDate)
.AndWhere(h => h.EventType == eventType);
}
public List<History> Failed()
{
return Query.Where(h => h.EventType == HistoryEventType.DownloadFailed);
}
public List<History> Grabbed()
{
return Query.Where(h => h.EventType == HistoryEventType.Grabbed);
}
public List<History> Imported()
{
return Query.Where(h => h.EventType == HistoryEventType.DownloadFolderImported);
}
public History MostRecentForEpisode(int episodeId)
{
return Query.Where(h => h.EpisodeId == episodeId)
@ -75,14 +40,27 @@ namespace NzbDrone.Core.History
.FirstOrDefault();
}
public List<History> FindBySourceTitle(string sourceTitle)
public History MostRecentForDownloadId(string downloadId)
{
return Query.Where(h => h.DownloadId == downloadId)
.OrderByDescending(h => h.Date)
.FirstOrDefault();
}
public List<History> FindByDownloadId(string downloadId)
{
return Query.Where(h => h.SourceTitle.Contains(sourceTitle));
return Query.Where(h => h.DownloadId == downloadId);
}
public List<History> AllForEpisode(int episodeId)
public List<History> FindDownloadHistory(int idSeriesId, QualityModel quality)
{
return Query.Where(h => h.EpisodeId == episodeId);
return Query.Where(h =>
h.SeriesId == idSeriesId &&
h.Quality == quality &&
(h.EventType == HistoryEventType.Grabbed ||
h.EventType == HistoryEventType.DownloadFailed ||
h.EventType == HistoryEventType.DownloadFolderImported)
).ToList();
}
protected override SortBuilder<History> GetPagedQuery(QueryBuilder<History> query, PagingSpec<History> pagingSpec)

@ -16,19 +16,12 @@ namespace NzbDrone.Core.History
{
public interface IHistoryService
{
List<History> All();
void Purge();
void Trim();
QualityModel GetBestQualityInHistory(Profile profile, int episodeId);
PagingSpec<History> Paged(PagingSpec<History> pagingSpec);
List<History> BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType);
List<History> Failed();
List<History> Grabbed();
List<History> Imported();
History MostRecentForEpisode(int episodeId);
History Get(int id);
List<History> FindBySourceTitle(string sourceTitle);
void UpdateHistoryData(Int32 historyId, Dictionary<String, String> data);
History MostRecentForDownloadId(string downloadId);
History Get(int historyId);
List<History> Find(string downloadId, HistoryEventType eventType);
}
public class HistoryService : IHistoryService,
@ -46,60 +39,31 @@ namespace NzbDrone.Core.History
_logger = logger;
}
public List<History> All()
{
return _historyRepository.All().ToList();
}
public PagingSpec<History> Paged(PagingSpec<History> pagingSpec)
{
return _historyRepository.GetPaged(pagingSpec);
}
public List<History> BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType)
{
return _historyRepository.BetweenDates(startDate, endDate, eventType);
}
public List<History> Failed()
{
return _historyRepository.Failed();
}
public List<History> Grabbed()
{
return _historyRepository.Grabbed();
}
public List<History> Imported()
{
return _historyRepository.Imported();
}
public History MostRecentForEpisode(int episodeId)
{
return _historyRepository.MostRecentForEpisode(episodeId);
}
public History Get(int id)
public History MostRecentForDownloadId(string downloadId)
{
return _historyRepository.Get(id);
return _historyRepository.MostRecentForDownloadId(downloadId);
}
public List<History> FindBySourceTitle(string sourceTitle)
public History Get(int historyId)
{
return _historyRepository.FindBySourceTitle(sourceTitle);
return _historyRepository.Get(historyId);
}
public void Purge()
public List<History> Find(string downloadId, HistoryEventType eventType)
{
_historyRepository.Purge();
return _historyRepository.FindByDownloadId(downloadId).Where(c => c.EventType == eventType).ToList();
}
public virtual void Trim()
{
_historyRepository.Trim();
}
public QualityModel GetBestQualityInHistory(Profile profile, int episodeId)
{
@ -109,13 +73,6 @@ namespace NzbDrone.Core.History
.FirstOrDefault();
}
public void UpdateHistoryData(Int32 historyId, Dictionary<String, String> data)
{
var history = _historyRepository.Get(historyId);
history.Data = data;
_historyRepository.Update(history);
}
public void Handle(EpisodeGrabbedEvent message)
{
foreach (var episode in message.Episode.Episodes)
@ -128,6 +85,7 @@ namespace NzbDrone.Core.History
SourceTitle = message.Episode.Release.Title,
SeriesId = episode.SeriesId,
EpisodeId = episode.Id,
DownloadId = message.DownloadId
};
history.Data.Add("Indexer", message.Episode.Release.Indexer);
@ -138,11 +96,6 @@ namespace NzbDrone.Core.History
history.Data.Add("PublishedDate", message.Episode.Release.PublishDate.ToString("s") + "Z");
history.Data.Add("DownloadClient", message.DownloadClient);
if (!String.IsNullOrWhiteSpace(message.DownloadClientId))
{
history.Data.Add("DownloadClientId", message.DownloadClientId);
}
if (!message.Episode.ParsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace())
{
history.Data.Add("ReleaseHash", message.Episode.ParsedEpisodeInfo.ReleaseHash);
@ -159,6 +112,13 @@ namespace NzbDrone.Core.History
return;
}
var downloadId = message.DownloadId;
if (downloadId.IsNullOrWhiteSpace())
{
downloadId = FindDownloadId(message);
}
foreach (var episode in message.EpisodeInfo.Episodes)
{
var history = new History
@ -168,7 +128,8 @@ namespace NzbDrone.Core.History
Quality = message.EpisodeInfo.Quality,
SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path),
SeriesId = message.ImportedEpisode.SeriesId,
EpisodeId = episode.Id
EpisodeId = episode.Id,
DownloadId = downloadId
};
//Won't have a value since we publish this event before saving to DB.
@ -176,12 +137,57 @@ namespace NzbDrone.Core.History
history.Data.Add("DroppedPath", message.EpisodeInfo.Path);
history.Data.Add("ImportedPath", Path.Combine(message.EpisodeInfo.Series.Path, message.ImportedEpisode.RelativePath));
history.Data.Add("DownloadClient", message.DownloadClient);
history.Data.Add("DownloadClientId", message.DownloadClientId);
_historyRepository.Insert(history);
}
}
private string FindDownloadId(EpisodeImportedEvent trackedDownload)
{
_logger.Debug("Trying to find downloadId for {0} from history", trackedDownload.ImportedEpisode.Path);
var episodeIds = trackedDownload.EpisodeInfo.Episodes.Select(c => c.Id).ToList();
var allHistory = _historyRepository.FindDownloadHistory(trackedDownload.EpisodeInfo.Series.Id, trackedDownload.ImportedEpisode.Quality);
//Find download related items for these episdoes
var episodesHistory = allHistory.Where(h => episodeIds.Contains(h.EpisodeId)).ToList();
var processedDownloadId = episodesHistory
.Where(c => c.EventType != HistoryEventType.Grabbed && c.DownloadId != null)
.Select(c => c.DownloadId);
var stillDownloading = episodesHistory.Where(c => c.EventType == HistoryEventType.Grabbed && !processedDownloadId.Contains(c.DownloadId)).ToList();
string downloadId = null;
if (stillDownloading.Any())
{
foreach (var matchingHistory in trackedDownload.EpisodeInfo.Episodes.Select(e => stillDownloading.Where(c => c.EpisodeId == e.Id).ToList()))
{
if (matchingHistory.Count != 1)
{
return null;
}
var newDownloadId = matchingHistory.Single().DownloadId;
if (downloadId == null || downloadId == newDownloadId)
{
downloadId = newDownloadId;
}
else
{
return null;
}
}
}
return downloadId;
}
public void Handle(DownloadFailedEvent message)
{
foreach (var episodeId in message.EpisodeIds)
@ -194,10 +200,10 @@ namespace NzbDrone.Core.History
SourceTitle = message.SourceTitle,
SeriesId = message.SeriesId,
EpisodeId = episodeId,
DownloadId = message.DownloadId
};
history.Data.Add("DownloadClient", message.DownloadClient);
history.Data.Add("DownloadClientId", message.DownloadClientId);
history.Data.Add("Message", message.Message);
_historyRepository.Insert(history);

@ -14,7 +14,5 @@ namespace NzbDrone.Core.MediaFiles.Commands
}
public Boolean SendUpdates { get; set; }
public String Path { get; set; }
public String DownloadClientId { get; set; }
}
}

@ -4,9 +4,7 @@ using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Messaging.Commands;
@ -16,22 +14,16 @@ namespace NzbDrone.Core.MediaFiles
public class DownloadedEpisodesCommandService : IExecute<DownloadedEpisodesScanCommand>
{
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
private readonly IDownloadTrackingService _downloadTrackingService;
private readonly ICompletedDownloadService _completedDownloadService;
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly Logger _logger;
public DownloadedEpisodesCommandService(IDownloadedEpisodesImportService downloadedEpisodesImportService,
IDownloadTrackingService downloadTrackingService,
ICompletedDownloadService completedDownloadService,
IDiskProvider diskProvider,
IConfigService configService,
Logger logger)
{
_downloadedEpisodesImportService = downloadedEpisodesImportService;
_downloadTrackingService = downloadTrackingService;
_completedDownloadService = completedDownloadService;
_diskProvider = diskProvider;
_configService = configService;
_logger = logger;
@ -56,43 +48,12 @@ namespace NzbDrone.Core.MediaFiles
return _downloadedEpisodesImportService.ProcessRootFolder(new DirectoryInfo(downloadedEpisodesFolder));
}
private List<ImportResult> ProcessFolder(DownloadedEpisodesScanCommand message)
{
if (!_diskProvider.FolderExists(message.Path))
{
_logger.Warn("Folder specified for import scan [{0}] doesn't exist.", message.Path);
return new List<ImportResult>();
}
if (message.DownloadClientId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _downloadTrackingService.GetQueuedDownloads().Where(v => v.DownloadItem.DownloadClientId == message.DownloadClientId).FirstOrDefault();
if (trackedDownload == null)
{
_logger.Warn("External directory scan request for unknown download {0}, attempting normal import. [{1}]", message.DownloadClientId, message.Path);
return _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(message.Path));
}
return _completedDownloadService.Import(trackedDownload, message.Path);
}
return _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(message.Path));
}
public void Execute(DownloadedEpisodesScanCommand message)
{
List<ImportResult> importResults;
if (message.Path.IsNotNullOrWhiteSpace())
{
importResults = ProcessFolder(message);
}
else
{
importResults = ProcessDroneFactoryFolder();
}
if (importResults == null || !importResults.Any(v => v.Result == ImportResultType.Imported))
var importResults = ProcessDroneFactoryFolder();
if (importResults == null || importResults.All(v => v.Result != ImportResultType.Imported))
{
// Atm we don't report it as a command failure, coz that would cause the download to be failed.
// Changing the message won't do a thing either, coz it will get set to 'Completed' a msec later.

@ -16,9 +16,7 @@ namespace NzbDrone.Core.MediaFiles
{
List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo);
List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null);
List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, Series series, DownloadClientItem downloadClientItem = null);
List<ImportResult> ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem = null);
List<ImportResult> ProcessFile(FileInfo fileInfo, Series series, DownloadClientItem downloadClientItem = null);
List<ImportResult> ProcessPath(string path, DownloadClientItem downloadClientItem = null);
}
public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService
@ -88,12 +86,12 @@ namespace NzbDrone.Core.MediaFiles
return ProcessFolder(directoryInfo, series, downloadClientItem);
}
public List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, Series series,
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, Series series,
DownloadClientItem downloadClientItem = null)
{
if (_seriesService.SeriesPathExists(directoryInfo.FullName))
{
_logger.Warn("Unable to process folder that contains sorted TV Shows");
_logger.Warn("Unable to process folder that is mapped to an existing show");
return new List<ImportResult>();
}
@ -147,7 +145,7 @@ namespace NzbDrone.Core.MediaFiles
return ProcessFile(fileInfo, series, downloadClientItem);
}
public List<ImportResult> ProcessFile(FileInfo fileInfo, Series series, DownloadClientItem downloadClientItem = null)
private List<ImportResult> ProcessFile(FileInfo fileInfo, Series series, DownloadClientItem downloadClientItem = null)
{
if (downloadClientItem == null)
{
@ -164,6 +162,16 @@ namespace NzbDrone.Core.MediaFiles
return _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
}
public List<ImportResult> ProcessPath(string path, DownloadClientItem downloadClientItem = null)
{
if (_diskProvider.FolderExists(path))
{
return ProcessFolder(new DirectoryInfo(path), downloadClientItem);
}
return ProcessFile(new FileInfo(path), downloadClientItem);
}
private string GetCleanedUpFolderName(string folder)
{
folder = folder.Replace("_UNPACK_", "")

@ -99,7 +99,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
if (downloadClientItem != null)
{
_eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadClientId));
_eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId));
}
else
{

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
@ -13,7 +14,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
get
{
return !Rejections.Any();
return Rejections.Empty();
}
}

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
@ -15,14 +14,17 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
get
{
//Approved and imported
if (Errors.Empty()) return ImportResultType.Imported;
if (Errors.Any())
{
if (ImportDecision.Approved)
{
return ImportResultType.Skipped;
}
//Decision was approved, but it was not imported
if (ImportDecision.Approved) return ImportResultType.Skipped;
return ImportResultType.Rejected;
}
//Decision was rejected
return ImportResultType.Rejected;
return ImportResultType.Imported;
}
}

@ -8,9 +8,9 @@ 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 Boolean NewDownload { get; private set; }
public String DownloadClient { get; private set; }
public String DownloadId { get; private set; }
public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload)
{
@ -19,13 +19,13 @@ namespace NzbDrone.Core.MediaFiles.Events
NewDownload = newDownload;
}
public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadClientId)
public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode, bool newDownload, string downloadClient, string downloadId)
{
EpisodeInfo = episodeInfo;
ImportedEpisode = importedEpisode;
NewDownload = newDownload;
DownloadClient = downloadClient;
DownloadClientId = downloadClientId;
DownloadId = downloadId;
}
}
}

@ -231,6 +231,7 @@
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
<Compile Include="Datastore\Migration\069_quality_proper.cs" />
<Compile Include="Datastore\Migration\071_unknown_quality_in_profile.cs" />
<Compile Include="Datastore\Migration\072_history_grabid.cs" />
<Compile Include="Datastore\Migration\070_delay_profile.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
@ -270,7 +271,6 @@
<Compile Include="DecisionEngine\Specifications\QualityAllowedByProfileSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\MinimumAgeSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RetentionSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RetrySpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\DelaySpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\HistorySpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\MonitoredEpisodeSpecification.cs" />
@ -353,7 +353,12 @@
<Compile Include="Download\Clients\uTorrent\UTorrentTorrent.cs" />
<Compile Include="Download\Clients\uTorrent\UTorrentTorrentStatus.cs" />
<Compile Include="Download\CompletedDownloadService.cs" />
<Compile Include="Download\TrackedDownloadStatusMessage.cs" />
<Compile Include="Download\DownloadEventHub.cs" />
<Compile Include="Download\TrackedDownloads\DownloadMonitoringService.cs" />
<Compile Include="Download\TrackedDownloads\TrackedDownload.cs" />
<Compile Include="Download\TrackedDownloads\TrackedDownloadService.cs" />
<Compile Include="Download\TrackedDownloads\TrackedDownloadStatusMessage.cs" />
<Compile Include="Download\TrackedDownloads\TrackedDownloadRefreshedEvent.cs" />
<Compile Include="Download\UsenetClientBase.cs" />
<Compile Include="Download\TorrentClientBase.cs" />
<Compile Include="Download\DownloadClientBase.cs" />
@ -367,7 +372,6 @@
<Compile Include="Download\DownloadFailedEvent.cs" />
<Compile Include="Download\DownloadItemStatus.cs" />
<Compile Include="Download\DownloadService.cs" />
<Compile Include="Download\DownloadTrackingService.cs" />
<Compile Include="Download\EpisodeGrabbedEvent.cs" />
<Compile Include="Download\FailedDownloadService.cs" />
<Compile Include="Download\IDownloadClient.cs" />
@ -378,7 +382,6 @@
<Compile Include="Download\ProcessDownloadDecisions.cs" />
<Compile Include="Download\ProcessedDecisions.cs" />
<Compile Include="Download\RedownloadFailedDownloadService.cs" />
<Compile Include="Download\TrackedDownload.cs" />
<Compile Include="Exceptions\BadRequestException.cs" />
<Compile Include="Exceptions\DownstreamException.cs" />
<Compile Include="Exceptions\NzbDroneClientException.cs" />
@ -765,9 +768,8 @@
<Compile Include="Qualities\QualityModel.cs" />
<Compile Include="Qualities\QualityModelComparer.cs" />
<Compile Include="Queue\Queue.cs" />
<Compile Include="Queue\QueueScheduler.cs" />
<Compile Include="Queue\QueueService.cs" />
<Compile Include="Queue\UpdateQueueEvent.cs" />
<Compile Include="Queue\QueueUpdatedEvent.cs" />
<Compile Include="Restrictions\Restriction.cs" />
<Compile Include="Restrictions\RestrictionRepository.cs" />
<Compile Include="Restrictions\RestrictionService.cs" />
@ -909,6 +911,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>

@ -15,8 +15,7 @@ namespace NzbDrone.Core.Parser
{
LocalEpisode GetLocalEpisode(string filename, Series series, bool sceneSource);
Series GetSeries(string title);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId, SearchCriteriaBase searchCriteria = null);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 seriesId, IEnumerable<Int32> episodeIds);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 tvRageId = 0, SearchCriteriaBase searchCriteria = null);
List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null);
ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvRageId, SearchCriteriaBase searchCriteria = null);
}
@ -119,18 +118,6 @@ namespace NzbDrone.Core.Parser
return remoteEpisode;
}
public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Int32 seriesId, IEnumerable<Int32> episodeIds)
{
var remoteEpisode = new RemoteEpisode
{
ParsedEpisodeInfo = parsedEpisodeInfo,
Series = _seriesService.GetSeries(seriesId),
Episodes = _episodeService.GetEpisodes(episodeIds)
};
return remoteEpisode;
}
public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null)
{
var result = new List<Episode>();

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Queue
{
@ -21,7 +21,7 @@ namespace NzbDrone.Core.Queue
public String Status { get; set; }
public String TrackedDownloadStatus { get; set; }
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
public RemoteEpisode RemoteEpisode { get; set; }
public String TrackingId { get; set; }
public RemoteEpisode RemoteEpisode { get; set; }
}
}

@ -1,58 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using Timer = System.Timers.Timer;
namespace NzbDrone.Core.Queue
{
public class QueueScheduler : IHandle<ApplicationStartedEvent>,
IHandle<ApplicationShutdownRequested>
{
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
private static readonly Timer Timer = new Timer();
private static CancellationTokenSource _cancellationTokenSource;
public QueueScheduler(IEventAggregator eventAggregator, Logger logger)
{
_eventAggregator = eventAggregator;
_logger = logger;
}
private void CheckQueue()
{
try
{
Timer.Enabled = false;
_eventAggregator.PublishEvent(new UpdateQueueEvent());
}
finally
{
if (!_cancellationTokenSource.IsCancellationRequested)
{
Timer.Enabled = true;
}
}
}
public void Handle(ApplicationStartedEvent message)
{
_cancellationTokenSource = new CancellationTokenSource();
Timer.Interval = 1000 * 30;
Timer.Elapsed += (o, args) => Task.Factory.StartNew(CheckQueue, _cancellationTokenSource.Token)
.LogExceptions();
Timer.Start();
}
public void Handle(ApplicationShutdownRequested message)
{
_logger.Info("Shutting down queue scheduler");
_cancellationTokenSource.Cancel(true);
Timer.Stop();
}
}
}

@ -1,7 +1,8 @@
using System;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Queue
{
@ -11,64 +12,63 @@ namespace NzbDrone.Core.Queue
Queue Find(int id);
}
public class QueueService : IQueueService
public class QueueService : IQueueService, IHandle<TrackedDownloadRefreshedEvent>
{
private readonly IDownloadTrackingService _downloadTrackingService;
private readonly IEventAggregator _eventAggregator;
private static List<Queue> _queue = new List<Queue>();
public QueueService(IDownloadTrackingService downloadTrackingService)
public QueueService(IEventAggregator eventAggregator)
{
_downloadTrackingService = downloadTrackingService;
_eventAggregator = eventAggregator;
}
public List<Queue> GetQueue()
{
var queueItems = _downloadTrackingService.GetQueuedDownloads()
.OrderBy(v => v.DownloadItem.RemainingTime)
.ToList();
return MapQueue(queueItems);
return _queue;
}
public Queue Find(int id)
{
return GetQueue().SingleOrDefault(q => q.Id == id);
return _queue.SingleOrDefault(q => q.Id == id);
}
private List<Queue> MapQueue(IEnumerable<TrackedDownload> trackedDownloads)
public void Handle(TrackedDownloadRefreshedEvent message)
{
var queued = new List<Queue>();
_queue = message.TrackedDownloads.OrderBy(c => c.DownloadItem.RemainingTime).SelectMany(MapQueue)
.ToList();
_eventAggregator.PublishEvent(new QueueUpdatedEvent());
}
foreach (var trackedDownload in trackedDownloads)
private static IEnumerable<Queue> MapQueue(TrackedDownload trackedDownload)
{
foreach (var episode in trackedDownload.RemoteEpisode.Episodes)
{
foreach (var episode in trackedDownload.RemoteEpisode.Episodes)
var queue = new Queue
{
var queue = new Queue
{
Id = episode.Id ^ (trackedDownload.DownloadItem.DownloadClientId.GetHashCode() << 16),
Series = trackedDownload.RemoteEpisode.Series,
Episode = episode,
Quality = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Quality,
Title = trackedDownload.DownloadItem.Title,
Size = trackedDownload.DownloadItem.TotalSize,
Sizeleft = trackedDownload.DownloadItem.RemainingSize,
Timeleft = trackedDownload.DownloadItem.RemainingTime,
Status = trackedDownload.DownloadItem.Status.ToString(),
RemoteEpisode = trackedDownload.RemoteEpisode,
TrackedDownloadStatus = trackedDownload.Status.ToString(),
StatusMessages = trackedDownload.StatusMessages,
TrackingId = trackedDownload.TrackingId
};
if (queue.Timeleft.HasValue)
{
queue.EstimatedCompletionTime = DateTime.UtcNow.Add(queue.Timeleft.Value);
}
Id = episode.Id ^ (trackedDownload.DownloadItem.DownloadId.GetHashCode() << 16),
Series = trackedDownload.RemoteEpisode.Series,
Episode = episode,
Quality = trackedDownload.RemoteEpisode.ParsedEpisodeInfo.Quality,
Title = trackedDownload.DownloadItem.Title,
Size = trackedDownload.DownloadItem.TotalSize,
Sizeleft = trackedDownload.DownloadItem.RemainingSize,
Timeleft = trackedDownload.DownloadItem.RemainingTime,
Status = trackedDownload.DownloadItem.Status.ToString(),
TrackedDownloadStatus = trackedDownload.Status.ToString(),
StatusMessages = trackedDownload.StatusMessages.ToList(),
RemoteEpisode = trackedDownload.RemoteEpisode,
TrackingId = trackedDownload.TrackingId
};
queued.Add(queue);
if (queue.Timeleft.HasValue)
{
queue.EstimatedCompletionTime = DateTime.UtcNow.Add(queue.Timeleft.Value);
}
yield return queue;
}
return queued;
}
}
}

@ -2,7 +2,7 @@
namespace NzbDrone.Core.Queue
{
public class UpdateQueueEvent : IEvent
public class QueueUpdatedEvent : IEvent
{
}
}

@ -25,9 +25,9 @@
<dd>{{downloadClient}}</dd>
{{/if}}
{{#if downloadClientId}}
<dt>Download Client ID:</dt>
<dd>{{downloadClientId}}</dd>
{{#if downloadId}}
<dt>Grab ID:</dt>
<dd>{{downloadId}}</dd>
{{/if}}
{{#if age}}

@ -12,20 +12,20 @@ define(
ui: {
completedDownloadHandlingCheckbox : '.x-completed-download-handling',
completedDownloadOptions : '.x-completed-download-options',
failedDownloadHandlingCheckbox : '.x-failed-download-handling',
failedAutoRedownladCheckbox : '.x-failed-auto-redownload',
failedDownloadOptions : '.x-failed-download-options'
},
events: {
'change .x-completed-download-handling' : '_setCompletedDownloadOptionsVisibility',
'change .x-failed-download-handling' : '_setFailedDownloadOptionsVisibility'
'change .x-failed-auto-redownload' : '_setFailedDownloadOptionsVisibility'
},
onRender: function () {
if (!this.ui.completedDownloadHandlingCheckbox.prop('checked')) {
this.ui.completedDownloadOptions.hide();
}
if (!this.ui.failedDownloadHandlingCheckbox.prop('checked')) {
if (!this.ui.failedAutoRedownladCheckbox.prop('checked')) {
this.ui.failedDownloadOptions.hide();
}
},
@ -42,7 +42,7 @@ define(
},
_setFailedDownloadOptionsVisibility: function () {
var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked');
var checked = this.ui.failedAutoRedownladCheckbox.prop('checked');
if (checked) {
this.ui.failedDownloadOptions.slideDown();
}

@ -14,15 +14,13 @@
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Import completed downloads in download client history"/>
<i class="icon-nd-form-warning" title="Download client history items that are stored in the drone factory will be ignored. Configure the Drone Factory for a different path"/>
<i class="icon-nd-form-info" title="Automatically import completed downloads from download client"/>
</span>
</div>
</div>
</div>
</div>
<div class="x-completed-download-options advanced-setting">
<div class="form-group">
<label class="col-sm-3 control-label">Remove</label>
@ -44,20 +42,19 @@
</span>
</div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="advanced-setting">
<fieldset>
<legend>Failed Download Handling</legend>
<div class="form-group">
<label class="col-sm-3 control-label">Enable</label>
<label class="col-sm-3 control-label">Redownload</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="enableFailedDownloadHandling" class="x-failed-download-handling"/>
<input type="checkbox" name="autoRedownloadFailed" class="x-failed-auto-redownload"/>
<p>
<span>Yes</span>
<span>No</span>
@ -66,39 +63,15 @@
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Process failed downloads and blacklist the release"/>
</span>
</div>
</div>
</div>
<div class="x-failed-download-options">
<div class="form-group">
<label class="col-sm-3 control-label">Redownload</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="autoRedownloadFailed"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Automatically search for and attempt to download another release"/>
<i class="icon-nd-form-info" title="Automatically search for and attempt to download a different release"/>
</span>
</div>
</div>
</div>
<div class="form-group">
</div>
<div class="x-failed-download-options advanced-setting">
<div class="form-group ">
<label class="col-sm-3 control-label">Remove</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
@ -110,48 +83,11 @@
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-nd-form-info" title="Remove failed downloads from download client history"/>
</span>
</div>
</div>
</div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Grace Period</label>
<div class="col-sm-1 col-sm-push-2 help-inline">
<i class="icon-nd-form-info" title="Age in hours (since posting) where a release can be retried instead of immediately blacklisted"/>
</div>
<div class="col-sm-2 col-sm-pull-1">
<input type="number" min="1" max="24" name="blacklistGracePeriod" class="form-control"/>
</div>
</div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Retry Interval</label>
<div class="col-sm-1 col-sm-push-2 help-inline">
<i class="icon-nd-form-info" title="Time in minutes before a failed download for a recent release will be retried"/>
</div>
<div class="col-sm-2 col-sm-pull-1">
<input type="number" min="5" max="120" name="blacklistRetryInterval" class="form-control"/>
</div>
</div>
<div class="form-group advanced-setting">
<label class="col-sm-3 control-label">Retry Count</label>
<div class="col-sm-1 col-sm-push-2 help-inline">
<i class="icon-nd-form-info" title="Number of times to retry a release before it is blacklisted"/>
</div>
<div class="col-sm-2 col-sm-pull-1">
<input type="number" min="0" max="10" name="blacklistRetryLimit" class="form-control"/>
</div>
</div>
</div>
</fieldset>

@ -1,11 +1,12 @@
<fieldset>
<fieldset class="advanced-setting">
<legend>Drone Factory Options</legend>
<div class="form-group">
<label class="col-sm-3 control-label">Drone Factory</label>
<div class="col-sm-1 col-sm-push-8 help-inline">
<i class="icon-nd-form-info" title="Optional folder to periodically scan for available imports"/>
<i class="icon-nd-form-info" title="Optional folder to periodically scan for possible imports"/>
<i class="icon-nd-form-warning" title="Do not use the folder that contains some or all of your sorted and named TV shows - doing so could cause data loss"></i>
<i class="icon-nd-form-warning" title="Download client history items that are stored in the drone factory will be ignored."/>
</div>
<div class="col-sm-8 col-sm-pull-1">
@ -13,7 +14,7 @@
</div>
</div>
<div class="form-group advanced-setting">
<div class="form-group">
<label class="col-sm-3 control-label">Drone Factory Interval</label>
<div class="col-sm-1 col-sm-push-2 help-inline">

Loading…
Cancel
Save