From 935c26d03e5061a13ca223cd70ff9da6a7c9cc71 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 9 Feb 2014 20:02:49 +0100 Subject: [PATCH 01/11] Support for loading of LazyLoaded properties via explicit join. --HG-- extra : source : 1b7c96cb2a644ae17603c9d72ec09d94bc877bf1 --- src/Marr.Data/EntityGraph.cs | 13 ++++++++++++- src/Marr.Data/QGen/QueryBuilder.cs | 11 +++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Marr.Data/EntityGraph.cs b/src/Marr.Data/EntityGraph.cs index aee376b61..72d28dcdf 100644 --- a/src/Marr.Data/EntityGraph.cs +++ b/src/Marr.Data/EntityGraph.cs @@ -160,6 +160,14 @@ namespace Marr.Data get { return _children; } } + /// + /// Adds an Child in the graph for LazyLoaded property. + /// + public void AddLazyRelationship(Relationship childRelationship) + { + _children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType.GetGenericArguments()[0], this, childRelationship)); + } + /// /// Adds an entity to the appropriate place in the object graph. /// @@ -182,7 +190,10 @@ namespace Marr.Data } else // RelationTypes.One { - _relationship.Setter(_parent._entity, entityInstance); + if (_relationship.IsLazyLoaded) + _relationship.Setter(_parent._entity, Activator.CreateInstance(_relationship.MemberType, entityInstance)); + else + _relationship.Setter(_parent._entity, entityInstance); } EntityReference entityRef = new EntityReference(entityInstance); diff --git a/src/Marr.Data/QGen/QueryBuilder.cs b/src/Marr.Data/QGen/QueryBuilder.cs index cd71c17bd..d4cddf45f 100644 --- a/src/Marr.Data/QGen/QueryBuilder.cs +++ b/src/Marr.Data/QGen/QueryBuilder.cs @@ -551,6 +551,17 @@ namespace Marr.Data.QGen return Join(joinType, rightMember, filterExpression); } + public virtual QueryBuilder Join(JoinType joinType, Expression>> rightEntity, Expression> filterExpression) + { + _isJoin = true; + MemberInfo rightMember = (rightEntity.Body as MemberExpression).Member; + + var relationship = EntGraph.Relationships.Single(v => v.Member == rightMember); + EntGraph.AddLazyRelationship(relationship); + + return Join(joinType, rightMember, filterExpression); + } + public virtual QueryBuilder Join(JoinType joinType, MemberInfo rightMember, Expression> filterExpression) { _isJoin = true; From d416dd4177d266eb26320e81fd07d3122a837c91 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 9 Feb 2014 20:03:49 +0100 Subject: [PATCH 02/11] 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 From 8f87f06ac6ebb5b81df21760663a1bd810803e54 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Thu, 13 Feb 2014 20:02:58 +0100 Subject: [PATCH 03/11] Processed PR Comments. Added tests for EpisodesWithCutoffUnmet. --- src/NzbDrone.Api/Wanted/CutoffModule.cs | 10 +- src/NzbDrone.Api/Wanted/MissingModule.cs | 8 +- .../NzbDrone.Core.Test.csproj | 1 + .../EpisodesWhereCutoffUnmetFixture.cs | 168 ++++++++++++++++++ .../EpisodesWithoutFilesFixture.cs | 41 ++++- src/NzbDrone.Core/Tv/EpisodeRepository.cs | 8 +- src/NzbDrone.Core/Tv/EpisodeService.cs | 10 +- .../Radio/RadioButtonCollectionView.js | 3 +- 8 files changed, 227 insertions(+), 22 deletions(-) create mode 100644 src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs index b68dd5f10..130feb308 100644 --- a/src/NzbDrone.Api/Wanted/CutoffModule.cs +++ b/src/NzbDrone.Api/Wanted/CutoffModule.cs @@ -29,14 +29,18 @@ namespace NzbDrone.Api.Wanted 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.EpisodesWhereCutoffUnmet, pagingSpec); - PagingResource resource = ApplyToPage(_episodeService.GetCutoffUnmetEpisodes, pagingSpec); - resource.Records = resource.Records.LoadSubtype(e => e.SeriesId, _seriesRepository).ToList(); return resource; diff --git a/src/NzbDrone.Api/Wanted/MissingModule.cs b/src/NzbDrone.Api/Wanted/MissingModule.cs index 4a5c34a90..dd4d97f69 100644 --- a/src/NzbDrone.Api/Wanted/MissingModule.cs +++ b/src/NzbDrone.Api/Wanted/MissingModule.cs @@ -28,13 +28,17 @@ namespace NzbDrone.Api.Wanted 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); + PagingResource resource = ApplyToPage(v => _episodeService.EpisodesWithoutFiles(v), pagingSpec); resource.Records = resource.Records.LoadSubtype(e => e.SeriesId, _seriesRepository).ToList(); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index fe5a842f8..15b3c68ab 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -211,6 +211,7 @@ + diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs new file mode 100644 index 000000000..9a8695887 --- /dev/null +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs @@ -0,0 +1,168 @@ +using System; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests +{ + [TestFixture] + public class EpisodesWhereCutoffUnmetFixture : DbTest + { + private Series _monitoredSeries; + private Series _unmonitoredSeries; + private PagingSpec _pagingSpec; + + [SetUp] + public void Setup() + { + var qualityProfile = new QualityProfile + { + Cutoff = Quality.WEBDL720p, + Items = Qualities.QualityFixture.GetDefaultQualities() + }; + + _monitoredSeries = Builder.CreateNew() + .With(s => s.Id = 0) + .With(s => s.TvRageId = RandomNumber) + .With(s => s.Runtime = 30) + .With(s => s.Monitored = true) + .With(s => s.TitleSlug = "Title3") + .With(s => s.QualityProfile = qualityProfile) + .Build(); + + _unmonitoredSeries = Builder.CreateNew() + .With(s => s.Id = 0) + .With(s => s.TvdbId = RandomNumber) + .With(s => s.Runtime = 30) + .With(s => s.Monitored = false) + .With(s => s.TitleSlug = "Title2") + .With(s => s.QualityProfile = qualityProfile) + .Build(); + + _monitoredSeries.Id = Db.Insert(_monitoredSeries).Id; + _unmonitoredSeries.Id = Db.Insert(_unmonitoredSeries).Id; + + _pagingSpec = new PagingSpec + { + Page = 1, + PageSize = 10, + SortKey = "AirDate", + SortDirection = SortDirection.Ascending + }; + + var qualityMet = new EpisodeFile { Path = "a", Quality = new QualityModel { Quality = Quality.WEBDL720p } }; + var qualityUnmet = new EpisodeFile { Path = "b", Quality = new QualityModel { Quality = Quality.WEBDL480p } }; + + MediaFileRepository fileRepository = Mocker.Resolve(); + + qualityMet = fileRepository.Insert(qualityMet); + qualityUnmet = fileRepository.Insert(qualityUnmet); + + var monitoredSeriesEpisodes = Builder.CreateListOfSize(3) + .All() + .With(e => e.Id = 0) + .With(e => e.SeriesId = _monitoredSeries.Id) + .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) + .With(e => e.Monitored = true) + .With(e => e.EpisodeFileId = qualityUnmet.Id) + .TheFirst(1) + .With(e => e.Monitored = false) + .With(e => e.EpisodeFileId = qualityMet.Id) + .TheLast(1) + .With(e => e.SeasonNumber = 0) + .Build(); + + var unmonitoredSeriesEpisodes = Builder.CreateListOfSize(3) + .All() + .With(e => e.Id = 0) + .With(e => e.SeriesId = _unmonitoredSeries.Id) + .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) + .With(e => e.Monitored = true) + .With(e => e.EpisodeFileId = qualityUnmet.Id) + .TheFirst(1) + .With(e => e.Monitored = false) + .With(e => e.EpisodeFileId = qualityMet.Id) + .TheLast(1) + .With(e => e.SeasonNumber = 0) + .Build(); + + + var unairedEpisodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.Id = 0) + .With(e => e.SeriesId = _monitoredSeries.Id) + .With(e => e.AirDateUtc = DateTime.Now.AddDays(5)) + .With(e => e.Monitored = true) + .With(e => e.EpisodeFileId = qualityUnmet.Id) + .Build(); + + Db.InsertMany(monitoredSeriesEpisodes); + Db.InsertMany(unmonitoredSeriesEpisodes); + Db.InsertMany(unairedEpisodes); + } + + private void GivenMonitoredFilterExpression() + { + _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true; + } + + private void GivenUnmonitoredFilterExpression() + { + _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false; + } + + [Test] + public void should_get_monitored_episodes() + { + GivenMonitoredFilterExpression(); + + var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); + + episodes.Should().HaveCount(1); + } + + [Test] + [Ignore("Specials not implemented")] + public void should_get_episode_including_specials() + { + var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, true); + + episodes.Should().HaveCount(2); + } + + [Test] + public void should_not_include_unmonitored_episodes() + { + GivenMonitoredFilterExpression(); + + var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); + + episodes.Should().NotContain(e => e.Monitored == false); + } + + [Test] + public void should_not_contain_unmonitored_series() + { + GivenMonitoredFilterExpression(); + + var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); + + episodes.Should().NotContain(e => e.SeriesId == _unmonitoredSeries.Id); + } + + [Test] + public void should_not_include_cutoff_met_episodes() + { + GivenMonitoredFilterExpression(); + + var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); + + episodes.Should().NotContain(e => e.EpisodeFile.Value.Quality.Quality == Quality.WEBDL720p); + } + } +} diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs index 50c4df605..5f7afc669 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWithoutFilesFixture.cs @@ -72,14 +72,37 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests .Build(); + var unairedEpisodes = Builder.CreateListOfSize(1) + .All() + .With(e => e.Id = 0) + .With(e => e.SeriesId = _monitoredSeries.Id) + .With(e => e.EpisodeFileId = 0) + .With(e => e.AirDateUtc = DateTime.Now.AddDays(5)) + .With(e => e.Monitored = true) + .Build(); + + Db.InsertMany(monitoredSeriesEpisodes); Db.InsertMany(unmonitoredSeriesEpisodes); + Db.InsertMany(unairedEpisodes); + } + + private void GivenMonitoredFilterExpression() + { + _pagingSpec.FilterExpression = e => e.Monitored == true && e.Series.Monitored == true; + } + + private void GivenUnmonitoredFilterExpression() + { + _pagingSpec.FilterExpression = e => e.Monitored == false || e.Series.Monitored == false; } [Test] public void should_get_monitored_episodes() { - var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); + GivenMonitoredFilterExpression(); + + var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); episodes.Records.Should().HaveCount(1); } @@ -88,7 +111,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Ignore("Specials not implemented")] public void should_get_episode_including_specials() { - var episodes = Subject.GetMissingEpisodes(_pagingSpec, true); + var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, true); episodes.Records.Should().HaveCount(2); } @@ -96,7 +119,9 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_not_include_unmonitored_episodes() { - var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); + GivenMonitoredFilterExpression(); + + var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); episodes.Records.Should().NotContain(e => e.Monitored == false); } @@ -104,17 +129,19 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_not_contain_unmonitored_series() { - var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); + GivenMonitoredFilterExpression(); + + var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); episodes.Records.Should().NotContain(e => e.SeriesId == _unmonitoredSeries.Id); } [Test] - public void should_have_count_of_one() + public void should_not_return_unaired() { - var episodes = Subject.GetMissingEpisodes(_pagingSpec, false); + var episodes = Subject.EpisodesWithoutFiles(_pagingSpec, false); - episodes.TotalRecords.Should().Be(1); + episodes.TotalRecords.Should().Be(4); } } } diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index d33f173ca..32a9840da 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -17,8 +17,8 @@ namespace NzbDrone.Core.Tv List GetEpisodes(int seriesId); List GetEpisodes(int seriesId, int seasonNumber); List GetEpisodeByFileId(int fileId); - PagingSpec GetMissingEpisodes(PagingSpec pagingSpec, bool includeSpecials); - List GetCutoffUnmetEpisodes(PagingSpec pagingSpec, bool includeSpecials); + PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials); + List EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, bool includeSpecials); Episode FindEpisodeBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); List EpisodesBetweenDates(DateTime startDate, DateTime endDate); void SetMonitoredFlat(Episode episode, bool monitored); @@ -82,7 +82,7 @@ namespace NzbDrone.Core.Tv return Query.Where(e => e.EpisodeFileId == fileId).ToList(); } - public PagingSpec GetMissingEpisodes(PagingSpec pagingSpec, bool includeSpecials) + public PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials) { var currentTime = DateTime.UtcNow; var startingSeasonNumber = 1; @@ -98,7 +98,7 @@ namespace NzbDrone.Core.Tv return pagingSpec; } - public List GetCutoffUnmetEpisodes(PagingSpec pagingSpec, bool includeSpecials) + public List EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, bool includeSpecials) { var currentTime = DateTime.UtcNow; var startingSeasonNumber = 1; diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 82dcb84f6..f974f5867 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -21,8 +21,8 @@ namespace NzbDrone.Core.Tv Episode FindEpisode(int seriesId, String date); List GetEpisodeBySeries(int seriesId); List GetEpisodesBySeason(int seriesId, int seasonNumber); - PagingSpec GetMissingEpisodes(PagingSpec pagingSpec); - PagingSpec GetCutoffUnmetEpisodes(PagingSpec pagingSpec); + PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec); + PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec); List GetEpisodesByFileId(int episodeFileId); void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); @@ -109,12 +109,12 @@ namespace NzbDrone.Core.Tv public PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec) { - var episodeResult = _episodeRepository.GetMissingEpisodes(pagingSpec, false); + var episodeResult = _episodeRepository.EpisodesWithoutFiles(pagingSpec, false); return episodeResult; } - public PagingSpec GetCutoffUnmetEpisodes(PagingSpec pagingSpec) + public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec) { var allSpec = new PagingSpec { @@ -123,7 +123,7 @@ namespace NzbDrone.Core.Tv FilterExpression = pagingSpec.FilterExpression }; - var allItems = _episodeRepository.GetCutoffUnmetEpisodes(allSpec, false); + var allItems = _episodeRepository.EpisodesWhereCutoffUnmet(allSpec, false); var qualityProfileComparers = _qualityProfileRepository.All().ToDictionary(v => v.Id, v => new { Profile = v, Comparer = new QualityModelComparer(v) }); diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js index e3793b341..c6b66abc8 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js @@ -22,8 +22,9 @@ define( setActive: function () { var storedKey = this.menu.defaultAction; - if (this.menu.storeState) + if (this.menu.storeState) { storedKey = Config.getValue(this.menu.menuKey, storedKey); + } if (!storedKey) return; From 45f748cf0304b025e8d91628af5073efc301b8fe Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 15 Feb 2014 00:15:45 +0100 Subject: [PATCH 04/11] Added tests to verify Marr.data changes. Fixed nested LazyLoading. --- src/Marr.Data/QGen/QueryBuilder.cs | 10 +- .../Datastore/MarrDataLazyLoadingFixture.cs | 105 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs diff --git a/src/Marr.Data/QGen/QueryBuilder.cs b/src/Marr.Data/QGen/QueryBuilder.cs index d4cddf45f..ba135ac07 100644 --- a/src/Marr.Data/QGen/QueryBuilder.cs +++ b/src/Marr.Data/QGen/QueryBuilder.cs @@ -556,8 +556,14 @@ namespace Marr.Data.QGen _isJoin = true; MemberInfo rightMember = (rightEntity.Body as MemberExpression).Member; - var relationship = EntGraph.Relationships.Single(v => v.Member == rightMember); - EntGraph.AddLazyRelationship(relationship); + foreach (var item in EntGraph) + { + if (item.EntityType == typeof(TLeft)) + { + var relationship = item.Relationships.Single(v => v.Member == rightMember); + item.AddLazyRelationship(relationship); + } + } return Join(joinType, rightMember, filterExpression); } diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs new file mode 100644 index 000000000..32aee7226 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs @@ -0,0 +1,105 @@ +using FizzWare.NBuilder; +using NUnit.Framework; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Core.Test.Datastore +{ + + [TestFixture] + public class MarrDataLazyLoadingFixture : DbTest + { + [SetUp] + public void Setup() + { + var qualityProfile = new NzbDrone.Core.Qualities.QualityProfile + { + Name = "Test", + Cutoff = Quality.WEBDL720p, + Items = NzbDrone.Core.Test.Qualities.QualityFixture.GetDefaultQualities() + }; + + + qualityProfile = Db.Insert(qualityProfile); + + var series = Builder.CreateListOfSize(1) + .All() + .With(v => v.QualityProfileId = qualityProfile.Id) + .BuildListOfNew(); + + Db.InsertMany(series); + + var episodeFiles = Builder.CreateListOfSize(1) + .All() + .With(v => v.SeriesId = series[0].Id) + .BuildListOfNew(); + + Db.InsertMany(episodeFiles); + + var episodes = Builder.CreateListOfSize(10) + .All() + .With(v => v.Monitored = true) + .With(v => v.EpisodeFileId = episodeFiles[0].Id) + .With(v => v.SeriesId = series[0].Id) + .BuildListOfNew(); + + Db.InsertMany(episodes); + } + + [Test] + public void should_lazy_load_qualityprofile_if_not_joined() + { + var db = Mocker.Resolve(); + var DataMapper = db.GetDataMapper(); + + var episodes = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id) + .ToList(); + + foreach (var episode in episodes) + { + Assert.IsNotNull(episode.Series); + Assert.IsFalse(episode.Series.QualityProfile.IsLoaded); + } + } + + [Test] + public void should_explicit_load_episodefile_if_joined() + { + var db = Mocker.Resolve(); + var DataMapper = db.GetDataMapper(); + + var episodes = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.EpisodeFile, (l, r) => l.EpisodeFileId == r.Id) + .ToList(); + + foreach (var episode in episodes) + { + Assert.IsNull(episode.Series); + Assert.IsTrue(episode.EpisodeFile.IsLoaded); + } + } + + [Test] + public void should_explicit_load_qualityprofile_if_joined() + { + var db = Mocker.Resolve(); + var DataMapper = db.GetDataMapper(); + + var episodes = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id) + .Join(Marr.Data.QGen.JoinType.Inner, v => v.QualityProfile, (l, r) => l.QualityProfileId == r.Id) + .ToList(); + + foreach (var episode in episodes) + { + Assert.IsNotNull(episode.Series); + Assert.IsTrue(episode.Series.QualityProfile.IsLoaded); + } + } + + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 15b3c68ab..3b03dfe3e 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -104,6 +104,7 @@ + From 79767aa7bf513862beeaaeba23e1a640dd19442c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 16 Feb 2014 21:58:09 -0800 Subject: [PATCH 05/11] Redirect /api/missing to new endpoint --- src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + .../Wanted/LegacyMissingModule.cs | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/NzbDrone.Api/Wanted/LegacyMissingModule.cs diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index a4f7bf194..3305a880d 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -151,6 +151,7 @@ + diff --git a/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs b/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs new file mode 100644 index 000000000..1fe0bb6ca --- /dev/null +++ b/src/NzbDrone.Api/Wanted/LegacyMissingModule.cs @@ -0,0 +1,34 @@ +using System; +using System.Text; +using Nancy; + +namespace NzbDrone.Api.Wanted +{ + class LegacyMissingModule : NzbDroneApiModule + { + public LegacyMissingModule() : base("missing") + { + Get["/"] = x => + { + string queryString = ConvertQueryParams(Request.Query); + var url = String.Format("/api/wanted/missing?{0}", queryString); + + return Response.AsRedirect(url); + }; + } + + private string ConvertQueryParams(DynamicDictionary query) + { + var sb = new StringBuilder(); + + foreach (var key in query) + { + var value = query[key]; + + sb.AppendFormat("&{0}={1}", key, value); + } + + return sb.ToString().Trim('&'); + } + } +} From 20dec3c205a4831804955212c4a9d44ed48ef023 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 22 Feb 2014 19:19:05 -0800 Subject: [PATCH 06/11] Wanted is much much faster now. --- src/NzbDrone.Api/Wanted/CutoffModule.cs | 9 ++- src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + .../Qualities/QualitiesBelowCutoff.cs | 17 +++++ src/NzbDrone.Core/Tv/EpisodeCutoffService.cs | 47 ++++++++++++ src/NzbDrone.Core/Tv/EpisodeRepository.cs | 71 ++++++++++++------- src/NzbDrone.Core/Tv/EpisodeService.cs | 29 -------- 6 files changed, 117 insertions(+), 58 deletions(-) create mode 100644 src/NzbDrone.Core/Qualities/QualitiesBelowCutoff.cs create mode 100644 src/NzbDrone.Core/Tv/EpisodeCutoffService.cs diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs index 130feb308..ee0b9f219 100644 --- a/src/NzbDrone.Api/Wanted/CutoffModule.cs +++ b/src/NzbDrone.Api/Wanted/CutoffModule.cs @@ -3,19 +3,18 @@ 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 IEpisodeCutoffService _episodeCutoffService; private readonly SeriesRepository _seriesRepository; - public CutoffModule(IEpisodeService episodeService, SeriesRepository seriesRepository) + public CutoffModule(IEpisodeCutoffService episodeCutoffService, SeriesRepository seriesRepository) :base("wanted/cutoff") { - _episodeService = episodeService; + _episodeCutoffService = episodeCutoffService; _seriesRepository = seriesRepository; GetResourcePaged = GetCutoffUnmetEpisodes; } @@ -39,7 +38,7 @@ namespace NzbDrone.Api.Wanted pagingSpec.FilterExpression = v => v.Monitored == true && v.Series.Monitored == true; } - PagingResource resource = ApplyToPage(_episodeService.EpisodesWhereCutoffUnmet, pagingSpec); + PagingResource resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec); resource.Records = resource.Records.LoadSubtype(e => e.SeriesId, _seriesRepository).ToList(); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 0c22a0648..e6194a412 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -512,6 +512,7 @@ + @@ -536,6 +537,7 @@ + diff --git a/src/NzbDrone.Core/Qualities/QualitiesBelowCutoff.cs b/src/NzbDrone.Core/Qualities/QualitiesBelowCutoff.cs new file mode 100644 index 000000000..7d1d2c498 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualitiesBelowCutoff.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Qualities +{ + public class QualitiesBelowCutoff + { + public Int32 ProfileId { get; set; } + public IEnumerable QualityIds { get; set; } + + public QualitiesBelowCutoff(int profileId, IEnumerable qualityIds) + { + ProfileId = profileId; + QualityIds = qualityIds; + } + } +} diff --git a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs new file mode 100644 index 000000000..f88dfc02e --- /dev/null +++ b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Tv +{ + public interface IEpisodeCutoffService + { + PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec); + } + + public class EpisodeCutoffService : IEpisodeCutoffService + { + private readonly IEpisodeRepository _episodeRepository; + private readonly IQualityProfileService _qualityProfileService; + private readonly Logger _logger; + + public EpisodeCutoffService(IEpisodeRepository episodeRepository, IQualityProfileService qualityProfileService, Logger logger) + { + _episodeRepository = episodeRepository; + _qualityProfileService = qualityProfileService; + _logger = logger; + } + + public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec) + { + var qualitiesBelowCutoff = new List(); + var qualityProfiles = _qualityProfileService.All(); + + //Get all items less than the cutoff + foreach (var qualityProfile in qualityProfiles) + { + var cutoffIndex = qualityProfile.Items.FindIndex(v => v.Quality == qualityProfile.Cutoff); + var belowCutoff = qualityProfile.Items.Take(cutoffIndex).ToList(); + + if (belowCutoff.Any()) + { + qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(qualityProfile.Id, belowCutoff.Select(i => i.Quality.Id))); + } + } + + return _episodeRepository.EpisodesWhereCutoffUnmet(pagingSpec, qualitiesBelowCutoff, false); + } + } +} diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 32a9840da..d3d95fc8c 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -5,6 +5,7 @@ using Marr.Data.QGen; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Tv { @@ -18,7 +19,7 @@ namespace NzbDrone.Core.Tv List GetEpisodes(int seriesId, int seasonNumber); List GetEpisodeByFileId(int fileId); PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials); - List EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, bool includeSpecials); + PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, bool includeSpecials); Episode FindEpisodeBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); List EpisodesBetweenDates(DateTime startDate, DateTime endDate); void SetMonitoredFlat(Episode episode, bool monitored); @@ -98,7 +99,7 @@ namespace NzbDrone.Core.Tv return pagingSpec; } - public List EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, bool includeSpecials) + public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, bool includeSpecials) { var currentTime = DateTime.UtcNow; var startingSeasonNumber = 1; @@ -108,29 +109,10 @@ namespace NzbDrone.Core.Tv 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); + pagingSpec.TotalRecords = EpisodesWhereCutoffUnmetQuery(pagingSpec, currentTime, qualitiesBelowCutoff, startingSeasonNumber).GetRowCount(); + pagingSpec.Records = EpisodesWhereCutoffUnmetQuery(pagingSpec, currentTime, qualitiesBelowCutoff, startingSeasonNumber).ToList(); - return query; + return pagingSpec; } public Episode FindEpisodeBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber) @@ -177,5 +159,46 @@ namespace NzbDrone.Core.Tv { SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId); } + + private SortBuilder GetMissingEpisodesQuery(PagingSpec pagingSpec, DateTime currentTime, int startingSeasonNumber) + { + return 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); + } + + private SortBuilder EpisodesWhereCutoffUnmetQuery(PagingSpec pagingSpec, DateTime currentTime, List qualitiesBelowCutoff, int startingSeasonNumber) + { + return 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) + .AndWhere(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)) + .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) + .Skip(pagingSpec.PagingOffset()) + .Take(pagingSpec.PageSize); + } + + private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff) + { + var clauses = new List(); + + foreach (var profile in qualitiesBelowCutoff) + { + foreach (var belowCutoff in profile.QualityIds) + { + clauses.Add(String.Format("([t1].[QualityProfileId] = {0} AND [t2].[Quality] LIKE '%_quality_:%{1}%')", profile.ProfileId, belowCutoff)); + } + } + + return String.Format("({0})", String.Join(" OR ", clauses)); + } } } diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index f974f5867..20a575884 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -22,7 +22,6 @@ namespace NzbDrone.Core.Tv List GetEpisodeBySeries(int seriesId); List GetEpisodesBySeason(int seriesId, int seasonNumber); PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec); - PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec); List GetEpisodesByFileId(int episodeFileId); void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); @@ -114,34 +113,6 @@ namespace NzbDrone.Core.Tv return episodeResult; } - public PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec) - { - var allSpec = new PagingSpec - { - SortKey = pagingSpec.SortKey, - SortDirection = pagingSpec.SortDirection, - FilterExpression = pagingSpec.FilterExpression - }; - - var allItems = _episodeRepository.EpisodesWhereCutoffUnmet(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); From 120a4cf9ecf5b53df902336139b0daf49837f639 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 22 Feb 2014 19:50:24 -0800 Subject: [PATCH 07/11] Fixed up some tests --- .../EpisodesWhereCutoffUnmetFixture.cs | 86 +++++++++---------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs index 9a8695887..cc46bad8b 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; @@ -16,33 +17,37 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests private Series _monitoredSeries; private Series _unmonitoredSeries; private PagingSpec _pagingSpec; + private List _qualitiesBelowCutoff; [SetUp] public void Setup() { var qualityProfile = new QualityProfile { - Cutoff = Quality.WEBDL720p, - Items = Qualities.QualityFixture.GetDefaultQualities() + Id = 1, + Cutoff = Quality.WEBDL480p, + Items = new List + { + new QualityProfileItem { Allowed = true, Quality = Quality.SDTV }, + new QualityProfileItem { Allowed = true, Quality = Quality.WEBDL480p } + } }; _monitoredSeries = Builder.CreateNew() - .With(s => s.Id = 0) - .With(s => s.TvRageId = RandomNumber) - .With(s => s.Runtime = 30) - .With(s => s.Monitored = true) - .With(s => s.TitleSlug = "Title3") - .With(s => s.QualityProfile = qualityProfile) - .Build(); + .With(s => s.TvRageId = RandomNumber) + .With(s => s.Runtime = 30) + .With(s => s.Monitored = true) + .With(s => s.TitleSlug = "Title3") + .With(s => s.Id = qualityProfile.Id) + .BuildNew(); _unmonitoredSeries = Builder.CreateNew() - .With(s => s.Id = 0) - .With(s => s.TvdbId = RandomNumber) - .With(s => s.Runtime = 30) - .With(s => s.Monitored = false) - .With(s => s.TitleSlug = "Title2") - .With(s => s.QualityProfile = qualityProfile) - .Build(); + .With(s => s.TvdbId = RandomNumber) + .With(s => s.Runtime = 30) + .With(s => s.Monitored = false) + .With(s => s.TitleSlug = "Title2") + .With(s => s.Id = qualityProfile.Id) + .BuildNew(); _monitoredSeries.Id = Db.Insert(_monitoredSeries).Id; _unmonitoredSeries.Id = Db.Insert(_unmonitoredSeries).Id; @@ -54,9 +59,14 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests SortKey = "AirDate", SortDirection = SortDirection.Ascending }; + + _qualitiesBelowCutoff = new List + { + new QualitiesBelowCutoff(qualityProfile.Id, new[] {Quality.SDTV.Id}) + }; - var qualityMet = new EpisodeFile { Path = "a", Quality = new QualityModel { Quality = Quality.WEBDL720p } }; - var qualityUnmet = new EpisodeFile { Path = "b", Quality = new QualityModel { Quality = Quality.WEBDL480p } }; + var qualityMet = new EpisodeFile { Path = "a", Quality = new QualityModel { Quality = Quality.WEBDL480p } }; + var qualityUnmet = new EpisodeFile { Path = "b", Quality = new QualityModel { Quality = Quality.SDTV } }; MediaFileRepository fileRepository = Mocker.Resolve(); @@ -117,52 +127,36 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests } [Test] - public void should_get_monitored_episodes() - { - GivenMonitoredFilterExpression(); - - var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); - - episodes.Should().HaveCount(1); - } - - [Test] - [Ignore("Specials not implemented")] - public void should_get_episode_including_specials() - { - var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, true); - - episodes.Should().HaveCount(2); - } - - [Test] - public void should_not_include_unmonitored_episodes() + public void should_include_episodes_where_cutoff_has_not_be_met() { GivenMonitoredFilterExpression(); - var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); + var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); - episodes.Should().NotContain(e => e.Monitored == false); + spec.Records.Should().HaveCount(1); + spec.Records.Should().OnlyContain(e => e.EpisodeFile.Value.Quality.Quality == Quality.SDTV); } [Test] - public void should_not_contain_unmonitored_series() + public void should_only_contain_monitored_episodes() { GivenMonitoredFilterExpression(); - var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); + var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); - episodes.Should().NotContain(e => e.SeriesId == _unmonitoredSeries.Id); + spec.Records.Should().HaveCount(1); + spec.Records.Should().OnlyContain(e => e.Monitored); } [Test] - public void should_not_include_cutoff_met_episodes() + public void should_only_contain_episode_with_monitored_series() { GivenMonitoredFilterExpression(); - var episodes = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, false); + var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); - episodes.Should().NotContain(e => e.EpisodeFile.Value.Quality.Quality == Quality.WEBDL720p); + spec.Records.Should().HaveCount(1); + spec.Records.Should().OnlyContain(e => e.Series.Monitored); } } } From ec88286d57553e8e711d9280f2714a671dcb23f1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 22 Feb 2014 20:08:24 -0800 Subject: [PATCH 08/11] Fixed jslint errors --- src/UI/Cells/EpisodeStatusCell.js | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js index f54f2c474..9d6c29567 100644 --- a/src/UI/Cells/EpisodeStatusCell.js +++ b/src/UI/Cells/EpisodeStatusCell.js @@ -31,8 +31,16 @@ define( var hasAired = Moment(this.model.get('airDateUtc')).isBefore(Moment()); var hasFile = this.model.get('hasFile'); - if (hasFile && reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId')); + if (hasFile) { + var episodeFile; + + if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { + episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId')); + } + + else { + episodeFile = this.model.get('episodeFile'); + } this.listenTo(episodeFile, 'change', this._refresh); @@ -52,24 +60,7 @@ 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')); From eddfe67eb193e7e004e12679c81ebb89c92d27ef Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 23 Feb 2014 16:02:28 +0100 Subject: [PATCH 09/11] Added test to verify 'like' clause and fixed issue. --- .../EpisodesWhereCutoffUnmetFixture.cs | 13 +++++++++---- src/NzbDrone.Core/Tv/EpisodeRepository.cs | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs index cc46bad8b..5a3f2e1fc 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs @@ -28,8 +28,9 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests Cutoff = Quality.WEBDL480p, Items = new List { - new QualityProfileItem { Allowed = true, Quality = Quality.SDTV }, - new QualityProfileItem { Allowed = true, Quality = Quality.WEBDL480p } + new QualityProfileItem { Allowed = true, Quality = Quality.SDTV }, + new QualityProfileItem { Allowed = true, Quality = Quality.WEBDL480p }, + new QualityProfileItem { Allowed = true, Quality = Quality.RAWHD } } }; @@ -64,16 +65,18 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests { new QualitiesBelowCutoff(qualityProfile.Id, new[] {Quality.SDTV.Id}) }; - + var qualityMet = new EpisodeFile { Path = "a", Quality = new QualityModel { Quality = Quality.WEBDL480p } }; var qualityUnmet = new EpisodeFile { Path = "b", Quality = new QualityModel { Quality = Quality.SDTV } }; + var qualityRawHD = new EpisodeFile { Path = "c", Quality = new QualityModel { Quality = Quality.RAWHD } }; MediaFileRepository fileRepository = Mocker.Resolve(); qualityMet = fileRepository.Insert(qualityMet); qualityUnmet = fileRepository.Insert(qualityUnmet); + qualityRawHD = fileRepository.Insert(qualityRawHD); - var monitoredSeriesEpisodes = Builder.CreateListOfSize(3) + var monitoredSeriesEpisodes = Builder.CreateListOfSize(4) .All() .With(e => e.Id = 0) .With(e => e.SeriesId = _monitoredSeries.Id) @@ -83,6 +86,8 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests .TheFirst(1) .With(e => e.Monitored = false) .With(e => e.EpisodeFileId = qualityMet.Id) + .TheNext(1) + .With(e => e.EpisodeFileId = qualityRawHD.Id) .TheLast(1) .With(e => e.SeasonNumber = 0) .Build(); diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index d3d95fc8c..3324ec189 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -194,7 +194,7 @@ namespace NzbDrone.Core.Tv { foreach (var belowCutoff in profile.QualityIds) { - clauses.Add(String.Format("([t1].[QualityProfileId] = {0} AND [t2].[Quality] LIKE '%_quality_:%{1}%')", profile.ProfileId, belowCutoff)); + clauses.Add(String.Format("([t1].[QualityProfileId] = {0} AND [t2].[Quality] LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); } } From 6a07bb1b9bc46c65f08d5d6a6b76d1296245c0cc Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 23 Feb 2014 16:02:30 +0100 Subject: [PATCH 10/11] Now creating Backbone.Model instance for EpisodeFile. --- src/UI/Cells/EpisodeStatusCell.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js index 9d6c29567..c48fffcf1 100644 --- a/src/UI/Cells/EpisodeStatusCell.js +++ b/src/UI/Cells/EpisodeStatusCell.js @@ -3,11 +3,12 @@ define( [ 'reqres', + 'backbone', 'Cells/NzbDroneCell', 'History/Queue/QueueCollection', 'moment', 'Shared/FormatHelpers' - ], function (reqres, NzbDroneCell, QueueCollection, Moment, FormatHelpers) { + ], function (reqres, Backbone, NzbDroneCell, QueueCollection, Moment, FormatHelpers) { return NzbDroneCell.extend({ className: 'episode-status-cell', @@ -39,7 +40,7 @@ define( } else { - episodeFile = this.model.get('episodeFile'); + episodeFile = new Backbone.Model(this.model.get('episodeFile')); } this.listenTo(episodeFile, 'change', this._refresh); From 321ce24e100f4980835a5ff68fca294babc4f4ce Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 23 Feb 2014 14:53:33 -0800 Subject: [PATCH 11/11] Fixed UI test for missing page --- src/NzbDrone.Automation.Test/MainPagesTest.cs | 6 +++--- src/NzbDrone.Automation.Test/PageModel/PageBase.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Automation.Test/MainPagesTest.cs b/src/NzbDrone.Automation.Test/MainPagesTest.cs index 0b20f3c35..f10969108 100644 --- a/src/NzbDrone.Automation.Test/MainPagesTest.cs +++ b/src/NzbDrone.Automation.Test/MainPagesTest.cs @@ -45,12 +45,12 @@ namespace NzbDrone.Automation.Test } [Test] - public void missing_page() + public void wanted_page() { - page.MissingNavIcon.Click(); + page.WantedNavIcon.Click(); page.WaitForNoSpinner(); - page.FindByClass("iv-missing-missinglayout").Should().NotBeNull(); + page.FindByClass("iv-wanted-missing-missinglayout").Should().NotBeNull(); } [Test] diff --git a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs index 3ed74c8fb..6ba89f5ea 100644 --- a/src/NzbDrone.Automation.Test/PageModel/PageBase.cs +++ b/src/NzbDrone.Automation.Test/PageModel/PageBase.cs @@ -72,11 +72,11 @@ namespace NzbDrone.Automation.Test.PageModel } } - public IWebElement MissingNavIcon + public IWebElement WantedNavIcon { get { - return Find(By.LinkText("Missing")); + return Find(By.LinkText("Wanted")); } }