Monitor and Process downloads separately

New: Queue remains up to date while importing file from remote file system
Fixed: Failed downloads still in queue won't result in failed search

Closes #668
Closes #907
Fixes #2973
This commit is contained in:
Mark McDowall 2019-05-05 10:59:11 -07:00 committed by Mark McDowall
parent 4e965e59a9
commit 3916495329
40 changed files with 1092 additions and 456 deletions

View File

@ -132,7 +132,7 @@ class Queue extends Component {
totalRecords, totalRecords,
isGrabbing, isGrabbing,
isRemoving, isRemoving,
isCheckForFinishedDownloadExecuting, isRefreshMonitoredDownloadsExecuting,
onRefreshPress, onRefreshPress,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -145,7 +145,7 @@ class Queue extends Component {
isPendingSelected isPendingSelected
} = this.state; } = this.state;
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting; const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId)); const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
const hasError = error || episodesError; const hasError = error || episodesError;
const selectedCount = this.getSelectedIds().length; const selectedCount = this.getSelectedIds().length;
@ -279,7 +279,7 @@ Queue.propTypes = {
totalRecords: PropTypes.number, totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired,
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired, isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired, onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired onRemoveSelectedPress: PropTypes.func.isRequired

View File

@ -18,13 +18,13 @@ function createMapStateToProps() {
(state) => state.episodes, (state) => state.episodes,
(state) => state.queue.options, (state) => state.queue.options,
(state) => state.queue.paged, (state) => state.queue.paged,
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD), createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => { (episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
return { return {
isEpisodesFetching: episodes.isFetching, isEpisodesFetching: episodes.isFetching,
isEpisodesPopulated: episodes.isPopulated, isEpisodesPopulated: episodes.isPopulated,
episodesError: episodes.error, episodesError: episodes.error,
isCheckForFinishedDownloadExecuting, isRefreshMonitoredDownloadsExecuting,
...options, ...options,
...queue ...queue
}; };
@ -129,7 +129,7 @@ class QueueConnector extends Component {
onRefreshPress = () => { onRefreshPress = () => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD name: commandNames.REFRESH_MONITORED_DOWNLOADS
}); });
} }

View File

@ -68,6 +68,7 @@ class QueueRow extends Component {
title, title,
status, status,
trackedDownloadStatus, trackedDownloadStatus,
trackedDownloadState,
statusMessages, statusMessages,
errorMessage, errorMessage,
series, series,
@ -100,8 +101,8 @@ class QueueRow extends Component {
} = this.state; } = this.state;
const progress = 100 - (sizeleft / size * 100); const progress = 100 - (sizeleft / size * 100);
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning'; const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable'; const isPending = status === 'delay' || status === 'downloadClientUnavailable';
return ( return (
<TableRow> <TableRow>
@ -129,6 +130,7 @@ class QueueRow extends Component {
sourceTitle={title} sourceTitle={title}
status={status} status={status}
trackedDownloadStatus={trackedDownloadStatus} trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages} statusMessages={statusMessages}
errorMessage={errorMessage} errorMessage={errorMessage}
/> />
@ -365,6 +367,7 @@ QueueRow.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string, trackedDownloadStatus: PropTypes.string,
trackedDownloadState: PropTypes.string,
statusMessages: PropTypes.arrayOf(PropTypes.object), statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
series: PropTypes.object, series: PropTypes.object,

View File

@ -37,13 +37,14 @@ function QueueStatusCell(props) {
const { const {
sourceTitle, sourceTitle,
status, status,
trackedDownloadStatus = 'Ok', trackedDownloadStatus,
trackedDownloadState,
statusMessages, statusMessages,
errorMessage errorMessage
} = props; } = props;
const hasWarning = trackedDownloadStatus === 'Warning'; const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'Error'; const hasError = trackedDownloadStatus === 'error';
// status === 'downloading' // status === 'downloading'
let iconName = icons.DOWNLOADING; let iconName = icons.DOWNLOADING;
@ -54,46 +55,58 @@ function QueueStatusCell(props) {
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
} }
if (status === 'Paused') { if (status === 'paused') {
iconName = icons.PAUSED; iconName = icons.PAUSED;
title = 'Paused'; title = 'Paused';
} }
if (status === 'Queued') { if (status === 'queued') {
iconName = icons.QUEUED; iconName = icons.QUEUED;
title = 'Queued'; title = 'Queued';
} }
if (status === 'Completed') { if (status === 'completed') {
iconName = icons.DOWNLOADED; iconName = icons.DOWNLOADED;
title = 'Downloaded'; title = 'Downloaded';
if (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
} }
if (status === 'Delay') { if (trackedDownloadState === 'importing') {
title += ' - Importing';
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
}
}
if (status === 'delay') {
iconName = icons.PENDING; iconName = icons.PENDING;
title = 'Pending'; title = 'Pending';
} }
if (status === 'DownloadClientUnavailable') { if (status === 'downloadClientUnavailable') {
iconName = icons.PENDING; iconName = icons.PENDING;
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
title = 'Pending - Download client is unavailable'; title = 'Pending - Download client is unavailable';
} }
if (status === 'Failed') { if (status === 'failed') {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = 'Download failed'; title = 'Download failed';
} }
if (status === 'Warning') { if (status === 'warning') {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
title = `Download warning: ${errorMessage || 'check download client for more details'}`; title = `Download warning: ${errorMessage || 'check download client for more details'}`;
} }
if (hasError) { if (hasError) {
if (status === 'Completed') { if (status === 'completed') {
iconName = icons.DOWNLOAD; iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = `Import failed: ${sourceTitle}`; title = `Import failed: ${sourceTitle}`;
@ -125,9 +138,15 @@ function QueueStatusCell(props) {
QueueStatusCell.propTypes = { QueueStatusCell.propTypes = {
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string, trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object), statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string errorMessage: PropTypes.string
}; };
QueueStatusCell.defaultProps = {
trackedDownloadStatus: 'Ok',
trackedDownloadState: 'Downloading'
};
export default QueueStatusCell; export default QueueStatusCell;

View File

@ -1,6 +1,6 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate'; export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup'; export const BACKUP = 'Backup';
export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload'; export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLACKLIST = 'ClearBlacklist'; export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_LOGS = 'ClearLog'; export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch'; export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';

View File

@ -262,7 +262,7 @@ class SignalRConnector extends Component {
} }
handleSystemTask = () => { handleSystemTask = () => {
// No-op for now, we may want this later this.props.dispatchFetchCommands();
} }
handleRootfolder = () => { handleRootfolder = () => {

View File

@ -168,7 +168,7 @@ class QueuedTaskRow extends Component {
isCancelConfirmModalOpen isCancelConfirmModalOpen
} = this.state; } = this.state;
let triggerIcon = icons.UNKNOWN; let triggerIcon = icons.QUICK;
if (trigger === 'manual') { if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE; triggerIcon = icons.INTERACTIVE;

View File

@ -86,12 +86,7 @@ namespace NzbDrone.Api.Queue
private object Import() private object Import()
{ {
var resource = Request.Body.FromJson<QueueResource>(); throw new BadRequestException("No longer available");
var trackedDownload = GetTrackedDownload(resource.Id);
_completedDownloadService.Process(trackedDownload, true);
return resource;
} }
private object Grab() private object Grab()

View File

@ -46,7 +46,7 @@ namespace NzbDrone.Api.Queue
Timeleft = model.Timeleft, Timeleft = model.Timeleft,
EstimatedCompletionTime = model.EstimatedCompletionTime, EstimatedCompletionTime = model.EstimatedCompletionTime,
Status = model.Status, Status = model.Status,
TrackedDownloadStatus = model.TrackedDownloadStatus, TrackedDownloadStatus = model.TrackedDownloadStatus.ToString(),
StatusMessages = model.StatusMessages, StatusMessages = model.StatusMessages,
DownloadId = model.DownloadId, DownloadId = model.DownloadId,
Protocol = model.Protocol Protocol = model.Protocol

View File

@ -4,6 +4,7 @@ using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Languages;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Qualities;
@ -79,11 +80,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Returns(new List<Queue.Queue>()); .Returns(new List<Queue.Queue>());
} }
private void GivenQueue(IEnumerable<RemoteEpisode> remoteEpisodes) private void GivenQueue(IEnumerable<RemoteEpisode> remoteEpisodes, TrackedDownloadState trackedDownloadState = TrackedDownloadState.Downloading)
{ {
var queue = remoteEpisodes.Select(remoteEpisode => new Queue.Queue var queue = remoteEpisodes.Select(remoteEpisode => new Queue.Queue
{ {
RemoteEpisode = remoteEpisode RemoteEpisode = remoteEpisode,
TrackedDownloadState = trackedDownloadState
}); });
Mocker.GetMock<IQueueService>() Mocker.GetMock<IQueueService>()
@ -432,5 +434,27 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
GivenQueue(new List<RemoteEpisode> { remoteEpisode }); GivenQueue(new List<RemoteEpisode> { remoteEpisode });
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
} }
[Test]
public void should_return_true_if_everything_is_the_same_for_failed_pending()
{
_series.QualityProfile.Value.Cutoff = Quality.Bluray1080p.Id;
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),
Language = Language.Spanish
})
.With(r => r.Release = _releaseInfo)
.Build();
GivenQueue(new List<RemoteEpisode> { remoteEpisode }, TrackedDownloadState.FailedPending);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
} }
} }

