Merge pull request #153 from Sonarr/queue-actions

Queue actions
pull/4/head
Mark McDowall 10 years ago
commit 452649ed1d

@ -101,6 +101,7 @@
<Compile Include="Profiles\Delay\DelayProfileModule.cs" />
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
<Compile Include="Profiles\Delay\DelayProfileValidator.cs" />
<Compile Include="Queue\QueueActionModule.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingModule.cs" />
<Compile Include="RemotePathMappings\RemotePathMappingResource.cs" />
<Compile Include="Config\UiConfigModule.cs" />
@ -202,6 +203,7 @@
<Compile Include="ResourceChangeMessage.cs" />
<Compile Include="Restrictions\RestrictionModule.cs" />
<Compile Include="Restrictions\RestrictionResource.cs" />
<Compile Include="REST\NotFoundException.cs" />
<Compile Include="REST\BadRequestException.cs" />
<Compile Include="REST\MethodNotAllowedException.cs" />
<Compile Include="REST\ResourceValidator.cs" />

@ -0,0 +1,113 @@
using System.Linq;
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.Queue;
namespace NzbDrone.Api.Queue
{
public class QueueActionModule : NzbDroneRestModule<QueueResource>
{
private readonly IQueueService _queueService;
private readonly IDownloadTrackingService _downloadTrackingService;
private readonly ICompletedDownloadService _completedDownloadService;
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IPendingReleaseService _pendingReleaseService;
private readonly IDownloadService _downloadService;
public QueueActionModule(IQueueService queueService,
IDownloadTrackingService downloadTrackingService,
ICompletedDownloadService completedDownloadService,
IProvideDownloadClient downloadClientProvider,
IPendingReleaseService pendingReleaseService,
IDownloadService downloadService)
{
_queueService = queueService;
_downloadTrackingService = downloadTrackingService;
_completedDownloadService = completedDownloadService;
_downloadClientProvider = downloadClientProvider;
_pendingReleaseService = pendingReleaseService;
_downloadService = downloadService;
Delete[@"/(?<id>[\d]{1,10})"] = x => Remove((int)x.Id);
Post["/import"] = x => Import();
Post["/grab"] = x => Grab();
}
private Response Remove(int id)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
if (pendingRelease != null)
{
_pendingReleaseService.RemovePendingQueueItem(id);
}
var trackedDownload = GetTrackedDownload(id);
if (trackedDownload == null)
{
throw new NotFoundException();
}
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
if (downloadClient == null)
{
throw new BadRequestException();
}
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId);
return new object().AsResponse();
}
private JsonResponse<QueueResource> Import()
{
var resource = Request.Body.FromJson<QueueResource>();
var trackedDownload = GetTrackedDownload(resource.Id);
_completedDownloadService.Import(trackedDownload);
return resource.AsResponse();
}
private JsonResponse<QueueResource> Grab()
{
var resource = Request.Body.FromJson<QueueResource>();
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(resource.Id);
if (pendingRelease == null)
{
throw new NotFoundException();
}
_downloadService.DownloadReport(pendingRelease.RemoteEpisode);
return resource.AsResponse();
}
private TrackedDownload GetTrackedDownload(int queueId)
{
var queueItem = _queueService.Find(queueId);
if (queueItem == null)
{
throw new NotFoundException();
}
var trackedDownload = _downloadTrackingService.Find(queueItem.TrackingId);
if (trackedDownload == null)
{
throw new NotFoundException();
}
return trackedDownload;
}
}
}

@ -0,0 +1,13 @@
using Nancy;
using NzbDrone.Api.ErrorManagement;
namespace NzbDrone.Api.REST
{
public class NotFoundException : ApiException
{
public NotFoundException(object content = null)
: base(HttpStatusCode.NotFound, content)
{
}
}
}

