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

@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.Download
private void GivenCompletedImport() private void GivenCompletedImport()
{ {
Mocker.GetMock<IDownloadedEpisodesImportService>() 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> .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" }))
@ -125,7 +125,7 @@ namespace NzbDrone.Core.Test.Download
private void GivenFailedImport() private void GivenFailedImport()
{ {
Mocker.GetMock<IDownloadedEpisodesImportService>() 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>() .Returns(new List<ImportResult>()
{ {
new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }, "Test Failure")) new ImportResult(new ImportDecision(new LocalEpisode() { Path = @"C:\TestPath\Droned.S01E01.mkv" }, "Test Failure"))
@ -135,13 +135,13 @@ namespace NzbDrone.Core.Test.Download
private void VerifyNoImports() private void VerifyNoImports()
{ {
Mocker.GetMock<IDownloadedEpisodesImportService>() 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() private void VerifyImports()
{ {
Mocker.GetMock<IDownloadedEpisodesImportService>() 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] [Test]
@ -473,7 +473,7 @@ namespace NzbDrone.Core.Test.Download
GivenNoImportedHistory(); GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>() 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> .Returns(new List<ImportResult>
{ {
new ImportResult( new ImportResult(
@ -505,7 +505,7 @@ namespace NzbDrone.Core.Test.Download
GivenNoImportedHistory(); GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>() 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> .Returns(new List<ImportResult>
{ {
new ImportResult( new ImportResult(
@ -537,7 +537,7 @@ namespace NzbDrone.Core.Test.Download
GivenNoImportedHistory(); GivenNoImportedHistory();
Mocker.GetMock<IDownloadedEpisodesImportService>() 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> .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"})),

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

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

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

@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Mocker.GetMock<IProcessDownloadDecisions>() Mocker.GetMock<IProcessDownloadDecisions>()
.Setup(s => s.ProcessDecisions(It.IsAny<List<DownloadDecision>>())) .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] [Test]

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

@ -118,13 +118,14 @@ namespace NzbDrone.Core.Download
if (_diskProvider.FolderExists(outputPath)) 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); ProcessImportResults(trackedDownload, outputPath, importResults);
} }
else if (_diskProvider.FileExists(outputPath)) 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); ProcessImportResults(trackedDownload, outputPath, importResults);
} }

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Download
{ {
IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol); IDownloadClient GetDownloadClient(DownloadProtocol downloadProtocol);
IEnumerable<IDownloadClient> GetDownloadClients(); IEnumerable<IDownloadClient> GetDownloadClients();
IDownloadClient Get(int id);
} }
public class DownloadClientProvider : IProvideDownloadClient public class DownloadClientProvider : IProvideDownloadClient
@ -28,5 +29,10 @@ namespace NzbDrone.Core.Download
{ {
return _downloadClientFactory.GetAvailableProviders(); 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[] GetCompletedDownloads();
TrackedDownload[] GetQueuedDownloads(); TrackedDownload[] GetQueuedDownloads();
TrackedDownload Find(string trackingId);
void MarkAsFailed(Int32 historyId); void MarkAsFailed(Int32 historyId);
} }
@ -88,6 +88,11 @@ namespace NzbDrone.Core.Download
}, TimeSpan.FromSeconds(5.0)); }, TimeSpan.FromSeconds(5.0));
} }
public TrackedDownload Find(string trackingId)
{
return GetQueuedDownloads().SingleOrDefault(t => t.TrackingId == trackingId);
}
public void MarkAsFailed(Int32 historyId) public void MarkAsFailed(Int32 historyId)
{ {
var item = _historyService.Get(historyId); var item = _historyService.Get(historyId);

@ -4,6 +4,7 @@ using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
@ -17,15 +18,19 @@ namespace NzbDrone.Core.Download.Pending
public interface IPendingReleaseService public interface IPendingReleaseService
{ {
void Add(DownloadDecision decision); void Add(DownloadDecision decision);
void RemoveGrabbed(List<DownloadDecision> grabbed);
void RemoveRejected(List<DownloadDecision> rejected);
List<ReleaseInfo> GetPending(); List<ReleaseInfo> GetPending();
List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId); List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId);
List<Queue.Queue> GetPendingQueue(); List<Queue.Queue> GetPendingQueue();
Queue.Queue FindPendingQueueItem(int queueId);
void RemovePendingQueueItem(int queueId);
RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable<int> episodeIds); 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 IPendingReleaseRepository _repository;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
@ -49,6 +54,7 @@ namespace NzbDrone.Core.Download.Pending
_logger = logger; _logger = logger;
} }
public void Add(DownloadDecision decision) public void Add(DownloadDecision decision)
{ {
var alreadyPending = GetPendingReleases(); var alreadyPending = GetPendingReleases();
@ -69,61 +75,6 @@ namespace NzbDrone.Core.Download.Pending
Insert(decision); 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() public List<ReleaseInfo> GetPending()
{ {
return _repository.All().Select(p => p.Release).ToList(); return _repository.All().Select(p => p.Release).ToList();
@ -165,6 +116,18 @@ namespace NzbDrone.Core.Download.Pending
return queued; 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) public RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable<int> episodeIds)
{ {
return GetPendingRemoteEpisodes(seriesId) return GetPendingRemoteEpisodes(seriesId)
@ -243,9 +206,73 @@ namespace NzbDrone.Core.Download.Pending
return delayProfile.GetProtocolDelay(remoteEpisode.Release.DownloadProtocol); 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) public void Handle(SeriesDeletedEvent message)
{ {
_repository.DeleteBySeriesId(message.Series.Id); _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) internal List<DownloadDecision> GetQualifiedReports(IEnumerable<DownloadDecision> decisions)

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

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

@ -1,8 +1,15 @@
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.Download;
namespace NzbDrone.Core.Indexers namespace NzbDrone.Core.Indexers
{ {
public class RssSyncCompleteEvent : IEvent 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"); _logger.ProgressInfo("Starting RSS Sync");
var reports = _rssFetcherAndParser.Fetch().Concat(_pendingReleaseService.GetPending()).ToList(); var reports = _rssFetcherAndParser.Fetch().Concat(_pendingReleaseService.GetPending()).ToList();
var decisions = _downloadDecisionMaker.GetRssDecision(reports); var decisions = _downloadDecisionMaker.GetRssDecision(reports);
var processed = _processDownloadDecisions.ProcessDecisions(decisions); 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); 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); _logger.ProgressInfo(message);
return processed.Grabbed.Concat(processed.Pending).ToList(); return processed;
} }
public void Execute(RssSyncCommand message) public void Execute(RssSyncCommand message)
{ {
var processed = Sync(); var processed = Sync();
var grabbedOrPending = processed.Grabbed.Concat(processed.Pending).ToList();
if (message.LastExecutionTime.HasValue && DateTime.UtcNow.Subtract(message.LastExecutionTime.Value).TotalHours > 3) 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); _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 System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.EpisodeImport; using NzbDrone.Core.MediaFiles.EpisodeImport;
using NzbDrone.Core.Parser; using NzbDrone.Core.Parser;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -17,7 +16,9 @@ namespace NzbDrone.Core.MediaFiles
{ {
List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo); List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo);
List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null); 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, DownloadClientItem downloadClientItem = null);
List<ImportResult> ProcessFile(FileInfo fileInfo, Series series, DownloadClientItem downloadClientItem = null);
} }
public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService public class DownloadedEpisodesImportService : IDownloadedEpisodesImportService
@ -26,7 +27,6 @@ namespace NzbDrone.Core.MediaFiles
private readonly IDiskScanService _diskScanService; private readonly IDiskScanService _diskScanService;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
private readonly IParsingService _parsingService; private readonly IParsingService _parsingService;
private readonly IConfigService _configService;
private readonly IMakeImportDecision _importDecisionMaker; private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes; private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly ISampleService _sampleService; private readonly ISampleService _sampleService;
@ -36,7 +36,6 @@ namespace NzbDrone.Core.MediaFiles
IDiskScanService diskScanService, IDiskScanService diskScanService,
ISeriesService seriesService, ISeriesService seriesService,
IParsingService parsingService, IParsingService parsingService,
IConfigService configService,
IMakeImportDecision importDecisionMaker, IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes, IImportApprovedEpisodes importApprovedEpisodes,
ISampleService sampleService, ISampleService sampleService,
@ -46,7 +45,6 @@ namespace NzbDrone.Core.MediaFiles
_diskScanService = diskScanService; _diskScanService = diskScanService;
_seriesService = seriesService; _seriesService = seriesService;
_parsingService = parsingService; _parsingService = parsingService;
_configService = configService;
_importDecisionMaker = importDecisionMaker; _importDecisionMaker = importDecisionMaker;
_importApprovedEpisodes = importApprovedEpisodes; _importApprovedEpisodes = importApprovedEpisodes;
_sampleService = sampleService; _sampleService = sampleService;
@ -74,26 +72,36 @@ namespace NzbDrone.Core.MediaFiles
public List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null) public List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, DownloadClientItem downloadClientItem = null)
{ {
if (_seriesService.SeriesPathExists(directoryInfo.FullName))
{
_logger.Warn("Unable to process folder that contains sorted TV Shows");
return new List<ImportResult>();
}
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name); var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName); var series = _parsingService.GetSeries(cleanedUpName);
var quality = QualityParser.ParseQuality(cleanedUpName);
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
if (series == null) if (series == null)
{ {
_logger.Debug("Unknown Series {0}", cleanedUpName); _logger.Debug("Unknown Series {0}", cleanedUpName);
return new List<ImportResult> return new List<ImportResult>
{ {
new ImportResult(new ImportDecision(null, "Unknown Series"), "Unknown Series") UnknownSeriesResult("Unknown Series")
}; };
} }
return ProcessFolder(directoryInfo, series, downloadClientItem);
}
public List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, Series series,
DownloadClientItem downloadClientItem = null)
{
if (_seriesService.SeriesPathExists(directoryInfo.FullName))
{
_logger.Warn("Unable to process folder that contains sorted TV Shows");
return new List<ImportResult>();
}
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var quality = QualityParser.ParseQuality(cleanedUpName);
_logger.Debug("{0} folder quality: {1}", cleanedUpName, quality);
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
if (downloadClientItem == null) if (downloadClientItem == null)
@ -102,20 +110,18 @@ namespace NzbDrone.Core.MediaFiles
{ {
if (_diskProvider.IsFileLocked(videoFile)) if (_diskProvider.IsFileLocked(videoFile))
{ {
_logger.Debug("[{0}] is currently locked by another process, skipping", videoFile);
return new List<ImportResult> 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 decisions = _importDecisionMaker.GetImportDecisions(videoFiles.ToList(), series, true, quality);
var importResults = _importApprovedEpisodes.Import(decisions, true, downloadClientItem); 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"); _logger.Debug("Deleting folder after importing valid files");
_diskProvider.DeleteFolder(directoryInfo.FullName, true); _diskProvider.DeleteFolder(directoryInfo.FullName, true);
@ -131,15 +137,26 @@ namespace NzbDrone.Core.MediaFiles
if (series == null) if (series == null)
{ {
_logger.Debug("Unknown Series for file: {0}", fileInfo.Name); _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 (downloadClientItem == null)
{ {
if (_diskProvider.IsFileLocked(fileInfo.FullName)) 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; return folder;
} }
private bool ShouldDeleteFolder(DirectoryInfo directoryInfo) private bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Series series)
{ {
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName); var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
var cleanedUpName = GetCleanedUpFolderName(directoryInfo.Name);
var series = _parsingService.GetSeries(cleanedUpName);
foreach (var videoFile in videoFiles) foreach (var videoFile in videoFiles)
{ {
@ -184,5 +199,18 @@ namespace NzbDrone.Core.MediaFiles
return true; 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 try
{ {
var parsedEpisode = _parsingService.GetLocalEpisode(file, series, sceneSource); var localEpisode = _parsingService.GetLocalEpisode(file, series, sceneSource);
if (parsedEpisode != null) if (localEpisode != null)
{ {
if (quality != null && if (quality != null &&
new QualityModelComparer(parsedEpisode.Series.Profile).Compare(quality, new QualityModelComparer(localEpisode.Series.Profile).Compare(quality,
parsedEpisode.Quality) > 0) localEpisode.Quality) > 0)
{ {
_logger.Debug("Using quality from folder: {0}", quality); _logger.Debug("Using quality from folder: {0}", quality);
parsedEpisode.Quality = quality; localEpisode.Quality = quality;
} }
parsedEpisode.Size = _diskProvider.GetFileSize(file); localEpisode.Size = _diskProvider.GetFileSize(file);
_logger.Debug("Size: {0}", parsedEpisode.Size); _logger.Debug("Size: {0}", localEpisode.Size);
parsedEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file); localEpisode.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
decision = GetDecision(parsedEpisode); decision = GetDecision(localEpisode);
} }
else else
{ {
parsedEpisode = new LocalEpisode(); localEpisode = new LocalEpisode();
parsedEpisode.Path = file; localEpisode.Path = file;
decision = new ImportDecision(parsedEpisode, "Unable to parse file"); decision = new ImportDecision(localEpisode, "Unable to parse file");
} }
} }
catch (EpisodeNotFoundException e) catch (EpisodeNotFoundException e)
{ {
var parsedEpisode = new LocalEpisode(); var localEpisode = new LocalEpisode();
parsedEpisode.Path = file; localEpisode.Path = file;
decision = new ImportDecision(parsedEpisode, e.Message); decision = new ImportDecision(localEpisode, e.Message);
} }
catch (Exception e) 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\ImportDecision.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" /> <Compile Include="MediaFiles\EpisodeImport\ImportDecisionMaker.cs" />
<Compile Include="MediaFiles\EpisodeImport\ImportResultType.cs" /> <Compile Include="MediaFiles\EpisodeImport\ImportResultType.cs" />
<Compile Include="MediaFiles\EpisodeImport\ManualImportService.cs" />
<Compile Include="MediaFiles\EpisodeImport\SampleService.cs" /> <Compile Include="MediaFiles\EpisodeImport\SampleService.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\FreeSpaceSpecification.cs" />
<Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" /> <Compile Include="MediaFiles\EpisodeImport\Specifications\FullSeasonSpecification.cs" />

@ -8,6 +8,7 @@ namespace NzbDrone.Core.Queue
public interface IQueueService public interface IQueueService
{ {
List<Queue> GetQueue(); List<Queue> GetQueue();
Queue Find(int id);
} }
public class QueueService : IQueueService public class QueueService : IQueueService
@ -28,6 +29,11 @@ namespace NzbDrone.Core.Queue
return MapQueue(queueItems); return MapQueue(queueItems);
} }
public Queue Find(int id)
{
return GetQueue().SingleOrDefault(q => q.Id == id);
}
private List<Queue> MapQueue(IEnumerable<TrackedDownload> trackedDownloads) private List<Queue> MapQueue(IEnumerable<TrackedDownload> trackedDownloads)
{ {
var queued = new List<Queue>(); 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/EpisodeTitleCell',
'Cells/QualityCell', 'Cells/QualityCell',
'Activity/Queue/QueueStatusCell', 'Activity/Queue/QueueStatusCell',
'Activity/Queue/QueueActionsCell',
'Activity/Queue/TimeleftCell', 'Activity/Queue/TimeleftCell',
'Activity/Queue/ProgressCell', 'Activity/Queue/ProgressCell',
'Shared/Grid/Pager' 'Shared/Grid/Pager'
@ -20,6 +21,7 @@ define(
EpisodeTitleCell, EpisodeTitleCell,
QualityCell, QualityCell,
QueueStatusCell, QueueStatusCell,
QueueActionsCell,
TimeleftCell, TimeleftCell,
ProgressCell, ProgressCell,
GridPager) { GridPager) {
@ -74,6 +76,12 @@ define(
label : 'Progress', label : 'Progress',
cell : ProgressCell, cell : ProgressCell,
cellValue : 'this' cellValue : 'this'
},
{
name : 'status',
label : '',
cell : QueueActionsCell,
cellValue : 'this'
} }
], ],

@ -155,6 +155,16 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce
text-align : center !important; text-align : center !important;
} }
.queue-actions-cell {
width : 55px;
text-align : right !important;
i {
margin-left : 3px;
margin-right : 3px;
}
}
.download-log-cell { .download-log-cell {
width : 80px; width : 80px;
} }

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