1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-16 11:37:58 +02:00

Episode file editor

New: Ability to delete all episode files in a series or season
New: Ability to change quality for all episode files in a series or season
This commit is contained in:
Mark McDowall 2015-02-19 22:14:04 -08:00
parent 47a0f55255
commit 8ab0b26773
13 changed files with 334 additions and 14 deletions

View File

@ -0,0 +1,19 @@
var reqres = require('../reqres');
var NzbDroneCell = require('./NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'episode-file-path-cell',
render : function() {
this.$el.empty();
if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) {
var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId'));
this.$el.html(episodeFile.get('relativePath'));
}
this.delegateEvents();
return this;
}
});

View File

@ -467,3 +467,7 @@
.icon-sonarr-backup-update {
.fa-icon-content(@fa-var-retweet);
}
.icon-sonarr-episode-file {
.fa-icon-content(@fa-var-file-video-o);
}

View File

@ -0,0 +1,5 @@
var Marionette = require('marionette');
module.exports = Marionette.CompositeView.extend({
template : 'EpisodeFile/Editor/EmptyViewTemplate'
});

View File

@ -0,0 +1,5 @@
<div class="row">
<div class="col-md-12">
No episode files
</div>
</div>

View File

@ -0,0 +1,185 @@
var _ = require('underscore');
var reqres = require('../../reqres');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
var SelectAllCell = require('../../Cells/SelectAllCell');
var EpisodeNumberCell = require('../../Series/Details/EpisodeNumberCell');
var SeasonEpisodeNumberCell = require('../../Cells/EpisodeNumberCell');
var EpisodeFilePathCell = require('../../Cells/EpisodeFilePathCell');
var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell');
var RelativeDateCell = require('../../Cells/RelativeDateCell');
var EpisodeCollection = require('../../Series/EpisodeCollection');
var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection');
var QualitySelectView = require('./QualitySelectView');
var EmptyView = require('./EmptyView');
module.exports = Marionette.Layout.extend({
className : 'modal-lg',
template : 'EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate',
regions : {
episodeGrid : '.x-episode-list',
quality : '.x-quality'
},
ui : {
seasonMonitored : '.x-season-monitored'
},
events : {
'click .x-season-monitored' : '_seasonMonitored',
'click .x-delete-files' : '_deleteFiles'
},
initialize : function(options) {
if (!options.series) {
throw 'series is required';
}
if (!options.episodeCollection) {
throw 'episodeCollection is required';
}
var filtered = options.episodeCollection.filter(function(episode) {
return episode.get('episodeFileId') > 0;
});
this.series = options.series;
this.episodeCollection = options.episodeCollection;
this.filteredEpisodes = new EpisodeCollection(filtered);
this.templateHelpers = {};
this.templateHelpers.series = this.series.toJSON();
this._getColumns();
},
onRender : function() {
this._getQualities();
this._showEpisodes();
},
_getColumns : function () {
var episodeCell = {};
if (this.model) {
episodeCell.name = 'episodeNumber';
episodeCell.label = '#';
episodeCell.cell = EpisodeNumberCell;
}
else {
episodeCell.name = 'this';
episodeCell.label = 'Episode';
episodeCell.cell = SeasonEpisodeNumberCell;
}
this.columns = [
{
name : '',
cell : SelectAllCell,
headerCell : 'select-all',
sortable : false
},
episodeCell,
{
name : 'episodeNumber',
label : 'Relative Path',
cell : EpisodeFilePathCell
},
{
name : 'airDateUtc',
label : 'Air Date',
cell : RelativeDateCell
},
{
name : 'status',
label : 'Quality',
cell : EpisodeStatusCell,
sortable : false
}
];
if (!this.model) {
this.columns[1].name = 'this';
this.columns[1].cell = SeasonEpisodeNumberCell;
}
},
_showEpisodes : function() {
if (this.filteredEpisodes.length === 0) {
this.episodeGrid.show(new EmptyView());
return;
}
this.episodeGridView = new Backgrid.Grid({
columns : this.columns,
collection : this.filteredEpisodes,
className : 'table table-hover season-grid'
});
this.episodeGrid.show(this.episodeGridView);
},
_getQualities : function() {
var self = this;
var profileSchemaCollection = new ProfileSchemaCollection();
var promise = profileSchemaCollection.fetch();
promise.done(function() {
var profile = profileSchemaCollection.first();
self.qualitySelectView = new QualitySelectView({ qualities: _.map(profile.get('items'), 'quality') });
self.listenTo(self.qualitySelectView, 'seasonedit:quality', self._changeQuality);
self.quality.show(self.qualitySelectView);
});
},
_changeQuality : function(options) {
var newQuality = {
quality : options.selected,
revision : {
version : 1,
real : 0
}
};
var selected = this._getSelectedEpisodeFileIds();
_.each(selected, function(episodeFileId) {
if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) {
var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId);
episodeFile.set('quality', newQuality);
episodeFile.save();
}
});
},
_deleteFiles : function() {
if (!window.confirm('Are you sure you want to delete the episode files for the selected episodes?')) {
return;
}
var selected = this._getSelectedEpisodeFileIds();
_.each(selected, function(episodeFileId) {
if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) {
var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId);
episodeFile.destroy();
}
});
_.each(this.episodeGridView.getSelectedModels(), function(episode) {
this.episodeGridView.removeRow(episode);
}, this);
},
_getSelectedEpisodeFileIds: function () {
return _.uniq(_.map(this.episodeGridView.getSelectedModels(), function (episode) {
return episode.get('episodeFileId');
}));
}
});