View File

@ -4,7 +4,6 @@ using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
@ -18,10 +17,10 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests
{ {
[TestFixture] [TestFixture]
public class CompletedDownloadServiceFixture : CoreTest<CompletedDownloadService> public class ImportFixture : CoreTest<CompletedDownloadService>
{ {
private TrackedDownload _trackedDownload; private TrackedDownload _trackedDownload;
private Episode _episode1; private Episode _episode1;
@ -44,7 +43,7 @@ namespace NzbDrone.Core.Test.Download
var remoteEpisode = BuildRemoteEpisode(); var remoteEpisode = BuildRemoteEpisode();
_trackedDownload = Builder<TrackedDownload>.CreateNew() _trackedDownload = Builder<TrackedDownload>.CreateNew()
.With(c => c.State = TrackedDownloadStage.Downloading) .With(c => c.State = TrackedDownloadState.Downloading)
.With(c => c.DownloadItem = completed) .With(c => c.DownloadItem = completed)
.With(c => c.RemoteEpisode = remoteEpisode) .With(c => c.RemoteEpisode = remoteEpisode)
.Build(); .Build();
@ -79,25 +78,6 @@ namespace NzbDrone.Core.Test.Download
}; };
} }
private void GivenNoGrabbedHistory()
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
.Returns((History.History)null);
}
private void GivenSuccessfulImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 } }))
});
}
private void GivenABadlyNamedDownload() private void GivenABadlyNamedDownload()
{ {
_trackedDownload.DownloadItem.DownloadId = "1234"; _trackedDownload.DownloadItem.DownloadId = "1234";
@ -122,94 +102,6 @@ namespace NzbDrone.Core.Test.Download
.Returns(_trackedDownload.RemoteEpisode.Series); .Returns(_trackedDownload.RemoteEpisode.Series);
} }
[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)
{
_trackedDownload.DownloadItem.Status = status;
Subject.Process(_trackedDownload);
AssertNoAttemptedImport();
}
[Test]
public void should_not_process_if_matching_history_is_not_found_and_no_category_specified()
{
_trackedDownload.DownloadItem.Category = null;
GivenNoGrabbedHistory();
Subject.Process(_trackedDownload);
AssertNoAttemptedImport();
}
[Test]
public void should_process_if_matching_history_is_not_found_but_category_specified()
{
_trackedDownload.DownloadItem.Category = "tv";
GivenNoGrabbedHistory();
GivenSeriesMatch();
GivenSuccessfulImport();
Subject.Process(_trackedDownload);
AssertCompletedDownload();
}
[Test]
public void should_not_process_if_output_path_is_empty()
{
_trackedDownload.DownloadItem.OutputPath = new OsPath();
Subject.Process(_trackedDownload);
AssertNoAttemptedImport();
}
[Test]
public void should_mark_as_imported_if_all_episodes_were_imported()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 }})),
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 }}))
});
Subject.Process(_trackedDownload);
AssertCompletedDownload();
}
[Test]
public void should_mark_as_imported_if_all_multi_episodes_were_imported()
{
_trackedDownload.RemoteEpisode.Episodes.Add(new Episode { Id = 2 });
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01E02.mkv", Episodes = { _episode1, _episode2 }}))
});
Subject.Process(_trackedDownload);
AssertCompletedDownload();
}
[Test] [Test]
public void should_not_mark_as_imported_if_all_files_were_rejected() public void should_not_mark_as_imported_if_all_files_were_rejected()
{ {
@ -226,12 +118,12 @@ namespace NzbDrone.Core.Test.Download
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 }},new Rejection("Rejected!")), "Test Failure") new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 }},new Rejection("Rejected!")), "Test Failure")
}); });
Subject.Process(_trackedDownload); Subject.Import(_trackedDownload);
Mocker.GetMock<IEventAggregator>() Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent<DownloadCompletedEvent>(It.IsAny<DownloadCompletedEvent>()), Times.Never()); .Verify(v => v.PublishEvent<DownloadCompletedEvent>(It.IsAny<DownloadCompletedEvent>()), Times.Never());
AssertNoCompletedDownload(); AssertNotImported();
} }
[Test] [Test]
@ -252,9 +144,9 @@ namespace NzbDrone.Core.Test.Download
_trackedDownload.RemoteEpisode.Episodes.Clear(); _trackedDownload.RemoteEpisode.Episodes.Clear();
Subject.Process(_trackedDownload); Subject.Import(_trackedDownload);
AssertNoCompletedDownload(); AssertNotImported();
} }
[Test] [Test]
@ -269,9 +161,154 @@ namespace NzbDrone.Core.Test.Download
}); });
Subject.Process(_trackedDownload); Subject.Import(_trackedDownload);
AssertNoCompletedDownload(); AssertNotImported();
}
[Test]
public void should_not_mark_as_imported_if_some_of_episodes_were_not_imported()
{
_trackedDownload.RemoteEpisode.Episodes = new List<Episode>
{
new Episode(),
new Episode(),
new Episode()
};
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), 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"}),"Test Failure")
});
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
.Returns(new List<History.History>());
Subject.Import(_trackedDownload);
AssertNotImported();
}
[Test]
public void should_not_mark_as_imported_if_some_of_episodes_were_not_imported_including_history()
{
_trackedDownload.RemoteEpisode.Episodes = new List<Episode>
{
new Episode(),
new Episode(),
new Episode()
};
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), 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"}),"Test Failure")
});
var history = Builder<History.History>.CreateListOfSize(2)
.BuildList();
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
.Returns(history);
Mocker.GetMock<ITrackedDownloadAlreadyImported>()
.Setup(s => s.IsImported(_trackedDownload, history))
.Returns(true);
Subject.Import(_trackedDownload);
AssertNotImported();
}
[Test]
public void should_mark_as_imported_if_all_episodes_were_imported()
{
var episode1 = new Episode {Id = 1};
var episode2 = new Episode {Id = 2};
_trackedDownload.RemoteEpisode.Episodes = new List<Episode> { episode1, episode2 };
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = new List<Episode> { episode1 } })),
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = new List<Episode> { episode2 } }))
});
Subject.Import(_trackedDownload);
AssertImported();
}
[Test]
public void should_mark_as_imported_if_all_episodes_were_imported_including_history()
{
var episode1 = new Episode { Id = 1 };
var episode2 = new Episode { Id = 2 };
_trackedDownload.RemoteEpisode.Episodes = new List<Episode> { episode1, episode2 };
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = new List<Episode> { episode1 } })),
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = new List<Episode> { episode2 } }),"Test Failure")
});
var history = Builder<History.History>.CreateListOfSize(2)
.BuildList();
Mocker.GetMock<IHistoryService>()
.Setup(s => s.FindByDownloadId(It.IsAny<string>()))
.Returns(history);
Mocker.GetMock<ITrackedDownloadAlreadyImported>()
.Setup(s => s.IsImported(It.IsAny<TrackedDownload>(), It.IsAny<List<History.History>>()))
.Returns(true);
Subject.Import(_trackedDownload);
AssertImported();
}
[Test]
public void should_mark_as_imported_if_double_episode_file_is_imported()
{
var episode1 = new Episode { Id = 1 };
var episode2 = new Episode { Id = 2 };
_trackedDownload.RemoteEpisode.Episodes = new List<Episode> { episode1, episode2 };
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
new ImportDecision(
new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01-E02.mkv", Episodes = new List<Episode> { episode1, episode2 } }))
});
Subject.Import(_trackedDownload);
AssertImported();
} }
[Test] [Test]
@ -288,38 +325,13 @@ namespace NzbDrone.Core.Test.Download
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>())) .Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult> .Returns(new List<ImportResult>
{ {
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 }})), new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = _trackedDownload.RemoteEpisode.Episodes})),
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 }}),"Test Failure") new ImportResult(new ImportDecision(new LocalEpisode{Path = @"C:\TestPath\Droned.S01E01.mkv"}),"Test Failure")
}); });
Subject.Process(_trackedDownload); Subject.Import(_trackedDownload);
AssertCompletedDownload(); AssertImported();
}
[Test]
public void should_mark_as_failed_if_some_of_episodes_were_not_imported()
{
_trackedDownload.RemoteEpisode.Episodes = new List<Episode>
{
_episode1,
_episode2,
_episode3
};
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 }})),
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E02.mkv", Episodes = { _episode2 }}),"Test Failure"),
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E03.mkv", Episodes = { _episode3 }}),"Test Failure")
});
Subject.Process(_trackedDownload);
AssertNoCompletedDownload();
} }
[Test] [Test]
@ -331,111 +343,27 @@ namespace NzbDrone.Core.Test.Download
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>())) .Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult> .Returns(new List<ImportResult>
{ {
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 }})) new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = _trackedDownload.RemoteEpisode.Episodes}))
}); });
Mocker.GetMock<ISeriesService>() Mocker.GetMock<ISeriesService>()
.Setup(v => v.GetSeries(It.IsAny<int>())) .Setup(v => v.GetSeries(It.IsAny<int>()))
.Returns(BuildRemoteEpisode().Series); .Returns(BuildRemoteEpisode().Series);
Subject.Process(_trackedDownload); Subject.Import(_trackedDownload);
AssertCompletedDownload(); AssertImported();
} }
[Test] private void AssertNotImported()
public void should_not_mark_as_imported_if_the_download_cannot_be_tracked_using_the_source_title_as_it_was_initiated_externally()
{
GivenABadlyNamedDownload();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 }}))
});
Mocker.GetMock<IHistoryService>()
.Setup(s => s.MostRecentForDownloadId(It.Is<string>(i => i == "1234")));
Subject.Process(_trackedDownload);
AssertNoCompletedDownload();
}
[Test]
public void should_not_import_when_there_is_a_title_mismatch()
{
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetSeries("Drone.S01E01.HDTV"))
.Returns((Series)null);
Subject.Process(_trackedDownload);
AssertNoCompletedDownload();
}
[Test]
public void should_mark_as_import_title_mismatch_if_ignore_warnings_is_true()
{
_trackedDownload.RemoteEpisode.Episodes = new List<Episode>
{
new Episode()
};
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv", Episodes = { _episode1 }}))
});
Subject.Process(_trackedDownload, true);
AssertCompletedDownload();
}
[Test]
public void should_warn_if_path_is_not_valid_for_windows()
{
WindowsOnly();
_trackedDownload.DownloadItem.OutputPath = new OsPath(@"/invalid/Windows/Path");
Subject.Process(_trackedDownload);
AssertNoAttemptedImport();
}
[Test]
public void should_warn_if_path_is_not_valid_for_linux()
{
MonoOnly();
_trackedDownload.DownloadItem.OutputPath = new OsPath(@"C:\Invalid\Mono\Path");
Subject.Process(_trackedDownload);
AssertNoAttemptedImport();
}
private void AssertNoAttemptedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Never());
AssertNoCompletedDownload();
}
private void AssertNoCompletedDownload()
{ {
Mocker.GetMock<IEventAggregator>() Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never()); .Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never());
_trackedDownload.State.Should().NotBe(TrackedDownloadStage.Imported); _trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending);
} }
private void AssertCompletedDownload() private void AssertImported()
{ {
Mocker.GetMock<IDownloadedEpisodesImportService>() Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once()); .Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteEpisode.Series, _trackedDownload.DownloadItem), Times.Once());
@ -443,7 +371,7 @@ namespace NzbDrone.Core.Test.Download
Mocker.GetMock<IEventAggregator>() Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Once()); .Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Once());
_trackedDownload.State.Should().Be(TrackedDownloadStage.Imported); _trackedDownload.State.Should().Be(TrackedDownloadState.Imported);
} }
} }
} }

