diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index af1b5fe55..3a91799b6 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -101,6 +101,7 @@ + @@ -202,6 +203,7 @@ + diff --git a/src/NzbDrone.Api/Queue/QueueActionModule.cs b/src/NzbDrone.Api/Queue/QueueActionModule.cs new file mode 100644 index 000000000..cba735d74 --- /dev/null +++ b/src/NzbDrone.Api/Queue/QueueActionModule.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 + { + 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[@"/(?[\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 Import() + { + var resource = Request.Body.FromJson(); + var trackedDownload = GetTrackedDownload(resource.Id); + + _completedDownloadService.Import(trackedDownload); + + return resource.AsResponse(); + } + + private JsonResponse Grab() + { + var resource = Request.Body.FromJson(); + + 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; + } + } +} diff --git a/src/NzbDrone.Api/REST/NotFoundException.cs b/src/NzbDrone.Api/REST/NotFoundException.cs new file mode 100644 index 000000000..92b4016a9 --- /dev/null +++ b/src/NzbDrone.Api/REST/NotFoundException.cs @@ -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) + { + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs index 116ac2874..324e11ac7 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -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 {new Episode {Id = 1}} @@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.Download private void GivenCompletedImport() { Mocker.GetMock() - .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { 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() - .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List() { 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() - .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } private void VerifyImports() { Mocker.GetMock() - .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(v => v.ProcessFolder(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Test] @@ -473,7 +473,7 @@ namespace NzbDrone.Core.Test.Download GivenNoImportedHistory(); Mocker.GetMock() - .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -505,7 +505,7 @@ namespace NzbDrone.Core.Test.Download GivenNoImportedHistory(); Mocker.GetMock() - .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult( @@ -537,7 +537,7 @@ namespace NzbDrone.Core.Test.Download GivenNoImportedHistory(); Mocker.GetMock() - .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny())) + .Setup(v => v.ProcessFolder(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new List { new ImportResult(new ImportDecision(new LocalEpisode() {Path = @"C:\TestPath\Droned.S01E01.mkv"})), diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index 800f9a720..89776300a 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -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 { _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 { _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 { _temporarilyRejected }); + Subject.Handle(new EpisodeGrabbedEvent(_remoteEpisode)); VerifyNoDelete(); } diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index c80780c4a..36e520a2d 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -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 { _temporarilyRejected }); + Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List(), + new List(), + new List { _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 { _temporarilyRejected }); + Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List(), + new List(), + new List { _temporarilyRejected }))); VerifyNoDelete(); } @@ -124,7 +130,9 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests { GivenHeldRelease(_release.Title, "AnotherIndexer", _release.PublishDate); - Subject.RemoveRejected(new List { _temporarilyRejected }); + Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List(), + new List(), + new List { _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 { _temporarilyRejected }); + Subject.Handle(new RssSyncCompleteEvent(new ProcessedDecisions(new List(), + new List(), + new List { _temporarilyRejected }))); VerifyNoDelete(); } diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/EpisodeInfoRefreshedSearchFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/EpisodeInfoRefreshedSearchFixture.cs index 5a57a6f34..6a60eae6c 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/EpisodeInfoRefreshedSearchFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/EpisodeInfoRefreshedSearchFixture.cs @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests Mocker.GetMock() .Setup(s => s.ProcessDecisions(It.IsAny>())) - .Returns(new ProcessedDecisions(new List(), new List())); + .Returns(new ProcessedDecisions(new List(), new List(), new List())); Subject.Handle(new EpisodeInfoRefreshedEvent(_series, _added, _updated)); diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs index 22ef726a7..3b3f15216 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/SeriesSearchServiceFixture.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests Mocker.GetMock() .Setup(s => s.ProcessDecisions(It.IsAny>())) - .Returns(new ProcessedDecisions(new List(), new List())); + .Returns(new ProcessedDecisions(new List(), new List(), new List())); } [Test] diff --git a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs index 398a5d704..22244a059 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/DownloadedEpisodesImportServiceFixture.cs @@ -87,6 +87,8 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_not_import_if_folder_is_a_series_path() { + GivenValidSeries(); + Mocker.GetMock() .Setup(s => s.SeriesPathExists(It.IsAny())) .Returns(true); @@ -97,8 +99,8 @@ namespace NzbDrone.Core.Test.MediaFiles Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory)); - Mocker.GetMock() - .Verify(v => v.GetSeries(It.IsAny()), Times.Never()); + Mocker.GetMock() + .Verify(v => v.GetVideoFiles(It.IsAny(), true), Times.Never()); ExceptionVerification.ExpectedWarns(1); } diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 33a279d38..9b15bd390 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -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); } diff --git a/src/NzbDrone.Core/Download/DownloadClientProvider.cs b/src/NzbDrone.Core/Download/DownloadClientProvider.cs index f8c931b3e..471a9901c 100644 --- a/src/NzbDrone.Core/Download/DownloadClientProvider.cs +++ b/src/NzbDrone.Core/Download/DownloadClientProvider.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Download { IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol); IEnumerable 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); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/DownloadTrackingService.cs b/src/NzbDrone.Core/Download/DownloadTrackingService.cs index 14aaef7ff..44190560c 100644 --- a/src/NzbDrone.Core/Download/DownloadTrackingService.cs +++ b/src/NzbDrone.Core/Download/DownloadTrackingService.cs @@ -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); diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index ba0c6a7b6..4963002ca 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -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 grabbed); - void RemoveRejected(List rejected); + List GetPending(); List GetPendingRemoteEpisodes(int seriesId); List GetPendingQueue(); + Queue.Queue FindPendingQueueItem(int queueId); + void RemovePendingQueueItem(int queueId); RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable episodeIds); } - public class PendingReleaseService : IPendingReleaseService, IHandle + public class PendingReleaseService : IPendingReleaseService, + IHandle, + IHandle, + IHandle { 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 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 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 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 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 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); + } } } diff --git a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs index 8482648c3..b006f6e59 100644 --- a/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessDownloadDecisions.cs @@ -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 GetQualifiedReports(IEnumerable decisions) diff --git a/src/NzbDrone.Core/Download/ProcessedDecisions.cs b/src/NzbDrone.Core/Download/ProcessedDecisions.cs index c274b931a..b59df6e1a 100644 --- a/src/NzbDrone.Core/Download/ProcessedDecisions.cs +++ b/src/NzbDrone.Core/Download/ProcessedDecisions.cs @@ -7,11 +7,13 @@ namespace NzbDrone.Core.Download { public List Grabbed { get; set; } public List Pending { get; set; } + public List Rejected { get; set; } - public ProcessedDecisions(List grabbed, List pending) + public ProcessedDecisions(List grabbed, List pending, List rejected) { Grabbed = grabbed; Pending = pending; + Rejected = rejected; } } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs b/src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs index 903a2553e..0f03d2279 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloadStatusMessage.cs @@ -8,6 +8,10 @@ namespace NzbDrone.Core.Download public String Title { get; set; } public List Messages { get; set; } + private TrackedDownloadStatusMessage() + { + } + public TrackedDownloadStatusMessage(String title, List messages) { Title = title; diff --git a/src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs b/src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs index af4fef0ff..a15a1fd0b 100644 --- a/src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs +++ b/src/NzbDrone.Core/Indexers/RssSyncCompleteEvent.cs @@ -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; + } } } diff --git a/src/NzbDrone.Core/Indexers/RssSyncService.cs b/src/NzbDrone.Core/Indexers/RssSyncService.cs index 246bc801b..3313ebf00 100644 --- a/src/NzbDrone.Core/Indexers/RssSyncService.cs +++ b/src/NzbDrone.Core/Indexers/RssSyncService.cs @@ -40,15 +40,13 @@ namespace NzbDrone.Core.Indexers } - private List 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)); } } } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index fa6509d3c..201e80f54 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -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 ProcessRootFolder(DirectoryInfo directoryInfo); List ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null); + List ProcessFolder(DirectoryInfo directoryInfo, Series series, DownloadClientItem downloadClientItem = null); List ProcessFile(FileInfo fileInfo, DownloadClientItem downloadClientItem = null); + List 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 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 + { + UnknownSeriesResult("Unknown Series") + }; + } + + return ProcessFolder(directoryInfo, series, downloadClientItem); + } + + public List 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 - { - 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 - { - 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() { new ImportResult(new ImportDecision(new LocalEpisode { Path = fileInfo.FullName }, "Unknown Series"), String.Format("Unknown Series for file: {0}", fileInfo.Name)) }; + + return new List + { + UnknownSeriesResult(String.Format("Unknown Series for file: {0}", fileInfo.Name), fileInfo.FullName) + }; } + return ProcessFile(fileInfo, series, downloadClientItem); + } + + public List 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(); + return new List + { + 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); + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs index b6a29ed43..afa5da0fa 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportDecisionMaker.cs @@ -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) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ManualImportService.cs new file mode 100644 index 000000000..d415f98d7 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ManualImportService.cs @@ -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) + { + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 85336a6f4..0c00d49d3 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -543,6 +543,7 @@ + diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index 60e49fe62..cfd87c944 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Queue public interface IQueueService { List 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 MapQueue(IEnumerable trackedDownloads) { var queued = new List(); diff --git a/src/UI/Activity/Queue/QueueActionsCell.js b/src/UI/Activity/Queue/QueueActionsCell.js new file mode 100644 index 000000000..9c0dede97 --- /dev/null +++ b/src/UI/Activity/Queue/QueueActionsCell.js @@ -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(''.format(icon, title) + + ''); + } + + 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); + }); + } + }); + }); diff --git a/src/UI/Activity/Queue/QueueLayout.js b/src/UI/Activity/Queue/QueueLayout.js index 6b86d46c1..cbcfc6ba4 100644 --- a/src/UI/Activity/Queue/QueueLayout.js +++ b/src/UI/Activity/Queue/QueueLayout.js @@ -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' } ], diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index 431df2260..00b39f8b5 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -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(); diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js similarity index 96% rename from src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js rename to src/UI/Settings/Quality/Definition/QualityDefinitionView.js index 821f02d2c..0d0e08a84 100644 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js +++ b/src/UI/Settings/Quality/Definition/QualityDefinitionView.js @@ -24,7 +24,8 @@ define( 'slide .x-slider': '_updateSize' }, - initialize: function () { + initialize: function (options) { + this.profileCollection = options.profiles; this.filesize = fileSize; }, diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs b/src/UI/Settings/Quality/Definition/QualityDefinitionViewTemplate.hbs similarity index 100% rename from src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs rename to src/UI/Settings/Quality/Definition/QualityDefinitionViewTemplate.hbs