From 056fb154a8ebb51c5de3a18685a8abdb0a28d824 Mon Sep 17 00:00:00 2001 From: Tim Turner Date: Tue, 21 Feb 2017 15:31:31 -0500 Subject: [PATCH] Patch/bulk import qol (#785) * Filter out existing movies upon import * Update collection based on what is imported * Ensure root folders are loaded before collectionview TODO: * Ensure grid region exists * Return information about what wasn't imported * Filter collection based on duplicates --- .../Movies/MovieBulkImportModule.cs | 20 ++++--- src/NzbDrone.Core/Tv/MovieService.cs | 10 +++- .../BulkImport/BulkImportSelectAllCell.js | 43 +++++++++++++++ src/UI/AddMovies/BulkImport/BulkImportView.js | 52 +++++++++++-------- .../BulkImport/BulkImportViewTemplate.hbs | 7 +++ src/UI/AddMovies/BulkImport/TmdbIdCell.js | 14 ++--- .../AddMovies/RootFolders/RootFolderLayout.js | 11 ++-- src/UI/Movies/Files/FilesLayout.js | 6 +-- src/UI/Movies/Files/FilesLayoutTemplate.hbs | 4 +- 9 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 src/UI/AddMovies/BulkImport/BulkImportSelectAllCell.js diff --git a/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs b/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs index a00405e25..8d085646b 100644 --- a/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs +++ b/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs @@ -34,9 +34,10 @@ namespace NzbDrone.Api.Movie private readonly IMakeImportDecision _importDecisionMaker; private readonly IDiskScanService _diskScanService; private readonly ICached _mappedMovies; + private readonly IMovieService _movieService; public MovieBulkImportModule(ISearchForNewMovie searchProxy, IRootFolderService rootFolderService, IMakeImportDecision importDecisionMaker, - IDiskScanService diskScanService, ICacheManager cacheManager) + IDiskScanService diskScanService, ICacheManager cacheManager, IMovieService movieService) : base("/movies/bulkimport") { _searchProxy = searchProxy; @@ -44,6 +45,7 @@ namespace NzbDrone.Api.Movie _importDecisionMaker = importDecisionMaker; _diskScanService = diskScanService; _mappedMovies = cacheManager.GetCache(GetType(), "mappedMoviesCache"); + _movieService = movieService; Get["/"] = x => Search(); } @@ -55,6 +57,8 @@ namespace NzbDrone.Api.Movie //Todo error handling } + //var existingMovieTmdbIds = _movieService.GetAllMovies().Select(m => m.TmdbId); + RootFolder rootFolder = _rootFolderService.Get(Request.Query.Id); int page = Request.Query.page; @@ -108,8 +112,6 @@ namespace NzbDrone.Api.Movie }; } - - var files = _diskScanService.GetVideoFiles(f.Path); var decisions = _importDecisionMaker.GetImportDecisions(files.ToList(), m); @@ -133,8 +135,10 @@ namespace NzbDrone.Api.Movie mappedMovie = _searchProxy.MapMovieToTmdbMovie(m); - if (mappedMovie != null) + if (mappedMovie != null /*&& !existingMovieTmdbIds.Contains(mappedMovie.TmdbId)*/) { + //Could split these checks to flag movie as possible duplicate + mappedMovie.Monitored = true; _mappedMovies.Set(f.Name, mappedMovie, TimeSpan.FromDays(2)); @@ -144,7 +148,7 @@ namespace NzbDrone.Api.Movie return null; }); - + return new PagingResource { Page = page, @@ -159,10 +163,10 @@ namespace NzbDrone.Api.Movie private static IEnumerable MapToResource(IEnumerable movies) { - foreach (var currentSeries in movies) + foreach (var currentMovie in movies) { - var resource = currentSeries.ToResource(); - var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + var resource = currentMovie.ToResource(); + var poster = currentMovie.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); if (poster != null) { resource.RemotePoster = poster.Url; diff --git a/src/NzbDrone.Core/Tv/MovieService.cs b/src/NzbDrone.Core/Tv/MovieService.cs index 9c33e1ecd..e6b1a0842 100644 --- a/src/NzbDrone.Core/Tv/MovieService.cs +++ b/src/NzbDrone.Core/Tv/MovieService.cs @@ -95,8 +95,6 @@ namespace NzbDrone.Core.Tv public List AddMovies(List newMovies) { - _logger.Debug("Adding {0} movies", newMovies.Count); - newMovies.ForEach(m => Ensure.That(m, () => m).IsNotNull()); newMovies.ForEach(m => @@ -112,8 +110,16 @@ namespace NzbDrone.Core.Tv m.Added = DateTime.UtcNow; }); + //var existingMovies = GetAllMovies(); + //var potentialMovieCount = newMovies.Count; + + //newMovies = newMovies.ExceptBy(n => n.TitleSlug, existingMovies, e => e.TitleSlug, StringComparer.InvariantCultureIgnoreCase).ToList(); + _movieRepository.InsertMany(newMovies); + //_logger.Debug("Adding {0} movies, {1} duplicates detected", newMovies.Count, potentialMovieCount - newMovies.Count); + _logger.Debug("Adding {0} movies", newMovies.Count); + newMovies.ForEach(m => { _eventAggregator.PublishEvent(new MovieAddedEvent(m)); diff --git a/src/UI/AddMovies/BulkImport/BulkImportSelectAllCell.js b/src/UI/AddMovies/BulkImport/BulkImportSelectAllCell.js new file mode 100644 index 000000000..ea46703ee --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportSelectAllCell.js @@ -0,0 +1,43 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var SelectAllCell = require('../../Cells/SelectAllCell'); +var Backgrid = require('backgrid'); +var MoviesCollection = require('../../Movies/MoviesCollection'); + +module.exports = SelectAllCell.extend({ + _originalRender : SelectAllCell.prototype.render, + + _originalInit : SelectAllCell.prototype.initialize, + + initialize : function() { + this._originalInit.apply(this, arguments); + + var tmdbId = this.model.get('tmdbId'); + var existingMovie = MoviesCollection.where({ tmdbId: tmdbId }); + this.isDuplicate = existingMovie.length > 0 ? true : false; + + this.listenTo(this.model, 'change', this._refresh); + }, + + onChange : function(e) { + if(!this.isDuplicate) { + var checked = $(e.target).prop('checked'); + this.$el.parent().toggleClass('selected', checked); + this.model.trigger('backgrid:selected', this.model, checked); + } else { + $(e.target).prop('checked', false); + } + }, + + render : function() { + this._originalRender.apply(this, arguments); + + this.$el.children(':first').prop('disabled', this.isDuplicate); + + return this; + }, + + _refresh: function() { + this.render(); + } +}); \ No newline at end of file diff --git a/src/UI/AddMovies/BulkImport/BulkImportView.js b/src/UI/AddMovies/BulkImport/BulkImportView.js index 2d81a4254..da2a6604e 100644 --- a/src/UI/AddMovies/BulkImport/BulkImportView.js +++ b/src/UI/AddMovies/BulkImport/BulkImportView.js @@ -7,7 +7,7 @@ var BulkImportCollection = require("./BulkImportCollection"); var QualityCell = require('./QualityCell'); var TmdbIdCell = require('./TmdbIdCell'); var GridPager = require('../../Shared/Grid/Pager'); -var SelectAllCell = require('../../Cells/SelectAllCell'); +var SelectAllCell = require('./BulkImportSelectAllCell'); var ProfileCell = require('./BulkImportProfileCellT'); var MonitorCell = require('./BulkImportMonitorCell'); var MoviePathCell = require("./MoviePathCell"); @@ -33,7 +33,7 @@ module.exports = Marionette.Layout.extend({ ui : { addSelectdBtn : '.x-add-selected', - addAllBtn : '.x-add-all', + //addAllBtn : '.x-add-all', pageSizeSelector : '.x-page-size' }, @@ -66,7 +66,8 @@ module.exports = Marionette.Layout.extend({ name : '', cell : SelectAllCell, headerCell : 'select-all', - sortable : false + sortable : false, + cellValue : 'this' }, { name : 'movie', @@ -107,7 +108,6 @@ module.exports = Marionette.Layout.extend({ cell : QualityCell, cellValue : 'this', sortable : false - } ], @@ -132,14 +132,14 @@ module.exports = Marionette.Layout.extend({ callback : this._addSelected, ownerContext : this, className : 'x-add-selected' - }, - { - title : 'Add All', - icon : 'icon-sonarr-add', - callback : this._addAll, - ownerContext : this, - className : 'x-add-all' - } + }//, + // { + // title : 'Add All', + // icon : 'icon-sonarr-add', + // callback : this._addAll, + // ownerContext : this, + // className : 'x-add-all' + // } ] }; @@ -155,13 +155,13 @@ module.exports = Marionette.Layout.extend({ _addSelected : function() { var selected = _.filter(this.bulkImportCollection.fullCollection.models, function(elem){ return elem.selected; - }) + }); console.log(selected); var promise = MoviesCollection.importFromList(selected); this.ui.addSelectdBtn.spinForPromise(promise); this.ui.addSelectdBtn.addClass('disabled'); - this.ui.addAllBtn.addClass('disabled'); + //this.ui.addAllBtn.addClass('disabled'); if (selected.length === 0) { Messenger.show({ @@ -169,7 +169,7 @@ module.exports = Marionette.Layout.extend({ message : 'No movies selected' }); return; - } + } Messenger.show({ message : "Importing {0} movies. This can take multiple minutes depending on how many movies should be imported. Don't close this browser window until it is finished!".format(selected.length), @@ -178,12 +178,19 @@ module.exports = Marionette.Layout.extend({ type : "error" }); + var _this = this; + promise.done(function() { - Messenger.show({ - message : "Imported movies from list.", - hideAfter : 8, - hideOnNavigate : true - }); + Messenger.show({ + message : "Imported movies from folder.", + hideAfter : 8, + hideOnNavigate : true + }); + + + _.forEach(selected, function(movie) { + movie.destroy(); //update the collection without the added movies + }); }); }, @@ -192,8 +199,8 @@ module.exports = Marionette.Layout.extend({ }, _handleEvent : function(event_name, data) { - if (event_name == "sync" || event_name == "content") { - this._showContent() + if (event_name === "sync" || event_name === "content") { + this._showContent(); } }, @@ -207,6 +214,7 @@ module.exports = Marionette.Layout.extend({ return; } + //TODO: override row in order to set an opacity based on duplication state of the movie this.importGrid = new Backgrid.Grid({ columns : this.columns, collection : this.bulkImportCollection, diff --git a/src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs b/src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs index 22a66cc68..7997f620b 100644 --- a/src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs +++ b/src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs @@ -1,5 +1,12 @@
{{> PageSizePartial }} + +
+
+ Disabled movies are possible duplicates. If the match is incorrect, update the Tmdb Id cell to import the proper movie. +
+
+
diff --git a/src/UI/AddMovies/BulkImport/TmdbIdCell.js b/src/UI/AddMovies/BulkImport/TmdbIdCell.js index 5559afdd8..ce9e1991a 100644 --- a/src/UI/AddMovies/BulkImport/TmdbIdCell.js +++ b/src/UI/AddMovies/BulkImport/TmdbIdCell.js @@ -21,7 +21,7 @@ module.exports = NzbDroneCell.extend({ }, _updateId : function() { - var field = this.$el.find('.x-tmdbId'); + var field = this.$el.find('.x-tmdbId'); var data = field.val(); var promise = $.ajax({ @@ -31,7 +31,7 @@ module.exports = NzbDroneCell.extend({ //field.spinForPromise(promise); - field.prop("disabled", true) + field.prop("disabled", true); var icon = this.$(".icon-sonarr-info"); @@ -48,15 +48,15 @@ module.exports = NzbDroneCell.extend({ promise.success(function(response) { _self.model.set(response); _self.model.set('monitored', cacheMonitored); //reset to the previous monitored value - _self.model.set('profileId', cacheProfile); - _self.model.set('path', cachePath); - _self.model.set('movieFile', cacheFile); // may be unneccessary. - field.prop("disabled", false) + _self.model.set('profileId', cacheProfile); + _self.model.set('path', cachePath); + _self.model.set('movieFile', cacheFile); // may be unneccessary. + field.prop("disabled", false); }); promise.error(function(request, status, error) { console.error("Status: " + status, "Error: " + error); - field.prop("disabled", false) + field.prop("disabled", false); }); } }); diff --git a/src/UI/AddMovies/RootFolders/RootFolderLayout.js b/src/UI/AddMovies/RootFolders/RootFolderLayout.js index f0d9f3614..4898f198b 100644 --- a/src/UI/AddMovies/RootFolders/RootFolderLayout.js +++ b/src/UI/AddMovies/RootFolders/RootFolderLayout.js @@ -24,9 +24,9 @@ var Layout = Marionette.Layout.extend({ initialize : function() { this.collection = RootFolderCollection; - this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection }); + this.rootfolderListView = null; - this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected); + }, onShow : function() { @@ -60,7 +60,12 @@ var Layout = Marionette.Layout.extend({ }, _showCurrentDirs : function() { - this.currentDirs.show(this.rootfolderListView); + if(!this.rootfolderListView) + { + this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection }); + this.currentDirs.show(this.rootfolderListView); + this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected); + } }, _keydown : function(e) { diff --git a/src/UI/Movies/Files/FilesLayout.js b/src/UI/Movies/Files/FilesLayout.js index 99bd4b383..6e9e226a6 100644 --- a/src/UI/Movies/Files/FilesLayout.js +++ b/src/UI/Movies/Files/FilesLayout.js @@ -42,9 +42,9 @@ module.exports = Marionette.Layout.extend({ cell : FileTitleCell }, { - name : "mediaInfo", - label : "Media Info", - cell : MediaInfoCell + name : "mediaInfo", + label : "Media Info", + cell : MediaInfoCell }, { name : 'edition', diff --git a/src/UI/Movies/Files/FilesLayoutTemplate.hbs b/src/UI/Movies/Files/FilesLayoutTemplate.hbs index ac6a3ca36..d343c8e22 100644 --- a/src/UI/Movies/Files/FilesLayoutTemplate.hbs +++ b/src/UI/Movies/Files/FilesLayoutTemplate.hbs @@ -1 +1,3 @@ -
+
+
+