@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.Download
.Build()
.ToList();
var remoteEpisode = new RemoteEpisode
var remoteEpisode = new RemoteEpisode
{
Series = new Series(),
Episodes = new List<Episode> {new Episode {Id = 1}}
@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.Download
private void GivenCompletedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.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" }))
@ -125,7 +125,7 @@ namespace NzbDrone.Core.Test.Download
private void GivenFailedImport()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.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"))
@ -135,13 +135,13 @@ namespace NzbDrone.Core.Test.Download
private void VerifyNoImports()
{
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Verify(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()), Times.Never());
.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<DownloadClientItem>()), Times.Once());
.Verify(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()), Times.Once());
}
[Test]
@ -473,7 +473,7 @@ namespace NzbDrone.Core.Test.Download
GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
@ -505,7 +505,7 @@ namespace NzbDrone.Core.Test.Download
GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<Series>(), It.IsAny<DownloadClientItem>()))
.Returns(new List<ImportResult>
{
new ImportResult(
@ -537,7 +537,7 @@ namespace NzbDrone.Core.Test.Download
GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>()
.Setup(v => v.ProcessFolder(It.IsAny<DirectoryInfo>(), It.IsAny<DownloadClientItem>()))
.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"})),

@ -5,7 +5,9 @@ using Marr.Data;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
@ -102,7 +104,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
GivenHeldRelease(_parsedEpisodeInfo.Quality);
Subject.RemoveGrabbed(new List<DownloadDecision> { _temporarilyRejected });
Subject.Handle(new EpisodeGrabbedEvent(_remoteEpisode));
VerifyDelete();
}
@ -112,7 +114,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
GivenHeldRelease(new QualityModel(Quality.SDTV));
Subject.RemoveGrabbed(new List<DownloadDecision> { _temporarilyRejected });
Subject.Handle(new EpisodeGrabbedEvent(_remoteEpisode));
VerifyDelete();
}
@ -122,7 +124,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
GivenHeldRelease(new QualityModel(Quality.Bluray720p));
Subject.RemoveGrabbed(new List<DownloadDecision> { _temporarilyRejected });
Subject.Handle(new EpisodeGrabbedEvent(_remoteEpisode));
VerifyNoDelete();
}

@ -5,7 +5,9 @@ using Marr.Data;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
@ -104,7 +106,9 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate);
Subject.RemoveRejected(new List<DownloadDecision> { _temporarilyRejected });
Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List<DownloadDecision>(),
new List<DownloadDecision>(),
new List<DownloadDecision> { _temporarilyRejected })));
VerifyDelete();
}
@ -114,7 +118,9 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
GivenHeldRelease(_release.Title + "-RP", _release.Indexer, _release.PublishDate);
Subject.RemoveRejected(new List<DownloadDecision> { _temporarilyRejected });
Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List<DownloadDecision>(),
new List<DownloadDecision>(),
new List<DownloadDecision> { _temporarilyRejected })));
VerifyNoDelete();
}
@ -124,7 +130,9 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
GivenHeldRelease(_release.Title, "AnotherIndexer", _release.PublishDate);
Subject.RemoveRejected(new List<DownloadDecision> { _temporarilyRejected });
Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List<DownloadDecision>(),
new List<DownloadDecision>(),
new List<DownloadDecision> { _temporarilyRejected })));
VerifyNoDelete();
}
@ -134,7 +142,9 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
GivenHeldRelease(_release.Title, _release.Indexer, _release.PublishDate.AddHours(1));
Subject.RemoveRejected(new List<DownloadDecision> { _temporarilyRejected });
Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List<DownloadDecision>(),
new List<DownloadDecision>(),
new List<DownloadDecision> { _temporarilyRejected })));
VerifyNoDelete();
}

@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Mocker.GetMock<IProcessDownloadDecisions>()
.Setup(s => s.ProcessDecisions(It.IsAny<List<DownloadDecision>>()))
.Returns(new ProcessedDecisions(new List<DownloadDecision>(), new List<DownloadDecision>()));
.Returns(new ProcessedDecisions(new List<DownloadDecision>(), new List<DownloadDecision>(), new List<DownloadDecision>()));
Subject.Handle(new EpisodeInfoRefreshedEvent(_series, _added, _updated));

@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Mocker.GetMock<IProcessDownloadDecisions>()
.Setup(s => s.ProcessDecisions(It.IsAny<List<DownloadDecision>>()))
.Returns(new ProcessedDecisions(new List<DownloadDecision>(), new List<DownloadDecision>()));
.Returns(new ProcessedDecisions(new List<DownloadDecision>(), new List<DownloadDecision>(), new List<DownloadDecision>()));
}
[Test]

@ -87,6 +87,8 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_not_import_if_folder_is_a_series_path()
{
GivenValidSeries();
Mocker.GetMock<ISeriesService>()
.Setup(s => s.SeriesPathExists(It.IsAny<String>()))
.Returns(true);
@ -97,8 +99,8 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
Mocker.GetMock<IParsingService>()
.Verify(v => v.GetSeries(It.IsAny<String>()), Times.Never());
Mocker.GetMock<IDiskScanService>()
.Verify(v => v.GetVideoFiles(It.IsAny<String>(), true), Times.Never());
ExceptionVerification.ExpectedWarns(1);
}

