Episode grid will show downloading on grab

New: Update episode status in UI on grab and download
pull/4/head
Mark McDowall 11 years ago
parent b6693a20a9
commit daeb2fc652

@ -1,16 +1,22 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using NzbDrone.Api.REST;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Api.Mapping;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Api.EpisodeFiles
{
public class EpisodeModule : NzbDroneRestModule<EpisodeFileResource>
public class EpisodeModule : NzbDroneRestModuleWithSignalR<EpisodeFileResource, EpisodeFile>,
IHandle<EpisodeFileAddedEvent>
{
private readonly IMediaFileService _mediaFileService;
public EpisodeModule(IMediaFileService mediaFileService)
: base("/episodefile")
public EpisodeModule(ICommandExecutor commandExecutor, IMediaFileService mediaFileService)
: base(commandExecutor)
{
_mediaFileService = mediaFileService;
GetResourceById = GetEpisodeFile;
@ -41,5 +47,10 @@ namespace NzbDrone.Api.EpisodeFiles
episodeFile.Quality = episodeFileResource.Quality;
_mediaFileService.Update(episodeFile);
}
public void Handle(EpisodeFileAddedEvent message)
{
BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id);
}
}
}

@ -54,7 +54,10 @@ namespace NzbDrone.Api.Episodes
{
foreach (var episode in message.Episode.Episodes)
{
BroadcastResourceChange(ModelAction.Updated, episode.Id);
var resource = episode.InjectTo<EpisodeResource>();
resource.Downloading = true;
BroadcastResourceChange(ModelAction.Updated, resource);
}
}

@ -26,5 +26,7 @@ namespace NzbDrone.Api.Episodes
public DateTime? GrabDate { get; set; }
public Core.Tv.Series Series { get; set; }
public String SeriesTitle { get; set; }
public Boolean Downloading { get; set; }
}
}

@ -126,6 +126,8 @@
<Compile Include="Missing\MissingModule.cs" />
<Compile Include="Config\NamingSampleResource.cs" />
<Compile Include="NzbDroneRestModuleWithSignalR.cs" />
<Compile Include="Queue\QueueModule.cs" />
<Compile Include="Queue\QueueResource.cs" />
<Compile Include="ResourceChangeMessage.cs" />
<Compile Include="Notifications\NotificationSchemaModule.cs" />
<Compile Include="Notifications\NotificationModule.cs" />

@ -34,6 +34,17 @@ namespace NzbDrone.Api
BroadcastResourceChange(message.Action, message.Model.Id);
}
protected void BroadcastResourceChange(ModelAction action, TResource resource)
{
var signalRMessage = new SignalRMessage
{
Name = Resource,
Body = new ResourceChangeMessage<TResource>(resource, action)
};
_commandExecutor.PublishCommand(new BroadcastSignalRMessage(signalRMessage));
}
protected void BroadcastResourceChange(ModelAction action, int id)
{
var resource = GetResourceById(id);

@ -0,0 +1,32 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Queue;
namespace NzbDrone.Api.Queue
{
public class QueueModule : NzbDroneRestModuleWithSignalR<QueueResource, Core.Queue.Queue>,
IHandle<UpdateQueueEvent>
{
private readonly IQueueService _queueService;
public QueueModule(ICommandExecutor commandExecutor, IQueueService queueService)
: base(commandExecutor)
{
_queueService = queueService;
GetResourceAll = GetQueue;
}
private List<QueueResource> GetQueue()
{
return ToListResource(_queueService.GetQueue);
}
public void Handle(UpdateQueueEvent message)
{
BroadcastResourceChange(ModelAction.Sync);
}
}
}

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using NzbDrone.Api.REST;
using NzbDrone.Core.Tv;
namespace NzbDrone.Api.Queue
{
public class QueueResource : RestResource
{
public Core.Tv.Series Series { get; set; }
public Episode Episode { get; set; }
public QualityModel Quality { get; set; }
public Decimal Size { get; set; }
public String Title { get; set; }
public Decimal SizeLeft { get; set; }
public TimeSpan Timeleft { get; set; }
}
}

@ -11,7 +11,7 @@ namespace NzbDrone.Api
public ResourceChangeMessage(ModelAction action)
{
if (action != ModelAction.Deleted || action != ModelAction.Sync)
if (action != ModelAction.Deleted && action != ModelAction.Sync)
{
throw new InvalidOperationException("Resource message without a resource needs to have Delete or Sync as action");
}

@ -22,6 +22,4 @@ namespace NzbDrone.Core.Datastore.Events
Deleted = 3,
Sync = 4
}
}

