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
This commit is contained in:
Leonardo Galli 2017-02-08 01:09:36 +01:00 committed by Tim Turner
parent 0d1150d4d2
commit 35b384439f
30 changed files with 924 additions and 20 deletions

View File

@ -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;

View File

@ -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<UnmappedFolder>
{
public int Compare(UnmappedFolder a, UnmappedFolder b)
{
return a.Name.CompareTo(b.Name);
}
}
public class MovieBulkImportModule : NzbDroneRestModule<MovieResource>
{
private readonly ISearchForNewMovie _searchProxy;
private readonly IRootFolderService _rootFolderService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IDiskScanService _diskScanService;
private readonly ICached<Core.Tv.Movie> _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<Core.Tv.Movie>(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<MovieFile>(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<MovieResource>
{
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<MovieResource> MapToResource(IEnumerable<Core.Tv.Movie> 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;
}
}
}
}

View File

@ -118,8 +118,9 @@
<Compile Include="Frontend\Mappers\RobotsTxtMapper.cs" />
<Compile Include="Indexers\ReleaseModuleBase.cs" />
<Compile Include="Indexers\ReleasePushModule.cs" />
<Compile Include="Movies\MovieBulkImportModule.cs" />
<Compile Include="Movies\MovieFileModule.cs" />
<Compile Include="Movies\MovieModule.cs" />
<Compile Include="Series\MovieModule.cs" />
<Compile Include="Movies\RenameMovieModule.cs" />
<Compile Include="Movies\RenameMovieResource.cs" />
<Compile Include="Movies\MovieEditorModule.cs" />
@ -245,7 +246,6 @@
<Compile Include="Series\SeriesEditorModule.cs" />
<Compile Include="Series\MovieLookupModule.cs" />
<Compile Include="Series\SeriesLookupModule.cs" />
<Compile Include="Series\MovieModule.cs" />
<Compile Include="Series\SeriesModule.cs" />
<Compile Include="Series\MovieResource.cs" />
<Compile Include="Series\SeriesResource.cs" />
@ -295,11 +295,11 @@
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@ -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<MovieResource>
{
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<MovieResource> MapToResource(IEnumerable<Core.Tv.Movie> movies)
{
foreach (var currentSeries in movies)

View File

@ -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;
}

View File

@ -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);
}
});

View File

@ -1,7 +1,8 @@
<div class="row">
<div class="col-md-12">
<div class="btn-group add-movies-btn-group btn-group-lg btn-block">
<button type="button" class="btn btn-default col-md-7 col-xs-4 add-movies-import-btn x-import">
<button class="btn btn-default col-md-3 col-xs-4 x-bulk-import"><i class="icon-sonarr-view-list hidden-xs"></i> Bulk Import Movies</button>
<button type="button" class="btn btn-default col-md-4 col-xs-4 add-movies-import-btn x-import">
<i class="icon-sonarr-hdd"/>
Import existing movies on disk
</button>

View File

@ -26,7 +26,6 @@ module.exports = Marionette.Layout.extend({
},
initialize : function(options) {
console.log(options);
this.isExisting = options.isExisting;
this.collection = new AddMoviesCollection();

View File

@ -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;

View File

@ -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;
}
});

View File

@ -0,0 +1,4 @@
<select class="col-md-2 form-control x-monitor">
<option value="all">Yes</option>
<option value="none">No</option>
</select>

View File

@ -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('<a href="https://www.themoviedb.org/movie/' + this.cellValue.get('tmdbId') +'">' + this.cellValue.get('title') + ' (' + this.cellValue.get('year') + ')' +'</a>');
}
});

View File

@ -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;
}*/
});

View File

@ -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;
}
});

View File

@ -0,0 +1,5 @@
<select class="col-md-2 form-control x-profile">
{{#each this}}
<option value="{{id}}">{{name}}</option>
{{/each}}
</select>

View File

@ -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
}));
}
});

View File

@ -0,0 +1,13 @@
<div id="x-toolbar"/>
{{> PageSizePartial }}
<div class="row">
<div class="col-md-12">
<div id="x-movies-bulk" class="queue table-responsive"/>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="x-movies-bulk-pager"/>
</div>
</div>

View File

@ -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;
}
});

View File

@ -0,0 +1,3 @@
<div class="text-center hint col-md-12">
<span>No movies found in folder {{folder}}. Have you already added all of them?</span>
</div>

View File

@ -0,0 +1,7 @@
var TemplatedCell = require('../../Cells/TemplatedCell');
module.exports = TemplatedCell.extend({
className : 'series-title-cell',
template : 'AddMovies/BulkImport/MoviePathTemplate',
});

View File

@ -0,0 +1,2 @@
{{path}}<br>
<span title="{{#if movieFile.relativePath}}&nbsp;{{movieFile.relativePath}}{{/if}}" class="hint" style="font-size: 12px;">{{#if movieFile.relativePath}}&nbsp;{{movieFile.relativePath}}{{else}}&nbsp;Movie File Not Found{{/if}}</span>

View File

@ -0,0 +1,8 @@
<select class="col-md-2 form-control page-size x-page-size">
<option value="15">15</option>
<option value="30">30</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>

View File

@ -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
});

View File

@ -0,0 +1,5 @@
{{#if_gt proper compare="1"}}
<span class="badge badge-info" title="PROPER">{{movieFile.quality.quality.name}}</span>
{{else}}
<span class="badge" title="{{#if movieFile.quality.hardcodedSubs}}Warning: {{movieFile.quality.hardcodedSubs}}{{/if}}">{{movieFile.quality.quality.name}}</span>
{{/if_gt}}

View File

@ -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('<i class="icon-sonarr-info hidden"></i><input type="text" class="x-tmdbId tmdbId-input form-control" value="' + this.cellValue.get('tmdbId') + '" />');
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)
});
}
});

View File

@ -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 {

View File

@ -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();

View File

@ -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);
};

View File

@ -8,6 +8,10 @@
max-width: 100%;
}
.tmdbId-input {
border-radius: 4px;
}
.movie-tabs-card {
.card;
.opacity(0.9);

View File

@ -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('<i class="icon-sonarr-spinner fa-spin"></i>');
this.collection.getPage(selectedPage);
}
});
});