From 35b384439f24e73be98051a0372feb2e612f1446 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Wed, 8 Feb 2017 01:09:36 +0100 Subject: [PATCH] Bulk Import. (#583) * First pass at bulk import. * First pass at paging implementation for bulk import. * Another pass at UI for bulk import WHY WON'T THE ROWS SELECT?!?! * Paging mostly done. UI needs to show loading still. * Fix for selection * fixes. * Add caching to bulk import * Tried to fix paging. * Fix has next * Fix link error. * Pageable now works almost perfectly. Collection now works really nicely when paged. Also movies from different pages can be added no problemo. * /bulk-import: ProfileCell Various other QoL changes * Profile selection works now Still kinda hacky * Default monitored to true. * Add Monitor Cell Update styling, added path tooltip as well * Update model when changing tmdbId Ensure monitor status doesn't change as well * Added spinner feedback for tmdbid cell. * /bulk-import: Add page-size selector --- src/NzbDrone.Api/Calendar/CalendarModule.cs | 2 + .../Movies/MovieBulkImportModule.cs | 169 ++++++++++++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 18 +- src/NzbDrone.Api/Series/MovieLookupModule.cs | 20 +- .../MetadataSource/SkyHook/SkyHookProxy.cs | 1 + src/UI/AddMovies/AddMoviesLayout.js | 13 ++ src/UI/AddMovies/AddMoviesLayoutTemplate.hbs | 3 +- src/UI/AddMovies/AddMoviesView.js | 1 - .../BulkImport/BulkImportCollection.js | 91 ++++++++ .../BulkImport/BulkImportMonitorCell.js | 79 +++++++ .../BulkImportMonitorCellTemplate.hbs | 4 + .../BulkImport/BulkImportMovieTitleCell.js | 21 ++ .../BulkImport/BulkImportProfileCell.js | 45 ++++ .../BulkImport/BulkImportProfileCellT.js | 77 ++++++ .../BulkImportProfileCellTemplate.hbs | 5 + src/UI/AddMovies/BulkImport/BulkImportView.js | 219 ++++++++++++++++++ .../BulkImport/BulkImportViewTemplate.hbs | 13 ++ src/UI/AddMovies/BulkImport/EmptyView.js | 11 + .../BulkImport/EmptyViewTemplate.hbs | 3 + src/UI/AddMovies/BulkImport/MoviePathCell.js | 7 + .../BulkImport/MoviePathTemplate.hbs | 2 + .../AddMovies/BulkImport/PageSizePartial.hbs | 8 + src/UI/AddMovies/BulkImport/QualityCell.js | 8 + .../BulkImport/QualityCellTemplate.hbs | 5 + src/UI/AddMovies/BulkImport/TmdbIdCell.js | 62 +++++ src/UI/AddMovies/addMovies.less | 11 + src/UI/Cells/cells.less | 23 +- src/UI/JsLibraries/backbone.pageable.js | 15 +- src/UI/Movies/movies.less | 4 + src/UI/Shared/Grid/Pager.js | 4 +- 30 files changed, 924 insertions(+), 20 deletions(-) create mode 100644 src/NzbDrone.Api/Movies/MovieBulkImportModule.cs create mode 100644 src/UI/AddMovies/BulkImport/BulkImportCollection.js create mode 100644 src/UI/AddMovies/BulkImport/BulkImportMonitorCell.js create mode 100644 src/UI/AddMovies/BulkImport/BulkImportMonitorCellTemplate.hbs create mode 100644 src/UI/AddMovies/BulkImport/BulkImportMovieTitleCell.js create mode 100644 src/UI/AddMovies/BulkImport/BulkImportProfileCell.js create mode 100644 src/UI/AddMovies/BulkImport/BulkImportProfileCellT.js create mode 100644 src/UI/AddMovies/BulkImport/BulkImportProfileCellTemplate.hbs create mode 100644 src/UI/AddMovies/BulkImport/BulkImportView.js create mode 100644 src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs create mode 100644 src/UI/AddMovies/BulkImport/EmptyView.js create mode 100644 src/UI/AddMovies/BulkImport/EmptyViewTemplate.hbs create mode 100644 src/UI/AddMovies/BulkImport/MoviePathCell.js create mode 100644 src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs create mode 100644 src/UI/AddMovies/BulkImport/PageSizePartial.hbs create mode 100644 src/UI/AddMovies/BulkImport/QualityCell.js create mode 100644 src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs create mode 100644 src/UI/AddMovies/BulkImport/TmdbIdCell.js diff --git a/src/NzbDrone.Api/Calendar/CalendarModule.cs b/src/NzbDrone.Api/Calendar/CalendarModule.cs index 804546655..9a33ebf29 100644 --- a/src/NzbDrone.Api/Calendar/CalendarModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarModule.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using Nancy; using NzbDrone.Api.Episodes; using NzbDrone.Api.Movie; +using NzbDrone.Api.Series; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaFiles; diff --git a/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs b/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs new file mode 100644 index 000000000..843f78fc7 --- /dev/null +++ b/src/NzbDrone.Api/Movies/MovieBulkImportModule.cs @@ -0,0 +1,169 @@ +using System.Collections; +using System.Collections.Generic; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Parser; +using System.Linq; +using System; +using Marr.Data; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.EpisodeImport; +using NzbDrone.Core.RootFolders; +using NzbDrone.Common.Cache; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Api.Movie +{ + + public class UnmappedComparer : IComparer + { + public int Compare(UnmappedFolder a, UnmappedFolder b) + { + return a.Name.CompareTo(b.Name); + } + } + + public class MovieBulkImportModule : NzbDroneRestModule + { + private readonly ISearchForNewMovie _searchProxy; + private readonly IRootFolderService _rootFolderService; + private readonly IMakeImportDecision _importDecisionMaker; + private readonly IDiskScanService _diskScanService; + private readonly ICached _mappedMovies; + + public MovieBulkImportModule(ISearchForNewMovie searchProxy, IRootFolderService rootFolderService, IMakeImportDecision importDecisionMaker, + IDiskScanService diskScanService, ICacheManager cacheManager) + : base("/movies/bulkimport") + { + _searchProxy = searchProxy; + _rootFolderService = rootFolderService; + _importDecisionMaker = importDecisionMaker; + _diskScanService = diskScanService; + _mappedMovies = cacheManager.GetCache(GetType(), "mappedMoviesCache"); + Get["/"] = x => Search(); + } + + + private Response Search() + { + if (Request.Query.Id == 0) + { + //Todo error handling + } + + RootFolder rootFolder = _rootFolderService.Get(Request.Query.Id); + + int page = Request.Query.page; + int per_page = Request.Query.per_page; + + int min = (page - 1) * per_page; + + int max = page * per_page; + + var unmapped = rootFolder.UnmappedFolders.OrderBy(f => f.Name).ToList(); + + int total_count = unmapped.Count; + + if (Request.Query.total_entries.HasValue) + { + total_count = Request.Query.total_entries; + } + + max = total_count >= max ? max : total_count; + + var paged = unmapped.GetRange(min, max-min); + + var mapped = paged.Select(f => + { + Core.Tv.Movie m = null; + + var mappedMovie = _mappedMovies.Find(f.Name); + + if (mappedMovie != null) + { + return mappedMovie; + } + + var parsedTitle = Parser.ParseMoviePath(f.Name); + if (parsedTitle == null) + { + m = new Core.Tv.Movie + { + Title = f.Name.Replace(".", " ").Replace("-", " "), + Path = f.Path, + }; + } + else + { + m = new Core.Tv.Movie + { + Title = parsedTitle.MovieTitle, + Year = parsedTitle.Year, + ImdbId = parsedTitle.ImdbId, + Path = f.Path + }; + } + + + + var files = _diskScanService.GetVideoFiles(f.Path); + + var decisions = _importDecisionMaker.GetImportDecisions(files.ToList(), m); + + var decision = decisions.Where(d => d.Approved && !d.Rejections.Any()).FirstOrDefault(); + + if (decision != null) + { + var local = decision.LocalMovie; + + m.MovieFile = new LazyLoaded(new MovieFile + { + Path = local.Path, + Edition = local.ParsedMovieInfo.Edition, + Quality = local.Quality, + MediaInfo = local.MediaInfo, + ReleaseGroup = local.ParsedMovieInfo.ReleaseGroup, + RelativePath = f.Path.GetRelativePath(local.Path) + }); + } + + mappedMovie = _searchProxy.MapMovieToTmdbMovie(m); + mappedMovie.Monitored = true; + + _mappedMovies.Set(f.Name, mappedMovie, TimeSpan.FromDays(2)); + + return mappedMovie; + }); + + return new PagingResource + { + Page = page, + PageSize = per_page, + SortDirection = SortDirection.Ascending, + SortKey = Request.Query.sort_by, + TotalRecords = total_count - mapped.Where(m => m == null).Count(), + Records = MapToResource(mapped.Where(m => m != null)).ToList() + }.AsResponse(); + } + + + private static IEnumerable MapToResource(IEnumerable movies) + { + foreach (var currentSeries in movies) + { + var resource = currentSeries.ToResource(); + var poster = currentSeries.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index d467e397e..0df46ff4a 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -118,8 +118,9 @@ + - + @@ -245,7 +246,6 @@ - @@ -295,11 +295,11 @@ - + \ No newline at end of file diff --git a/src/NzbDrone.Api/Series/MovieLookupModule.cs b/src/NzbDrone.Api/Series/MovieLookupModule.cs index 1120b3046..fcf0ca5a4 100644 --- a/src/NzbDrone.Api/Series/MovieLookupModule.cs +++ b/src/NzbDrone.Api/Series/MovieLookupModule.cs @@ -4,18 +4,35 @@ using NzbDrone.Api.Extensions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using System.Linq; +using System; +using NzbDrone.Api.REST; namespace NzbDrone.Api.Movie { public class MovieLookupModule : NzbDroneRestModule { private readonly ISearchForNewMovie _searchProxy; + private readonly IProvideMovieInfo _movieInfo; - public MovieLookupModule(ISearchForNewMovie searchProxy) + public MovieLookupModule(ISearchForNewMovie searchProxy, IProvideMovieInfo movieInfo) : base("/movies/lookup") { + _movieInfo = movieInfo; _searchProxy = searchProxy; Get["/"] = x => Search(); + Get["/tmdb"] = x => SearchByTmdbId(); + } + + private Response SearchByTmdbId() + { + int tmdbId = -1; + if(Int32.TryParse(Request.Query.tmdbId, out tmdbId)) + { + var result = _movieInfo.GetMovieInfo(tmdbId, null); + return result.ToResource().AsResponse(); + } + + throw new BadRequestException("Tmdb Id was not valid"); } @@ -25,7 +42,6 @@ namespace NzbDrone.Api.Movie return MapToResource(imdbResults).AsResponse(); } - private static IEnumerable MapToResource(IEnumerable movies) { foreach (var currentSeries in movies) diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 124957358..aaa9e9f78 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -637,6 +637,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook newMovie.RootFolderPath = movie.RootFolderPath; newMovie.ProfileId = movie.ProfileId; newMovie.Monitored = movie.Monitored; + newMovie.MovieFile = movie.MovieFile; return newMovie; } diff --git a/src/UI/AddMovies/AddMoviesLayout.js b/src/UI/AddMovies/AddMoviesLayout.js index 0a62ac755..460be57e0 100644 --- a/src/UI/AddMovies/AddMoviesLayout.js +++ b/src/UI/AddMovies/AddMoviesLayout.js @@ -7,6 +7,7 @@ var AddMoviesView = require('./AddMoviesView'); var ProfileCollection = require('../Profile/ProfileCollection'); var AddFromListView = require("./List/AddFromListView"); var RootFolderCollection = require('./RootFolders/RootFolderCollection'); +var BulkImportView = require("./BulkImport/BulkImportView"); require('../Movies/MoviesCollection'); module.exports = Marionette.Layout.extend({ @@ -22,6 +23,7 @@ module.exports = Marionette.Layout.extend({ events : { 'click .x-import' : '_importMovies', + 'click .x-bulk-import' : '_bulkImport', 'click .x-add-new' : '_addMovies', "click .x-add-lists" : "_addFromList", 'click .x-show-existing' : '_toggleExisting' @@ -59,6 +61,11 @@ module.exports = Marionette.Layout.extend({ this.workspace.show(new ExistingMoviesCollectionView({ model : options.model })); }, + _bulkFolderSelected : function(options) { + vent.trigger(vent.Commands.CloseModalCommand); + this.workspace.show(new BulkImportView({ model : options.model})); + }, + _importMovies : function() { this.rootFolderLayout = new RootFolderLayout(); this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); @@ -72,5 +79,11 @@ module.exports = Marionette.Layout.extend({ _addFromList : function() { //this.ui.$existing.hide(); this.workspace.show(new AddFromListView()); + }, + + _bulkImport : function() { + this.bulkRootFolderLayout = new RootFolderLayout(); + this.listenTo(this.bulkRootFolderLayout, 'folderSelected', this._bulkFolderSelected); + AppLayout.modalRegion.show(this.bulkRootFolderLayout); } }); diff --git a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs index 48667d34c..802afb657 100644 --- a/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs +++ b/src/UI/AddMovies/AddMoviesLayoutTemplate.hbs @@ -1,7 +1,8 @@
- + diff --git a/src/UI/AddMovies/AddMoviesView.js b/src/UI/AddMovies/AddMoviesView.js index cf40fa838..a871af517 100644 --- a/src/UI/AddMovies/AddMoviesView.js +++ b/src/UI/AddMovies/AddMoviesView.js @@ -26,7 +26,6 @@ module.exports = Marionette.Layout.extend({ }, initialize : function(options) { - console.log(options); this.isExisting = options.isExisting; this.collection = new AddMoviesCollection(); diff --git a/src/UI/AddMovies/BulkImport/BulkImportCollection.js b/src/UI/AddMovies/BulkImport/BulkImportCollection.js new file mode 100644 index 000000000..58ee9c86c --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportCollection.js @@ -0,0 +1,91 @@ +var _ = require('underscore'); +var PageableCollection = require('backbone.pageable'); +var MovieModel = require('../../Movies/MovieModel'); +var AsSortedCollection = require('../../Mixins/AsSortedCollection'); +var AsPageableCollection = require('../../Mixins/AsPageableCollection'); +var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); + +var BulkImportCollection = PageableCollection.extend({ + url : window.NzbDrone.ApiRoot + '/movies/bulkimport', + model : MovieModel, + mode: "infinite", + tableName : 'bulkimport', + + state : { + pageSize : 15, + sortKey: 'sortTitle', + firstPage: 1 + }, + + queryParams: { + totalPages: null, + totalRecords: null, + sortKey: "sort", + order: "direction", + directions: { + "-1": "asc", + "1": "desc" + } + }, + + // queryParams : { + // totalPages : null, + // totalRecords : null, + // pageSize : 'pageSize', + // sortKey : 'sortKey' + // }, + + /*parse : function(response) { + var self = this; + + _.each(response.records, function(model) { + model.id = undefined; + }); + + return response; + },*/ + + parseState : function(resp) { + return { totalRecords : resp.totalRecords }; + }, + + parseRecords : function(resp) { + if (resp) { + return resp.records; + } + + return resp; + }, + + fetch : function(options) { + + options = options || {}; + + var data = options.data || {}; + + if (data.id === undefined || data.folder === undefined) { + data.id = this.folderId; + data.folder = this.folder; + } + + options.data = data; + + return PageableCollection.prototype.fetch.call(this, options); + }, + + parseLinks : function(options) { + console.log(options); + return { + first : this.url, + next: this.url, + last : this.url + } + } +}); + + +BulkImportCollection = AsSortedCollection.call(BulkImportCollection); +BulkImportCollection = AsPageableCollection.call(BulkImportCollection); +BulkImportCollection = AsPersistedStateCollection.call(BulkImportCollection); + +module.exports = BulkImportCollection; diff --git a/src/UI/AddMovies/BulkImport/BulkImportMonitorCell.js b/src/UI/AddMovies/BulkImport/BulkImportMonitorCell.js new file mode 100644 index 000000000..88d599ad6 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportMonitorCell.js @@ -0,0 +1,79 @@ +var Backgrid = require('backgrid'); +var Config = require('../../Config'); +var _ = require('underscore'); +var vent = require("vent"); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var NzbDroneCell = require("../../Cells/NzbDroneCell"); + +module.exports = TemplatedCell.extend({ + className : 'monitor-cell', + template : 'AddMovies/BulkImport/BulkImportMonitorCell', + + _orig : TemplatedCell.prototype.initialize, + _origRender : TemplatedCell.prototype.initialize, + + ui : { + monitor : ".x-monitor", + }, + + events: { "change .x-monitor" : "_monitorChanged" }, + + initialize : function () { + this._orig.apply(this, arguments); + + this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); + + this.defaultMonitor = Config.getValue(Config.Keys.MonitorEpisodes, 'all'); + + this.model.set('monitored', this._convertMonitorToBool(this.defaultMonitor)); + + this.$el.find('.x-monitor').val(this.defaultMonitor); + // this.ui.monitor.val(this.defaultProfile);//this.ui.profile.val(this.defaultProfile); + // this.model.set("profileId", this.defaultProfile); + + // this.cellValue = ProfileCollection; + + + //this.render(); + //this.listenTo(ProfileCollection, 'sync', this.render); + + }, + + _convertMonitorToBool : function(monitorString) { + return monitorString === 'all' ? true : false; + }, + + _monitorChanged : function() { + Config.setValue(Config.Keys.MonitorEpisodes, this.$el.find('.x-monitor').val()); + this.defaultMonitor = this.$el.find('.x-monitor').val(); + this.model.set("monitored", this._convertMonitorToBool(this.$el.find('.x-monitor').val())); + }, + + _onConfigUpdated : function(options) { + if (options.key === Config.Keys.MonitorEpisodes) { + this.$el.find('.x-monitor').val(options.value); + } + }, + + render : function() { + var templateName = this.column.get('template') || this.template; + + // this.cellValue = ProfileCollection; + + this.templateFunction = Marionette.TemplateCache.get(templateName); + this.$el.empty(); + + if (this.cellValue) { + var data = this.cellValue.toJSON(); + var html = this.templateFunction(data); + this.$el.html(html); + } + + this.delegateEvents(); + + this.$el.find('.x-monitor').val(this.defaultMonitor); + + return this; + } + +}); diff --git a/src/UI/AddMovies/BulkImport/BulkImportMonitorCellTemplate.hbs b/src/UI/AddMovies/BulkImport/BulkImportMonitorCellTemplate.hbs new file mode 100644 index 000000000..5ef509ce1 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportMonitorCellTemplate.hbs @@ -0,0 +1,4 @@ + diff --git a/src/UI/AddMovies/BulkImport/BulkImportMovieTitleCell.js b/src/UI/AddMovies/BulkImport/BulkImportMovieTitleCell.js new file mode 100644 index 000000000..84c26b236 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportMovieTitleCell.js @@ -0,0 +1,21 @@ +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var BulkImportCollection = require("./BulkImportCollection"); + +module.exports = NzbDroneCell.extend({ + className : 'series-title-cell', + + render : function() { + var collection = this.model.collection; + //this.listenTo(collection, 'sync', this._renderCell); + + this._renderCell(); + + return this; + }, + + _renderCell : function() { + this.$el.empty(); + + this.$el.html('' + this.cellValue.get('title') + ' (' + this.cellValue.get('year') + ')' +''); + } +}); diff --git a/src/UI/AddMovies/BulkImport/BulkImportProfileCell.js b/src/UI/AddMovies/BulkImport/BulkImportProfileCell.js new file mode 100644 index 000000000..20fa7baa3 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportProfileCell.js @@ -0,0 +1,45 @@ +var Backgrid = require('backgrid'); +var ProfileCollection = require('../../Profile/ProfileCollection'); +var Config = require('../../Config'); +var _ = require('underscore'); + +module.exports = Backgrid.SelectCell.extend({ + className : 'profile-cell', + + _orig : Backgrid.SelectCell.prototype.initialize, + + initialize : function () { + this._orig.apply(this, arguments); + + this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); + if(ProfileCollection.get(this.defaultProfile)) + { + this.profile = this.defaultProfile; + } + + this.render(); + //this.listenTo(ProfileCollection, 'sync', this.render); + + }, + + optionValues : function() { + //debugger; + return _.map(ProfileCollection.models, function(model){ + return [model.get("name"), model.get("id")+""]; + }); + } + + /*render : function() { + + this.$el.empty(); + var profileId = this.model.get(this.column.get('name')); + + var profile = _.findWhere(ProfileCollection.models, { id : profileId }); + + if (profile) { + this.$el.html(profile.get('name')); + } + + return this; + }*/ +}); diff --git a/src/UI/AddMovies/BulkImport/BulkImportProfileCellT.js b/src/UI/AddMovies/BulkImport/BulkImportProfileCellT.js new file mode 100644 index 000000000..263304942 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportProfileCellT.js @@ -0,0 +1,77 @@ +var Backgrid = require('backgrid'); +var ProfileCollection = require('../../Profile/ProfileCollection'); +var Config = require('../../Config'); +var _ = require('underscore'); +var vent = require("vent"); +var TemplatedCell = require('../../Cells/TemplatedCell'); +var NzbDroneCell = require("../../Cells/NzbDroneCell"); + +module.exports = TemplatedCell.extend({ + className : 'profile-cell', + template : 'AddMovies/BulkImport/BulkImportProfileCell', + + _orig : TemplatedCell.prototype.initialize, + _origRender : TemplatedCell.prototype.initialize, + + ui : { + profile : ".x-profile", + }, + + events: { "change .x-profile" : "_profileChanged" }, + + initialize : function () { + this._orig.apply(this, arguments); + + this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); + + this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); + if(ProfileCollection.get(this.defaultProfile)) + { + this.profile = this.defaultProfile; + this.$(".x-profile").val(this.defaultProfile);//this.ui.profile.val(this.defaultProfile); + this.model.set("profileId", this.defaultProfile) + } + + this.cellValue = ProfileCollection; + + + //this.render(); + //this.listenTo(ProfileCollection, 'sync', this.render); + + }, + + _profileChanged : function() { + Config.setValue(Config.Keys.DefaultProfileId, this.$(".x-profile").val()); + this.model.set("profileId", this.$(".x-profile").val()); + }, + + _onConfigUpdated : function(options) { + if (options.key === Config.Keys.DefaultProfileId) { + this.defaultProfile = options.value; + this.$(".x-profile").val(this.defaultProfile); + // + //this.render(); + //this.ui.profile.val(options.value); + } + }, + + render : function() { + var templateName = this.column.get('template') || this.template; + + this.cellValue = ProfileCollection; + + this.templateFunction = Marionette.TemplateCache.get(templateName); + this.$el.empty(); + + if (this.cellValue) { + var data = this.cellValue.toJSON(); + var html = this.templateFunction(data); + this.$el.html(html); + } + + this.delegateEvents(); + this.$(".x-profile").val(this.defaultProfile); + return this; + } + +}); diff --git a/src/UI/AddMovies/BulkImport/BulkImportProfileCellTemplate.hbs b/src/UI/AddMovies/BulkImport/BulkImportProfileCellTemplate.hbs new file mode 100644 index 000000000..7124319eb --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportProfileCellTemplate.hbs @@ -0,0 +1,5 @@ + diff --git a/src/UI/AddMovies/BulkImport/BulkImportView.js b/src/UI/AddMovies/BulkImport/BulkImportView.js new file mode 100644 index 000000000..9beb7f89b --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportView.js @@ -0,0 +1,219 @@ +var $ = require('jquery'); +var _ = require('underscore'); +var Marionette = require('marionette'); +var Backgrid = require('backgrid'); +var MovieTitleCell = require('./BulkImportMovieTitleCell'); +var BulkImportCollection = require("./BulkImportCollection"); +var QualityCell = require('./QualityCell'); +var TmdbIdCell = require('./TmdbIdCell'); +var GridPager = require('../../Shared/Grid/Pager'); +var SelectAllCell = require('../../Cells/SelectAllCell'); +var ProfileCell = require('./BulkImportProfileCellT'); +var MonitorCell = require('./BulkImportMonitorCell'); +var MoviePathCell = require("./MoviePathCell"); +var LoadingView = require('../../Shared/LoadingView'); +var EmptyView = require("./EmptyView"); +var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); +var CommandController = require('../../Commands/CommandController'); +var Messenger = require('../../Shared/Messenger'); +var MoviesCollection = require('../../Movies/MoviesCollection'); +var ProfileCollection = require('../../Profile/ProfileCollection'); + +require('backgrid.selectall'); +require('../../Mixins/backbone.signalr.mixin'); + +module.exports = Marionette.Layout.extend({ + template : 'AddMovies/BulkImport/BulkImportViewTemplate', + + regions : { + toolbar : '#x-toolbar', + table : '#x-movies-bulk', + pager : '#x-movies-bulk-pager' + }, + + ui : { + addSelectdBtn : '.x-add-selected', + addAllBtn : '.x-add-all', + pageSizeSelector : '.x-page-size' + }, + + events: { "change .x-page-size" : "_pageSizeChanged" }, + + initialize : function(options) { + ProfileCollection.fetch(); + this.bulkImportCollection = new BulkImportCollection().bindSignalR({ updateOnly : true }); + this.model = options.model; + this.folder = this.model.get("path"); + this.folderId = this.model.get("id"); + this.bulkImportCollection.folderId = this.folderId; + this.bulkImportCollection.folder = this.folder; + this.bulkImportCollection.fetch(); + this.listenTo(this.bulkImportCollection, {"sync" : this._showContent, "error" : this._showContent, "backgrid:selected" : this._select}); + }, + + _pageSizeChanged : function(event) { + var pageSize = parseInt($(event.target).val()); + this.bulkImportCollection.setPageSize(pageSize); + this.bulkImportCollection.fetch(); + }, + + columns : [ + { + name : '', + cell : SelectAllCell, + headerCell : 'select-all', + sortable : false + }, + { + name : 'movie', + label : 'Movie', + cell : MovieTitleCell, + cellValue : 'this', + sortable : false, + }, + { + name : "path", + label : "Path", + cell : MoviePathCell, + cellValue : 'this', + sortable : false, + }, + { + name : 'tmdbId', + label : 'Tmdb Id', + cell : TmdbIdCell, + cellValue : 'this', + sortable: false + }, + { + name :'monitor', + label: 'Monitor', + cell : MonitorCell, + cellValue : 'this' + }, + { + name : 'profileId', + label : 'Profile', + cell : ProfileCell, + cellValue : "this", + }, + { + name : 'quality', + label : 'Quality', + cell : QualityCell, + cellValue : 'this', + sortable : false + + } + ], + + _showContent : function() { + this._showToolbar(); + this._showTable(); + }, + + onShow : function() { + this.table.show(new LoadingView()); + }, + + _showToolbar : function() { + var leftSideButtons = { + type : 'default', + storeState: false, + collapse : true, + items : [ + { + title : 'Add Selected', + icon : 'icon-sonarr-add', + 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' + } + ] + }; + + this.toolbar.show(new ToolbarLayout({ + left : [leftSideButtons], + right : [], + context : this + })); + + $('#x-toolbar').addClass('inline'); + }, + + _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'); + + if (selected.length === 0) { + Messenger.show({ + type : 'error', + 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), + hideOnNavigate : false, + hideAfter : 30, + type : "error" + }); + + promise.done(function() { + Messenger.show({ + message : "Imported movies from list.", + hideAfter : 8, + hideOnNavigate : true + }); + }); + }, + + _addAll : function() { + console.log("TODO"); + }, + + _handleEvent : function(event_name, data) { + if (event_name == "sync" || event_name == "content") { + this._showContent() + } + }, + + _select : function(model, selected) { + model.selected = selected; + }, + + _showTable : function() { + if (this.bulkImportCollection.length === 0) { + this.table.show(new EmptyView({ folder : this.folder })); + return; + } + + this.importGrid = new Backgrid.Grid({ + columns : this.columns, + collection : this.bulkImportCollection, + className : 'table table-hover' + }); + + this.table.show(this.importGrid); + + this.pager.show(new GridPager({ + columns : this.columns, + collection : this.bulkImportCollection + })); + } +}); diff --git a/src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs b/src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs new file mode 100644 index 000000000..22a66cc68 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/BulkImportViewTemplate.hbs @@ -0,0 +1,13 @@ +
+{{> PageSizePartial }} +
+
+
+
+
+ +
+
+
+
+
diff --git a/src/UI/AddMovies/BulkImport/EmptyView.js b/src/UI/AddMovies/BulkImport/EmptyView.js new file mode 100644 index 000000000..109feba40 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/EmptyView.js @@ -0,0 +1,11 @@ +var Marionette = require('marionette'); + +module.exports = Marionette.CompositeView.extend({ + template : 'AddMovies/BulkImport/EmptyViewTemplate', + + + initialize : function (options) { + this.templateHelpers = {}; + this.templateHelpers.folder = options.folder; + } +}); diff --git a/src/UI/AddMovies/BulkImport/EmptyViewTemplate.hbs b/src/UI/AddMovies/BulkImport/EmptyViewTemplate.hbs new file mode 100644 index 000000000..aa0854efa --- /dev/null +++ b/src/UI/AddMovies/BulkImport/EmptyViewTemplate.hbs @@ -0,0 +1,3 @@ +
+ No movies found in folder {{folder}}. Have you already added all of them? +
diff --git a/src/UI/AddMovies/BulkImport/MoviePathCell.js b/src/UI/AddMovies/BulkImport/MoviePathCell.js new file mode 100644 index 000000000..7fcc140be --- /dev/null +++ b/src/UI/AddMovies/BulkImport/MoviePathCell.js @@ -0,0 +1,7 @@ +var TemplatedCell = require('../../Cells/TemplatedCell'); + +module.exports = TemplatedCell.extend({ + className : 'series-title-cell', + template : 'AddMovies/BulkImport/MoviePathTemplate', + +}); diff --git a/src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs b/src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs new file mode 100644 index 000000000..62529c955 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/MoviePathTemplate.hbs @@ -0,0 +1,2 @@ +{{path}}
+{{#if movieFile.relativePath}} {{movieFile.relativePath}}{{else}} Movie File Not Found{{/if}} diff --git a/src/UI/AddMovies/BulkImport/PageSizePartial.hbs b/src/UI/AddMovies/BulkImport/PageSizePartial.hbs new file mode 100644 index 000000000..2695622e5 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/PageSizePartial.hbs @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/src/UI/AddMovies/BulkImport/QualityCell.js b/src/UI/AddMovies/BulkImport/QualityCell.js new file mode 100644 index 000000000..3746f75ce --- /dev/null +++ b/src/UI/AddMovies/BulkImport/QualityCell.js @@ -0,0 +1,8 @@ +var TemplatedCell = require('../../Cells/TemplatedCell'); +var QualityCellEditor = require('../../Cells/Edit/QualityCellEditor'); + +module.exports = TemplatedCell.extend({ + className : 'quality-cell', + template : 'AddMovies/BulkImport/QualityCellTemplate', + editor : QualityCellEditor +}); diff --git a/src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs b/src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs new file mode 100644 index 000000000..d1f3da9ba --- /dev/null +++ b/src/UI/AddMovies/BulkImport/QualityCellTemplate.hbs @@ -0,0 +1,5 @@ +{{#if_gt proper compare="1"}} + {{movieFile.quality.quality.name}} +{{else}} + {{movieFile.quality.quality.name}} +{{/if_gt}} diff --git a/src/UI/AddMovies/BulkImport/TmdbIdCell.js b/src/UI/AddMovies/BulkImport/TmdbIdCell.js new file mode 100644 index 000000000..5559afdd8 --- /dev/null +++ b/src/UI/AddMovies/BulkImport/TmdbIdCell.js @@ -0,0 +1,62 @@ +var vent = require('vent'); +var _ = require('underscore'); +var $ = require('jquery'); +var NzbDroneCell = require('../../Cells/NzbDroneCell'); +var CommandController = require('../../Commands/CommandController'); + +module.exports = NzbDroneCell.extend({ + className : 'tmdbId-cell', + + // would like to use change with a _.debounce eventually + events : { + 'blur input.tmdbId-input' : '_updateId' + }, + + render : function() { + this.$el.empty(); + + this.$el.html(''); + + return this; + }, + + _updateId : function() { + var field = this.$el.find('.x-tmdbId'); + var data = field.val(); + + var promise = $.ajax({ + url : window.NzbDrone.ApiRoot + '/movies/lookup/tmdb?tmdbId=' + data, + type : 'GET', + }); + + //field.spinForPromise(promise); + + field.prop("disabled", true) + + var icon = this.$(".icon-sonarr-info"); + + icon.removeClass("hidden"); + + icon.spinForPromise(promise); + var _self = this; + var cacheMonitored = this.model.get('monitored'); + var cacheProfile = this.model.get("profileId"); + var cachePath = this.model.get("path"); + var cacheFile = this.model.get("movieFile"); + var cacheRoot = this.model.get("rootFolderPath"); + + 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) + }); + + promise.error(function(request, status, error) { + console.error("Status: " + status, "Error: " + error); + field.prop("disabled", false) + }); + } +}); diff --git a/src/UI/AddMovies/addMovies.less b/src/UI/AddMovies/addMovies.less index e1eaad2c8..7481eb5c8 100644 --- a/src/UI/AddMovies/addMovies.less +++ b/src/UI/AddMovies/addMovies.less @@ -1,6 +1,17 @@ @import "../Shared/Styles/card.less"; @import "../Shared/Styles/clickable.less"; +.inline { + display: inline-block; +} + +.page-size { + display: inline-block; + width: 200px; + float: right; + margin-top: 8px; +} + #add-movies-screen { .existing-movies { diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less index a5a13707a..53fb18355 100644 --- a/src/UI/Cells/cells.less +++ b/src/UI/Cells/cells.less @@ -8,13 +8,34 @@ .series-title-cell { .text-overflow(); - max-width: 450px; + max-width: 350px; @media @sm { max-width: 250px } } +.tmdbId-cell { + .text-overflow(); + + max-width: 100px; + min-width: 100px; +} + +.monitor-cell { + .text-overflow(); + + max-width: 150px; + min-width: 100px; +} + +.profile-cell { + .text-overflow(); + + max-width: 150px; + min-width: 100px; +} + .episode-title-cell { .text-overflow(); diff --git a/src/UI/JsLibraries/backbone.pageable.js b/src/UI/JsLibraries/backbone.pageable.js index f6cdbcacd..c377cf5d8 100644 --- a/src/UI/JsLibraries/backbone.pageable.js +++ b/src/UI/JsLibraries/backbone.pageable.js @@ -572,7 +572,7 @@ if (mode == "infinite") { if (!links[currentPage + '']) { - throw new RangeError("No link found for page " + currentPage); + //throw new RangeError("No link found for page " + currentPage); } } else if (currentPage < firstPage || @@ -756,7 +756,7 @@ hasNext: function () { var state = this.state; var currentPage = this.state.currentPage; - if (this.mode != "infinite") return currentPage < state.lastPage; + if (true/*this.mode != "infinite"*/) return currentPage < state.lastPage; return !!this.links[currentPage + 1]; }, @@ -1207,9 +1207,16 @@ if (_isUndefined(options.silent)) delete opts.silent; else opts.silent = options.silent; + //console.log(_extend({at: fullCol.length}, opts)); + var models = col.models; - if (mode == "client") fullCol.reset(models, opts); - else fullCol.add(models, _extend({at: fullCol.length}, opts)); + if (mode == "client") { + fullCol.reset(models, opts); + } else { + opts.remove = false; + fullCol.add(models, _extend({at: fullCol.length}, opts)); + opts.remove = true; + } if (success) success(col, resp, opts); }; diff --git a/src/UI/Movies/movies.less b/src/UI/Movies/movies.less index cdcbe23bd..89db4206a 100644 --- a/src/UI/Movies/movies.less +++ b/src/UI/Movies/movies.less @@ -8,6 +8,10 @@ max-width: 100%; } +.tmdbId-input { + border-radius: 4px; +} + .movie-tabs-card { .card; .opacity(0.9); diff --git a/src/UI/Shared/Grid/Pager.js b/src/UI/Shared/Grid/Pager.js index 618117cf9..5919727b5 100644 --- a/src/UI/Shared/Grid/Pager.js +++ b/src/UI/Shared/Grid/Pager.js @@ -83,7 +83,7 @@ module.exports = Paginator.extend({ var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; var windowEnd = Math.min(lastPage + 1, windowStart + this.windowSize); - if (collection.mode !== 'infinite') { + if (true/*collection.mode !== 'infinite'*/) { for (var i = windowStart; i < windowEnd; i++) { handles.push({ label : i + 1, @@ -185,4 +185,4 @@ module.exports = Paginator.extend({ this.$el.find('.x-page-number').html(''); this.collection.getPage(selectedPage); } -}); \ No newline at end of file +});