@ -118,13 +118,14 @@ namespace NzbDrone.Core.Download
if (_diskProvider.FolderExists(outputPath))
{
importResults = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(outputPath), trackedDownload.DownloadItem);
importResults = _downloadedEpisodesImportService.ProcessFolder(new DirectoryInfo(outputPath), trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
ProcessImportResults(trackedDownload, outputPath, importResults);
}
else if (_diskProvider.FileExists(outputPath))
{
importResults = _downloadedEpisodesImportService.ProcessFile(new FileInfo(outputPath), trackedDownload.DownloadItem);
importResults = _downloadedEpisodesImportService.ProcessFile(new FileInfo(outputPath), trackedDownload.RemoteEpisode.Series, trackedDownload.DownloadItem);
ProcessImportResults(trackedDownload, outputPath, importResults);
}

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Download
{
IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol);
IEnumerable<IDownloadClient> GetDownloadClients();
IDownloadClient Get(int id);
}
public class DownloadClientProvider : IProvideDownloadClient
@ -28,5 +29,10 @@ namespace NzbDrone.Core.Download
{
return _downloadClientFactory.GetAvailableProviders();
}
public IDownloadClient Get(int id)
{
return _downloadClientFactory.GetAvailableProviders().Single(d => d.Definition.Id == id);
}
}
}

@ -19,7 +19,7 @@ namespace NzbDrone.Core.Download
{
TrackedDownload[] GetCompletedDownloads();
TrackedDownload[] GetQueuedDownloads();
TrackedDownload Find(string trackingId);
void MarkAsFailed(Int32 historyId);
}
@ -88,6 +88,11 @@ namespace NzbDrone.Core.Download
}, 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);