View File

@ -0,0 +1,192 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests
{
[TestFixture]
public class ProcessFixture : CoreTest<CompletedDownloadService>
{
private TrackedDownload _trackedDownload;
[SetUp]
public void Setup()
{
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();
var remoteEpisode = BuildRemoteEpisode();
_trackedDownload = Builder<TrackedDownload>.CreateNew()
.With(c => c.State = TrackedDownloadState.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<IProvideDownloadClient>()
.Setup(c => c.Get(It.IsAny<int>()))
.Returns(Mocker.GetMock<IDownloadClient>().Object);
Mocker.GetMock<IHistoryService>()
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
.Returns(new History.History());
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetSeries("Drone.S01E01.HDTV"))
.Returns(remoteEpisode.Series);
}
private RemoteEpisode BuildRemoteEpisode()
{
return new RemoteEpisode
{
Series = new Series(),
Episodes = new List<Episode> { new Episode { Id = 1 } }
};
}
private void GivenNoGrabbedHistory()
{
Mocker.GetMock<IHistoryService>()
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
.Returns((History.History)null);
}
private void GivenSeriesMatch()
{
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetSeries(It.IsAny<string>()))
.Returns(_trackedDownload.RemoteEpisode.Series);
}
private void GivenABadlyNamedDownload()
{
_trackedDownload.DownloadItem.DownloadId = "1234";
_trackedDownload.DownloadItem.Title = "Droned Pilot"; // Set a badly named download
Mocker.GetMock<IHistoryService>()
.Setup(s => s.MostRecentForDownloadId(It.Is<string>(i => i == "1234")))
.Returns(new History.History() { SourceTitle = "Droned S01E01" });
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetSeries(It.IsAny<string>()))
.Returns((Series)null);
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetSeries("Droned S01E01"))
.Returns(BuildRemoteEpisode().Series);
}
[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)
{
_trackedDownload.DownloadItem.Status = status;
Subject.Check(_trackedDownload);
AssertNotReadyToImport();
}
[Test]
public void should_not_process_if_matching_history_is_not_found_and_no_category_specified()
{
_trackedDownload.DownloadItem.Category = null;
GivenNoGrabbedHistory();
Subject.Check(_trackedDownload);
AssertNotReadyToImport();
}
[Test]
public void should_process_if_matching_history_is_not_found_but_category_specified()
{
_trackedDownload.DownloadItem.Category = "tv";
GivenNoGrabbedHistory();
GivenSeriesMatch();
Subject.Check(_trackedDownload);
AssertReadyToImport();
}
[Test]
public void should_not_process_if_output_path_is_empty()
{
_trackedDownload.DownloadItem.OutputPath = new OsPath();
Subject.Check(_trackedDownload);
AssertNotReadyToImport();
}
[Test]
public void should_not_process_if_the_download_cannot_be_tracked_using_the_source_title_as_it_was_initiated_externally()
{
GivenABadlyNamedDownload();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode {Path = @"C:\TestPath\Droned.S01E01.mkv"}))
});
Mocker.GetMock<IHistoryService>()
.Setup(s => s.MostRecentForDownloadId(It.Is<string>(i => i == "1234")));
Subject.Check(_trackedDownload);
AssertNotReadyToImport();
}
[Test]
public void should_not_process_when_there_is_a_title_mismatch()
{
Mocker.GetMock<IParsingService>()
.Setup(s => s.GetSeries("Drone.S01E01.HDTV"))
.Returns((Series)null);
Subject.Check(_trackedDownload);
AssertNotReadyToImport();
}
private void AssertNotReadyToImport()
{
_trackedDownload.State.Should().NotBe(TrackedDownloadState.ImportPending);
}
private void AssertReadyToImport()
{
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending);
}
}
}

