diff --git a/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs b/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs index 15b4e65ed..f9df68dee 100644 --- a/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs +++ b/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs @@ -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 + public class EpisodeModule : NzbDroneRestModuleWithSignalR, + IHandle { 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 @@ private void SetQuality(EpisodeFileResource episodeFileResource) episodeFile.Quality = episodeFileResource.Quality; _mediaFileService.Update(episodeFile); } + + public void Handle(EpisodeFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id); + } } } \ No newline at end of file diff --git a/NzbDrone.Api/Episodes/EpisodeModule.cs b/NzbDrone.Api/Episodes/EpisodeModule.cs index d057b3658..aaab212a8 100644 --- a/NzbDrone.Api/Episodes/EpisodeModule.cs +++ b/NzbDrone.Api/Episodes/EpisodeModule.cs @@ -54,7 +54,10 @@ public void Handle(EpisodeGrabbedEvent message) { foreach (var episode in message.Episode.Episodes) { - BroadcastResourceChange(ModelAction.Updated, episode.Id); + var resource = episode.InjectTo(); + resource.Downloading = true; + + BroadcastResourceChange(ModelAction.Updated, resource); } } diff --git a/NzbDrone.Api/Episodes/EpisodeResource.cs b/NzbDrone.Api/Episodes/EpisodeResource.cs index 03ae675c5..a767ef9e6 100644 --- a/NzbDrone.Api/Episodes/EpisodeResource.cs +++ b/NzbDrone.Api/Episodes/EpisodeResource.cs @@ -26,5 +26,7 @@ public class EpisodeResource : RestResource public DateTime? GrabDate { get; set; } public Core.Tv.Series Series { get; set; } public String SeriesTitle { get; set; } + + public Boolean Downloading { get; set; } } } diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index b88c2277d..8de55e952 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -126,6 +126,8 @@ + + diff --git a/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs b/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs index 2a70103b1..bb35273df 100644 --- a/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs +++ b/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs @@ -34,6 +34,17 @@ public void Handle(ModelEvent message) BroadcastResourceChange(message.Action, message.Model.Id); } + protected void BroadcastResourceChange(ModelAction action, TResource resource) + { + var signalRMessage = new SignalRMessage + { + Name = Resource, + Body = new ResourceChangeMessage(resource, action) + }; + + _commandExecutor.PublishCommand(new BroadcastSignalRMessage(signalRMessage)); + } + protected void BroadcastResourceChange(ModelAction action, int id) { var resource = GetResourceById(id); diff --git a/NzbDrone.Api/Queue/QueueModule.cs b/NzbDrone.Api/Queue/QueueModule.cs new file mode 100644 index 000000000..efc5ac7ff --- /dev/null +++ b/NzbDrone.Api/Queue/QueueModule.cs @@ -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, + IHandle + { + private readonly IQueueService _queueService; + + public QueueModule(ICommandExecutor commandExecutor, IQueueService queueService) + : base(commandExecutor) + { + _queueService = queueService; + GetResourceAll = GetQueue; + } + + private List GetQueue() + { + return ToListResource(_queueService.GetQueue); + } + + public void Handle(UpdateQueueEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/Queue/QueueResource.cs b/NzbDrone.Api/Queue/QueueResource.cs new file mode 100644 index 000000000..4d91534dc --- /dev/null +++ b/NzbDrone.Api/Queue/QueueResource.cs @@ -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; } + } +} diff --git a/NzbDrone.Api/ResourceChangeMessage.cs b/NzbDrone.Api/ResourceChangeMessage.cs index 6b7efb50a..6319dcc39 100644 --- a/NzbDrone.Api/ResourceChangeMessage.cs +++ b/NzbDrone.Api/ResourceChangeMessage.cs @@ -11,7 +11,7 @@ public class ResourceChangeMessage where TResource : RestResource 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"); } diff --git a/NzbDrone.Core/Datastore/Events/ModelEvent.cs b/NzbDrone.Core/Datastore/Events/ModelEvent.cs index 91f74887b..72ff55296 100644 --- a/NzbDrone.Core/Datastore/Events/ModelEvent.cs +++ b/NzbDrone.Core/Datastore/Events/ModelEvent.cs @@ -22,6 +22,4 @@ public enum ModelAction Deleted = 3, Sync = 4 } - - } \ No newline at end of file diff --git a/NzbDrone.Core/Download/SabQueueItem.cs b/NzbDrone.Core/Download/QueueItem.cs similarity index 98% rename from NzbDrone.Core/Download/SabQueueItem.cs rename to NzbDrone.Core/Download/QueueItem.cs index eed0879ab..f9de11ff0 100644 --- a/NzbDrone.Core/Download/SabQueueItem.cs +++ b/NzbDrone.Core/Download/QueueItem.cs @@ -4,14 +4,10 @@ namespace NzbDrone.Core.Download { public class QueueItem { - public decimal Size { get; set; } - - public string Title { get; set; } - - public decimal SizeLeft { get; set; } - public string Id { get; set; } - + public decimal Size { get; set; } + public string Title { get; set; } + public decimal SizeLeft { get; set; } public TimeSpan Timeleft { get; set; } } } diff --git a/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index a725176e7..6b56d4d00 100644 --- a/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -70,12 +70,16 @@ public List Import(List decisions, bool newDownl { 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) { diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 23d3657e3..3c48dd31d 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -110,6 +110,7 @@ + @@ -216,7 +217,7 @@ - + @@ -419,6 +420,10 @@ + + + + diff --git a/NzbDrone.Core/Queue/Queue.cs b/NzbDrone.Core/Queue/Queue.cs new file mode 100644 index 000000000..7b82aeb80 --- /dev/null +++ b/NzbDrone.Core/Queue/Queue.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; } + } +} diff --git a/NzbDrone.Core/Queue/QueueScheduler.cs b/NzbDrone.Core/Queue/QueueScheduler.cs new file mode 100644 index 000000000..bce64d8f3 --- /dev/null +++ b/NzbDrone.Core/Queue/QueueScheduler.cs @@ -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, + IHandle + { + 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(); + } + } +} diff --git a/NzbDrone.Core/Queue/QueueService.cs b/NzbDrone.Core/Queue/QueueService.cs new file mode 100644 index 000000000..019645c6e --- /dev/null +++ b/NzbDrone.Core/Queue/QueueService.cs @@ -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 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 GetQueue() + { + var downloadClient = _downloadClientProvider.GetDownloadClient(); + var queueItems = downloadClient.GetQueue(); + + return MapQueue(queueItems); + } + + private List MapQueue(IEnumerable queueItems) + { + var queued = new List(); + + 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; + } + } +} diff --git a/NzbDrone.Core/Queue/UpdateQueueEvent.cs b/NzbDrone.Core/Queue/UpdateQueueEvent.cs new file mode 100644 index 000000000..38535a89d --- /dev/null +++ b/NzbDrone.Core/Queue/UpdateQueueEvent.cs @@ -0,0 +1,8 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Queue +{ + public class UpdateQueueEvent : IEvent + { + } +} diff --git a/UI/Cells/EpisodeNumberCell.js b/UI/Cells/EpisodeNumberCell.js index 85be40255..0daf55b81 100644 --- a/UI/Cells/EpisodeNumberCell.js +++ b/UI/Cells/EpisodeNumberCell.js @@ -1,4 +1,4 @@ -'use strict'; + 'use strict'; define( [ diff --git a/UI/Cells/EpisodeStatusCell.js b/UI/Cells/EpisodeStatusCell.js index 091064380..a8f5c8c00 100644 --- a/UI/Cells/EpisodeStatusCell.js +++ b/UI/Cells/EpisodeStatusCell.js @@ -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('{1}'.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(''.format(icon, tooltip)); } - - return this; } }); }); diff --git a/UI/Controller.js b/UI/Controller.js index d252b0f32..97efe7b3d 100644 --- a/UI/Controller.js +++ b/UI/Controller.js @@ -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 () { diff --git a/UI/History/Collection.js b/UI/History/HistoryCollection.js similarity index 97% rename from UI/History/Collection.js rename to UI/History/HistoryCollection.js index e3f19ca04..821dd086b 100644 --- a/UI/History/Collection.js +++ b/UI/History/HistoryCollection.js @@ -1,7 +1,7 @@ 'use strict'; define( [ - 'History/Model', + 'History/HistoryModel', 'backbone.pageable' ], function (HistoryModel, PageableCollection) { return PageableCollection.extend({ diff --git a/UI/History/HistoryLayout.js b/UI/History/HistoryLayout.js index bf3d9928b..c055984f8 100644 --- a/UI/History/HistoryLayout.js +++ b/UI/History/HistoryLayout.js @@ -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 - } - ], - - - initialize: function () { - this.collection = new HistoryCollection(); - this.listenTo(this.collection, 'sync', this._showTable); + ui: { + historyTab: '.x-history-tab', + queueTab : '.x-queue-tab' }, + events: { + 'click .x-history-tab' : '_showHistory', + 'click .x-queue-tab' : '_showQueue' + }, - _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 - })); + initialize: function (options) { + if (options.action) { + this.action = options.action.toLowerCase(); + } }, onShow: function () { - this.history.show(new LoadingView()); - this.collection.fetch(); - } + switch (this.action) { + case 'queue': + this._showQueue(); + break; + default: + this._showHistory(); + } + }, + _navigate:function(route){ + require(['Router'], function(){ + App.Router.navigate(route); + }); + }, + + _showHistory: function (e) { + if (e) { + e.preventDefault(); + } + + this.history.show(new HistoryTableLayout()); + this.ui.historyTab.tab('show'); + this._navigate('/history'); + }, + + _showQueue: function (e) { + if (e) { + e.preventDefault(); + } + + this.queueRegion.show(new QueueLayout()); + this.ui.queueTab.tab('show'); + this._navigate('/history/queue'); + } }); }); diff --git a/UI/History/HistoryLayoutTemplate.html b/UI/History/HistoryLayoutTemplate.html index ae43e6692..50c981bf0 100644 --- a/UI/History/HistoryLayoutTemplate.html +++ b/UI/History/HistoryLayoutTemplate.html @@ -1,11 +1,9 @@ -
-
-
-
-
-
-
-
-
-
-
+ + +
+
+
+
\ No newline at end of file diff --git a/UI/History/Model.js b/UI/History/HistoryModel.js similarity index 100% rename from UI/History/Model.js rename to UI/History/HistoryModel.js diff --git a/UI/History/IndexerTemplate.html b/UI/History/IndexerTemplate.html deleted file mode 100644 index 5f493178b..000000000 --- a/UI/History/IndexerTemplate.html +++ /dev/null @@ -1 +0,0 @@ -{{indexer}} \ No newline at end of file diff --git a/UI/History/Queue/QueueCollection.js b/UI/History/Queue/QueueCollection.js new file mode 100644 index 000000000..d7340341d --- /dev/null +++ b/UI/History/Queue/QueueCollection.js @@ -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; + }); diff --git a/UI/History/Queue/QueueLayout.js b/UI/History/Queue/QueueLayout.js new file mode 100644 index 000000000..d397d4fd7 --- /dev/null +++ b/UI/History/Queue/QueueLayout.js @@ -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' + })); + } + }); + }); diff --git a/UI/History/Queue/QueueLayoutTemplate.html b/UI/History/Queue/QueueLayoutTemplate.html new file mode 100644 index 000000000..113673518 --- /dev/null +++ b/UI/History/Queue/QueueLayoutTemplate.html @@ -0,0 +1,5 @@ +
+
+
+
+
diff --git a/UI/History/Queue/QueueModel.js b/UI/History/Queue/QueueModel.js new file mode 100644 index 000000000..4af579646 --- /dev/null +++ b/UI/History/Queue/QueueModel.js @@ -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; + } + }); + }); diff --git a/UI/History/Queue/TimeleftCell.js b/UI/History/Queue/TimeleftCell.js new file mode 100644 index 000000000..00b924398 --- /dev/null +++ b/UI/History/Queue/TimeleftCell.js @@ -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; + } + }); + }); diff --git a/UI/History/ControlsColumnTemplate.html b/UI/History/Table/ControlsColumnTemplate.html similarity index 100% rename from UI/History/ControlsColumnTemplate.html rename to UI/History/Table/ControlsColumnTemplate.html diff --git a/UI/History/EventTypeCell.js b/UI/History/Table/EventTypeCell.js similarity index 100% rename from UI/History/EventTypeCell.js rename to UI/History/Table/EventTypeCell.js diff --git a/UI/History/HistoryDetailsCell.js b/UI/History/Table/HistoryDetailsCell.js similarity index 100% rename from UI/History/HistoryDetailsCell.js rename to UI/History/Table/HistoryDetailsCell.js diff --git a/UI/History/Table/HistoryTableLayout.js b/UI/History/Table/HistoryTableLayout.js new file mode 100644 index 000000000..1ab551fe7 --- /dev/null +++ b/UI/History/Table/HistoryTableLayout.js @@ -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(); + } + + }); + }); diff --git a/UI/History/Table/HistoryTableLayoutTemplate.html b/UI/History/Table/HistoryTableLayoutTemplate.html new file mode 100644 index 000000000..ae43e6692 --- /dev/null +++ b/UI/History/Table/HistoryTableLayoutTemplate.html @@ -0,0 +1,11 @@ +
+
+
+
+
+
+
+
+
+
+
diff --git a/UI/Mixins/backbone.signalr.mixin.js b/UI/Mixins/backbone.signalr.mixin.js index e0154e6af..759488776 100644 --- a/UI/Mixins/backbone.signalr.mixin.js +++ b/UI/Mixins/backbone.signalr.mixin.js @@ -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)); diff --git a/UI/Router.js b/UI/Router.js index f792762b5..a37039da1 100644 --- a/UI/Router.js +++ b/UI/Router.js @@ -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', diff --git a/UI/Series/Details/SeriesDetailsLayout.js b/UI/Series/Details/SeriesDetailsLayout.js index ea4024675..5e0066564 100644 --- a/UI/Series/Details/SeriesDetailsLayout.js +++ b/UI/Series/Details/SeriesDetailsLayout.js @@ -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({