@ -4,6 +4,7 @@ using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
@ -17,15 +18,19 @@ namespace NzbDrone.Core.Download.Pending
public interface IPendingReleaseService
{
void Add(DownloadDecision decision);
void RemoveGrabbed(List<DownloadDecision> grabbed);
void RemoveRejected(List<DownloadDecision> rejected);
List<ReleaseInfo> GetPending();
List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId);
List<Queue.Queue> GetPendingQueue();
Queue.Queue FindPendingQueueItem(int queueId);
void RemovePendingQueueItem(int queueId);
RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable<int> episodeIds);
}
public class PendingReleaseService : IPendingReleaseService, IHandle<SeriesDeletedEvent>
public class PendingReleaseService : IPendingReleaseService,
IHandle<SeriesDeletedEvent>,
IHandle<EpisodeGrabbedEvent>,
IHandle<RssSyncCompleteEvent>
{
private readonly IPendingReleaseRepository _repository;
private readonly ISeriesService _seriesService;
@ -49,6 +54,7 @@ namespace NzbDrone.Core.Download.Pending
_logger = logger;
}
public void Add(DownloadDecision decision)
{
var alreadyPending = GetPendingReleases();
@ -69,61 +75,6 @@ namespace NzbDrone.Core.Download.Pending
Insert(decision);
}
public void RemoveGrabbed(List<DownloadDecision> grabbed)
{
_logger.Debug("Removing grabbed releases from pending");
var alreadyPending = GetPendingReleases();
foreach (var decision in grabbed)
{
var decisionLocal = decision;
var episodeIds = decisionLocal.RemoteEpisode.Episodes.Select(e => e.Id);
var existingReports = alreadyPending.Where(r => r.RemoteEpisode.Episodes.Select(e => e.Id)
.Intersect(episodeIds)
.Any())
.ToList();
if (existingReports.Empty())
{
continue;
}
var profile = decisionLocal.RemoteEpisode.Series.Profile.Value;
foreach (var existingReport in existingReports)
{
var compare = new QualityModelComparer(profile).Compare(decision.RemoteEpisode.ParsedEpisodeInfo.Quality,
existingReport.RemoteEpisode.ParsedEpisodeInfo.Quality);
//Only remove lower/equal quality pending releases
//It is safer to retry these releases on the next round than remove it and try to re-add it (if its still in the feed)
if (compare >= 0)
{
_logger.Debug("Removing previously pending release, as it was grabbed.");
Delete(existingReport);
}
}
}
}
public void RemoveRejected(List<DownloadDecision> rejected)
{
_logger.Debug("Removing failed releases from pending");
var pending = GetPendingReleases();
foreach (var rejectedRelease in rejected)
{
var matching = pending.SingleOrDefault(MatchingReleasePredicate(rejectedRelease));
if (matching != null)
{
_logger.Debug("Removing previously pending release, as it has now been rejected.");
Delete(matching);
}
}
}
public List<ReleaseInfo> GetPending()
{
return _repository.All().Select(p => p.Release).ToList();
@ -165,6 +116,18 @@ namespace NzbDrone.Core.Download.Pending
return queued;
}
public Queue.Queue FindPendingQueueItem(int queueId)
{
return GetPendingQueue().SingleOrDefault(p => p.Id == queueId);
}
public void RemovePendingQueueItem(int queueId)
{
var id = FindPendingReleaseId(queueId);
_repository.Delete(id);
}
public RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable<int> episodeIds)
{
return GetPendingRemoteEpisodes(seriesId)
@ -243,9 +206,73 @@ namespace NzbDrone.Core.Download.Pending
return delayProfile.GetProtocolDelay(remoteEpisode.Release.DownloadProtocol);
}
private void RemoveGrabbed(RemoteEpisode remoteEpisode)
{
var pendingReleases = GetPendingReleases();
var episodeIds = remoteEpisode.Episodes.Select(e => e.Id);
var existingReports = pendingReleases.Where(r => r.RemoteEpisode.Episodes.Select(e => e.Id)
.Intersect(episodeIds)
.Any())
.ToList();
if (existingReports.Empty())
{
return;
}
var profile = remoteEpisode.Series.Profile.Value;
foreach (var existingReport in existingReports)
{
var compare = new QualityModelComparer(profile).Compare(remoteEpisode.ParsedEpisodeInfo.Quality,
existingReport.RemoteEpisode.ParsedEpisodeInfo.Quality);
//Only remove lower/equal quality pending releases
//It is safer to retry these releases on the next round than remove it and try to re-add it (if its still in the feed)
if (compare >= 0)
{
_logger.Debug("Removing previously pending release, as it was grabbed.");
Delete(existingReport);
}
}
}
private void RemoveRejected(List<DownloadDecision> rejected)
{
_logger.Debug("Removing failed releases from pending");
var pending = GetPendingReleases();
foreach (var rejectedRelease in rejected)
{
var matching = pending.SingleOrDefault(MatchingReleasePredicate(rejectedRelease));
if (matching != null)
{
_logger.Debug("Removing previously pending release, as it has now been rejected.");
Delete(matching);
}
}
}
private int FindPendingReleaseId(int queueId)
{
return GetPendingReleases().First(p => p.RemoteEpisode.Episodes.Any(e => queueId == (e.Id ^ (p.Id << 16)))).Id;
}
public void Handle(SeriesDeletedEvent message)
{
_repository.DeleteBySeriesId(message.Series.Id);
}
public void Handle(EpisodeGrabbedEvent message)
{
RemoveGrabbed(message.Episode);
}
public void Handle(RssSyncCompleteEvent message)
{
RemoveRejected(message.ProcessedDecisions.Rejected);
}
}
}

@ -82,7 +82,7 @@ namespace NzbDrone.Core.Download
}
}
return new ProcessedDecisions(grabbed, pending);
return new ProcessedDecisions(grabbed, pending, decisions.Where(d => d.Rejected).ToList());
}
internal List<DownloadDecision> GetQualifiedReports(IEnumerable<DownloadDecision> decisions)

@ -7,11 +7,13 @@ namespace NzbDrone.Core.Download
{
public List<DownloadDecision> Grabbed { get; set; }
public List<DownloadDecision> Pending { get; set; }
public List<DownloadDecision> Rejected { get; set; }
public ProcessedDecisions(List<DownloadDecision> grabbed, List<DownloadDecision> pending)
public ProcessedDecisions(List<DownloadDecision> grabbed, List<DownloadDecision> pending, List<DownloadDecision> rejected)
{
Grabbed = grabbed;
Pending = pending;
Rejected = rejected;
}
}
}