View File

@ -0,0 +1,104 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests
{
[TestFixture]
public class ProcessFailedFixture : CoreTest<FailedDownloadService>
{
private TrackedDownload _trackedDownload;
private List<History.History> _grabHistory;
[SetUp]
public void Setup()
{
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();
_grabHistory = Builder<History.History>.CreateListOfSize(2).BuildList();
var remoteEpisode = new RemoteEpisode
{
Series = new Series(),
Episodes = new List<Episode> { new Episode { Id = 1 } }
};
_trackedDownload = Builder<TrackedDownload>.CreateNew()
.With(c => c.State = TrackedDownloadState.FailedPending)
.With(c => c.DownloadItem = completed)
.With(c => c.RemoteEpisode = remoteEpisode)
.Build();
Mocker.GetMock<IHistoryService>()
.Setup(s => s.Find(_trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed))
.Returns(_grabHistory);
}
[Test]
public void should_mark_failed_if_encrypted()
{
_trackedDownload.DownloadItem.IsEncrypted = true;
Subject.ProcessFailed(_trackedDownload);
AssertDownloadFailed();
}
[Test]
public void should_mark_failed_if_download_item_is_failed()
{
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
Subject.ProcessFailed(_trackedDownload);
AssertDownloadFailed();
}
[Test]
public void should_include_tracked_download_in_message()
{
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
Subject.ProcessFailed(_trackedDownload);
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.Is<DownloadFailedEvent>(c => c.TrackedDownload != null)), Times.Once());
AssertDownloadFailed();
}
private void AssertDownloadNotFailed()
{
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Never());
_trackedDownload.State.Should().NotBe(TrackedDownloadState.Failed);
}
private void AssertDownloadFailed()
{
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Once());
_trackedDownload.State.Should().Be(TrackedDownloadState.Failed);
}
}
}

View File

@ -13,10 +13,10 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests
{ {
[TestFixture] [TestFixture]
public class FailedDownloadServiceFixture : CoreTest<FailedDownloadService> public class ProcessFixture : CoreTest<FailedDownloadService>
{ {
private TrackedDownload _trackedDownload; private TrackedDownload _trackedDownload;
private List<History.History> _grabHistory; private List<History.History> _grabHistory;
@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.Download
}; };
_trackedDownload = Builder<TrackedDownload>.CreateNew() _trackedDownload = Builder<TrackedDownload>.CreateNew()
.With(c => c.State = TrackedDownloadStage.Downloading) .With(c => c.State = TrackedDownloadState.Downloading)
.With(c => c.DownloadItem = completed) .With(c => c.DownloadItem = completed)
.With(c => c.RemoteEpisode = remoteEpisode) .With(c => c.RemoteEpisode = remoteEpisode)
.Build(); .Build();
@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.Download
{ {
GivenNoGrabbedHistory(); GivenNoGrabbedHistory();
Subject.Process(_trackedDownload); Subject.Check(_trackedDownload);
AssertDownloadNotFailed(); AssertDownloadNotFailed();
} }
@ -74,7 +74,7 @@ namespace NzbDrone.Core.Test.Download
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
GivenNoGrabbedHistory(); GivenNoGrabbedHistory();
Subject.Process(_trackedDownload); Subject.Check(_trackedDownload);
_trackedDownload.StatusMessages.Should().NotBeEmpty(); _trackedDownload.StatusMessages.Should().NotBeEmpty();
} }
@ -85,50 +85,17 @@ namespace NzbDrone.Core.Test.Download
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed; _trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
GivenNoGrabbedHistory(); GivenNoGrabbedHistory();
Subject.Process(_trackedDownload); Subject.Check(_trackedDownload);
_trackedDownload.StatusMessages.Should().NotBeEmpty(); _trackedDownload.StatusMessages.Should().NotBeEmpty();
} }
[Test]
public void should_mark_failed_if_encrypted()
{
_trackedDownload.DownloadItem.IsEncrypted = true;
Subject.Process(_trackedDownload);
AssertDownloadFailed();
}
[Test]
public void should_mark_failed_if_download_item_is_failed()
{
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
Subject.Process(_trackedDownload);
AssertDownloadFailed();
}
[Test]
public void should_include_tracked_download_in_message()
{
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
Subject.Process(_trackedDownload);
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.Is<DownloadFailedEvent>(c => c.TrackedDownload != null)), Times.Once());
AssertDownloadFailed();
}
private void AssertDownloadNotFailed() private void AssertDownloadNotFailed()
{ {
Mocker.GetMock<IEventAggregator>() Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Never()); .Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Never());
_trackedDownload.State.Should().NotBe(TrackedDownloadStage.DownloadFailed); _trackedDownload.State.Should().NotBe(TrackedDownloadState.Failed);
} }
@ -137,7 +104,7 @@ namespace NzbDrone.Core.Test.Download
Mocker.GetMock<IEventAggregator>() Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Once()); .Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Once());
_trackedDownload.State.Should().Be(TrackedDownloadStage.DownloadFailed); _trackedDownload.State.Should().Be(TrackedDownloadState.Failed);
} }
} }
} }

View File

@ -0,0 +1,128 @@
using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using FizzWare.NBuilder;
namespace NzbDrone.Core.Test.Download.TrackedDownloads
{
[TestFixture]
public class TrackedDownloadAlreadyImportedFixture : CoreTest<TrackedDownloadAlreadyImported>
{
private List<Episode> _episodes;
private TrackedDownload _trackedDownload;
private List<History.History> _historyItems;
[SetUp]
public void Setup()
{
_episodes = new List<Episode>();
var remoteEpisode = Builder<RemoteEpisode>.CreateNew()
.With(r => r.Episodes = _episodes)
.Build();
_trackedDownload = Builder<TrackedDownload>.CreateNew()
.With(t => t.RemoteEpisode = remoteEpisode)
.Build();
_historyItems = new List<History.History>();
}
public void GivenEpisodes(int count)
{
_episodes.AddRange(Builder<Episode>.CreateListOfSize(count)
.BuildList());
}
public void GivenHistoryForEpisode(Episode episode, params HistoryEventType[] eventTypes)
{
foreach (var eventType in eventTypes)
{
_historyItems.Add(
Builder<History.History>.CreateNew()
.With(h => h.EpisodeId = episode.Id)
.With(h => h.EventType = eventType)
.Build()
);
}
}
[Test]
public void should_return_false_if_there_is_no_history()
{
GivenEpisodes(1);
Subject.IsImported(_trackedDownload, _historyItems)
.Should()
.BeFalse();
}
[Test]
public void should_return_false_if_single_episode_download_is_not_imported()
{
GivenEpisodes(1);
GivenHistoryForEpisode(_episodes[0], HistoryEventType.Grabbed);
Subject.IsImported(_trackedDownload, _historyItems)
.Should()
.BeFalse();
}
[Test]
public void should_return_false_if_no_episode_in_multi_episode_download_is_imported()
{
GivenEpisodes(2);
GivenHistoryForEpisode(_episodes[0], HistoryEventType.Grabbed);
GivenHistoryForEpisode(_episodes[1], HistoryEventType.Grabbed);
Subject.IsImported(_trackedDownload, _historyItems)
.Should()
.BeFalse();
}
[Test]
public void should_should_return_false_if_only_one_episode_in_multi_episode_download_is_imported()
{
GivenEpisodes(2);
GivenHistoryForEpisode(_episodes[0], HistoryEventType.DownloadFolderImported, HistoryEventType.Grabbed);
GivenHistoryForEpisode(_episodes[1], HistoryEventType.Grabbed);
Subject.IsImported(_trackedDownload, _historyItems)
.Should()
.BeFalse();
}
[Test]
public void should_return_true_if_single_episode_download_is_imported()
{
GivenEpisodes(1);
GivenHistoryForEpisode(_episodes[0], HistoryEventType.DownloadFolderImported, HistoryEventType.Grabbed);
Subject.IsImported(_trackedDownload, _historyItems)
.Should()
.BeTrue();
}
[Test]
public void should_return_true_if_multi_episode_download_is_imported()
{
GivenEpisodes(2);
GivenHistoryForEpisode(_episodes[0], HistoryEventType.DownloadFolderImported, HistoryEventType.Grabbed);
GivenHistoryForEpisode(_episodes[1], HistoryEventType.DownloadFolderImported, HistoryEventType.Grabbed);
Subject.IsImported(_trackedDownload, _historyItems)
.Should()
.BeTrue();
}
}
}

