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:
parent
47a0f55255
commit
8ab0b26773
19
src/UI/Cells/EpisodeFilePathCell.js
Normal file
19
src/UI/Cells/EpisodeFilePathCell.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
@ -467,3 +467,7 @@
|
|||||||
.icon-sonarr-backup-update {
|
.icon-sonarr-backup-update {
|
||||||
.fa-icon-content(@fa-var-retweet);
|
.fa-icon-content(@fa-var-retweet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-sonarr-episode-file {
|
||||||
|
.fa-icon-content(@fa-var-file-video-o);
|
||||||
|
}
|
||||||
|
5
src/UI/EpisodeFile/Editor/EmptyView.js
Normal file
5
src/UI/EpisodeFile/Editor/EmptyView.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
var Marionette = require('marionette');
|
||||||
|
|
||||||
|
module.exports = Marionette.CompositeView.extend({
|
||||||
|
template : 'EpisodeFile/Editor/EmptyViewTemplate'
|
||||||
|
});
|
5
src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs
Normal file
5
src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
No episode files
|
||||||
|
</div>
|
||||||
|
</div>
|
185
src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js
Normal file
185
src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js
Normal 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');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
@ -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">×</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>
|
35
src/UI/EpisodeFile/Editor/QualitySelectView.js
Normal file
35
src/UI/EpisodeFile/Editor/QualitySelectView.js
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
10
src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs
Normal file
10
src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs
Normal 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>
|
@ -9,6 +9,7 @@ var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell');
|
|||||||
var EpisodeNumberCell = require('./EpisodeNumberCell');
|
var EpisodeNumberCell = require('./EpisodeNumberCell');
|
||||||
var EpisodeWarningCell = require('./EpisodeWarningCell');
|
var EpisodeWarningCell = require('./EpisodeWarningCell');
|
||||||
var CommandController = require('../../Commands/CommandController');
|
var CommandController = require('../../Commands/CommandController');
|
||||||
|
var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
var _ = require('underscore');
|
var _ = require('underscore');
|
||||||
var Messenger = require('../../Shared/Messenger');
|
var Messenger = require('../../Shared/Messenger');
|
||||||
@ -23,11 +24,12 @@ module.exports = Marionette.Layout.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
events : {
|
events : {
|
||||||
'click .x-season-monitored' : '_seasonMonitored',
|
'click .x-season-episode-file-editor' : '_openEpisodeFileEditor',
|
||||||
'click .x-season-search' : '_seasonSearch',
|
'click .x-season-monitored' : '_seasonMonitored',
|
||||||
'click .x-season-rename' : '_seasonRename',
|
'click .x-season-search' : '_seasonSearch',
|
||||||
'click .x-show-hide-episodes' : '_showHideEpisodes',
|
'click .x-season-rename' : '_seasonRename',
|
||||||
'dblclick .series-season h2' : '_showHideEpisodes'
|
'click .x-show-hide-episodes' : '_showHideEpisodes',
|
||||||
|
'dblclick .series-season h2' : '_showHideEpisodes'
|
||||||
},
|
},
|
||||||
|
|
||||||
regions : {
|
regions : {
|
||||||
@ -104,7 +106,7 @@ module.exports = Marionette.Layout.extend({
|
|||||||
|
|
||||||
initialize : function(options) {
|
initialize : function(options) {
|
||||||
if (!options.episodeCollection) {
|
if (!options.episodeCollection) {
|
||||||
throw 'episodeCollection is needed';
|
throw 'episodeCollection is required';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.series = options.series;
|
this.series = options.series;
|
||||||
@ -117,7 +119,7 @@ module.exports = Marionette.Layout.extend({
|
|||||||
this.listenTo(this.model, 'sync', this._afterSeasonMonitored);
|
this.listenTo(this.model, 'sync', this._afterSeasonMonitored);
|
||||||
this.listenTo(this.episodeCollection, 'sync', this.render);
|
this.listenTo(this.episodeCollection, 'sync', this.render);
|
||||||
|
|
||||||
this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpsiodes);
|
this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRender : function() {
|
onRender : function() {
|
||||||
@ -281,8 +283,18 @@ module.exports = Marionette.Layout.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_refreshEpsiodes : function() {
|
_refreshEpisodes : function() {
|
||||||
this._updateEpisodeCollection();
|
this._updateEpisodeCollection();
|
||||||
this.render();
|
this.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
_openEpisodeFileEditor : function() {
|
||||||
|
var view = new EpisodeFileEditorLayout({
|
||||||
|
model : this.model,
|
||||||
|
series : this.series,
|
||||||
|
episodeCollection : this.episodeCollection
|
||||||
|
});
|
||||||
|
|
||||||
|
vent.trigger(vent.Commands.OpenModalCommand, view);
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -24,6 +24,9 @@
|
|||||||
{{/if_eq}}
|
{{/if_eq}}
|
||||||
|
|
||||||
<span class="season-actions pull-right">
|
<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">
|
<div class="x-season-rename">
|
||||||
<i class="icon-sonarr-rename" title="Preview rename for all episodes in season {{seasonNumber}}"/>
|
<i class="icon-sonarr-rename" title="Preview rename for all episodes in season {{seasonNumber}}"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@ var SeasonCollectionView = require('./SeasonCollectionView');
|
|||||||
var InfoView = require('./InfoView');
|
var InfoView = require('./InfoView');
|
||||||
var CommandController = require('../../Commands/CommandController');
|
var CommandController = require('../../Commands/CommandController');
|
||||||
var LoadingView = require('../../Shared/LoadingView');
|
var LoadingView = require('../../Shared/LoadingView');
|
||||||
|
var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
|
||||||
require('backstrech');
|
require('backstrech');
|
||||||
require('../../Mixins/backbone.signalr.mixin');
|
require('../../Mixins/backbone.signalr.mixin');
|
||||||
|
|
||||||
@ -34,11 +35,12 @@ module.exports = Marionette.Layout.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
events : {
|
events : {
|
||||||
'click .x-monitored' : '_toggleMonitored',
|
'click .x-episode-file-editor' : '_openEpisodeFileEditor',
|
||||||
'click .x-edit' : '_editSeries',
|
'click .x-monitored' : '_toggleMonitored',
|
||||||
'click .x-refresh' : '_refreshSeries',
|
'click .x-edit' : '_editSeries',
|
||||||
'click .x-rename' : '_renameSeries',
|
'click .x-refresh' : '_refreshSeries',
|
||||||
'click .x-search' : '_seriesSearch'
|
'click .x-rename' : '_renameSeries',
|
||||||
|
'click .x-search' : '_seriesSearch'
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize : function() {
|
initialize : function() {
|
||||||
@ -219,5 +221,14 @@ module.exports = Marionette.Layout.extend({
|
|||||||
|
|
||||||
this._setMonitoredState();
|
this._setMonitoredState();
|
||||||
this._showInfo();
|
this._showInfo();
|
||||||
|
},
|
||||||
|
|
||||||
|
_openEpisodeFileEditor : function() {
|
||||||
|
var view = new EpisodeFileEditorLayout({
|
||||||
|
series : this.model,
|
||||||
|
episodeCollection : this.episodeCollection
|
||||||
|
});
|
||||||
|
|
||||||
|
vent.trigger(vent.Commands.OpenModalCommand, view);
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -8,6 +8,9 @@
|
|||||||
<i class="x-monitored" title="Toggle monitored state for entire series"/>
|
<i class="x-monitored" title="Toggle monitored state for entire series"/>
|
||||||
{{title}}
|
{{title}}
|
||||||
<div class="series-actions pull-right">
|
<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">
|
<div class="x-refresh">
|
||||||
<i class="icon-sonarr-refresh icon-can-spin" title="Update series info and scan disk"/>
|
<i class="icon-sonarr-refresh icon-can-spin" title="Update series info and scan disk"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -312,7 +312,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.season-actions {
|
.season-actions {
|
||||||
width: 70px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.season-actions, .series-actions {
|
.season-actions, .series-actions {
|
||||||
|
Loading…
Reference in New Issue
Block a user