@ -8,6 +8,10 @@ namespace NzbDrone.Core.Download
public String Title { get; set; }
public List<String> Messages { get; set; }
private TrackedDownloadStatusMessage()
{
}
public TrackedDownloadStatusMessage(String title, List<String> messages)
{
Title = title;

@ -1,8 +1,15 @@
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Download;
namespace NzbDrone.Core.Indexers
{
public class RssSyncCompleteEvent : IEvent
{
public ProcessedDecisions ProcessedDecisions { get; private set; }
public RssSyncCompleteEvent(ProcessedDecisions processedDecisions)
{
ProcessedDecisions = processedDecisions;
}
}
}

@ -40,15 +40,13 @@ namespace NzbDrone.Core.Indexers
}
private List<DownloadDecision> Sync()
private ProcessedDecisions Sync()
{
_logger.ProgressInfo("Starting RSS Sync");
var reports = _rssFetcherAndParser.Fetch().Concat(_pendingReleaseService.GetPending()).ToList();
var decisions = _downloadDecisionMaker.GetRssDecision(reports);
var processed = _processDownloadDecisions.ProcessDecisions(decisions);
_pendingReleaseService.RemoveGrabbed(processed.Grabbed);
_pendingReleaseService.RemoveRejected(decisions.Where(d => d.Rejected).ToList());
var message = String.Format("RSS Sync Completed. Reports found: {0}, Reports grabbed: {1}", reports.Count, processed.Grabbed.Count);
@ -59,20 +57,21 @@ namespace NzbDrone.Core.Indexers
_logger.ProgressInfo(message);
return processed.Grabbed.Concat(processed.Pending).ToList();
return processed;
}
public void Execute(RssSyncCommand message)
{
var processed = Sync();
var grabbedOrPending = processed.Grabbed.Concat(processed.Pending).ToList();
if (message.LastExecutionTime.HasValue && DateTime.UtcNow.Subtract(message.LastExecutionTime.Value).TotalHours > 3)
{
_logger.Info("RSS Sync hasn't run since: {0}. Searching for any missing episodes since then.", message.LastExecutionTime.Value);
_episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1), processed.SelectMany(d => d.RemoteEpisode.Episodes).Select(e => e.Id));
_episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1), grabbedOrPending.SelectMany(d => d.RemoteEpisode.Episodes).Select(e => e.Id));
}
_eventAggregator.PublishEvent(new RssSyncCompleteEvent());
_eventAggregator.PublishEvent(new RssSyncCompleteEvent(processed));
}
}
}