View File

@ -49,7 +49,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{ {
DownloadItem = downloadItem, DownloadItem = downloadItem,
RemoteEpisode = remoteEpisode, RemoteEpisode = remoteEpisode,
State = TrackedDownloadStage.Downloading State = TrackedDownloadState.Downloading
}; };
} }

View File

@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{ {
DownloadItem = downloadItem, DownloadItem = downloadItem,
RemoteEpisode = remoteEpisode, RemoteEpisode = remoteEpisode,
State = TrackedDownloadStage.Downloading State = TrackedDownloadState.Downloading
}; };
} }

View File

@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Messaging.Commands
[Test] [Test]
public void should_not_remove_commands_for_five_minutes_after_they_end() public void should_not_remove_commands_for_five_minutes_after_they_end()
{ {
var command = Subject.Push<CheckForFinishedDownloadCommand>(new CheckForFinishedDownloadCommand()); var command = Subject.Push<RefreshMonitoredDownloadsCommand>(new RefreshMonitoredDownloadsCommand());
// Start the command to mimic CommandQueue's behaviour // Start the command to mimic CommandQueue's behaviour
command.StartedAt = DateTime.Now; command.StartedAt = DateTime.Now;

View File

@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Profiles.Releases;
@ -42,6 +43,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
var qualityProfile = subject.Series.QualityProfile.Value; var qualityProfile = subject.Series.QualityProfile.Value;
var languageProfile = subject.Series.LanguageProfile.Value; var languageProfile = subject.Series.LanguageProfile.Value;
// To avoid a race make sure it's not FailedPending (failed awaiting removal/search).
// Failed items (already searching for a replacement) won't be part of the queue since
// it's a copy, of the tracked download, not a reference.
if (queueItem.TrackedDownloadState == TrackedDownloadState.FailedPending)
{
continue;
}
_logger.Debug("Checking if existing release in queue meets cutoff. Queued: {0} - {1}", remoteEpisode.ParsedEpisodeInfo.Quality, remoteEpisode.ParsedEpisodeInfo.Language); _logger.Debug("Checking if existing release in queue meets cutoff. Queued: {0} - {1}", remoteEpisode.ParsedEpisodeInfo.Quality, remoteEpisode.ParsedEpisodeInfo.Language);
var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, queueItem.Title); var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, queueItem.Title);

View File

@ -4,6 +4,5 @@ namespace NzbDrone.Core.Download
{ {
public class CheckForFinishedDownloadCommand : Command public class CheckForFinishedDownloadCommand : Command
{ {
public override bool RequiresDiskAccess => true;
} }
} }

View File

@ -1,11 +1,8 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.History; using NzbDrone.Core.History;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
@ -18,45 +15,47 @@ namespace NzbDrone.Core.Download
{ {
public interface ICompletedDownloadService public interface ICompletedDownloadService
{ {
void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false); void Check(TrackedDownload trackedDownload);
void Import(TrackedDownload trackedDownload);
} }
public class CompletedDownloadService : ICompletedDownloadService public class CompletedDownloadService : ICompletedDownloadService
{ {
private readonly IConfigService _configService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IHistoryService _historyService; private readonly IHistoryService _historyService;
private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService; private readonly IDownloadedEpisodesImportService _downloadedEpisodesImportService;
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly Logger _logger;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported;
public CompletedDownloadService(IConfigService configService, public CompletedDownloadService(IEventAggregator eventAggregator,
IEventAggregator eventAggregator,
IHistoryService historyService, IHistoryService historyService,
IDownloadedEpisodesImportService downloadedEpisodesImportService, IDownloadedEpisodesImportService downloadedEpisodesImportService,
IParsingService parsingService, IParsingService parsingService,
ISeriesService seriesService, ISeriesService seriesService,
Logger logger) ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported)
{ {
_configService = configService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_historyService = historyService; _historyService = historyService;
_downloadedEpisodesImportService = downloadedEpisodesImportService; _downloadedEpisodesImportService = downloadedEpisodesImportService;
_parsingService = parsingService; _parsingService = parsingService;
_logger = logger;
_seriesService = seriesService; _seriesService = seriesService;
_trackedDownloadAlreadyImported = trackedDownloadAlreadyImported;
} }
public void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false) public void Check(TrackedDownload trackedDownload)
{ {
if (trackedDownload.DownloadItem.Status != DownloadItemStatus.Completed) if (trackedDownload.DownloadItem.Status != DownloadItemStatus.Completed)
{ {
return; return;
} }
if (!ignoreWarnings) // Only process tracked downloads that are still downloading
if (trackedDownload.State != TrackedDownloadState.Downloading)
{ {
return;
}
var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId); var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId);
if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace()) if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace())
@ -95,29 +94,55 @@ namespace NzbDrone.Core.Download
return; return;
} }
} }
trackedDownload.State = TrackedDownloadState.ImportPending;
} }
Import(trackedDownload); public void Import(TrackedDownload trackedDownload)
}
private void Import(TrackedDownload trackedDownload)
{ {
trackedDownload.State = TrackedDownloadState.Importing;
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath; var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem); var importResults = _downloadedEpisodesImportService.ProcessPath(outputPath, ImportMode.Auto,
trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
var allEpisodesImported = importResults.Where(c => c.Result == ImportResultType.Imported)
.SelectMany(c => c.ImportDecision.LocalEpisode.Episodes)
.Count() >= Math.Max(1,
trackedDownload.RemoteEpisode.Episodes.Count);
if (allEpisodesImported)
{
trackedDownload.State = TrackedDownloadState.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
}
// Double check if all episodes were imported by checking the history if at least one
// file was imported. This will allow the decision engine to reject already imported
// episode files and still mark the download complete when all files are imported.
if (importResults.Any(c => c.Result == ImportResultType.Imported))
{
var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId)
.OrderByDescending(h => h.Date)
.ToList();
var allEpisodesImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems);
if (allEpisodesImportedInHistory)
{
trackedDownload.State = TrackedDownloadState.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
}
}
trackedDownload.State = TrackedDownloadState.ImportPending;
if (importResults.Empty()) if (importResults.Empty())
{ {
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath); trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
return;
}
var importedEpisodes = importResults.Where(c => c.Result == ImportResultType.Imported).SelectMany(c => c.ImportDecision.LocalEpisode.Episodes).ToList();
if (importedEpisodes.Count() >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
return;
} }
if (importResults.Any(c => c.Result != ImportResultType.Imported)) if (importResults.Any(c => c.Result != ImportResultType.Imported))

View File

