From f07f2e77f6f03fa1f7f791905e6fcb5d74f090e2 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Fri, 24 Feb 2017 19:52:40 +0100 Subject: [PATCH] Paging for movies :) (#861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First steps. * Not really sure what I am doing here. * Pretty hacky, but it works :) * First filter works now. * Fix all filters. * Fix some filters. * PageSize saving now works. * Fixed items being added when a refresh movie is done. * Downloaded sort not working. * Sorting by downloaded status now works. Extremely hacky, but ¯\_(ツ)_/¯ * Fixed issue where users were stuck when filtering. * Sorting via that button works now. * Removed temp thingy. --- src/NzbDrone.Api/PagingResource.cs | 13 ++- src/NzbDrone.Api/Series/MovieModule.cs | 39 ++++++++ src/NzbDrone.Core/Tv/MovieRepository.cs | 39 ++++++++ src/NzbDrone.Core/Tv/MovieService.cs | 6 ++ src/NzbDrone.Core/Tv/RefreshMovieService.cs | 2 +- src/UI/JsLibraries/backbone.backgrid.js | 4 +- src/UI/JsLibraries/backbone.js | 2 +- src/UI/JsLibraries/backbone.pageable.js | 4 +- src/UI/Movies/Index/MoviesIndexLayout.js | 73 +++++++++++---- .../Index/MoviesIndexLayoutTemplate.hbs | 6 ++ src/UI/Movies/MoviesCollection.js | 90 ++++++++++++++++--- src/UI/Settings/SettingsModelBase.js | 1 + src/UI/Settings/UI/UiSettingsModel.js | 18 +++- src/UI/Settings/UI/UiViewTemplate.hbs | 23 +++++ src/UI/Shared/Grid/Pager.js | 2 + .../Sorting/SortingButtonCollectionView.js | 9 +- 16 files changed, 291 insertions(+), 40 deletions(-) diff --git a/src/NzbDrone.Api/PagingResource.cs b/src/NzbDrone.Api/PagingResource.cs index b8025efc4..3398e6e1d 100644 --- a/src/NzbDrone.Api/PagingResource.cs +++ b/src/NzbDrone.Api/PagingResource.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; using NzbDrone.Core.Datastore; namespace NzbDrone.Api @@ -38,5 +40,14 @@ namespace NzbDrone.Api return pagingSpec; } + + /*public static Expression> CreateFilterExpression(string filterKey, string filterValue) + { + Type type = typeof(TModel); + ParameterExpression parameterExpression = Expression.Parameter(type, "x"); + Expression expressionBody = parameterExpression; + + return expressionBody; + }*/ } } diff --git a/src/NzbDrone.Api/Series/MovieModule.cs b/src/NzbDrone.Api/Series/MovieModule.cs index 052bcf319..6d8b0474f 100644 --- a/src/NzbDrone.Api/Series/MovieModule.cs +++ b/src/NzbDrone.Api/Series/MovieModule.cs @@ -15,6 +15,7 @@ using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Validation; using NzbDrone.SignalR; +using NzbDrone.Core.Datastore; namespace NzbDrone.Api.Movie { @@ -52,6 +53,7 @@ namespace NzbDrone.Api.Movie _coverMapper = coverMapper; GetResourceAll = AllMovie; + GetResourcePaged = GetMoviePaged; GetResourceById = GetMovie; CreateResource = AddMovie; UpdateResource = UpdateMovie; @@ -104,6 +106,43 @@ namespace NzbDrone.Api.Movie return MapToResource(movies); } + private PagingResource GetMoviePaged(PagingResource pagingResource) + { + var pagingSpec = pagingResource.MapToPagingSpec(); + + if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + { + pagingSpec.FilterExpression = v => v.Monitored == false; + } + else if (pagingResource.FilterKey == "monitored") + { + pagingSpec.FilterExpression = v => v.Monitored == true; + } + + if (pagingResource.FilterKey == "status") + { + switch (pagingResource.FilterValue) + { + case "released": + pagingSpec.FilterExpression = v => v.Status == MovieStatusType.Released; + break; + case "inCinemas": + pagingSpec.FilterExpression = v => v.Status == MovieStatusType.InCinemas; + break; + case "announced": + pagingSpec.FilterExpression = v => v.Status == MovieStatusType.Announced; + break; + } + } + + if (pagingResource.FilterKey == "downloaded") + { + pagingSpec.FilterExpression = v => v.MovieFileId != 0; + } + + return ApplyToPage(_moviesService.Paged, pagingSpec, MovieResourceMapper.ToResource); + } + protected MovieResource MapToResource(Core.Tv.Movie movies) { if (movies == null) return null; diff --git a/src/NzbDrone.Core/Tv/MovieRepository.cs b/src/NzbDrone.Core/Tv/MovieRepository.cs index 683ff71a7..d94a365b1 100644 --- a/src/NzbDrone.Core/Tv/MovieRepository.cs +++ b/src/NzbDrone.Core/Tv/MovieRepository.cs @@ -43,9 +43,12 @@ namespace NzbDrone.Core.Tv }; //If a movie has more than 10 parts fuck 'em. + protected IMainDatabase _database; + public MovieRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { + _database = database; } public bool MoviePathExists(string path) @@ -192,6 +195,42 @@ namespace NzbDrone.Core.Tv return pagingSpec; } + public override PagingSpec GetPaged(PagingSpec pagingSpec) + { + if (pagingSpec.SortKey == "downloadedQuality") + { + var mapper = _database.GetDataMapper(); + var offset = pagingSpec.PagingOffset(); + var limit = pagingSpec.PageSize; + var direction = "ASC"; + if (pagingSpec.SortDirection == NzbDrone.Core.Datastore.SortDirection.Descending) + { + direction = "DESC"; + } + var q = mapper.Query($"SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title {direction} LIMIT {offset},{limit};"); + var q2 = mapper.Query("SELECT * from \"Movies\" , \"MovieFiles\", \"QualityDefinitions\" WHERE Movies.MovieFileId=MovieFiles.Id AND instr(MovieFiles.Quality, ('quality\": ' || QualityDefinitions.Quality || \",\")) > 0 ORDER BY QualityDefinitions.Title ASC;"); + + //var ok = q.BuildQuery(); + + pagingSpec.Records = q.ToList(); + pagingSpec.TotalRecords = q2.Count(); + + } + else + { + pagingSpec = base.GetPaged(pagingSpec); + } + + if (pagingSpec.Records.Count == 0 && pagingSpec.PageSize != 1) + { + var lastPossiblePage = pagingSpec.TotalRecords / pagingSpec.PageSize + 1; + pagingSpec.Page = lastPossiblePage; + return GetPaged(pagingSpec); + } + + return pagingSpec; + } + public SortBuilder GetMoviesWithoutFilesQuery(PagingSpec pagingSpec) { return Query.Where(pagingSpec.FilterExpression) diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index 95940fcfc..d15d3bf80 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.Tv { Movie GetMovie(int movieId); List GetMovies(IEnumerable movieIds); + PagingSpec Paged(PagingSpec pagingSpec); Movie AddMovie(Movie newMovie); List AddMovies(List newMovies); Movie FindByImdbId(string imdbid); @@ -111,6 +112,11 @@ namespace NzbDrone.Core.Tv return _movieRepository.Get(movieIds).ToList(); } + public PagingSpec Paged(PagingSpec pagingSpec) + { + return _movieRepository.GetPaged(pagingSpec); + } + public Movie AddMovie(Movie newMovie) { Ensure.That(newMovie, () => newMovie).IsNotNull(); diff --git a/src/NzbDrone.Core/Tv/RefreshMovieService.cs b/src/NzbDrone.Core/Tv/RefreshMovieService.cs index 495f00e05..ea2827e84 100644 --- a/src/NzbDrone.Core/Tv/RefreshMovieService.cs +++ b/src/NzbDrone.Core/Tv/RefreshMovieService.cs @@ -110,7 +110,7 @@ namespace NzbDrone.Core.Tv if (message.MovieId.HasValue) { var movie = _movieService.GetMovie(message.MovieId.Value); - RefreshMovieInfo(movie); + RefreshMovieInfo(movie); } else { diff --git a/src/UI/JsLibraries/backbone.backgrid.js b/src/UI/JsLibraries/backbone.backgrid.js index 6a0af616c..b8c2157e4 100644 --- a/src/UI/JsLibraries/backbone.backgrid.js +++ b/src/UI/JsLibraries/backbone.backgrid.js @@ -2404,7 +2404,7 @@ var Body = Backgrid.Body = Backbone.View.extend({ See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator) */ sort: function (column, direction) { - + //debugger; if (_.isString(column)) column = this.columns.findWhere({name: column}); var collection = this.collection; @@ -2761,4 +2761,4 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ }); return Backgrid; -})); \ No newline at end of file +})); diff --git a/src/UI/JsLibraries/backbone.js b/src/UI/JsLibraries/backbone.js index 70a854d31..7941ab684 100644 --- a/src/UI/JsLibraries/backbone.js +++ b/src/UI/JsLibraries/backbone.js @@ -815,7 +815,7 @@ sort: function(options) { if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); options || (options = {}); - + //debugger; // Run sort based on type of `comparator`. if (_.isString(this.comparator) || this.comparator.length === 1) { this.models = this.sortBy(this.comparator, this); diff --git a/src/UI/JsLibraries/backbone.pageable.js b/src/UI/JsLibraries/backbone.pageable.js index c377cf5d8..2dc895a94 100644 --- a/src/UI/JsLibraries/backbone.pageable.js +++ b/src/UI/JsLibraries/backbone.pageable.js @@ -324,9 +324,11 @@ if (comparator && options.full) { this.comparator = null; fullCollection.comparator = comparator; + } else if (options.full){ + fullCollection.comparator = this.comparator; } - if (options.full) fullCollection.sort(); + //if (options.full) fullCollection.sort(); // make sure the models in the current page and full collection have the // same references diff --git a/src/UI/Movies/Index/MoviesIndexLayout.js b/src/UI/Movies/Index/MoviesIndexLayout.js index ffd7cf227..6917eb418 100644 --- a/src/UI/Movies/Index/MoviesIndexLayout.js +++ b/src/UI/Movies/Index/MoviesIndexLayout.js @@ -15,6 +15,7 @@ var MovieStatusCell = require('../../Cells/MovieStatusCell'); var MovieDownloadStatusCell = require('../../Cells/MovieDownloadStatusCell'); var DownloadedQualityCell = require('../../Cells/DownloadedQualityCell'); var FooterView = require('./FooterView'); +var GridPager = require('../../Shared/Grid/Pager'); var FooterModel = require('./FooterModel'); var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); require('../../Mixins/backbone.signalr.mixin'); @@ -26,12 +27,14 @@ module.exports = Marionette.Layout.extend({ seriesRegion : '#x-series', toolbar : '#x-toolbar', toolbar2 : '#x-toolbar2', - footer : '#x-series-footer' + footer : '#x-series-footer', + pager : "#x-movie-pager", + pagerTop : "#x-movie-pager-top" }, columns : [ { - name : 'statusWeight', + name : 'status', label : '', cell : MovieStatusCell }, @@ -45,6 +48,7 @@ module.exports = Marionette.Layout.extend({ name : "downloadedQuality", label : "Downloaded", cell : DownloadedQualityCell, + sortable : true }, { name : 'profileId', @@ -67,6 +71,7 @@ module.exports = Marionette.Layout.extend({ name : "this", label : "Status", cell : MovieDownloadStatusCell, + sortable : false, sortValue : function(m, k) { if (m.get("downloaded")) { return -1; @@ -115,31 +120,40 @@ module.exports = Marionette.Layout.extend({ initialize : function() { this.seriesCollection = MoviesCollection.clone(); - this.seriesCollection.shadowCollection.bindSignalR(); + this.seriesCollection.bindSignalR(); - this.listenTo(this.seriesCollection.shadowCollection, 'sync', function(model, collection, options) { - this.seriesCollection.fullCollection.resetFiltered(); + + + this.listenTo(this.seriesCollection, 'sync', function(model, collection, options) { + //this.seriesCollection.fullCollection.resetFiltered(); this._renderView(); }); - this.listenTo(this.seriesCollection.shadowCollection, 'add', function(model, collection, options) { - this.seriesCollection.fullCollection.resetFiltered(); - this._renderView(); + this.listenTo(MoviesCollection, "sync", function(eventName) { + this.seriesCollection = MoviesCollection.clone(); + //this._showTable(); + this._renderView(); }); - this.listenTo(this.seriesCollection.shadowCollection, 'remove', function(model, collection, options) { - this.seriesCollection.fullCollection.resetFiltered(); - this._renderView(); + this.listenTo(this.seriesCollection, 'add', function(model, collection, options) { + //this.seriesCollection.fullCollection.resetFiltered(); + //this._renderView(); + }); + + this.listenTo(this.seriesCollection, 'remove', function(model, collection, options) { + //this.seriesCollection.fullCollection.resetFiltered(); + //this._showTable(); }); this.sortingOptions = { type : 'sorting', storeState : false, viewCollection : this.seriesCollection, + callback : this._sort, items : [ { title : 'Title', - name : 'sortTitle' + name : 'title' }, { title: 'Downloaded', @@ -153,10 +167,10 @@ module.exports = Marionette.Layout.extend({ title : 'In Cinemas', name : 'inCinemas' }, - { + /*{ title : "Status", name : "status", - } + }*/ ] }; @@ -254,10 +268,13 @@ module.exports = Marionette.Layout.extend({ className : 'table table-hover' }); + this._showPager(); + this._renderView(); }, _showList : function() { + //this.current = "list"; this.currentView = new ListCollectionView({ collection : this.seriesCollection }); @@ -273,6 +290,10 @@ module.exports = Marionette.Layout.extend({ this._renderView(); }, + _sort : function() { + console.warn("Sorting"); + }, + _renderView : function() { if (MoviesCollection.length === 0) { this.seriesRegion.show(new EmptyView()); @@ -280,8 +301,12 @@ module.exports = Marionette.Layout.extend({ this.toolbar.close(); this.toolbar2.close(); } else { - this.seriesRegion.show(this.currentView); + this.seriesRegion.show(this.currentView); + this.listenTo(this.currentView.collection, "sync", function(eventName){ + this._showPager(); + //debugger; + }); this._showToolbar(); this._showFooter(); } @@ -293,7 +318,6 @@ module.exports = Marionette.Layout.extend({ _setFilter : function(buttonContext) { var mode = buttonContext.model.get('key'); - this.seriesCollection.setFilterMode(mode); }, @@ -321,6 +345,19 @@ module.exports = Marionette.Layout.extend({ })); }, + _showPager : function() { + var pager = new GridPager({ + columns : this.columns, + collection : this.seriesCollection, + }); + var pagerTop = new GridPager({ + columns : this.columns, + collection : this.seriesCollection, + }); + this.pager.show(pager); + this.pagerTop.show(pagerTop); + }, + _showFooter : function() { var footerModel = new FooterModel(); var movies = MoviesCollection.models.length; @@ -330,12 +367,12 @@ module.exports = Marionette.Layout.extend({ var released = 0; var monitored = 0; - + var downloaded =0; var missingMonitored=0; var missingNotMonitored=0; var missingNotAvailable=0; - var missingMonitoredAvailable=0; + var missingMonitoredAvailable=0; var downloadedNotMonitored=0; diff --git a/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs b/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs index 0c41b4108..d5432d81b 100644 --- a/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs +++ b/src/UI/Movies/Index/MoviesIndexLayoutTemplate.hbs @@ -3,10 +3,16 @@
+
+
+
+
+
+ diff --git a/src/UI/Movies/MoviesCollection.js b/src/UI/Movies/MoviesCollection.js index bbf0a3ae4..0ffc86498 100644 --- a/src/UI/Movies/MoviesCollection.js +++ b/src/UI/Movies/MoviesCollection.js @@ -7,22 +7,70 @@ var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); var AsSortedCollection = require('../Mixins/AsSortedCollection'); var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); var moment = require('moment'); +var UiSettings = require('../Shared/UiSettingsModel'); require('../Mixins/backbone.signalr.mixin'); +var Config = require('../Config'); + +var pageSize = parseInt(Config.getValue("pageSize")) || 1000; + var Collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/movie', model : MovieModel, tableName : 'movie', + origSetSorting : PageableCollection.prototype.setSorting, + origAdd : PageableCollection.prototype.add, + state : { sortKey : 'sortTitle', order : -1, - pageSize : 100000, + pageSize : pageSize, secondarySortKey : 'sortTitle', secondarySortOrder : -1 }, - mode : 'client', + queryParams : { + totalPages : null, + totalRecords : null, + pageSize : 'pageSize', + sortKey : 'sortKey', + order : 'sortDir', + directions : { + '-1' : 'asc', + '1' : 'desc' + } + }, + + sortMappings : { + 'movie' : { sortKey : 'series.sortTitle' } + }, + + parseState : function(resp) { + var direction = -1; + if (resp.sortDirection == "descending") { + direction = 1; + } + return { totalRecords : resp.totalRecords, order : direction, currentPage : resp.page }; + }, + + parseRecords : function(resp) { + if (resp) { + return resp.records; + } + + return resp; + }, + + mode : 'server', + + setSorting : function(sortKey, order, options) { + return this.origSetSorting.call(this, sortKey, order, options); + }, + + sort : function(options){ + //debugger; + }, save : function() { var self = this; @@ -90,18 +138,18 @@ var Collection = PageableCollection.extend({ false ], 'released' : [ - null, - null, + "status", + "released", function(model) { return model.getStatus() == "released"; } ], 'announced' : [ - null, - null, + "status", + "announced", function(model) { return model.getStatus() == "announced"; } ], 'cinemas' : [ - null, - null, + "status", + "inCinemas", function(model) { return model.getStatus() == "inCinemas"; } ] }, @@ -127,10 +175,10 @@ var Collection = PageableCollection.extend({ downloadedQuality : { sortValue : function(model, attr) { if (model.get("movieFile")) { - return 1000-model.get("movieFile").quality.quality.id; + return model.get("movieFile").quality.quality.name; } - return -1; + return ""; } }, nextAiring : { @@ -184,6 +232,24 @@ var Collection = PageableCollection.extend({ return path.toLowerCase(); } } + }, + + add : function(model, options) { + if (this.length >= this.state.pageSize) { + return; + } + this.origAdd.call(this, model, options); + }, + + setFilterMode : function(mode){ + var arr = this.filterModes[mode]; + this.state.filterKey = arr[0]; + this.state.filterValue = arr[1]; + this.fetch(); + }, + + comparator: function (model) { + return model.get('sortTitle'); } }); @@ -191,6 +257,6 @@ Collection = AsFilteredCollection.call(Collection); Collection = AsSortedCollection.call(Collection); Collection = AsPersistedStateCollection.call(Collection); -var data = ApiData.get('movie'); +var data = ApiData.get('movie?page=1&pageSize='+pageSize+'&sortKey=sortTitle&sortDir=asc'); -module.exports = new Collection(data, { full : true }).bindSignalR(); +module.exports = new Collection(data.records, { full : false, state : { totalRecords : data.totalRecords} }).bindSignalR(); diff --git a/src/UI/Settings/SettingsModelBase.js b/src/UI/Settings/SettingsModelBase.js index f08773f91..7640bb5de 100644 --- a/src/UI/Settings/SettingsModelBase.js +++ b/src/UI/Settings/SettingsModelBase.js @@ -8,6 +8,7 @@ var model = DeepModel.extend({ initialize : function() { this.listenTo(vent, vent.Commands.SaveSettings, this.saveSettings); this.listenTo(this, 'destroy', this._stopListening); + }, saveSettings : function() { diff --git a/src/UI/Settings/UI/UiSettingsModel.js b/src/UI/Settings/UI/UiSettingsModel.js index baf6a5297..ced788780 100644 --- a/src/UI/Settings/UI/UiSettingsModel.js +++ b/src/UI/Settings/UI/UiSettingsModel.js @@ -1,7 +1,21 @@ var SettingsModelBase = require('../SettingsModelBase'); +var Config = require('../../Config'); module.exports = SettingsModelBase.extend({ url : window.NzbDrone.ApiRoot + '/config/ui', successMessage : 'UI settings saved', - errorMessage : 'Failed to save UI settings' -}); \ No newline at end of file + errorMessage : 'Failed to save UI settings', + + origSave : SettingsModelBase.prototype.saveSettings, + origInit : SettingsModelBase.prototype.initialize, + + initialize : function() { + this.set("pageSize", Config.getValue("pageSize")); + this.origInit.call(this); + }, + + saveSettings : function() { + Config.setValue("pageSize", this.get("pageSize")); + this.origSave.call(this); + } +}); diff --git a/src/UI/Settings/UI/UiViewTemplate.hbs b/src/UI/Settings/UI/UiViewTemplate.hbs index 5a3d46d27..c0cd483b5 100644 --- a/src/UI/Settings/UI/UiViewTemplate.hbs +++ b/src/UI/Settings/UI/UiViewTemplate.hbs @@ -1,4 +1,27 @@
+
+ Movies + +
+ + +
+ +
+ + + + +
+
+
Calendar diff --git a/src/UI/Shared/Grid/Pager.js b/src/UI/Shared/Grid/Pager.js index 5919727b5..599eb30f5 100644 --- a/src/UI/Shared/Grid/Pager.js +++ b/src/UI/Shared/Grid/Pager.js @@ -72,6 +72,8 @@ module.exports = Paginator.extend({ var handles = []; var collection = this.collection; + + var state = collection.state; // convert all indices to 0-based here diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js index 6db8995a2..8dc9d375d 100644 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js @@ -31,8 +31,13 @@ module.exports = Marionette.CompositeView.extend({ } collection.setSorting(sortModel.get('name'), order); - collection.fullCollection.sort(); + if (collection.mode == "server"){ + collection.fetch({reset: true}); + } else { + collection.fullCollection.sort(); + } + return this; } -}); \ No newline at end of file +});