@ -4,14 +4,10 @@ namespace NzbDrone.Core.Download
{
public class QueueItem
{
public string Id { get; set; }
public decimal Size { get; set; }
public string Title { get; set; }
public decimal SizeLeft { get; set; }
public string Id { get; set; }
public TimeSpan Timeleft { get; set; }
}
}

@ -70,12 +70,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{
episodeFile.SceneName = Path.GetFileNameWithoutExtension(localEpisode.Path.CleanFilePath());
episodeFile.Path = _episodeFileUpgrader.UpgradeEpisodeFile(episodeFile, localEpisode);
_eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile));
_eventAggregator.PublishEvent(new EpisodeDownloadedEvent(localEpisode));
}
_mediaFileService.Add(episodeFile);
imported.Add(importDecision);
if (newDownload)
{
_eventAggregator.PublishEvent(new EpisodeImportedEvent(localEpisode, episodeFile));
_eventAggregator.PublishEvent(new EpisodeDownloadedEvent(localEpisode));
}
}
catch (Exception e)
{

@ -110,6 +110,7 @@
</Reference>
<Reference Include="System.Drawing" />
<Reference Include="System.Web" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.XML" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
@ -216,7 +217,7 @@
<Compile Include="Download\DownloadApprovedReports.cs" />
<Compile Include="Download\DownloadClientProvider.cs" />
<Compile Include="Download\DownloadClientType.cs" />
<Compile Include="Download\SabQueueItem.cs" />
<Compile Include="Download\QueueItem.cs" />
<Compile Include="Exceptions\BadRequestException.cs" />
<Compile Include="Exceptions\DownstreamException.cs" />
<Compile Include="Exceptions\NzbDroneClientException.cs" />
@ -419,6 +420,10 @@
<Compile Include="Qualities\QualityProfileInUseException.cs" />
<Compile Include="Qualities\QualitySizeRepository.cs" />
<Compile Include="Qualities\QualityProfileRepository.cs" />
<Compile Include="Queue\Queue.cs" />
<Compile Include="Queue\UpdateQueueEvent.cs" />
<Compile Include="Queue\QueueScheduler.cs" />
<Compile Include="Queue\QueueService.cs" />
<Compile Include="Rest\RestSharpExtensions.cs" />
<Compile Include="Rest\RestException.cs" />
<Compile Include="SeriesStats\SeriesStatisticsService.cs" />

@ -0,0 +1,17 @@
using System;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Queue
{
public class Queue : ModelBase
{
public Series Series { get; set; }
public Episode Episode { get; set; }
public QualityModel Quality { get; set; }
public Decimal Size { get; set; }
public String Title { get; set; }
public Decimal SizeLeft { get; set; }
public TimeSpan Timeleft { get; set; }
}
}

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

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Queue
{
public interface IQueueService
{
List<Queue> GetQueue();
}
public class QueueService : IQueueService
{
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IParsingService _parsingService;
private readonly Logger _logger;
public QueueService(IProvideDownloadClient downloadClientProvider, IParsingService parsingService, Logger logger)
{
_downloadClientProvider = downloadClientProvider;
_parsingService = parsingService;
_logger = logger;
}
public List<Queue> GetQueue()
{
var downloadClient = _downloadClientProvider.GetDownloadClient();
var queueItems = downloadClient.GetQueue();
return MapQueue(queueItems);
}
private List<Queue> MapQueue(IEnumerable<QueueItem> queueItems)
{
var queued = new List<Queue>();
foreach (var queueItem in queueItems)
{
var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title);
if (parsedEpisodeInfo != null && !string.IsNullOrWhiteSpace(parsedEpisodeInfo.SeriesTitle))
{
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0);
if (remoteEpisode.Series == null)
{
continue;
}
foreach (var episode in remoteEpisode.Episodes)
{
var queue = new Queue();
queue.Id = queueItem.Id.GetHashCode();
queue.Series = remoteEpisode.Series;
queue.Episode = episode;
queue.Quality = remoteEpisode.ParsedEpisodeInfo.Quality;
queue.Title = queueItem.Title;
queue.Size = queueItem.Size;
queue.SizeLeft = queueItem.SizeLeft;
queue.Timeleft = queueItem.Timeleft;
queued.Add(queue);
}
}
}
return queued;
}
}
}