@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Download
{
public class DownloadProcessingService : IExecute<ProcessMonitoredDownloadsCommand>
{
private readonly IConfigService _configService;
private readonly ICompletedDownloadService _completedDownloadService;
private readonly IFailedDownloadService _failedDownloadService;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IEventAggregator _eventAggregator;
public DownloadProcessingService(IConfigService configService,
ICompletedDownloadService completedDownloadService,
IFailedDownloadService failedDownloadService,
ITrackedDownloadService trackedDownloadService,
IEventAggregator eventAggregator)
{
_configService = configService;
_completedDownloadService = completedDownloadService;
_failedDownloadService = failedDownloadService;
_trackedDownloadService = trackedDownloadService;
_eventAggregator = eventAggregator;
}
private void RemoveCompletedDownloads(List<TrackedDownload> trackedDownloads)
{
foreach (var trackedDownload in trackedDownloads.Where(c => c.DownloadItem.CanBeRemoved && c.State == TrackedDownloadState.Imported))
{
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
}
}
public void Execute(ProcessMonitoredDownloadsCommand message)
{
var enableCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling;
var trackedDownloads = _trackedDownloadService.GetTrackedDownloads();
foreach (var trackedDownload in trackedDownloads)
{
if (trackedDownload.State == TrackedDownloadState.FailedPending)
{
_failedDownloadService.ProcessFailed(trackedDownload);
}
if (enableCompletedDownloadHandling && trackedDownload.State == TrackedDownloadState.ImportPending)
{
_completedDownloadService.Import(trackedDownload);
}
}
if (enableCompletedDownloadHandling && _configService.RemoveCompletedDownloads)
{
// Remove tracked downloads that are now complete
RemoveCompletedDownloads(trackedDownloads);
}
_eventAggregator.PublishEvent(new DownloadsProcessedEvent());
}
}
}

View File

@ -0,0 +1,11 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Download
{
public class DownloadsProcessedEvent : IEvent
{
public DownloadsProcessedEvent()
{
}
}
}

View File

@ -11,7 +11,8 @@ namespace NzbDrone.Core.Download
{ {
void MarkAsFailed(int historyId); void MarkAsFailed(int historyId);
void MarkAsFailed(string downloadId); void MarkAsFailed(string downloadId);
void Process(TrackedDownload trackedDownload); void Check(TrackedDownload trackedDownload);
void ProcessFailed(TrackedDownload trackedDownload);
} }
public class FailedDownloadService : IFailedDownloadService public class FailedDownloadService : IFailedDownloadService
@ -52,22 +53,19 @@ namespace NzbDrone.Core.Download
} }
} }
public void Process(TrackedDownload trackedDownload) public void Check(TrackedDownload trackedDownload)
{ {
string failure = null; // Only process tracked downloads that are still downloading
if (trackedDownload.State != TrackedDownloadState.Downloading)
if (trackedDownload.DownloadItem.IsEncrypted)
{ {
failure = "Encrypted download detected"; return;
}
else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
{
failure = trackedDownload.DownloadItem.Message ?? "Failed download detected";
} }
if (failure != null) if (trackedDownload.DownloadItem.IsEncrypted ||
trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
{ {
var grabbedItems = _historyService.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed) var grabbedItems = _historyService
.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed)
.ToList(); .ToList();
if (grabbedItems.Empty()) if (grabbedItems.Empty())
@ -76,11 +74,41 @@ namespace NzbDrone.Core.Download
return; return;
} }
trackedDownload.State = TrackedDownloadStage.DownloadFailed; trackedDownload.State = TrackedDownloadState.FailedPending;
PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload);
} }
} }
public void ProcessFailed(TrackedDownload trackedDownload)
{
if (trackedDownload.State != TrackedDownloadState.FailedPending)
{
return;
}
var grabbedItems = _historyService
.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed)
.ToList();
if (grabbedItems.Empty())
{
return;
}
var failure = "Failed download detected";
if (trackedDownload.DownloadItem.IsEncrypted)
{
failure = "Encrypted download detected";
}
else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed && trackedDownload.DownloadItem.Message.IsNotNullOrWhiteSpace())
{
failure = trackedDownload.DownloadItem.Message;
}
trackedDownload.State = TrackedDownloadState.Failed;
PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload);
}
private void PublishDownloadFailedEvent(List<History.History> historyItems, string message, TrackedDownload trackedDownload = null) private void PublishDownloadFailedEvent(List<History.History> historyItems, string message, TrackedDownload trackedDownload = null)
{ {
var historyItem = historyItems.First(); var historyItem = historyItems.First();

View File

@ -0,0 +1,9 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Download
{
public class ProcessMonitoredDownloadsCommand : Command
{
public override bool RequiresDiskAccess => true;
}
}

View File

@ -2,13 +2,14 @@
using NLog; using NLog;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.Messaging;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Download namespace NzbDrone.Core.Download
{ {
public class RedownloadFailedDownloadService : IHandleAsync<DownloadFailedEvent> public class RedownloadFailedDownloadService : IHandle<DownloadFailedEvent>
{ {
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
@ -26,7 +27,8 @@ namespace NzbDrone.Core.Download
_logger = logger; _logger = logger;
} }
public void HandleAsync(DownloadFailedEvent message) [EventHandleOrder(EventHandleOrder.Last)]
public void Handle(DownloadFailedEvent message)
{ {
if (!_configService.AutoRedownloadFailed) if (!_configService.AutoRedownloadFailed)
{ {

View File

@ -0,0 +1,8 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Download
{
public class RefreshMonitoredDownloadsCommand : Command
{
}
}

View File

@ -11,9 +11,10 @@ using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Download.TrackedDownloads namespace NzbDrone.Core.Download.TrackedDownloads
{ {
public class DownloadMonitoringService : IExecute<CheckForFinishedDownloadCommand>, public class DownloadMonitoringService : IExecute<RefreshMonitoredDownloadsCommand>,
IHandle<EpisodeGrabbedEvent>, IHandle<EpisodeGrabbedEvent>,
IHandle<EpisodeImportedEvent>, IHandle<EpisodeImportedEvent>,
IHandle<DownloadsProcessedEvent>,
IHandle<TrackedDownloadsRemovedEvent> IHandle<TrackedDownloadsRemovedEvent>
{ {
private readonly IDownloadClientStatusService _downloadClientStatusService; private readonly IDownloadClientStatusService _downloadClientStatusService;
@ -52,7 +53,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
private void QueueRefresh() private void QueueRefresh()
{ {
_manageCommandQueue.Push(new CheckForFinishedDownloadCommand()); _manageCommandQueue.Push(new RefreshMonitoredDownloadsCommand());
} }
private void Refresh() private void Refresh()
@ -73,6 +74,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
_trackedDownloadService.UpdateTrackable(trackedDownloads); _trackedDownloadService.UpdateTrackable(trackedDownloads);
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads));
_manageCommandQueue.Push(new ProcessMonitoredDownloadsCommand());
} }
finally finally
{ {
@ -82,12 +84,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads
private List<TrackedDownload> ProcessClientDownloads(IDownloadClient downloadClient) private List<TrackedDownload> ProcessClientDownloads(IDownloadClient downloadClient)
{ {
List<DownloadClientItem> downloadClientHistory = new List<DownloadClientItem>(); var downloadClientItems = new List<DownloadClientItem>();
var trackedDownloads = new List<TrackedDownload>(); var trackedDownloads = new List<TrackedDownload>();
try try
{ {
downloadClientHistory = downloadClient.GetItems().ToList(); downloadClientItems = downloadClient.GetItems().ToList();
_downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id); _downloadClientStatusService.RecordSuccess(downloadClient.Definition.Id);
} }
@ -99,59 +101,40 @@ namespace NzbDrone.Core.Download.TrackedDownloads
_logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name); _logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name);
} }
foreach (var downloadItem in downloadClientHistory) foreach (var downloadItem in downloadClientItems)
{ {
var newItems = ProcessClientItems(downloadClient, downloadItem); var item = ProcessClientItem(downloadClient, downloadItem);
trackedDownloads.AddRange(newItems); trackedDownloads.AddIfNotNull(item);
}
if (_configService.EnableCompletedDownloadHandling && _configService.RemoveCompletedDownloads)
{
RemoveCompletedDownloads(trackedDownloads);
} }
return trackedDownloads; return trackedDownloads;
} }
private void RemoveCompletedDownloads(List<TrackedDownload> trackedDownloads) private TrackedDownload ProcessClientItem(IDownloadClient downloadClient, DownloadClientItem downloadItem)
{ {
foreach (var trackedDownload in trackedDownloads.Where(c => c.DownloadItem.CanBeRemoved && c.State == TrackedDownloadStage.Imported))
{
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
}
}
private List<TrackedDownload> ProcessClientItems(IDownloadClient downloadClient, DownloadClientItem downloadItem)
{
var trackedDownloads = new List<TrackedDownload>();
try try
{ {
var trackedDownload = _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, downloadItem); var trackedDownload = _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, downloadItem);
if (trackedDownload != null && trackedDownload.State == TrackedDownloadStage.Downloading) if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading)
{ {
_failedDownloadService.Process(trackedDownload); _failedDownloadService.Check(trackedDownload);
_completedDownloadService.Check(trackedDownload);
if (_configService.EnableCompletedDownloadHandling)
{
_completedDownloadService.Process(trackedDownload);
}
} }
trackedDownloads.AddIfNotNull(trackedDownload); return trackedDownload;
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Error(e, "Couldn't process tracked download {0}", downloadItem.Title); _logger.Error(e, "Couldn't process tracked download {0}", downloadItem.Title);
} }
return trackedDownloads; return null;
} }
private bool DownloadIsTrackable(TrackedDownload trackedDownload) private bool DownloadIsTrackable(TrackedDownload trackedDownload)
{ {
// If the download has already been imported or failed don't track it // If the download has already been imported or failed don't track it
if (trackedDownload.State != TrackedDownloadStage.Downloading) if (trackedDownload.State == TrackedDownloadState.Imported || trackedDownload.State == TrackedDownloadState.Failed)
{ {
return false; return false;
} }
@ -165,8 +148,14 @@ namespace NzbDrone.Core.Download.TrackedDownloads
return true; return true;
} }
public void Execute(RefreshMonitoredDownloadsCommand message)
{
Refresh();
}
public void Execute(CheckForFinishedDownloadCommand message) public void Execute(CheckForFinishedDownloadCommand message)
{ {
_logger.Warn("A third party app used the deprecated CheckForFinishedDownload command, it should be updated RefreshMonitoredDownloads instead");
Refresh(); Refresh();
} }
@ -180,6 +169,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
_refreshDebounce.Execute(); _refreshDebounce.Execute();
} }
public void Handle(DownloadsProcessedEvent message)
{
var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList();
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads));
}
public void Handle(TrackedDownloadsRemovedEvent message) public void Handle(TrackedDownloadsRemovedEvent message)
{ {
var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList(); var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList();

View File

@ -7,7 +7,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
{ {
public int DownloadClient { get; set; } public int DownloadClient { get; set; }
public DownloadClientItem DownloadItem { get; set; } public DownloadClientItem DownloadItem { get; set; }
public TrackedDownloadStage State { get; set; } public TrackedDownloadState State { get; set; }
public TrackedDownloadStatus Status { get; private set; } public TrackedDownloadStatus Status { get; private set; }
public RemoteEpisode RemoteEpisode { get; set; } public RemoteEpisode RemoteEpisode { get; set; }
public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } public TrackedDownloadStatusMessage[] StatusMessages { get; private set; }
@ -33,16 +33,20 @@ namespace NzbDrone.Core.Download.TrackedDownloads
} }
} }
public enum TrackedDownloadStage public enum TrackedDownloadState
{ {
Downloading, Downloading,
ImportPending,
Importing,
Imported, Imported,
DownloadFailed FailedPending,
Failed
} }
public enum TrackedDownloadStatus public enum TrackedDownloadStatus
{ {
Ok, Ok,
Warning Warning,
Error
} }
} }

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.History;
namespace NzbDrone.Core.Download.TrackedDownloads
{
public interface ITrackedDownloadAlreadyImported
{
bool IsImported(TrackedDownload trackedDownload, List<History.History> historyItems);
}
public class TrackedDownloadAlreadyImported : ITrackedDownloadAlreadyImported
{
public bool IsImported(TrackedDownload trackedDownload, List<History.History> historyItems)
{
if (historyItems.Empty())
{
return false;
}
var allEpisodesImportedInHistory = trackedDownload.RemoteEpisode.Episodes.All(e =>
{
var lastHistoryItem = historyItems.FirstOrDefault(h => h.EpisodeId == e.Id);
if (lastHistoryItem == null)
{
return false;
}
return lastHistoryItem.EventType == HistoryEventType.DownloadFolderImported;
});
return allEpisodesImportedInHistory;
}
}
}

