From d416dd4177d266eb26320e81fd07d3122a837c91 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 9 Feb 2014 20:03:49 +0100 Subject: [PATCH] Repurposed the Missing page to include filter options and display episodes that haven't reached cutoff. --HG-- rename : src/NzbDrone.Api/Missing/MissingModule.cs => src/NzbDrone.Api/Wanted/MissingModule.cs rename : src/UI/Missing/ControlsColumnTemplate.html => src/UI/Wanted/ControlsColumnTemplate.html rename : src/UI/Missing/MissingCollection.js => src/UI/Wanted/Missing/MissingCollection.js rename : src/UI/Missing/MissingLayout.js => src/UI/Wanted/WantedLayout.js rename : src/UI/Missing/MissingLayoutTemplate.html => src/UI/Wanted/WantedLayoutTemplate.html extra : source : 2c76f3e423d39446f3bd7799b7344d7be63c70f5 --- src/NzbDrone.Api/NzbDrone.Api.csproj | 3 +- src/NzbDrone.Api/Wanted/CutoffModule.cs | 45 ++++ .../{Missing => Wanted}/MissingModule.cs | 12 +- .../EpisodesWithoutFilesFixture.cs | 10 +- src/NzbDrone.Core/Tv/EpisodeRepository.cs | 59 +++-- src/NzbDrone.Core/Tv/EpisodeService.cs | 40 +++- .../runConfigurations/Debug___Chrome.xml | 2 +- .../runConfigurations/Debug___Firefox.xml | 2 +- src/UI/Cells/EpisodeStatusCell.js | 17 ++ src/UI/Controller.js | 10 +- src/UI/Mixins/AsFilteredCollection.js | 2 +- src/UI/Navbar/NavbarTemplate.html | 4 +- src/UI/Router.js | 3 +- .../Radio/RadioButtonCollectionView.js | 12 +- .../ControlsColumnTemplate.html | 0 .../Cutoff/CutoffUnmetCollection.js} | 21 +- src/UI/Wanted/Cutoff/CutoffUnmetLayout.js | 206 ++++++++++++++++++ .../Cutoff/CutoffUnmetLayoutTemplate.html} | 0 src/UI/Wanted/Missing/MissingCollection.js | 54 +++++ src/UI/{ => Wanted}/Missing/MissingLayout.js | 66 +++++- .../Wanted/Missing/MissingLayoutTemplate.html | 11 + src/UI/Wanted/WantedLayout.js | 69 ++++++ src/UI/Wanted/WantedLayoutTemplate.html | 10 + 23 files changed, 596 insertions(+), 62 deletions(-) create mode 100644 src/NzbDrone.Api/Wanted/CutoffModule.cs rename src/NzbDrone.Api/{Missing => Wanted}/MissingModule.cs (68%) rename src/UI/{Missing => Wanted}/ControlsColumnTemplate.html (100%) rename src/UI/{Missing/MissingCollection.js => Wanted/Cutoff/CutoffUnmetCollection.js} (60%) create mode 100644 src/UI/Wanted/Cutoff/CutoffUnmetLayout.js rename src/UI/{Missing/MissingLayoutTemplate.html => Wanted/Cutoff/CutoffUnmetLayoutTemplate.html} (100%) create mode 100644 src/UI/Wanted/Missing/MissingCollection.js rename src/UI/{ => Wanted}/Missing/MissingLayout.js (67%) create mode 100644 src/UI/Wanted/Missing/MissingLayoutTemplate.html create mode 100644 src/UI/Wanted/WantedLayout.js create mode 100644 src/UI/Wanted/WantedLayoutTemplate.html diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 9ca8abe50..a4f7bf194 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -150,7 +150,8 @@ - + + diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs new file mode 100644 index 000000000..b68dd5f10 --- /dev/null +++ b/src/NzbDrone.Api/Wanted/CutoffModule.cs @@ -0,0 +1,45 @@ +using System.Linq; +using NzbDrone.Api.Episodes; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Api.Wanted +{ + public class CutoffModule : NzbDroneRestModule + { + private readonly IEpisodeService _episodeService; + private readonly SeriesRepository _seriesRepository; + + public CutoffModule(IEpisodeService episodeService, SeriesRepository seriesRepository) + :base("wanted/cutoff") + { + _episodeService = episodeService; + _seriesRepository = seriesRepository; + GetResourcePaged = GetCutoffUnmetEpisodes; + } + + private PagingResource GetCutoffUnmetEpisodes(PagingResource pagingResource) + { + var pagingSpec = new PagingSpec + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + pagingSpec.FilterExpression = v => v.Monitored == false || v.Series.Monitored == false; + else + pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true; + + PagingResource resource = ApplyToPage(_episodeService.GetCutoffUnmetEpisodes, pagingSpec); + + resource.Records = resource.Records.LoadSubtype(e => e.SeriesId, _seriesRepository).ToList(); + + return resource; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Missing/MissingModule.cs b/src/NzbDrone.Api/Wanted/MissingModule.cs similarity index 68% rename from src/NzbDrone.Api/Missing/MissingModule.cs rename to src/NzbDrone.Api/Wanted/MissingModule.cs index 968da7aab..4a5c34a90 100644 --- a/src/NzbDrone.Api/Missing/MissingModule.cs +++ b/src/NzbDrone.Api/Wanted/MissingModule.cs @@ -4,7 +4,7 @@ using NzbDrone.Api.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Tv; -namespace NzbDrone.Api.Missing +namespace NzbDrone.Api.Wanted { public class MissingModule : NzbDroneRestModule { @@ -12,7 +12,7 @@ namespace NzbDrone.Api.Missing private readonly SeriesRepository _seriesRepository; public MissingModule(IEpisodeService episodeService, SeriesRepository seriesRepository) - :base("missing") + :base("wanted/missing") { _episodeService = episodeService; _seriesRepository = seriesRepository; @@ -28,8 +28,14 @@ namespace NzbDrone.Api.Missing SortKey = pagingResource.SortKey, SortDirection = pagingResource.SortDirection }; + + if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + pagingSpec.FilterExpression = v => v.Monitored == false || v.Series.Monitored == false; + else + pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true; + + PagingResource resource = ApplyToPage(v => _episodeService.GetMissingEpisodes(v), pagingSpec); - var resource = ApplyToPage(_episodeService.EpisodesWithoutFiles, pagingSpec); resource.Records = resource.Records.LoadSubtype(e => e.SeriesId, _seriesRepository).ToList(); return resource; diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs index e59c67dec..50c4df605 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs @@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_get_monitored_episodes() { - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); + var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); episodes.Records.Should().HaveCount(1); } @@ -88,7 +88,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Ignore("Specials not implemented")] public void should_get_episode_including_specials() { - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, true); + var episodes = Subject.GetMissingEpisodes(_pagingSpec, true); episodes.Records.Should().HaveCount(2); } @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_not_include_unmonitored_episodes() { - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); + var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); episodes.Records.Should().NotContain(e => e.Monitored == false); } @@ -104,7 +104,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_not_contain_unmonitored_series() { - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); + var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); episodes.Records.Should().NotContain(e => e.SeriesId == _unmonitoredSeries.Id); } @@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_have_count_of_one() { - var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); + var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); episodes.TotalRecords.Should().Be(1); } diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index dcbe99e1e..d33f173ca 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -4,7 +4,7 @@ using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; - +using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Tv { @@ -17,7 +17,8 @@ namespace NzbDrone.Core.Tv List GetEpisodes(int seriesId); List GetEpisodes(int seriesId, int seasonNumber); List GetEpisodeByFileId(int fileId); - PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials); + PagingSpec GetMissingEpisodes(PagingSpec pagingSpec, bool includeSpecials); + List GetCutoffUnmetEpisodes(PagingSpec pagingSpec, bool includeSpecials); Episode FindEpisodeBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); List EpisodesBetweenDates(DateTime startDate, DateTime endDate); void SetMonitoredFlat(Episode episode, bool monitored); @@ -81,7 +82,7 @@ namespace NzbDrone.Core.Tv return Query.Where(e => e.EpisodeFileId == fileId).ToList(); } - public PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials) + public PagingSpec GetMissingEpisodes(PagingSpec pagingSpec, bool includeSpecials) { var currentTime = DateTime.UtcNow; var startingSeasonNumber = 1; @@ -91,12 +92,47 @@ namespace NzbDrone.Core.Tv startingSeasonNumber = 0; } - pagingSpec.Records = GetEpisodesWithoutFilesQuery(pagingSpec, currentTime, startingSeasonNumber).ToList(); - pagingSpec.TotalRecords = GetEpisodesWithoutFilesQuery(pagingSpec, currentTime, startingSeasonNumber).GetRowCount(); + pagingSpec.TotalRecords = GetMissingEpisodesQuery(pagingSpec, currentTime, startingSeasonNumber).GetRowCount(); + pagingSpec.Records = GetMissingEpisodesQuery(pagingSpec, currentTime, startingSeasonNumber).ToList(); return pagingSpec; } + public List GetCutoffUnmetEpisodes(PagingSpec pagingSpec, bool includeSpecials) + { + var currentTime = DateTime.UtcNow; + var startingSeasonNumber = 1; + + if (includeSpecials) + { + startingSeasonNumber = 0; + } + + var query = Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) + .Join(JoinType.Left, e => e.EpisodeFile, (e, s) => e.EpisodeFileId == s.Id) + .Where(pagingSpec.FilterExpression) + .AndWhere(e => e.EpisodeFileId != 0) + .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) + .AndWhere(e => e.AirDateUtc <= currentTime) + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()); + + return query.ToList(); + } + + private SortBuilder GetMissingEpisodesQuery(PagingSpec pagingSpec, DateTime currentTime, int startingSeasonNumber) + { + var query = Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) + .Where(pagingSpec.FilterExpression) + .AndWhere(e => e.EpisodeFileId == 0) + .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) + .AndWhere(e => e.AirDateUtc <= currentTime) + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize); + + return query; + } + public Episode FindEpisodeBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber) { return Query.Where(s => s.SeriesId == seriesId) @@ -141,18 +177,5 @@ namespace NzbDrone.Core.Tv { SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId); } - - private SortBuilder GetEpisodesWithoutFilesQuery(PagingSpec pagingSpec, DateTime currentTime, int startingSeasonNumber) - { - return Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) - .Where(e => e.EpisodeFileId == 0) - .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) - .AndWhere(e => e.AirDateUtc <= currentTime) - .AndWhere(e => e.Monitored) - .AndWhere(e => e.Series.Monitored) - .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) - .Skip(pagingSpec.PagingOffset()) - .Take(pagingSpec.PageSize); - } } } diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index df27033a9..82dcb84f6 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Tv { @@ -20,7 +21,8 @@ namespace NzbDrone.Core.Tv Episode FindEpisode(int seriesId, String date); List GetEpisodeBySeries(int seriesId); List GetEpisodesBySeason(int seriesId, int seasonNumber); - PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec); + PagingSpec GetMissingEpisodes(PagingSpec pagingSpec); + PagingSpec GetCutoffUnmetEpisodes(PagingSpec pagingSpec); List GetEpisodesByFileId(int episodeFileId); void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); @@ -40,12 +42,14 @@ namespace NzbDrone.Core.Tv { private readonly IEpisodeRepository _episodeRepository; + private readonly IQualityProfileRepository _qualityProfileRepository; private readonly IConfigService _configService; private readonly Logger _logger; - public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, Logger logger) + public EpisodeService(IEpisodeRepository episodeRepository, IQualityProfileRepository qualityProfileRepository, IConfigService configService, Logger logger) { _episodeRepository = episodeRepository; + _qualityProfileRepository = qualityProfileRepository; _configService = configService; _logger = logger; } @@ -88,7 +92,7 @@ namespace NzbDrone.Core.Tv { return _episodeRepository.GetEpisodes(seriesId, seasonNumber); } - + public Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle) { // TODO: can replace this search mechanism with something smarter/faster/better @@ -105,11 +109,39 @@ namespace NzbDrone.Core.Tv public PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec) { - var episodeResult = _episodeRepository.EpisodesWithoutFiles(pagingSpec, false); + var episodeResult = _episodeRepository.GetMissingEpisodes(pagingSpec, false); return episodeResult; } + public PagingSpec GetCutoffUnmetEpisodes(PagingSpec pagingSpec) + { + var allSpec = new PagingSpec + { + SortKey = pagingSpec.SortKey, + SortDirection = pagingSpec.SortDirection, + FilterExpression = pagingSpec.FilterExpression + }; + + var allItems = _episodeRepository.GetCutoffUnmetEpisodes(allSpec, false); + + var qualityProfileComparers = _qualityProfileRepository.All().ToDictionary(v => v.Id, v => new { Profile = v, Comparer = new QualityModelComparer(v) }); + + var filtered = allItems.Where(episode => + { + var profile = qualityProfileComparers[episode.Series.QualityProfileId]; + return profile.Comparer.Compare(episode.EpisodeFile.Value.Quality.Quality, profile.Profile.Cutoff) < 0; + }).ToList(); + + pagingSpec.Records = filtered + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize) + .ToList(); + pagingSpec.TotalRecords = filtered.Count; + + return pagingSpec; + } + public List GetEpisodesByFileId(int episodeFileId) { return _episodeRepository.GetEpisodeByFileId(episodeFileId); diff --git a/src/UI/.idea/runConfigurations/Debug___Chrome.xml b/src/UI/.idea/runConfigurations/Debug___Chrome.xml index 82eb4863d..47bd06dc9 100644 --- a/src/UI/.idea/runConfigurations/Debug___Chrome.xml +++ b/src/UI/.idea/runConfigurations/Debug___Chrome.xml @@ -6,7 +6,7 @@ - + diff --git a/src/UI/.idea/runConfigurations/Debug___Firefox.xml b/src/UI/.idea/runConfigurations/Debug___Firefox.xml index 2e020afbc..d9e99acc3 100644 --- a/src/UI/.idea/runConfigurations/Debug___Firefox.xml +++ b/src/UI/.idea/runConfigurations/Debug___Firefox.xml @@ -6,7 +6,7 @@ - + diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js index d8ab15259..f54f2c474 100644 --- a/src/UI/Cells/EpisodeStatusCell.js +++ b/src/UI/Cells/EpisodeStatusCell.js @@ -52,7 +52,24 @@ define( return; } + else if (hasFile && this.model.get('episodeFile')) { + var episodeFile = this.model.get('episodeFile'); + + var quality = episodeFile.quality; + var size = FormatHelpers.bytes(episodeFile.size); + var title = 'Episode downloaded'; + if (quality.proper) { + title += ' [PROPER] - {0}'.format(size); + this.$el.html('{1}'.format(title, quality.quality.name)); + } + else { + title += ' - {0}'.format(size); + this.$el.html('{1}'.format(title, quality.quality.name)); + } + + return; + } else { var model = this.model; var downloading = QueueCollection.findEpisode(model.get('id')); diff --git a/src/UI/Controller.js b/src/UI/Controller.js index db0b13e2b..19d7c3760 100644 --- a/src/UI/Controller.js +++ b/src/UI/Controller.js @@ -7,7 +7,7 @@ define( 'History/HistoryLayout', 'Settings/SettingsLayout', 'AddSeries/AddSeriesLayout', - 'Missing/MissingLayout', + 'Wanted/WantedLayout', 'Calendar/CalendarLayout', 'Release/ReleaseLayout', 'System/SystemLayout', @@ -20,7 +20,7 @@ define( HistoryLayout, SettingsLayout, AddSeriesLayout, - MissingLayout, + WantedLayout, CalendarLayout, ReleaseLayout, SystemLayout, @@ -44,10 +44,10 @@ define( this.showMainRegion(new SettingsLayout({ action: action })); }, - missing: function () { - this.setTitle('Missing'); + wanted: function (action) { + this.setTitle('Wanted'); - this.showMainRegion(new MissingLayout()); + this.showMainRegion(new WantedLayout({ action: action })); }, history: function (action) { diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js index 2a0e17991..469059cfc 100644 --- a/src/UI/Mixins/AsFilteredCollection.js +++ b/src/UI/Mixins/AsFilteredCollection.js @@ -24,7 +24,7 @@ define( }; this.prototype.setFilterMode = function(mode, options) { - this.setFilter(this.filterModes[mode], options); + return this.setFilter(this.filterModes[mode], options); }; var originalMakeFullCollection = this.prototype._makeFullCollection; diff --git a/src/UI/Navbar/NavbarTemplate.html b/src/UI/Navbar/NavbarTemplate.html index 19284def2..fdc1128b7 100644 --- a/src/UI/Navbar/NavbarTemplate.html +++ b/src/UI/Navbar/NavbarTemplate.html @@ -29,10 +29,10 @@
  • - +
    - Missing + Wanted
  • diff --git a/src/UI/Router.js b/src/UI/Router.js index f2927787a..6c268a7ef 100644 --- a/src/UI/Router.js +++ b/src/UI/Router.js @@ -14,7 +14,8 @@ define( 'calendar' : 'calendar', 'settings' : 'settings', 'settings/:action(/:query)' : 'settings', - 'missing' : 'missing', + 'wanted' : 'wanted', + 'wanted/:action' : 'wanted', 'history' : 'history', 'history/:action' : 'history', 'rss' : 'rss', diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js index 240cd1445..e3793b341 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js @@ -16,13 +16,17 @@ define( initialize: function (options) { this.menu = options.menu; - if (this.menu.storeState) { - this.setActive(); - } + this.setActive(); }, setActive: function () { - var storedKey = Config.getValue(this.menu.menuKey, this.menu.defaultAction); + var storedKey = this.menu.defaultAction; + + if (this.menu.storeState) + storedKey = Config.getValue(this.menu.menuKey, storedKey); + + if (!storedKey) + return; this.collection.each(function (model) { if (model.get('key').toLocaleLowerCase() === storedKey.toLowerCase()) { diff --git a/src/UI/Missing/ControlsColumnTemplate.html b/src/UI/Wanted/ControlsColumnTemplate.html similarity index 100% rename from src/UI/Missing/ControlsColumnTemplate.html rename to src/UI/Wanted/ControlsColumnTemplate.html diff --git a/src/UI/Missing/MissingCollection.js b/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js similarity index 60% rename from src/UI/Missing/MissingCollection.js rename to src/UI/Wanted/Cutoff/CutoffUnmetCollection.js index d58b6d133..a42c12dba 100644 --- a/src/UI/Missing/MissingCollection.js +++ b/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js @@ -1,19 +1,21 @@ 'use strict'; define( [ + 'underscore', 'Series/EpisodeModel', 'backbone.pageable', + 'Mixins/AsFilteredCollection', 'Mixins/AsPersistedStateCollection' - ], function (EpisodeModel, PagableCollection, AsPersistedStateCollection) { + ], function (_, EpisodeModel, PagableCollection, AsFilteredCollection, AsPersistedStateCollection) { var collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/missing', + url : window.NzbDrone.ApiRoot + '/wanted/cutoff', model: EpisodeModel, - tableName: 'missing', + tableName: 'wanted.cutoff', state: { - pageSize: 15, - sortKey : 'airDateUtc', - order : 1 + pageSize : 15, + sortKey : 'airDateUtc', + order : 1 }, queryParams: { @@ -27,6 +29,12 @@ define( '1' : 'desc' } }, + + // Filter Modes + filterModes: { + 'monitored' : ['monitored', 'true'], + 'unmonitored' : ['monitored', 'false'], + }, parseState: function (resp) { return {totalRecords: resp.totalRecords}; @@ -41,5 +49,6 @@ define( } }); + collection = AsFilteredCollection.call(collection); return AsPersistedStateCollection.call(collection); }); diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js new file mode 100644 index 000000000..aeca14fbb --- /dev/null +++ b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js @@ -0,0 +1,206 @@ +'use strict'; +define( + [ + 'underscore', + 'marionette', + 'backgrid', + 'Wanted/Cutoff/CutoffUnmetCollection', + 'Cells/SeriesTitleCell', + 'Cells/EpisodeNumberCell', + 'Cells/EpisodeTitleCell', + 'Cells/RelativeDateCell', + 'Cells/EpisodeStatusCell', + 'Shared/Grid/Pager', + 'Shared/Toolbar/ToolbarLayout', + 'Shared/LoadingView', + 'Shared/Messenger', + 'Commands/CommandController', + 'backgrid.selectall' + ], function (_, + Marionette, + Backgrid, + CutoffUnmetCollection, + SeriesTitleCell, + EpisodeNumberCell, + EpisodeTitleCell, + RelativeDateCell, + EpisodeStatusCell, + GridPager, + ToolbarLayout, + LoadingView, + Messenger, + CommandController) { + return Marionette.Layout.extend({ + template: 'Wanted/Cutoff/CutoffUnmetLayoutTemplate', + + regions: { + missing: '#x-missing', + toolbar: '#x-toolbar', + pager : '#x-pager' + }, + + ui: { + searchSelectedButton: '.btn i.icon-search' + }, + + columns: + [ + { + name : '', + cell : 'select-row', + headerCell: 'select-all', + sortable : false + }, + { + name : 'series', + label : 'Series Title', + sortable : false, + cell : SeriesTitleCell + }, + { + name : 'this', + label : 'Episode', + sortable : false, + cell : EpisodeNumberCell + }, + { + name : 'this', + label : 'Episode Title', + sortable : false, + cell : EpisodeTitleCell, + }, + { + name : 'airDateUtc', + label : 'Air Date', + cell : RelativeDateCell + }, + { + name : 'status', + label : 'Status', + cell : EpisodeStatusCell, + sortable: false + } + ], + + initialize: function () { + this.collection = new CutoffUnmetCollection(); + + this.listenTo(this.collection, 'sync', this._showTable); + }, + + onShow: function () { + this.missing.show(new LoadingView()); + this._showToolbar(); + this.collection.fetch(); + }, + + _showTable: function () { + this.missingGrid = new Backgrid.Grid({ + columns : this.columns, + collection: this.collection, + className : 'table table-hover' + }); + + this.missing.show(this.missingGrid); + + this.pager.show(new GridPager({ + columns : this.columns, + collection: this.collection + })); + }, + + _showToolbar: function () { + var leftSideButtons = { + type : 'default', + storeState: false, + items : + [ + { + title: 'Search Selected', + icon : 'icon-search', + callback: this._searchSelected, + ownerContext: this + }, + { + title: 'Season Pass', + icon : 'icon-bookmark', + route: 'seasonpass' + } + ] + }; + + var filterOptions = { + type : 'radio', + storeState : false, + menuKey : 'wanted.filterMode', + defaultAction : 'monitored', + items : + [ + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-nd-monitored', + callback : this._setFilter + }, + { + key : 'unmonitored', + title : '', + tooltip : 'Unmonitored Only', + icon : 'icon-nd-unmonitored', + callback : this._setFilter + }, + ] + }; + + this.toolbar.show(new ToolbarLayout({ + left : + [ + leftSideButtons + ], + right : + [ + filterOptions + ], + context: this + })); + + CommandController.bindToCommand({ + element: this.$('.x-toolbar-left-1 .btn i.icon-search'), + command: { + name: 'episodeSearch' + } + }); + }, + + _setFilter: function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.collection.state.currentPage = 1; + var promise = this.collection.setFilterMode(mode); + + if (buttonContext) + buttonContext.ui.icon.spinForPromise(promise); + }, + + _searchSelected: function () { + var selected = this.missingGrid.getSelectedModels(); + + if (selected.length === 0) { + Messenger.show({ + type: 'error', + message: 'No episodes selected' + }); + + return; + } + + var ids = _.pluck(selected, 'id'); + + CommandController.Execute('episodeSearch', { + name : 'episodeSearch', + episodeIds: ids + }); + } + }); + }); diff --git a/src/UI/Missing/MissingLayoutTemplate.html b/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html similarity index 100% rename from src/UI/Missing/MissingLayoutTemplate.html rename to src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.html diff --git a/src/UI/Wanted/Missing/MissingCollection.js b/src/UI/Wanted/Missing/MissingCollection.js new file mode 100644 index 000000000..61564359f --- /dev/null +++ b/src/UI/Wanted/Missing/MissingCollection.js @@ -0,0 +1,54 @@ +'use strict'; +define( + [ + 'underscore', + 'Series/EpisodeModel', + 'backbone.pageable', + 'Mixins/AsFilteredCollection', + 'Mixins/AsPersistedStateCollection' + ], function (_, EpisodeModel, PagableCollection, AsFilteredCollection, AsPersistedStateCollection) { + var collection = PagableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/wanted/missing', + model: EpisodeModel, + tableName: 'wanted.missing', + + state: { + pageSize : 15, + sortKey : 'airDateUtc', + order : 1 + }, + + queryParams: { + totalPages : null, + totalRecords: null, + pageSize : 'pageSize', + sortKey : 'sortKey', + order : 'sortDir', + directions : { + '-1': 'asc', + '1' : 'desc' + } + }, + + // Filter Modes + filterModes: { + 'monitored' : ['monitored', 'true'], + 'unmonitored' : ['monitored', 'false'], + }, + + parseState: function (resp) { + return {totalRecords: resp.totalRecords}; + }, + + parseRecords: function (resp) { + if (resp) { + return resp.records; + } + + return resp; + } + }); + + collection = AsFilteredCollection.call(collection); + return AsPersistedStateCollection.call(collection); + }); diff --git a/src/UI/Missing/MissingLayout.js b/src/UI/Wanted/Missing/MissingLayout.js similarity index 67% rename from src/UI/Missing/MissingLayout.js rename to src/UI/Wanted/Missing/MissingLayout.js index 7240277f5..bd8f20843 100644 --- a/src/UI/Missing/MissingLayout.js +++ b/src/UI/Wanted/Missing/MissingLayout.js @@ -4,11 +4,12 @@ define( 'underscore', 'marionette', 'backgrid', - 'Missing/MissingCollection', + 'Wanted/Missing/MissingCollection', 'Cells/SeriesTitleCell', 'Cells/EpisodeNumberCell', 'Cells/EpisodeTitleCell', 'Cells/RelativeDateCell', + 'Cells/EpisodeStatusCell', 'Shared/Grid/Pager', 'Shared/Toolbar/ToolbarLayout', 'Shared/LoadingView', @@ -23,13 +24,14 @@ define( EpisodeNumberCell, EpisodeTitleCell, RelativeDateCell, + EpisodeStatusCell, GridPager, ToolbarLayout, LoadingView, Messenger, CommandController) { return Marionette.Layout.extend({ - template: 'Missing/MissingLayoutTemplate', + template: 'Wanted/Missing/MissingLayoutTemplate', regions: { missing: '#x-missing', @@ -52,25 +54,31 @@ define( { name : 'series', label : 'Series Title', - sortable: false, + sortable : false, cell : SeriesTitleCell }, { name : 'this', label : 'Episode', - sortable: false, + sortable : false, cell : EpisodeNumberCell }, { name : 'this', label : 'Episode Title', - sortable: false, - cell : EpisodeTitleCell + sortable : false, + cell : EpisodeTitleCell, }, { - name : 'airDateUtc', - label: 'Air Date', - cell : RelativeDateCell + name : 'airDateUtc', + label : 'Air Date', + cell : RelativeDateCell + }, + { + name : 'status', + label : 'Status', + cell : EpisodeStatusCell, + sortable: false } ], @@ -82,8 +90,8 @@ define( onShow: function () { this.missing.show(new LoadingView()); - this.collection.fetch(); this._showToolbar(); + this.collection.fetch(); }, _showTable: function () { @@ -120,12 +128,40 @@ define( } ] }; + + var filterOptions = { + type : 'radio', + storeState : false, + menuKey : 'wanted.filterMode', + defaultAction : 'monitored', + items : + [ + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-nd-monitored', + callback : this._setFilter + }, + { + key : 'unmonitored', + title : '', + tooltip : 'Unmonitored Only', + icon : 'icon-nd-unmonitored', + callback : this._setFilter + }, + ] + }; this.toolbar.show(new ToolbarLayout({ left : [ leftSideButtons ], + right : + [ + filterOptions + ], context: this })); @@ -136,6 +172,16 @@ define( } }); }, + + _setFilter: function(buttonContext) { + var mode = buttonContext.model.get('key'); + + this.collection.state.currentPage = 1; + var promise = this.collection.setFilterMode(mode); + + if (buttonContext) + buttonContext.ui.icon.spinForPromise(promise); + }, _searchSelected: function () { var selected = this.missingGrid.getSelectedModels(); diff --git a/src/UI/Wanted/Missing/MissingLayoutTemplate.html b/src/UI/Wanted/Missing/MissingLayoutTemplate.html new file mode 100644 index 000000000..958d5aa5e --- /dev/null +++ b/src/UI/Wanted/Missing/MissingLayoutTemplate.html @@ -0,0 +1,11 @@ +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/src/UI/Wanted/WantedLayout.js b/src/UI/Wanted/WantedLayout.js new file mode 100644 index 000000000..bca0b3435 --- /dev/null +++ b/src/UI/Wanted/WantedLayout.js @@ -0,0 +1,69 @@ +'use strict'; +define( + [ + 'marionette', + 'backbone', + 'backgrid', + 'Wanted/Missing/MissingLayout', + 'Wanted/Cutoff/CutoffUnmetLayout' + ], function (Marionette, Backbone, Backgrid, MissingLayout, CutoffUnmetLayout) { + return Marionette.Layout.extend({ + template: 'Wanted/WantedLayoutTemplate', + + regions: { + content : '#content' + //missing : '#missing', + //cutoff : '#cutoff' + }, + + ui: { + missingTab : '.x-missing-tab', + cutoffTab : '.x-cutoff-tab' + }, + + events: { + 'click .x-missing-tab' : '_showMissing', + 'click .x-cutoff-tab' : '_showCutoffUnmet' + }, + + initialize: function (options) { + if (options.action) { + this.action = options.action.toLowerCase(); + } + }, + + onShow: function () { + switch (this.action) { + case 'cutoff': + this._showCutoffUnmet(); + break; + default: + this._showMissing(); + } + }, + + _navigate: function (route) { + Backbone.history.navigate(route); + }, + + _showMissing: function (e) { + if (e) { + e.preventDefault(); + } + + this.content.show(new MissingLayout()); + this.ui.missingTab.tab('show'); + this._navigate('/wanted/missing'); + }, + + _showCutoffUnmet: function (e) { + if (e) { + e.preventDefault(); + } + + this.content.show(new CutoffUnmetLayout()); + this.ui.cutoffTab.tab('show'); + this._navigate('/wanted/cutoff'); + } + }); + }); diff --git a/src/UI/Wanted/WantedLayoutTemplate.html b/src/UI/Wanted/WantedLayoutTemplate.html new file mode 100644 index 000000000..6665fb3d1 --- /dev/null +++ b/src/UI/Wanted/WantedLayoutTemplate.html @@ -0,0 +1,10 @@ + + +
    + \ No newline at end of file