View File

@ -0,0 +1,28 @@
<div class="modal-content">
<div class="edit-season-modal">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>
{{#if seasonNumber}}
{{#if_eq seasonNumber compare="0"}}
{{series.title}} - Specials
{{else}}
{{series.title}} - Season {{seasonNumber}}
{{/if_eq}}
{{else}}
{{series.title}}
{{/if}}
</h3>
</div>
<div class="modal-body">
<div class="x-episode-list"></div>
<div class="x-quality"></div>
</div>
<div class="modal-footer">
<button class="btn btn-danger x-delete-files">delete files</button>
<button class="btn btn-default" data-dismiss="modal">close</button>
</div>
</div>
</div>

View File

@ -0,0 +1,35 @@
var _ = require('underscore');
var Marionette = require('marionette');
module.exports = Marionette.ItemView.extend({
template : 'EpisodeFile/Editor/QualitySelectViewTemplate',
ui : {
select : '.x-select'
},
events : {
'change .x-select' : '_changeSelect'
},
initialize : function (options) {
this.qualities = options.qualities;
this.templateHelpers = {
qualities : this.qualities
};
},
_changeSelect : function () {
var value = this.ui.select.val();
if (value === 'choose') {
return;
}
var quality = _.find(this.qualities, { 'id': parseInt(value) });
this.trigger('seasonedit:quality', { selected : quality });
this.ui.select.val('choose');
}
});

View File

@ -0,0 +1,10 @@
<div class="row">
<div class="form-group col-md-3 col-md-offset-9">
<select class="form-control x-select">
<option value="choose">Select quality</option>
{{#each qualities}}
<option value="{{id}}">{{name}}</option>
{{/each}}
</select>
</div>
</div>

View File

@ -9,6 +9,7 @@ var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell');
var EpisodeNumberCell = require('./EpisodeNumberCell');
var EpisodeWarningCell = require('./EpisodeWarningCell');
var CommandController = require('../../Commands/CommandController');
var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
var moment = require('moment');
var _ = require('underscore');
var Messenger = require('../../Shared/Messenger');
@ -23,11 +24,12 @@ module.exports = Marionette.Layout.extend({
},
events : {
'click .x-season-monitored' : '_seasonMonitored',
'click .x-season-search' : '_seasonSearch',
'click .x-season-rename' : '_seasonRename',
'click .x-show-hide-episodes' : '_showHideEpisodes',
'dblclick .series-season h2' : '_showHideEpisodes'
'click .x-season-episode-file-editor' : '_openEpisodeFileEditor',
'click .x-season-monitored' : '_seasonMonitored',
'click .x-season-search' : '_seasonSearch',
'click .x-season-rename' : '_seasonRename',
'click .x-show-hide-episodes' : '_showHideEpisodes',
'dblclick .series-season h2' : '_showHideEpisodes'
},
regions : {
@ -104,7 +106,7 @@ module.exports = Marionette.Layout.extend({
initialize : function(options) {
if (!options.episodeCollection) {
throw 'episodeCollection is needed';
throw 'episodeCollection is required';
}
this.series = options.series;
@ -117,7 +119,7 @@ module.exports = Marionette.Layout.extend({
this.listenTo(this.model, 'sync', this._afterSeasonMonitored);
this.listenTo(this.episodeCollection, 'sync', this.render);
this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpsiodes);
this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes);
},
onRender : function() {
@ -281,8 +283,18 @@ module.exports = Marionette.Layout.extend({
});
},
_refreshEpsiodes : function() {
_refreshEpisodes : function() {
this._updateEpisodeCollection();
this.render();
},
_openEpisodeFileEditor : function() {
var view = new EpisodeFileEditorLayout({
model : this.model,
series : this.series,
episodeCollection : this.episodeCollection
});
vent.trigger(vent.Commands.OpenModalCommand, view);
}
});

View File

@ -24,6 +24,9 @@
{{/if_eq}}
<span class="season-actions pull-right">
<div class="x-season-episode-file-editor">
<i class="icon-sonarr-episode-file" title="Modify episode files for season"/>
</div>
<div class="x-season-rename">
<i class="icon-sonarr-rename" title="Preview rename for all episodes in season {{seasonNumber}}"/>
</div>

View File

@ -12,6 +12,7 @@ var SeasonCollectionView = require('./SeasonCollectionView');
var InfoView = require('./InfoView');
var CommandController = require('../../Commands/CommandController');
var LoadingView = require('../../Shared/LoadingView');
var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
require('backstrech');
require('../../Mixins/backbone.signalr.mixin');
@ -34,11 +35,12 @@ module.exports = Marionette.Layout.extend({
},
events : {
'click .x-monitored' : '_toggleMonitored',
'click .x-edit' : '_editSeries',
'click .x-refresh' : '_refreshSeries',
'click .x-rename' : '_renameSeries',
'click .x-search' : '_seriesSearch'
'click .x-episode-file-editor' : '_openEpisodeFileEditor',
'click .x-monitored' : '_toggleMonitored',
'click .x-edit' : '_editSeries',
'click .x-refresh' : '_refreshSeries',
'click .x-rename' : '_renameSeries',
'click .x-search' : '_seriesSearch'
},
initialize : function() {
@ -219,5 +221,14 @@ module.exports = Marionette.Layout.extend({
this._setMonitoredState();
this._showInfo();
},
_openEpisodeFileEditor : function() {
var view = new EpisodeFileEditorLayout({
series : this.model,
episodeCollection : this.episodeCollection
});
vent.trigger(vent.Commands.OpenModalCommand, view);
}
});

View File

@ -8,6 +8,9 @@
<i class="x-monitored" title="Toggle monitored state for entire series"/>
{{title}}
<div class="series-actions pull-right">
<div class="x-episode-file-editor">
<i class="icon-sonarr-episode-file" title="Modify episode files for series"/>
</div>
<div class="x-refresh">
<i class="icon-sonarr-refresh icon-can-spin" title="Update series info and scan disk"/>
</div>

View File

@ -312,7 +312,7 @@
}
.season-actions {
width: 70px;
width: 100px;
}
.season-actions, .series-actions {