View File

@ -25,6 +25,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly IHistoryService _historyService; private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported;
private readonly Logger _logger; private readonly Logger _logger;
private readonly ICached<TrackedDownload> _cache; private readonly ICached<TrackedDownload> _cache;
@ -32,11 +33,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
ICacheManager cacheManager, ICacheManager cacheManager,
IHistoryService historyService, IHistoryService historyService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported,
Logger logger) Logger logger)
{ {
_parsingService = parsingService; _parsingService = parsingService;
_historyService = historyService; _historyService = historyService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_trackedDownloadAlreadyImported = trackedDownloadAlreadyImported;
_cache = cacheManager.GetCache<TrackedDownload>(GetType()); _cache = cacheManager.GetCache<TrackedDownload>(GetType());
_logger = logger; _logger = logger;
} }
@ -72,7 +75,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
{ {
var existingItem = Find(downloadItem.DownloadId); var existingItem = Find(downloadItem.DownloadId);
if (existingItem != null && existingItem.State != TrackedDownloadStage.Downloading) if (existingItem != null && existingItem.State != TrackedDownloadState.Downloading)
{ {
LogItemChange(existingItem, existingItem.DownloadItem, downloadItem); LogItemChange(existingItem, existingItem.DownloadItem, downloadItem);
@ -93,7 +96,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads
try try
{ {
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title); var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId); var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId)
.OrderByDescending(h => h.Date)
.ToList();
if (parsedEpisodeInfo != null) if (parsedEpisodeInfo != null)
{ {
@ -102,8 +107,22 @@ namespace NzbDrone.Core.Download.TrackedDownloads
if (historyItems.Any()) if (historyItems.Any())
{ {
var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First(); var firstHistoryItem = historyItems.First();
trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType); var state = GetStateFromHistory(firstHistoryItem.EventType);
// One potential issue here is if the latest is imported, but other episodes are ignored or never imported.
// It's unlikely that will happen, but could happen if additional episodes are added to season after it's already imported.
if (state == TrackedDownloadState.Imported)
{
var allImported = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems);
trackedDownload.State = allImported ? TrackedDownloadState.Imported : TrackedDownloadState.Downloading;
}
else
{
trackedDownload.State = state;
}
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == HistoryEventType.Grabbed); var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == HistoryEventType.Grabbed);
trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; trackedDownload.Indexer = grabbedEvent?.Data["indexer"];
@ -174,16 +193,16 @@ namespace NzbDrone.Core.Download.TrackedDownloads
} }
} }
private static TrackedDownloadStage GetStateFromHistory(HistoryEventType eventType) private static TrackedDownloadState GetStateFromHistory(HistoryEventType eventType)
{ {
switch (eventType) switch (eventType)
{ {
case HistoryEventType.DownloadFolderImported: case HistoryEventType.DownloadFolderImported:
return TrackedDownloadStage.Imported; return TrackedDownloadState.Imported;
case HistoryEventType.DownloadFailed: case HistoryEventType.DownloadFailed:
return TrackedDownloadStage.DownloadFailed; return TrackedDownloadState.Failed;
default: default:
return TrackedDownloadStage.Downloading; return TrackedDownloadState.Downloading;
} }
} }
} }

View File

@ -61,7 +61,7 @@ namespace NzbDrone.Core.Jobs
{ {
var defaultTasks = new[] var defaultTasks = new[]
{ {
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(RefreshMonitoredDownloadsCommand).FullName},
new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName},
new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName},
new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName},

View File

@ -348,7 +348,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual
if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count)) if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, trackedDownload.RemoteEpisode.Episodes.Count))
{ {
trackedDownload.State = TrackedDownloadStage.Imported; trackedDownload.State = TrackedDownloadState.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload)); _eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
} }
} }

View File