@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv;
@ -17,7 +16,9 @@ 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);
}
public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService
@ -26,7 +27,6 @@ namespace NzbDrone.Core.MediaFiles
private readonly IDiskScanService _diskScanService;
private readonly ISeriesService _seriesService;
private readonly IParsingService _parsingService;
private readonly IConfigService _configService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly ISampleService _sampleService;
@ -36,7 +36,6 @@ namespace NzbDrone.Core.MediaFiles
IDiskScanService diskScanService,
ISeriesService seriesService,
IParsingService parsingService,
IConfigService configService,
IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes,
ISampleService sampleService,
@ -46,7 +45,6 @@ namespace NzbDrone.Core.MediaFiles
_diskScanService = diskScanService;
_seriesService = seriesService;
_parsingService = parsingService;
_configService = configService;
_importDecisionMaker = importDecisionMaker;
_importApprovedEpisodes = importApprovedEpisodes;
_sampleService = sampleService;
@ -73,6 +71,25 @@ namespace NzbDrone.Core.MediaFiles
}
public List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null)
{
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName);
if (series == null)
{
_logger.Debug("Unknown Series {0}", cleanedUpName);
return new List<ImportResult>
{
UnknownSeriesResult("Unknown Series")
};
}
return ProcessFolder(directoryInfo, series, downloadClientItem);
}
public List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, Series series,
DownloadClientItem downloadClientItem = null)
{
if (_seriesService.SeriesPathExists(directoryInfo.FullName))
{
@ -81,18 +98,9 @@ namespace NzbDrone.Core.MediaFiles
}
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName);
var quality = QualityParser.ParseQuality(cleanedUpName);
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
if (series == null)
{
_logger.Debug("Unknown Series {0}", cleanedUpName);
return new List<ImportResult>
{
new ImportResult(new ImportDecision(null, "Unknown Series"), "Unknown Series")
};
}
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
@ -102,20 +110,18 @@ namespace NzbDrone.Core.MediaFiles
{
if (_diskProvider.IsFileLocked(videoFile))
{
_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile);
return new List<ImportResult>
{
new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, "Locked file, try again later"), "Locked file, try again later")
};
{
FileIsLockedResult(videoFile)
};
}
}
}
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality);
var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem);
if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && importResults.Any() && ShouldDeleteFolder(directoryInfo))
if ((downloadClientItem == null || !downloadClientItem.IsReadOnly) && importResults.Any() && ShouldDeleteFolder(directoryInfo, series))
{
_logger.Debug("Deleting folder after importing valid files");
_diskProvider.DeleteFolder(directoryInfo.FullName, true);
@ -131,15 +137,26 @@ namespace NzbDrone.Core.MediaFiles
if (series == null)
{
_logger.Debug("Unknown Series for file: {0}", fileInfo.Name);
return new List<ImportResult>() { new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, "Unknown Series"), String.Format("Unknown Series for file: {0}", fileInfo.Name)) };
return new List<ImportResult>
{
UnknownSeriesResult(String.Format("Unknown Series for file: {0}", fileInfo.Name), fileInfo.FullName)
};
}
return ProcessFile(fileInfo, series, downloadClientItem);
}
public List<ImportResult> ProcessFile(FileInfo fileInfo, Series series, DownloadClientItem downloadClientItem = null)
{
if (downloadClientItem == null)
{
if (_diskProvider.IsFileLocked(fileInfo.FullName))
{
_logger.Debug("[{0}] is currently locked by another process, skipping", fileInfo.FullName);
return new List<ImportResult>();
return new List<ImportResult>
{
FileIsLockedResult(fileInfo.FullName)
};
}
}
@ -155,11 +172,9 @@ namespace NzbDrone.Core.MediaFiles
return folder;
}
private bool ShouldDeleteFolder(DirectoryInfo directoryInfo)
private bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series)
{
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName);
foreach (var videoFile in videoFiles)
{
@ -184,5 +199,18 @@ namespace NzbDrone.Core.MediaFiles
return true;
}
private ImportResult FileIsLockedResult(string videoFile)
{
_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile);
return new ImportResult(new ImportDecision(new LocalEpisode { Path = videoFile }, "Locked file, try again later"), "Locked file, try again later");
}
private ImportResult UnknownSeriesResult(string message, string videoFile = null)
{
var localEpisode = videoFile == null ? null : new LocalEpisode { Path = videoFile };
return new ImportResult(new ImportDecision(localEpisode, "Unknown Series"), message);
}
}
}
}

@ -59,40 +59,40 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
try
{
var parsedEpisode = _parsingService.GetLocalEpisode(file, series, sceneSource);
var localEpisode = _parsingService.GetLocalEpisode(file, series, sceneSource);
if (parsedEpisode != null)
if (localEpisode != null)
{
if (quality != null &&
new QualityModelComparer(parsedEpisode.Series.Profile).Compare(quality,
parsedEpisode.Quality) > 0)
new QualityModelComparer(localEpisode.Series.Profile).Compare(quality,
localEpisode.Quality) > 0)
{
_logger.Debug("Using quality from folder: {0}", quality);
parsedEpisode.Quality = quality;
localEpisode.Quality = quality;
}
parsedEpisode.Size = _diskProvider.GetFileSize(file);
_logger.Debug("Size: {0}", parsedEpisode.Size);
localEpisode.Size = _diskProvider.GetFileSize(file);
_logger.Debug("Size: {0}", localEpisode.Size);
parsedEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
decision = GetDecision(parsedEpisode);
decision = GetDecision(localEpisode);
}
else
{
parsedEpisode = new LocalEpisode();
parsedEpisode.Path = file;
localEpisode = new LocalEpisode();
localEpisode.Path = file;
decision = new ImportDecision(parsedEpisode, "Unable to parse file");
decision = new ImportDecision(localEpisode, "Unable to parse file");
}
}
catch (EpisodeNotFoundException e)
{
var parsedEpisode = new LocalEpisode();
parsedEpisode.Path = file;
var localEpisode = new LocalEpisode();
localEpisode.Path = file;
decision = new ImportDecision(parsedEpisode, e.Message);
decision = new ImportDecision(localEpisode, e.Message);
}
catch (Exception e)
{

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
public class ManualImportService
{
public ManualImportService(Logger logger)
{
}
}
}