@ -0,0 +1,8 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Queue
{
public class UpdateQueueEvent : IEvent
{
}
}

@ -1,4 +1,4 @@
'use strict';
'use strict';
define(
[

@ -3,15 +3,25 @@
define(
[
'app',
'underscore',
'Cells/NzbDroneCell',
'History/Queue/QueueCollection',
'moment',
'Shared/FormatHelpers'
], function (App, NzbDroneCell, Moment, FormatHelpers) {
], function (App, _, NzbDroneCell, QueueCollection, Moment, FormatHelpers) {
return NzbDroneCell.extend({
className: 'episode-status-cell',
render: function () {
this.listenTo(QueueCollection, 'sync', this._renderCell);
this._renderCell();
return this;
},
_renderCell: function () {
this.$el.empty();
if (this.model) {
@ -41,10 +51,17 @@ define(
this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name));
}
return this;
return;
}
else {
if (this.model.get('downloading')) {
var model = this.model;
var downloading = _.find(QueueCollection.models, function (queueModel) {
return queueModel.get('episode').id === model.get('id');
});
if (downloading || this.model.get('downloading')) {
icon = 'icon-nd-downloading';
tooltip = 'Episode is downloading';
}
@ -66,8 +83,6 @@ define(
this.$el.html('<i class="{0}" title="{1}"/>'.format(icon, tooltip));
}
return this;
}
});
});

@ -73,10 +73,10 @@ define(
App.mainRegion.show(new MissingLayout());
},
history: function () {
history: function (action) {
this._setTitle('History');
App.mainRegion.show(new HistoryLayout());
App.mainRegion.show(new HistoryLayout({ action: action }));
},
rss: function () {

@ -1,7 +1,7 @@
'use strict';
define(
[
'History/Model',
'History/HistoryModel',
'backbone.pageable'
], function (HistoryModel, PageableCollection) {
return PageableCollection.extend({

@ -1,108 +1,74 @@
'use strict';
define(
[
'app',
'marionette',
'backgrid',
'History/Collection',
'History/EventTypeCell',
'Cells/SeriesTitleCell',
'Cells/EpisodeNumberCell',
'Cells/EpisodeTitleCell',
'Cells/QualityCell',
'Cells/RelativeDateCell',
'History/HistoryDetailsCell',
'Shared/Grid/Pager',
'Shared/LoadingView'
], function (Marionette,
'History/Table/HistoryTableLayout',
'History/Queue/QueueLayout'
], function (App,
Marionette,
Backgrid,
HistoryCollection,
EventTypeCell,
SeriesTitleCell,
EpisodeNumberCell,
EpisodeTitleCell,
QualityCell,
RelativeDateCell,
HistoryDetailsCell,
GridPager,
LoadingView) {
HistoryTableLayout,
QueueLayout) {
return Marionette.Layout.extend({
template: 'History/HistoryLayoutTemplate',
regions: {
history: '#x-history',
toolbar: '#x-toolbar',
pager : '#x-pager'
history: '#history',
queueRegion : '#queue'
},
columns:
[
{
name : 'eventType',
label : '',
cell : EventTypeCell,
cellValue: 'this'
},
{
name : 'series',
label: 'Series',
cell : SeriesTitleCell
},
{
name : 'episode',
label : 'Episode',
sortable: false,
cell : EpisodeNumberCell
},
{
name : 'episode',
label : 'Episode Title',
sortable: false,
cell : EpisodeTitleCell
},
{
name : 'quality',
label : 'Quality',
cell : QualityCell,
sortable: false
},
{
name : 'date',
label: 'Date',
cell : RelativeDateCell
},
{
name : 'this',
label : '',
cell : HistoryDetailsCell,
sortable: false
}
],
ui: {
historyTab: '.x-history-tab',
queueTab : '.x-queue-tab'
},
events: {
'click .x-history-tab' : '_showHistory',
'click .x-queue-tab' : '_showQueue'
},
initialize: function () {
this.collection = new HistoryCollection();
this.listenTo(this.collection, 'sync', this._showTable);
initialize: function (options) {
if (options.action) {
this.action = options.action.toLowerCase();
}
},
onShow: function () {
switch (this.action) {
case 'queue':
this._showQueue();
break;
default:
this._showHistory();
}
},
_showTable: function (collection) {
_navigate:function(route){
require(['Router'], function(){
App.Router.navigate(route);
});
},
this.history.show(new Backgrid.Grid({
columns : this.columns,
collection: collection,
className : 'table table-hover'
}));
_showHistory: function (e) {
if (e) {
e.preventDefault();
}
this.pager.show(new GridPager({
columns : this.columns,
collection: collection
}));
this.history.show(new HistoryTableLayout());
this.ui.historyTab.tab('show');
this._navigate('/history');
},
onShow: function () {
this.history.show(new LoadingView());
this.collection.fetch();
}
_showQueue: function (e) {
if (e) {
e.preventDefault();
}
this.queueRegion.show(new QueueLayout());
this.ui.queueTab.tab('show');
this._navigate('/history/queue');
}
});
});

@ -1,11 +1,9 @@
<div id="x-toolbar"/>
<div class="row">
<div class="span12">
<div id="x-history"/>
</div>
</div>
<div class="row">
<div class="span12">
<div id="x-pager"/>
</div>
</div>
<ul class="nav nav-tabs">
<li><a href="#history" class="x-history-tab no-router">History</a></li>
<li><a href="#queue" class="x-queue-tab no-router">Queue</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="history"></div>
<div class="tab-pane" id="queue"></div>
</div>

@ -1 +0,0 @@
<img src="favicon.ico" alt="{{indexer}}"/>

@ -0,0 +1,17 @@
'use strict';
define(
[
'backbone',
'History/Queue/QueueModel',
'Mixins/backbone.signalr.mixin'
], function (Backbone, QueueModel) {
var QueueCollection = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/queue',
model: QueueModel
});
var collection = new QueueCollection().bindSignalR();
collection.fetch();
return collection;
});

@ -0,0 +1,77 @@
'use strict';
define(
[
'marionette',
'backgrid',
'History/Queue/QueueCollection',
'Cells/SeriesTitleCell',
'Cells/EpisodeNumberCell',
'Cells/EpisodeTitleCell',
'Cells/QualityCell',
'History/Queue/TimeleftCell'
], function (Marionette,
Backgrid,
QueueCollection,
SeriesTitleCell,
EpisodeNumberCell,
EpisodeTitleCell,
QualityCell,
TimeleftCell) {
return Marionette.Layout.extend({
template: 'History/Queue/QueueLayoutTemplate',
regions: {
table: '#x-queue'
},
columns:
[
{
name : 'series',
label: 'Series',
cell : SeriesTitleCell
},
{
name : 'episode',
label : 'Episode',
sortable: false,
cell : EpisodeNumberCell
},
{
name : 'episode',
label : 'Episode Title',
sortable: false,
cell : EpisodeTitleCell
},
{
name : 'quality',
label : 'Quality',
cell : QualityCell,
sortable: false
},
{
name : 'timeleft',
label : 'Timeleft',
cell : TimeleftCell,
cellValue : 'this'
}
],
initialize: function () {
this.listenTo(QueueCollection, 'sync', this._showTable);
},
onShow: function () {
this._showTable();
},
_showTable: function () {
this.table.show(new Backgrid.Grid({
columns : this.columns,
collection: QueueCollection,
className : 'table table-hover'
}));
}
});
});

@ -0,0 +1,5 @@
<div class="row">
<div class="span12">
<div id="x-queue"/>
</div>
</div>

@ -0,0 +1,16 @@
'use strict';
define(
[
'backbone',
'Series/SeriesModel',
'Series/EpisodeModel'
], function (Backbone, SeriesModel, EpisodeModel) {
return Backbone.Model.extend({
parse: function (model) {
model.series = new SeriesModel(model.series);
model.episode = new EpisodeModel(model.episode);
model.episode.set('series', model.series);
return model;
}
});
});

@ -0,0 +1,27 @@
'use strict';
define(
[
'Cells/NzbDroneCell'
], function (NzbDroneCell) {
return NzbDroneCell.extend({
className: 'history-event-type-cell',
render: function () {
this.$el.empty();
if (this.cellValue) {
var timeleft = this.cellValue.get('timeleft');
var size = this.cellValue.get('size');
var sizeLeft = this.cellValue.get('sizeLeft');
this.$el.html(timeleft);
this.$el.attr('title', '{0} MB / {1} MB'.format(sizeLeft, size));
}
return this;
}
});
});

@ -0,0 +1,108 @@
'use strict';
define(
[
'marionette',
'backgrid',
'History/HistoryCollection',
'History/Table/EventTypeCell',
'Cells/SeriesTitleCell',
'Cells/EpisodeNumberCell',
'Cells/EpisodeTitleCell',
'Cells/QualityCell',
'Cells/RelativeDateCell',
'History/Table/HistoryDetailsCell',
'Shared/Grid/Pager',
'Shared/LoadingView'
], function (Marionette,
Backgrid,
HistoryCollection,
EventTypeCell,
SeriesTitleCell,
EpisodeNumberCell,
EpisodeTitleCell,
QualityCell,
RelativeDateCell,
HistoryDetailsCell,
GridPager,
LoadingView) {
return Marionette.Layout.extend({
template: 'History/Table/HistoryTableLayoutTemplate',
regions: {
history: '#x-history',
toolbar: '#x-toolbar',
pager : '#x-pager'
},
columns:
[
{
name : 'eventType',
label : '',
cell : EventTypeCell,
cellValue: 'this'
},
{
name : 'series',
label: 'Series',
cell : SeriesTitleCell
},
{
name : 'episode',
label : 'Episode',
sortable: false,
cell : EpisodeNumberCell
},
{
name : 'episode',
label : 'Episode Title',
sortable: false,
cell : EpisodeTitleCell
},
{
name : 'quality',
label : 'Quality',
cell : QualityCell,
sortable: false
},
{
name : 'date',
label: 'Date',
cell : RelativeDateCell
},
{
name : 'this',
label : '',
cell : HistoryDetailsCell,
sortable: false
}
],
initialize: function () {
this.collection = new HistoryCollection();
this.listenTo(this.collection, 'sync', this._showTable);
},
_showTable: function (collection) {
this.history.show(new Backgrid.Grid({
columns : this.columns,
collection: collection,
className : 'table table-hover'
}));
this.pager.show(new GridPager({
columns : this.columns,
collection: collection
}));
},
onShow: function () {
this.history.show(new LoadingView());
this.collection.fetch();
}
});
});

@ -0,0 +1,11 @@
<div id="x-toolbar"/>
<div class="row">
<div class="span12">
<div id="x-history"/>
</div>
</div>
<div class="row">
<div class="span12">
<div id="x-pager"/>
</div>
</div>

@ -13,6 +13,13 @@ define(
var processMessage = function (options) {
if (options.action === 'sync') {
console.log('sync received, refetching collection');
collection.fetch();
return;
}
var model = new collection.model(options.resource, {parse: true});
collection.add(model, {merge: true});
console.log(options.action + ': {0}}'.format(options.resource));

@ -8,6 +8,7 @@ require(
'Series/SeriesCollection',
'ProgressMessaging/ProgressMessageCollection',
'Commands/CommandMessengerCollectionView',
'History/Queue/QueueCollection',
'Navbar/NavbarView',
'jQuery/RouteBinder',
'jquery'
@ -18,6 +19,7 @@ require(
SeriesCollection,
ProgressMessageCollection,
CommandMessengerCollectionView,
QueueCollection,
NavbarView,
RouterBinder,
$) {
@ -36,6 +38,7 @@ require(
'settings/:action(/:query)' : 'settings',
'missing' : 'missing',
'history' : 'history',
'history/:action' : 'history',
'rss' : 'rss',
'system' : 'system',
'system/:action' : 'system',

@ -150,7 +150,6 @@ define(
},
_renameSeries: function () {
CommandController.Execute('renameSeries', {
name : 'renameSeries',
seriesId: this.model.id
@ -172,7 +171,7 @@ define(
this.seasonCollection = new SeasonCollection(this.model.get('seasons'));
this.episodeCollection = new EpisodeCollection({ seriesId: this.model.id }).bindSignalR();
this.episodeFileCollection = new EpisodeFileCollection({ seriesId: this.model.id });
this.episodeFileCollection = new EpisodeFileCollection({ seriesId: this.model.id }).bindSignalR();
$.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function () {
var seasonCollectionView = new SeasonCollectionView({

Loading…
Cancel
Save