@ -0,0 +1,61 @@
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
{
public class AlreadyImportedSpecification : IImportDecisionEngineSpecification
{
private readonly IHistoryService _historyService;
private readonly Logger _logger;
public AlreadyImportedSpecification(IHistoryService historyService,
Logger logger)
{
_historyService = historyService;
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Database;
public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem)
{
if (downloadClientItem == null)
{
_logger.Debug("No download client information is available, skipping");
return Decision.Accept();
}
foreach (var episode in localEpisode.Episodes)
{
if (!episode.HasFile)
{
_logger.Debug("Skipping already imported check for episode without file");
continue;
}
var episodeHistory = _historyService.FindByEpisodeId(episode.Id);
var lastImported = episodeHistory.FirstOrDefault(h => h.EventType == HistoryEventType.DownloadFolderImported);
if (lastImported == null)
{
continue;
}
// TODO: Ignore last imported check if the same release was grabbed again
// See: https://github.com/Sonarr/Sonarr/issues/2393
if (lastImported.DownloadId == downloadClientItem.DownloadId)
{
_logger.Debug("Episode file previously imported at {0}", lastImported.Date);
return Decision.Reject("Episode file already imported at {0}", lastImported.Date);
}
}
return Decision.Accept();
}
}
}

View File

@ -19,16 +19,12 @@ namespace NzbDrone.Core.Messaging.Events
private class EventSubscribers<TEvent> where TEvent : class, IEvent private class EventSubscribers<TEvent> where TEvent : class, IEvent
{ {
private IServiceFactory _serviceFactory;
public IHandle<TEvent>[] _syncHandlers; public IHandle<TEvent>[] _syncHandlers;
public IHandleAsync<TEvent>[] _asyncHandlers; public IHandleAsync<TEvent>[] _asyncHandlers;
public IHandleAsync<IEvent>[] _globalHandlers; public IHandleAsync<IEvent>[] _globalHandlers;
public EventSubscribers(IServiceFactory serviceFactory) public EventSubscribers(IServiceFactory serviceFactory)
{ {
_serviceFactory = serviceFactory;
_syncHandlers = serviceFactory.BuildAll<IHandle<TEvent>>() _syncHandlers = serviceFactory.BuildAll<IHandle<TEvent>>()
.OrderBy(GetEventHandleOrder) .OrderBy(GetEventHandleOrder)
.ToArray(); .ToArray();
@ -139,8 +135,7 @@ namespace NzbDrone.Core.Messaging.Events
internal static int GetEventHandleOrder<TEvent>(IHandle<TEvent> eventHandler) where TEvent : class, IEvent internal static int GetEventHandleOrder<TEvent>(IHandle<TEvent> eventHandler) where TEvent : class, IEvent
{ {
// TODO: Convert "Handle" to nameof(eventHandler.Handle) after .net 4.5 var method = eventHandler.GetType().GetMethod(nameof(eventHandler.Handle), new Type[] {typeof(TEvent)});
var method = eventHandler.GetType().GetMethod("Handle", new Type[] {typeof(TEvent)});
if (method == null) if (method == null)
{ {

View File

@ -22,7 +22,8 @@ namespace NzbDrone.Core.Queue
public TimeSpan? Timeleft { get; set; } public TimeSpan? Timeleft { get; set; }
public DateTime? EstimatedCompletionTime { get; set; } public DateTime? EstimatedCompletionTime { get; set; }
public string Status { get; set; } public string Status { get; set; }
public string TrackedDownloadStatus { get; set; } public TrackedDownloadStatus? TrackedDownloadStatus { get; set; }
public TrackedDownloadState? TrackedDownloadState { get; set; }
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; } public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
public RemoteEpisode RemoteEpisode { get; set; } public RemoteEpisode RemoteEpisode { get; set; }

View File

@ -70,7 +70,8 @@ namespace NzbDrone.Core.Queue
Sizeleft = trackedDownload.DownloadItem.RemainingSize, Sizeleft = trackedDownload.DownloadItem.RemainingSize,
Timeleft = trackedDownload.DownloadItem.RemainingTime, Timeleft = trackedDownload.DownloadItem.RemainingTime,
Status = trackedDownload.DownloadItem.Status.ToString(), Status = trackedDownload.DownloadItem.Status.ToString(),
TrackedDownloadStatus = trackedDownload.Status.ToString(), TrackedDownloadStatus = trackedDownload.Status,
TrackedDownloadState = trackedDownload.State,
StatusMessages = trackedDownload.StatusMessages.ToList(), StatusMessages = trackedDownload.StatusMessages.ToList(),
ErrorMessage = trackedDownload.DownloadItem.Message, ErrorMessage = trackedDownload.DownloadItem.Message,
RemoteEpisode = trackedDownload.RemoteEpisode, RemoteEpisode = trackedDownload.RemoteEpisode,

View File

@ -86,7 +86,7 @@ namespace NzbDrone.App.Test
public void should_return_same_instance_of_singletons_by_different_interfaces() public void should_return_same_instance_of_singletons_by_different_interfaces()
{ {
var first = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single(); var first = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
var second = (DownloadMonitoringService)_container.Resolve<IExecute<CheckForFinishedDownloadCommand>>(); var second = (DownloadMonitoringService)_container.Resolve<IExecute<RefreshMonitoredDownloadsCommand>>();
first.Should().BeSameAs(second); first.Should().BeSameAs(second);
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages; using NzbDrone.Core.Languages;
@ -25,7 +26,8 @@ namespace Sonarr.Api.V3.Queue
public TimeSpan? Timeleft { get; set; } public TimeSpan? Timeleft { get; set; }
public DateTime? EstimatedCompletionTime { get; set; } public DateTime? EstimatedCompletionTime { get; set; }
public string Status { get; set; } public string Status { get; set; }
public string TrackedDownloadStatus { get; set; } public TrackedDownloadStatus? TrackedDownloadStatus { get; set; }
public TrackedDownloadState? TrackedDownloadState { get; set; }
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; } public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
public string ErrorMessage { get; set; } public string ErrorMessage { get; set; }
public string DownloadId { get; set; } public string DownloadId { get; set; }
@ -55,8 +57,9 @@ namespace Sonarr.Api.V3.Queue
Sizeleft = model.Sizeleft, Sizeleft = model.Sizeleft,
Timeleft = model.Timeleft, Timeleft = model.Timeleft,
EstimatedCompletionTime = model.EstimatedCompletionTime, EstimatedCompletionTime = model.EstimatedCompletionTime,
Status = model.Status, Status = model.Status.FirstCharToLower(),
TrackedDownloadStatus = model.TrackedDownloadStatus, TrackedDownloadStatus = model.TrackedDownloadStatus,
TrackedDownloadState = model.TrackedDownloadState,
StatusMessages = model.StatusMessages, StatusMessages = model.StatusMessages,
ErrorMessage = model.ErrorMessage, ErrorMessage = model.ErrorMessage,
DownloadId = model.DownloadId, DownloadId = model.DownloadId,

View File

@ -3,6 +3,7 @@ using System.Linq;
using NzbDrone.Common.TPL; using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Queue; using NzbDrone.Core.Queue;
using NzbDrone.SignalR; using NzbDrone.SignalR;
@ -47,10 +48,10 @@ namespace Sonarr.Api.V3.Queue
TotalCount = queue.Count + pending.Count, TotalCount = queue.Count + pending.Count,
Count = queue.Count(q => q.Series != null) + pending.Count, Count = queue.Count(q => q.Series != null) + pending.Count,
UnknownCount = queue.Count(q => q.Series == null), UnknownCount = queue.Count(q => q.Series == null),
Errors = queue.Any(q => q.Series != null && q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), Errors = queue.Any(q => q.Series != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error),
Warnings = queue.Any(q => q.Series != null && q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)), Warnings = queue.Any(q => q.Series != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning),
UnknownErrors = queue.Any(q => q.Series == null && q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), UnknownErrors = queue.Any(q => q.Series == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error),
UnknownWarnings = queue.Any(q => q.Series == null && q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)) UnknownWarnings = queue.Any(q => q.Series == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning)
}; };
_broadcastDebounce.Resume(); _broadcastDebounce.Resume();