@ -543,6 +543,7 @@
<Compile Include="MediaFiles\EpisodeImport\ImportDecision.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportResultType.cs" />
<Compile Include="MediaFiles\EpisodeImport\ManualImportService.cs" />
<Compile Include="MediaFiles\EpisodeImport\SampleService.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Queue
public interface IQueueService
{
List<Queue> GetQueue();
Queue Find(int id);
}
public class QueueService : IQueueService
@ -28,6 +29,11 @@ namespace NzbDrone.Core.Queue
return MapQueue(queueItems);
}
public Queue Find(int id)
{
return GetQueue().SingleOrDefault(q => q.Id == id);
}
private List<Queue> MapQueue(IEnumerable<TrackedDownload> trackedDownloads)
{
var queued = new List<Queue>();

@ -0,0 +1,93 @@
'use strict';
define(
[
'jquery',
'marionette',
'Cells/NzbDroneCell'
], function ($, Marionette, NzbDroneCell) {
return NzbDroneCell.extend({
className : 'queue-actions-cell',
events: {
'click .x-remove' : '_remove',
'click .x-import' : '_import',
'click .x-grab' : '_grab'
},
render: function () {
this.$el.empty();
if (this.cellValue) {
var status = this.cellValue.get('status').toLowerCase();
var trackedDownloadStatus = this.cellValue.has('trackedDownloadStatus') ? this.cellValue.get('trackedDownloadStatus').toLowerCase() : 'ok';
var icon = '';
var title = '';
if (status === 'completed' && trackedDownloadStatus === 'warning') {
icon = 'icon-inbox x-import';
title = 'Force import';
}
if (status === 'pending') {
icon = 'icon-download-alt x-grab';
title = 'Add to download queue (Override Delay Profile)';
}
//TODO: Show manual import if its completed or option to blacklist
//if (trackedDownloadStatus === 'error') {
// if (status === 'completed') {
// icon = 'icon-nd-import-failed';
// title = 'Import failed: ' + itemTitle;
// }
//TODO: What do we show when waiting for retry to take place?
// else {
// icon = 'icon-nd-download-failed';
// title = 'Download failed';
// }
//}
this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title) +
'<i class="icon-nd-delete x-remove" title="Remove from Download Client"></i>');
}
return this;
},
_remove : function () {
this.model.destroy();
},
_import : function () {
var self = this;
var promise = $.ajax({
url: window.NzbDrone.ApiRoot + '/queue/import',
type: 'POST',
data: JSON.stringify(this.model.toJSON())
});
promise.success(function () {
//find models that have the same series id and episode ids and remove them
self.model.trigger('destroy', self.model);
});
},
_grab : function () {
var self = this;
var promise = $.ajax({
url: window.NzbDrone.ApiRoot + '/queue/grab',
type: 'POST',
data: JSON.stringify(this.model.toJSON())
});
promise.success(function () {
//find models that have the same series id and episode ids and remove them
self.model.trigger('destroy', self.model);
});
}
});
});

@ -9,6 +9,7 @@ define(
'Cells/EpisodeTitleCell',
'Cells/QualityCell',
'Activity/Queue/QueueStatusCell',
'Activity/Queue/QueueActionsCell',
'Activity/Queue/TimeleftCell',
'Activity/Queue/ProgressCell',
'Shared/Grid/Pager'
@ -20,6 +21,7 @@ define(
EpisodeTitleCell,
QualityCell,
QueueStatusCell,
QueueActionsCell,
TimeleftCell,
ProgressCell,
GridPager) {
@ -74,6 +76,12 @@ define(
label : 'Progress',
cell : ProgressCell,
cellValue : 'this'
},
{
name : 'status',
label : '',
cell : QueueActionsCell,
cellValue : 'this'
}
],

@ -145,26 +145,36 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce
}
.timeleft-cell {
cursor : default;
width : 80px;
text-align: center;
cursor : default;
width : 80px;
text-align : center;
}
.queue-status-cell {
width: 20px;
text-align: center !important;
width : 20px;
text-align : center !important;
}
.queue-actions-cell {
width : 55px;
text-align : right !important;
i {
margin-left : 3px;
margin-right : 3px;
}
}
.download-log-cell {
width: 80px;
width : 80px;
}
td.delete-episode-file-cell {
.clickable();
text-align: center;
width: 20px;
font-size: 20px;
text-align : center;
width : 20px;
font-size : 20px;
i {
.clickable();

@ -24,7 +24,8 @@ define(
'slide .x-slider': '_updateSize'
},
initialize: function () {
initialize: function (options) {
this.profileCollection = options.profiles;
this.filesize = fileSize;
},
Loading…
Cancel
Save