Merge pull request #15 from Qstick/feature/UIWork

Rename AddArtist, Artist UI folders. Other UI Work
pull/1/head
Joseph Milazzo 7 years ago committed by GitHub
commit da898cfd84

@ -17,8 +17,10 @@ gulp.task('less', function() {
paths.src.content + 'theme.less',
paths.src.content + 'overrides.less',
paths.src.root + 'Series/series.less',
paths.src.root + 'Artist/artist.less',
paths.src.root + 'Activity/activity.less',
paths.src.root + 'AddSeries/addSeries.less',
paths.src.root + 'AddArtist/addArtist.less',
paths.src.root + 'Calendar/calendar.less',
paths.src.root + 'Cells/cells.less',
paths.src.root + 'ManualImport/manualimport.less',

@ -10,7 +10,7 @@
<mapping url="http://localhost:8686/Quality" local-file="$PROJECT_DIR$/Quality" />
<mapping url="http://localhost:8686/Config.js" local-file="$PROJECT_DIR$/Config.js" />
<mapping url="http://localhost:8686/Shared" local-file="$PROJECT_DIR$/Shared" />
<mapping url="http://localhost:8686/AddSeries" local-file="$PROJECT_DIR$/AddSeries" />
<mapping url="http://localhost:8686/AddArtist" local-file="$PROJECT_DIR$/AddArtist" />
<mapping url="http://localhost:8686/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
<mapping url="http://localhost:8686" local-file="$PROJECT_DIR$" />
<mapping url="http://localhost:8686/Routing.js" local-file="$PROJECT_DIR$/Routing.js" />

@ -9,7 +9,7 @@
<mapping url="http://localhost:8686/Wanted" local-file="$PROJECT_DIR$/Wanted" />
<mapping url="http://localhost:8686/Config.js" local-file="$PROJECT_DIR$/Config.js" />
<mapping url="http://localhost:8686/Quality" local-file="$PROJECT_DIR$/Quality" />
<mapping url="http://localhost:8686/AddSeries" local-file="$PROJECT_DIR$/AddSeries" />
<mapping url="http://localhost:8686/AddArtist" local-file="$PROJECT_DIR$/AddArtist" />
<mapping url="http://localhost:8686/Shared" local-file="$PROJECT_DIR$/Shared" />
<mapping url="http://localhost:8686/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" />
<mapping url="http://localhost:8686" local-file="$PROJECT_DIR$" />

@ -0,0 +1,23 @@
var Backbone = require('backbone');
var ArtistModel = require('../Artist/ArtistModel');
var _ = require('underscore');
module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/artist/lookup',
model : ArtistModel,
parse : function(response) {
var self = this;
_.each(response, function(model) {
model.id = undefined;
if (self.unmappedFolderModel) {
model.path = self.unmappedFolderModel.get('folder').path;
}
});
console.log('response: ', response);
return response;
}
});

@ -0,0 +1,53 @@
var vent = require('vent');
var AppLayout = require('../AppLayout');
var Marionette = require('marionette');
var RootFolderLayout = require('./RootFolders/RootFolderLayout');
var ExistingArtistCollectionView = require('./Existing/AddExistingArtistCollectionView');
var AddArtistView = require('./AddArtistView');
var ProfileCollection = require('../Profile/ProfileCollection');
var RootFolderCollection = require('./RootFolders/RootFolderCollection');
require('../Artist/ArtistCollection');
module.exports = Marionette.Layout.extend({
template : 'AddArtist/AddArtistLayoutTemplate',
regions : {
workspace : '#add-artist-workspace'
},
events : {
'click .x-import' : '_importArtist',
'click .x-add-new' : '_addArtist'
},
attributes : {
id : 'add-artist-screen'
},
initialize : function() {
ProfileCollection.fetch();
RootFolderCollection.fetch().done(function() {
RootFolderCollection.synced = true;
});
},
onShow : function() {
this.workspace.show(new AddArtistView());
},
_folderSelected : function(options) {
vent.trigger(vent.Commands.CloseModalCommand);
this.workspace.show(new ExistingArtistCollectionView({ model : options.model }));
},
_importArtist : function() {
this.rootFolderLayout = new RootFolderLayout();
this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected);
AppLayout.modalRegion.show(this.rootFolderLayout);
},
_addArtist : function() {
this.workspace.show(new AddArtistView());
}
});

@ -0,0 +1,17 @@
<div class="row">
<div class="col-md-12">
<div class="btn-group add-artist-btn-group btn-group-lg btn-block">
<button type="button" class="btn btn-default col-md-10 col-xs-8 add-artist-import-btn x-import">
<i class="icon-lidarr-hdd"/>
Import existing artists on disk
</button>
<button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-lidarr-active hidden-xs"></i> Add New Artist</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="add-artist-workspace"></div>
</div>
</div>

@ -0,0 +1,183 @@
var _ = require('underscore');
var vent = require('vent');
var Marionette = require('marionette');
var AddArtistCollection = require('./AddArtistCollection');
var SearchResultCollectionView = require('./SearchResultCollectionView');
var EmptyView = require('./EmptyView');
var NotFoundView = require('./NotFoundView');
var ErrorView = require('./ErrorView');
var LoadingView = require('../Shared/LoadingView');
module.exports = Marionette.Layout.extend({
template : 'AddArtist/AddArtistViewTemplate',
regions : {
searchResult : '#search-result'
},
ui : {
artistSearch : '.x-artist-search',
searchBar : '.x-search-bar',
loadMore : '.x-load-more'
},
events : {
'click .x-load-more' : '_onLoadMore'
},
initialize : function(options) {
this.isExisting = options.isExisting;
this.collection = new AddArtistCollection();
console.log('this.collection:', this.collection);
if (this.isExisting) {
this.collection.unmappedFolderModel = this.model;
}
if (this.isExisting) {
this.className = 'existing-artist';
} else {
this.className = 'new-artist';
}
this.listenTo(vent, vent.Events.ArtistAdded, this._onArtistAdded);
this.listenTo(this.collection, 'sync', this._showResults);
this.resultCollectionView = new SearchResultCollectionView({
collection : this.collection,
isExisting : this.isExisting
});
this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this);
},
onRender : function() {
var self = this;
this.$el.addClass(this.className);
this.ui.artistSearch.keyup(function(e) {
if (_.contains([
9,
16,
17,
18,
19,
20,
33,
34,
35,
36,
37,
38,
39,
40,
91,
92,
93
], e.keyCode)) {
return;
}
self._abortExistingSearch();
self.throttledSearch({
term : self.ui.artistSearch.val()
});
});
this._clearResults();
if (this.isExisting) {
this.ui.searchBar.hide();
}
},
onShow : function() {
this.ui.artistSearch.focus();
},
search : function(options) {
var self = this;
this.collection.reset();
if (!options.term || options.term === this.collection.term) {
return Marionette.$.Deferred().resolve();
}
this.searchResult.show(new LoadingView());
this.collection.term = options.term;
this.currentSearchPromise = this.collection.fetch({
data : { term : options.term }
});
this.currentSearchPromise.fail(function() {
self._showError();
});
return this.currentSearchPromise;
},
_onArtistAdded : function(options) {
if (this.isExisting && options.artist.get('path') === this.model.get('folder').path) {
this.close();
}
else if (!this.isExisting) {
this.collection.term = '';
this.collection.reset();
this._clearResults();
this.ui.artistSearch.val('');
this.ui.artistSearch.focus();
}
},
_onLoadMore : function() {
var showingAll = this.resultCollectionView.showMore();
this.ui.searchBar.show();
if (showingAll) {
this.ui.loadMore.hide();
}
},
_clearResults : function() {
if (!this.isExisting) {
this.searchResult.show(new EmptyView());
} else {
this.searchResult.close();
}
},
_showResults : function() {
if (!this.isClosed) {
if (this.collection.length === 0) {
this.ui.searchBar.show();
this.searchResult.show(new NotFoundView({ term : this.collection.term }));
} else {
this.searchResult.show(this.resultCollectionView);
if (!this.showingAll && this.isExisting) {
this.ui.loadMore.show();
}
}
}
},
_abortExistingSearch : function() {
if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) {
console.log('aborting previous pending search request.');
this.currentSearchPromise.abort();
} else {
this._clearResults();
}
},
_showError : function() {
if (!this.isClosed) {
this.ui.searchBar.show();
this.searchResult.show(new ErrorView({ term : this.collection.term }));
this.collection.term = '';
}
}
});

@ -0,0 +1,24 @@
{{#if folder.path}}
<div class="unmapped-folder-path">
<div class="col-md-12">
{{folder.path}}
</div>
</div>{{/if}}
<div class="x-search-bar">
<div class="input-group input-group-lg add-artist-search">
<span class="input-group-addon"><i class="icon-lidarr-search"/></span>
{{#if folder}}
<input type="text" class="form-control x-artist-search" value="{{folder.name}}">
{{else}}
<input type="text" class="form-control x-artist-search" placeholder="Start typing the name of an artist or album...">
{{/if}}
</div>
</div>
<div class="row">
<div id="search-result" class="result-list col-md-12"/>
</div>
<div class="btn btn-block text-center new-artist-loadmore x-load-more" style="display: none;">
<i class="icon-lidarr-load-more"/>
more
</div>

@ -0,0 +1,3 @@
<select class="form-control col-md-2 x-artist-type" name="artistType">
<option value="standard">Standard</option>
</select>

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

@ -0,0 +1,3 @@
<div class="text-center hint col-md-12">
<span>You can also search by Spotify using the spotify: prefixes.</span>
</div>

@ -0,0 +1,13 @@
var Marionette = require('marionette');
module.exports = Marionette.CompositeView.extend({
template : 'AddArtist/ErrorViewTemplate',
initialize : function(options) {
this.options = options;
},
templateHelpers : function() {
return this.options;
}
});

@ -0,0 +1,7 @@
<div class="text-center col-md-12">
<h3>
There was an error searching for '{{term}}'.
</h3>
If the artist name contains non-alphanumeric characters try removing them, otherwise try your search again later.
</div>

@ -0,0 +1,51 @@
var Marionette = require('marionette');
var AddArtistView = require('../AddArtistView');
var UnmappedFolderCollection = require('./UnmappedFolderCollection');
module.exports = Marionette.CompositeView.extend({
itemView : AddArtistView,
itemViewContainer : '.x-loading-folders',
template : 'AddArtist/Existing/AddExistingArtistCollectionViewTemplate',
ui : {
loadingFolders : '.x-loading-folders'
},
initialize : function() {
this.collection = new UnmappedFolderCollection();
this.collection.importItems(this.model);
},
showCollection : function() {
this._showAndSearch(0);
},
appendHtml : function(collectionView, itemView, index) {
collectionView.ui.loadingFolders.before(itemView.el);
},
_showAndSearch : function(index) {
var self = this;
var model = this.collection.at(index);
if (model) {
var currentIndex = index;
var folderName = model.get('folder').name;
this.addItemView(model, this.getItemView(), index);
this.children.findByModel(model).search({ term : folderName }).always(function() {
if (!self.isClosed) {
self._showAndSearch(currentIndex + 1);
}
});
}
else {
this.ui.loadingFolders.hide();
}
},
itemViewOptions : {
isExisting : true
}
});

@ -0,0 +1,5 @@
<div class="x-existing-folders">
<div class="loading-folders x-loading-folders">
Loading search results from server for your artists, this may take a few minutes.
</div>
</div>

@ -0,0 +1,20 @@
var Backbone = require('backbone');
var UnmappedFolderModel = require('./UnmappedFolderModel');
var _ = require('underscore');
module.exports = Backbone.Collection.extend({
model : UnmappedFolderModel,
importItems : function(rootFolderModel) {
this.reset();
var rootFolder = rootFolderModel;
_.each(rootFolderModel.get('unmappedFolders'), function(folder) {
this.push(new UnmappedFolderModel({
rootFolder : rootFolder,
folder : folder
}));
}, this);
}
});

@ -0,0 +1,3 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({});

@ -0,0 +1,18 @@
<dl class="monitor-tooltip-contents">
<dt>All</dt>
<dd>Monitor all tracks except specials</dd>
<dt>Future</dt>
<dd>Monitor tracks that have not been released yet</dd>
<dt>Missing</dt>
<dd>Monitor tracks that do not have files or have not aired yet</dd>
<dt>Existing</dt>
<dd>Monitor tracks that have files or have not aired yet</dd>
<dt>First Season</dt>
<dd>Monitor all tracks of the first album. All other albums will be ignored</dd>
<dt>Latest Season</dt>
<dd>Monitor all tracks of the latest album and future albums</dd>
<dt>None</dt>
<dd>No tracks will be monitored.</dd>
<!--<dt>Latest Season</dt>-->
<!--<dd>Monitor all tracks the latest album only, previous albums will be ignored</dd>-->
</dl>

@ -0,0 +1,13 @@
var Marionette = require('marionette');
module.exports = Marionette.CompositeView.extend({
template : 'AddArtist/NotFoundViewTemplate',
initialize : function(options) {
this.options = options;
},
templateHelpers : function() {
return this.options;
}
});

@ -0,0 +1,7 @@
<div class="text-center col-md-12">
<h3>
Sorry. We couldn't find any artist matching '{{term}}'
</h3>
<a href="https://github.com/mattman86/Lidarr/wiki/FAQ#wiki-why-cant-i-add-a-new-show-to-nzbdrone-its-on-thetvdb">Why can't I find my artist?</a>
</div>

@ -0,0 +1,10 @@
var Backbone = require('backbone');
var RootFolderModel = require('./RootFolderModel');
require('../../Mixins/backbone.signalr.mixin');
var RootFolderCollection = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/rootfolder',
model : RootFolderModel
});
module.exports = new RootFolderCollection();

@ -0,0 +1,8 @@
var Marionette = require('marionette');
var RootFolderItemView = require('./RootFolderItemView');
module.exports = Marionette.CompositeView.extend({
template : 'AddArtist/RootFolders/RootFolderCollectionViewTemplate',
itemViewContainer : '.x-root-folders',
itemView : RootFolderItemView
});

@ -0,0 +1,13 @@
<table class="table table-hover">
<thead>
<tr>
<th class="col-md-10 ">
Path
</th>
<th class="col-md-3">
Free Space
</th>
</tr>
</thead>
<tbody class="x-root-folders"></tbody>
</table>

@ -0,0 +1,28 @@
var Marionette = require('marionette');
module.exports = Marionette.ItemView.extend({
template : 'AddArtist/RootFolders/RootFolderItemViewTemplate',
className : 'recent-folder',
tagName : 'tr',
initialize : function() {
this.listenTo(this.model, 'change', this.render);
},
events : {
'click .x-delete' : 'removeFolder',
'click .x-folder' : 'folderSelected'
},
removeFolder : function() {
var self = this;
this.model.destroy().success(function() {
self.close();
});
},
folderSelected : function() {
this.trigger('folderSelected', this.model);
}
});

@ -0,0 +1,9 @@
<td class="col-md-10 x-folder folder-path">
{{path}}
</td>
<td class="col-md-3 x-folder folder-free-space">
<span>{{Bytes freeSpace}}</span>
</td>
<td class="col-md-1">
<i class="icon-lidarr-delete x-delete"></i>
</td>

@ -0,0 +1,80 @@
var Marionette = require('marionette');
var RootFolderCollectionView = require('./RootFolderCollectionView');
var RootFolderCollection = require('./RootFolderCollection');
var RootFolderModel = require('./RootFolderModel');
var LoadingView = require('../../Shared/LoadingView');
var AsValidatedView = require('../../Mixins/AsValidatedView');
require('../../Mixins/FileBrowser');
var Layout = Marionette.Layout.extend({
template : 'AddArtist/RootFolders/RootFolderLayoutTemplate',
ui : {
pathInput : '.x-path'
},
regions : {
currentDirs : '#current-dirs'
},
events : {
'click .x-add' : '_addFolder',
'keydown .x-path input' : '_keydown'
},
initialize : function() {
this.collection = RootFolderCollection;
this.rootfolderListView = null;
},
onShow : function() {
this.listenTo(RootFolderCollection, 'sync', this._showCurrentDirs);
this.currentDirs.show(new LoadingView());
if (RootFolderCollection.synced) {
this._showCurrentDirs();
}
this.ui.pathInput.fileBrowser();
},
_onFolderSelected : function(options) {
this.trigger('folderSelected', options);
},
_addFolder : function() {
var self = this;
var newDir = new RootFolderModel({
Path : this.ui.pathInput.val()
});
this.bindToModelValidation(newDir);
newDir.save().done(function() {
RootFolderCollection.add(newDir);
self.trigger('folderSelected', { model : newDir });
});
},
_showCurrentDirs : function() {
if (!this.rootfolderListView) {
this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection });
this.currentDirs.show(this.rootfolderListView);
this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected);
}
},
_keydown : function(e) {
if (e.keyCode !== 13) {
return;
}
this._addFolder();
}
});
var Layout = AsValidatedView.apply(Layout);
module.exports = Layout;

@ -0,0 +1,36 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Select Folder</h3>
</div>
<div class="modal-body root-folders-modal">
<div class="validation-errors"></div>
<div class="alert alert-info">Enter the path that contains some or all of your Music, you will be able to choose which artist you want to import<button type="button" class="close" data-dismiss="alert">×</button></div>
<div class="row">
<div class="form-group">
<div class="col-md-12">
<div class="input-group">
<span class="input-group-addon">&nbsp;<i class="icon-lidarr-folder-open"></i></span>
<input class="form-control x-path" type="text" validation-name="path" placeholder="Enter path to folder that contains your music">
<span class="input-group-btn"><button class="btn btn-success x-add"><i class="icon-lidarr-ok"/></button></span>
</div>
</div>
</div>
</div>
<div class="row root-folders">
<div class="col-md-12">
{{#if items}}
<h4>Recent Folders</h4>
{{/if}}
<div id="current-dirs" class="root-folders-list"></div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">Close</button>
</div>
</div>

@ -0,0 +1,8 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
urlRoot : window.NzbDrone.ApiRoot + '/rootfolder',
defaults : {
freeSpace : 0
}
});

@ -0,0 +1,11 @@
<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath">
{{#if this}}
{{#each this}}
<option value="{{id}}">{{path}}</option>
{{/each}}
{{else}}
<option value="">Select Path</option>
{{/if}}
<option value="addNew">Add a different path</option>
</select>

@ -0,0 +1,29 @@
var Marionette = require('marionette');
var SearchResultView = require('./SearchResultView');
module.exports = Marionette.CollectionView.extend({
itemView : SearchResultView,
initialize : function(options) {
this.isExisting = options.isExisting;
this.showing = 1;
},
showAll : function() {
this.showingAll = true;
this.render();
},
showMore : function() {
this.showing += 5;
this.render();
return this.showing >= this.collection.length;
},
appendHtml : function(collectionView, itemView, index) {
if (!this.isExisting || index < this.showing || index === 0) {
collectionView.$el.append(itemView.el);
}
}
});

@ -0,0 +1,297 @@
var _ = require('underscore');
var vent = require('vent');
var AppLayout = require('../AppLayout');
var Backbone = require('backbone');
var Marionette = require('marionette');
var Profiles = require('../Profile/ProfileCollection');
var RootFolders = require('./RootFolders/RootFolderCollection');
var RootFolderLayout = require('./RootFolders/RootFolderLayout');
var ArtistCollection = require('../Artist/ArtistCollection');
var Config = require('../Config');
var Messenger = require('../Shared/Messenger');
var AsValidatedView = require('../Mixins/AsValidatedView');
require('jquery.dotdotdot');
var view = Marionette.ItemView.extend({
template : 'AddArtist/SearchResultViewTemplate',
ui : {
profile : '.x-profile',
rootFolder : '.x-root-folder',
albumFolder : '.x-album-folder',
artistType : '.x-artist-type',
monitor : '.x-monitor',
monitorTooltip : '.x-monitor-tooltip',
addButton : '.x-add',
addAlbumButton : '.x-add-album',
addSearchButton : '.x-add-search',
addAlbumSearchButton : '.x-add-album-search',
overview : '.x-overview'
},
events : {
'click .x-add' : '_addWithoutSearch',
'click .x-add-album' : '_addWithoutSearch',
'click .x-add-search' : '_addAndSearch',
'click .x-add-album-search' : '_addAndSearch',
'change .x-profile' : '_profileChanged',
'change .x-root-folder' : '_rootFolderChanged',
'change .x-album-folder' : '_albumFolderChanged',
'change .x-artist-type' : '_artistTypeChanged',
'change .x-monitor' : '_monitorChanged'
},
initialize : function() {
if (!this.model) {
throw 'model is required';
}
this.templateHelpers = {};
this._configureTemplateHelpers();
this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated);
this.listenTo(this.model, 'change', this.render);
this.listenTo(RootFolders, 'all', this._rootFoldersUpdated);
},
onRender : function() {
var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId);
var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId);
var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true);
var defaultArtistType = Config.getValue(Config.Keys.DefaultSeriesType, 'standard');
var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing');
if (Profiles.get(defaultProfile)) {
this.ui.profile.val(defaultProfile);
}
if (RootFolders.get(defaultRoot)) {
this.ui.rootFolder.val(defaultRoot);
}
this.ui.albumFolder.prop('checked', useSeasonFolder);
this.ui.artistType.val(defaultArtistType);
this.ui.monitor.val(defaultMonitorEpisodes);
//TODO: make this work via onRender, FM?
//works with onShow, but stops working after the first render
this.ui.overview.dotdotdot({
height : 120
});
this.templateFunction = Marionette.TemplateCache.get('AddArtist/MonitoringTooltipTemplate');
var content = this.templateFunction();
this.ui.monitorTooltip.popover({
content : content,
html : true,
trigger : 'hover',
title : 'Track Monitoring Options',
placement : 'right',
container : this.$el
});
},
_configureTemplateHelpers : function() {
var existingArtist = ArtistCollection.where({ SpotifyId : this.model.get('spotifyId') });
if (existingArtist.length > 0) {
this.templateHelpers.existing = existingArtist[0].toJSON();
}
this.templateHelpers.profiles = Profiles.toJSON();
if (!this.model.get('isExisting')) {
this.templateHelpers.rootFolders = RootFolders.toJSON();
}
},
_onConfigUpdated : function(options) {
if (options.key === Config.Keys.DefaultProfileId) {
this.ui.profile.val(options.value);
}
else if (options.key === Config.Keys.DefaultRootFolderId) {
this.ui.rootFolder.val(options.value);
}
else if (options.key === Config.Keys.UseAlbumFolder) {
this.ui.seasonFolder.prop('checked', options.value);
}
else if (options.key === Config.Keys.DefaultArtistType) {
this.ui.artistType.val(options.value);
}
else if (options.key === Config.Keys.MonitorEpisodes) {
this.ui.monitor.val(options.value);
}
},
_profileChanged : function() {
Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val());
},
_albumFolderChanged : function() {
Config.setValue(Config.Keys.UseAlbumFolder, this.ui.albumFolder.prop('checked'));
},
_rootFolderChanged : function() {
var rootFolderValue = this.ui.rootFolder.val();
if (rootFolderValue === 'addNew') {
var rootFolderLayout = new RootFolderLayout();
this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder);
AppLayout.modalRegion.show(rootFolderLayout);
} else {
Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue);
}
},
_artistTypeChanged : function() {
Config.setValue(Config.Keys.DefaultArtistType, this.ui.artistType.val());
},
_monitorChanged : function() {
Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val());
},
_setRootFolder : function(options) {
vent.trigger(vent.Commands.CloseModalCommand);
this.ui.rootFolder.val(options.model.id);
this._rootFolderChanged();
},
_addWithoutSearch : function(evt) {
console.log(evt);
this._addArtist(false);
},
_addAndSearch : function() {
this._addArtist(true);
},
_addArtist : function(searchForMissing) {
// TODO: Refactor to handle multiple add buttons/albums
var addButton = this.ui.addButton;
var addSearchButton = this.ui.addSearchButton;
console.log('_addArtist, searchForMissing=', searchForMissing);
addButton.addClass('disabled');
addSearchButton.addClass('disabled');
var profile = this.ui.profile.val();
var rootFolderPath = this.ui.rootFolder.children(':selected').text();
var artistType = this.ui.artistType.val(); // Perhaps make this a differnitator between artist or Album?
var albumFolder = this.ui.albumFolder.prop('checked');
var options = this._getAddArtistOptions();
options.searchForMissing = searchForMissing;
this.model.set({
profileId : profile,
rootFolderPath : rootFolderPath,
albumFolder : albumFolder,
artistType : artistType,
addOptions : options,
monitored : true
}, { silent : true });
var self = this;
var promise = this.model.save();
if (searchForMissing) {
this.ui.addSearchButton.spinForPromise(promise);
}
else {
this.ui.addButton.spinForPromise(promise);
}
promise.always(function() {
addButton.removeClass('disabled');
addSearchButton.removeClass('disabled');
});
promise.done(function() {
console.log('[SearchResultView] _addArtist promise resolve:', self.model);
ArtistCollection.add(self.model);
self.close();
Messenger.show({
message : 'Added: ' + self.model.get('artistName'),
actions : {
goToArtist : {
label : 'Go to Artist',
action : function() {
Backbone.history.navigate('/artist/' + self.model.get('artistSlug'), { trigger : true });
}
}
},
hideAfter : 8,
hideOnNavigate : true
});
vent.trigger(vent.Events.ArtistAdded, { artist : self.model });
});
},
_rootFoldersUpdated : function() {
this._configureTemplateHelpers();
this.render();
},
_getAddArtistOptions : function() {
var monitor = this.ui.monitor.val();
//[TODO]: Refactor for albums
var lastSeason = _.max(this.model.get('seasons'), 'seasonNumber');
var firstSeason = _.min(_.reject(this.model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber');
//this.model.setSeasonPass(firstSeason.seasonNumber); // TODO
var options = {
ignoreTracksWithFiles : false,
ignoreTracksWithoutFiles : false
};
if (monitor === 'all') {
return options;
}
else if (monitor === 'future') {
options.ignoreTracksWithFiles = true;
options.ignoreTracksWithoutFiles = true;
}
/*else if (monitor === 'latest') {
this.model.setSeasonPass(lastSeason.seasonNumber);
}
else if (monitor === 'first') {
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
this.model.setSeasonMonitored(firstSeason.seasonNumber);
}*/
else if (monitor === 'missing') {
options.ignoreTracksWithFiles = true;
}
else if (monitor === 'existing') {
options.ignoreTracksWithoutFiles = true;
}
/*else if (monitor === 'none') {
this.model.setSeasonPass(lastSeason.seasonNumber + 1);
}*/
return options;
}
});
AsValidatedView.apply(view);
module.exports = view;

@ -0,0 +1,146 @@
<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}">
<div class="row">
<div class="col-md-10">
<div class="row">
<div class="col-md-12">
<h2 class="artist-title">
<!--{{titleWithYear}}-->
{{artistName}}
<!--<span class="labels">
<span class="label label-default">{{network}}</span>
{{#unless_eq status compare="continuing"}}
<span class="label label-danger">Ended</span>
{{/unless_eq}}
</span>-->
</h2>
</div>
</div>
<!-- <div class="row new-artist-overview x-overview">
<div class="col-md-12 overview-internal">
{{overview}}
</div>
</div> -->
<div class="row">
{{#unless existing}}
{{#unless path}}
<div class="form-group col-md-4">
<label>Path</label>
{{> RootFolderSelectionPartial rootFolders}}
</div>
{{/unless}}
<div class="form-group col-md-2">
<label>Monitor <i class="icon-lidarr-form-info monitor-tooltip x-monitor-tooltip"></i></label>
<select class="form-control col-md-2 x-monitor">
<option value="all">All</option>
<option value="future">Future</option>
<option value="missing">Missing</option>
<option value="existing">Existing</option>
<option value="none">None</option>
</select>
</div>
<div class="form-group col-md-2">
<label>Profile</label>
{{> ProfileSelectionPartial profiles}}
</div>
<!--<div class="form-group col-md-2">
</div>-->
<div class="form-group col-md-2">
<label>Album Folders</label>
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" class="x-season-folder"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
</div>
</div>
{{/unless}}
</div>
<div class="row">
{{#unless existing}}
{{#if artistName}}
<div class="form-group col-md-2 col-md-offset-10">
<!--Uncomment if we need to add even more controls to add artist-->
<!--<label style="visibility: hidden">Add</label>-->
<div class="btn-group">
<button class="btn btn-success add x-add" title="Add" data-artist="{{artistName}}">
<i class="icon-lidarr-add"></i>
</button>
<button class="btn btn-success add x-add-search" title="Add and Search for missing tracks" data-artist="{{artistName}}">
<i class="icon-lidarr-search"></i>
</button>
</div>
</div>
{{else}}
<div class="col-md-2 col-md-offset-10">
<button class="btn add-artist disabled">
Add
</button>
</div>
{{/if}}
{{else}}
<div class="col-md-2 col-md-offset-10">
<a class="btn btn-default" href="{{route}}">
Already Exists
</a>
</div>
{{/unless}}
</div>
</div>
</div>
<div class="row">
{{#each albums}}
<div class="col-md-12" style="border:1px dashed black;">
<div class="col-md-2">
<a href="{{artworkUrl}}" target="_blank">
<!-- {{poster}} -->
<img class="album-poster" src="{{artworkUrl}}">
</a>
</div>
<div class="col-md-8">
<h2>{{albumName}} ({{year}})</h2>
{{#unless existing}}
{{#if albumName}}
<div class="form-group col-md-offset-10">
<!--Uncomment if we need to add even more controls to add artist-->
<!--<label style="visibility: hidden">Add</label>-->
<div class="btn-group">
<button class="btn btn-success add x-add-album" title="Add" data-album="{{albumName}}">
<i class="icon-lidarr-add"></i>
</button>
<button class="btn btn-success add x-add-album-search" title="Add and Search for missing tracks" data-album="{{albumName}}">
<i class="icon-lidarr-search"></i>
</button>
</div>
</div>
{{else}}
<div class="col-md-2 col-md-offset-10">
<button class="btn add-artist disabled">
Add
</button>
</div>
{{/if}}
{{else}}
<div class="col-md-2 col-md-offset-10">
<a class="btn btn-default" href="{{route}}">
Already Exists
</a>
</div>
{{/unless}}
</div>
</div>
{{/each}}
</div>
</div>

@ -0,0 +1,13 @@
<select class="form-control col-md-2 starting-album x-starting-album">
{{#each this}}
{{#if_eq seasonNumber compare="0"}}
<option value="{{seasonNumber}}">Specials</option>
{{else}}
<option value="{{seasonNumber}}">Album {{seasonNumber}}</option>
{{/if_eq}}
{{/each}}
<option value="5000000">None</option>
</select>

@ -0,0 +1,181 @@
@import "../Shared/Styles/card.less";
@import "../Shared/Styles/clickable.less";
#add-artist-screen {
.existing-artist {
.card();
margin : 30px 0px;
.unmapped-folder-path {
padding: 20px;
margin-left : 0px;
font-weight : 100;
font-size : 25px;
text-align : center;
}
.new-artist-loadmore {
font-size : 30px;
font-weight : 300;
padding-top : 10px;
padding-bottom : 10px;
}
}
.new-artist {
.search-item {
.card();
margin : 40px 0px;
}
}
.add-artist-search {
margin-top : 20px;
margin-bottom : 20px;
}
.search-item {
padding-bottom : 20px;
.artist-title {
margin-top : 5px;
.labels {
margin-left : 10px;
.label {
font-size : 12px;
vertical-align : middle;
}
}
.year {
font-style : italic;
color : #aaaaaa;
}
}
.new-artist-overview {
overflow : hidden;
height : 103px;
.overview-internal {
overflow : hidden;
height : 80px;
}
}
.artist-poster {
min-width : 138px;
min-height : 203px;
max-width : 138px;
max-height : 203px;
margin : 10px;
}
.album-poster {
min-width : 100px;
min-height : 100px;
max-width : 138px;
max-height : 203px;
margin : 10px;
}
a {
color : #343434;
}
a:hover {
text-decoration : none;
}
select {
font-size : 14px;
}
.checkbox {
margin-top : 0px;
}
.add {
i {
&:before {
color : #ffffff;
}
}
}
.monitor-tooltip {
margin-left : 5px;
}
}
.loading-folders {
margin : 30px 0px;
text-align: center;
}
.hint {
color : #999999;
font-style : italic;
}
.monitor-tooltip-contents {
padding-bottom : 0px;
dd {
padding-bottom : 8px;
}
}
}
li.add-new {
.clickable;
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 20px;
color: rgb(51, 51, 51);
white-space: nowrap;
}
li.add-new:hover {
text-decoration: none;
color: rgb(255, 255, 255);
background-color: rgb(0, 129, 194);
}
.root-folders-modal {
overflow : visible;
.root-folders-list {
overflow-y : auto;
max-height : 300px;
i {
.clickable();
}
}
.validation-errors {
display : none;
}
.input-group {
.form-control {
background-color : white;
}
}
.root-folders {
margin-top : 20px;
}
.recent-folder {
.clickable();
}
}

@ -0,0 +1,10 @@
var Backbone = require('backbone');
var AlbumModel = require('./AlbumModel');
module.exports = Backbone.Collection.extend({
model : AlbumModel,
comparator : function(season) {
return -season.get('seasonNumber');
}
});

@ -0,0 +1,11 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
defaults : {
seasonNumber : 0
},
initialize : function() {
this.set('id', this.get('seasonNumber'));
}
});

@ -119,6 +119,6 @@ Collection = AsFilteredCollection.call(Collection);
Collection = AsSortedCollection.call(Collection);
Collection = AsPersistedStateCollection.call(Collection);
var data = ApiData.get('series'); // TOOD: Build backend for artist
var data = ApiData.get('artist'); // TOOD: Build backend for artist
module.exports = new Collection(data, { full : true }).bindSignalR();

@ -1,8 +1,8 @@
var NzbDroneController = require('../Shared/NzbDroneController');
var AppLayout = require('../AppLayout');
var ArtistCollection = require('./ArtistCollection');
var SeriesIndexLayout = require('../Series/Index/SeriesIndexLayout');
var SeriesDetailsLayout = require('../Series/Details/SeriesDetailsLayout');
var ArtistIndexLayout = require('./Index/ArtistIndexLayout');
var ArtistDetailsLayout = require('./Details/ArtistDetailsLayout');
module.exports = NzbDroneController.extend({
_originalInit : NzbDroneController.prototype.initialize,
@ -17,18 +17,18 @@ module.exports = NzbDroneController.extend({
artist : function() {
this.setTitle('Lidarr');
this.showMainRegion(new SeriesIndexLayout());
this.showMainRegion(new ArtistIndexLayout());
},
artistDetails : function(query) {
var artists = ArtistCollection.where({ artistNameSlug : query });
var artists = ArtistCollection.where({ artistSlug : query });
console.log('artistDetails, artists: ', artists);
if (artists.length !== 0) {
var targetSeries = artists[0];
console.log("[ArtistController] targetSeries: ", targetSeries);
this.setTitle(targetSeries.get('artistName')); // TODO: Update NzbDroneController
var targetArtist = artists[0];
console.log("[ArtistController] targetArtist: ", targetArtist);
this.setTitle(targetArtist.get('artistName')); // TODO: Update NzbDroneController
//this.setArtistName(targetSeries.get('artistName'));
this.showMainRegion(new SeriesDetailsLayout({ model : targetSeries }));
this.showMainRegion(new ArtistDetailsLayout({ model : targetArtist }));
} else {
this.showNotFound();
}

@ -13,7 +13,7 @@ module.exports = Backbone.Model.extend({
setAlbumsMonitored : function(albumName) {
_.each(this.get('albums'), function(album) {
if (season.albumName === albumName) {
if (album.albumName === albumName) {
album.monitored = !album.monitored;
}
});

@ -0,0 +1,50 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Delete {{title}}</h3>
</div>
<div class="modal-body delete-artist-modal">
<div class="row">
<div class="col-sm-3 hidden-xs">
{{poster}}
</div>
<div class="col-sm-9">
<div class="form-horizontal">
<h3 class="path">{{path}}</h3>
<div class="form-group">
<label class="col-sm-4 control-label">Delete all files</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" class="x-delete-files"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn slide-button btn-danger"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-lidarr-form-info" title="Do you want to delete all files from disk?"/>
<i class="icon-lidarr-form-warning" title="This option is irreversible, use with extreme caution"/>
</span>
</div>
</div>
</div>
<div class="col-md-offset-1 col-md-5 delete-files-info x-delete-files-info">
{{episodeFileCount}} track files will be deleted
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span>
<button class="btn" data-dismiss="modal">Cancel</button>
<button class="btn btn-danger x-confirm-delete">Delete</button>
</div>
</div>

@ -0,0 +1,41 @@
var vent = require('vent');
var Marionette = require('marionette');
module.exports = Marionette.ItemView.extend({
template : 'Series/Delete/DeleteSeriesTemplate',
events : {
'click .x-confirm-delete' : 'removeSeries',
'change .x-delete-files' : 'changeDeletedFiles'
},
ui : {
deleteFiles : '.x-delete-files',
deleteFilesInfo : '.x-delete-files-info',
indicator : '.x-indicator'
},
removeSeries : function() {
var self = this;
var deleteFiles = this.ui.deleteFiles.prop('checked');
this.ui.indicator.show();
this.model.destroy({
data : { 'deleteFiles' : deleteFiles },
wait : true
}).done(function() {
vent.trigger(vent.Events.SeriesDeleted, { series : self.model });
vent.trigger(vent.Commands.CloseModalCommand);
});
},
changeDeletedFiles : function() {
var deleteFiles = this.ui.deleteFiles.prop('checked');
if (deleteFiles) {
this.ui.deleteFilesInfo.show();
} else {
this.ui.deleteFilesInfo.hide();
}
}
});

@ -0,0 +1,44 @@
var _ = require('underscore');
var Marionette = require('marionette');
var SeasonLayout = require('./AlbumLayout');
var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView');
var view = Marionette.CollectionView.extend({
itemView : SeasonLayout,
initialize : function(options) {
if (!options.trackCollection) {
throw 'trackCollection is needed';
}
this.trackCollection = options.trackCollection;
this.artist = options.artist;
},
itemViewOptions : function() {
return {
trackCollection : this.trackCollection,
artist : this.artist
};
},
onTrackGrabbed : function(message) {
if (message.track.artist.id !== this.trackCollection.artistId) {
return;
}
var self = this;
_.each(message.track.tracks, function(track) {
var ep = self.TrackCollection.get(track.id);
ep.set('downloading', true);
});
this.render();
}
});
AsSortedCollectionView.call(view);
module.exports = view;

@ -0,0 +1,301 @@
var vent = require('vent');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
var ToggleCell = require('../../Cells/EpisodeMonitoredCell');
var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell');
var RelativeDateCell = require('../../Cells/RelativeDateCell');
var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell');
var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell');
var TrackNumberCell = require('./TrackNumberCell');
var TrackWarningCell = require('./TrackWarningCell');
var CommandController = require('../../Commands/CommandController');
var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
var moment = require('moment');
var _ = require('underscore');
var Messenger = require('../../Shared/Messenger');
module.exports = Marionette.Layout.extend({
template : 'Artist/Details/ArtistLayoutTemplate',
ui : {
seasonSearch : '.x-album-search',
seasonMonitored : '.x-album-monitored',
seasonRename : '.x-album-rename'
},
events : {
'click .x-album-episode-file-editor' : '_openEpisodeFileEditor',
'click .x-album-monitored' : '_albumMonitored',
'click .x-album-search' : '_albumSearch',
'click .x-album-rename' : '_albumRename',
'click .x-show-hide-episodes' : '_showHideEpisodes',
'dblclick .artist-album h2' : '_showHideEpisodes'
},
regions : {
episodeGrid : '.x-episode-grid'
},
columns : [
{
name : 'monitored',
label : '',
cell : ToggleCell,
trueClass : 'icon-lidarr-monitored',
falseClass : 'icon-lidarr-unmonitored',
tooltip : 'Toggle monitored status',
sortable : false
},
{
name : 'trackNumber',
label : '#',
cell : TrackNumberCell
},
{
name : 'this',
label : '',
cell : TrackWarningCell,
sortable : false,
className : 'track-warning-cell'
},
{
name : 'this',
label : 'Title',
hideSeriesLink : true,
cell : EpisodeTitleCell,
sortable : false
},
{
name : 'airDateUtc',
label : 'Air Date',
cell : RelativeDateCell
},
{
name : 'status',
label : 'Status',
cell : EpisodeStatusCell,
sortable : false
},
{
name : 'this',
label : '',
cell : EpisodeActionsCell,
sortable : false
}
],
templateHelpers : function() {
var episodeCount = this.episodeCollection.filter(function(episode) {
return episode.get('hasFile') || episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment());
}).length;
var episodeFileCount = this.episodeCollection.where({ hasFile : true }).length;
var percentOfEpisodes = 100;
if (episodeCount > 0) {
percentOfEpisodes = episodeFileCount / episodeCount * 100;
}
return {
showingEpisodes : this.showingEpisodes,
episodeCount : episodeCount,
episodeFileCount : episodeFileCount,
percentOfEpisodes : percentOfEpisodes
};
},
initialize : function(options) {
if (!options.episodeCollection) {
throw 'episodeCollection is required';
}
this.series = options.series;
this.fullEpisodeCollection = options.episodeCollection;
this.episodeCollection = this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber'));
this._updateEpisodeCollection();
this.showingEpisodes = this._shouldShowEpisodes();
this.listenTo(this.model, 'sync', this._afterSeasonMonitored);
this.listenTo(this.episodeCollection, 'sync', this.render);
this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes);
},
onRender : function() {
if (this.showingEpisodes) {
this._showEpisodes();
}
this._setSeasonMonitoredState();
CommandController.bindToCommand({
element : this.ui.seasonSearch,
command : {
name : 'seasonSearch',
seriesId : this.series.id,
seasonNumber : this.model.get('seasonNumber')
}
});
CommandController.bindToCommand({
element : this.ui.seasonRename,
command : {
name : 'renameFiles',
seriesId : this.series.id,
seasonNumber : this.model.get('seasonNumber')
}
});
},
_seasonSearch : function() {
CommandController.Execute('seasonSearch', {
name : 'seasonSearch',
seriesId : this.series.id,
seasonNumber : this.model.get('seasonNumber')
});
},
_seasonRename : function() {
vent.trigger(vent.Commands.ShowRenamePreview, {
series : this.series,
seasonNumber : this.model.get('seasonNumber')
});
},
_seasonMonitored : function() {
if (!this.series.get('monitored')) {
Messenger.show({
message : 'Unable to change monitored state when series is not monitored',
type : 'error'
});
return;
}
var name = 'monitored';
this.model.set(name, !this.model.get(name));
this.series.setSeasonMonitored(this.model.get('seasonNumber'));
var savePromise = this.series.save().always(this._afterSeasonMonitored.bind(this));
this.ui.seasonMonitored.spinForPromise(savePromise);
},
_afterSeasonMonitored : function() {
var self = this;
_.each(this.episodeCollection.models, function(episode) {
episode.set({ monitored : self.model.get('monitored') });
});
this.render();
},
_setSeasonMonitoredState : function() {
this.ui.seasonMonitored.removeClass('icon-lidarr-spinner fa-spin');
if (this.model.get('monitored')) {
this.ui.seasonMonitored.addClass('icon-lidarr-monitored');
this.ui.seasonMonitored.removeClass('icon-lidarr-unmonitored');
} else {
this.ui.seasonMonitored.addClass('icon-lidarr-unmonitored');
this.ui.seasonMonitored.removeClass('icon-lidarr-monitored');
}
},
_showEpisodes : function() {
this.episodeGrid.show(new Backgrid.Grid({
columns : this.columns,
collection : this.episodeCollection,
className : 'table table-hover season-grid'
}));
},
_shouldShowEpisodes : function() {
var startDate = moment().add('month', -1);
var endDate = moment().add('year', 1);
return this.episodeCollection.some(function(episode) {
var airDate = episode.get('airDateUtc');
if (airDate) {
var airDateMoment = moment(airDate);
if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) {
return true;
}
}
return false;
});
},
_showHideEpisodes : function() {
if (this.showingEpisodes) {
this.showingEpisodes = false;
this.episodeGrid.close();
} else {
this.showingEpisodes = true;
this._showEpisodes();
}
this.templateHelpers.showingEpisodes = this.showingEpisodes;
this.render();
},
_episodeMonitoredToggled : function(options) {
var model = options.model;
var shiftKey = options.shiftKey;
if (!this.episodeCollection.get(model.get('id'))) {
return;
}
if (!shiftKey) {
return;
}
var lastToggled = this.episodeCollection.lastToggled;
if (!lastToggled) {
return;
}
var currentIndex = this.episodeCollection.indexOf(model);
var lastIndex = this.episodeCollection.indexOf(lastToggled);
var low = Math.min(currentIndex, lastIndex);
var high = Math.max(currentIndex, lastIndex);
var range = _.range(low + 1, high);
this.episodeCollection.lastToggled = model;
},
_updateEpisodeCollection : function() {
var self = this;
this.episodeCollection.add(this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')).models, { merge : true });
this.episodeCollection.each(function(model) {
model.episodeCollection = self.episodeCollection;
});
},
_refreshEpisodes : function() {
this._updateEpisodeCollection();
this.episodeCollection.fullCollection.sort();
this.render();
},
_openEpisodeFileEditor : function() {
var view = new EpisodeFileEditorLayout({
model : this.model,
series : this.series,
episodeCollection : this.episodeCollection
});
vent.trigger(vent.Commands.OpenModalCommand, view);
}
});

@ -0,0 +1,50 @@
<div class="artist-album" id="season-{{seasonNumber}}">
<h2>
<i class="x-album-monitored album-monitored clickable" title="Toggle album monitored status"/>
{{#if seasonNumber}}
Season {{seasonNumber}}
{{else}}
Specials
{{/if}}
{{#if_eq episodeCount compare=0}}
{{#if monitored}}
<span class="badge badge-primary album-status" title="No aired episodes">&nbsp;</span>
{{else}}
<span class="badge badge-warning album-status" title="Album is not monitored">&nbsp;</span>
{{/if}}
{{else}}
{{#if_eq percentOfEpisodes compare=100}}
<span class="badge badge-success album-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span>
{{else}}
<span class="badge badge-danger album-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span>
{{/if_eq}}
{{/if_eq}}
<span class="album-actions pull-right">
<div class="x-album-episode-file-editor">
<i class="icon-lidarr-episode-file" title="Modify episode files for album"/>
</div>
<div class="x-album-rename">
<i class="icon-lidarr-rename" title="Preview rename for album {{seasonNumber}}"/>
</div>
<div class="x-album-search">
<i class="icon-lidarr-search" title="Search for monitored episodes in album {{seasonNumber}}"/>
</div>
</span>
</h2>
<div class="show-hide-episodes x-show-hide-episodes">
<h4>
{{#if showingEpisodes}}
<i class="icon-lidarr-panel-hide"/>
Hide Episodes
{{else}}
<i class="icon-lidarr-panel-show"/>
Show Episodes
{{/if}}
</h4>
</div>
<div class="x-episode-grid table-responsive"></div>
</div>

@ -0,0 +1,259 @@
var $ = require('jquery');
var _ = require('underscore');
var vent = require('vent');
var reqres = require('../../reqres');
var Marionette = require('marionette');
var Backbone = require('backbone');
var ArtistCollection = require('../ArtistCollection');
var TrackCollection = require('../TrackCollection');
var TrackFileCollection = require('../TrackFileCollection');
var AlbumCollection = require('../AlbumCollection');
var AlbumCollectionView = require('./AlbumCollectionView');
var InfoView = require('./InfoView');
var CommandController = require('../../Commands/CommandController');
var LoadingView = require('../../Shared/LoadingView');
var TrackFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout');
require('backstrech');
require('../../Mixins/backbone.signalr.mixin');
module.exports = Marionette.Layout.extend({
itemViewContainer : '.x-artist-albums',
template : 'Artist/Details/ArtistDetailsTemplate',
regions : {
albums : '#albums',
info : '#info'
},
ui : {
header : '.x-header',
monitored : '.x-monitored',
edit : '.x-edit',
refresh : '.x-refresh',
rename : '.x-rename',
search : '.x-search',
poster : '.x-artist-poster'
},
events : {
'click .x-episode-file-editor' : '_openEpisodeFileEditor',
'click .x-monitored' : '_toggleMonitored',
'click .x-edit' : '_editArtist',
'click .x-refresh' : '_refreshArtist',
'click .x-rename' : '_renameArtist',
'click .x-search' : '_artistSearch'
},
initialize : function() {
this.artistCollection = ArtistCollection.clone();
this.artistCollection.shadowCollection.bindSignalR();
this.listenTo(this.model, 'change:monitored', this._setMonitoredState);
this.listenTo(this.model, 'remove', this._artistRemoved);
this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete);
this.listenTo(this.model, 'change', function(model, options) {
if (options && options.changeSource === 'signalr') {
this._refresh();
}
});
this.listenTo(this.model, 'change:images', this._updateImages);
},
onShow : function() {
this._showBackdrop();
this._showSeasons();
this._setMonitoredState();
this._showInfo();
},
onRender : function() {
CommandController.bindToCommand({
element : this.ui.refresh,
command : {
name : 'refreshArtist'
}
});
CommandController.bindToCommand({
element : this.ui.search,
command : {
name : 'artistSearch'
}
});
CommandController.bindToCommand({
element : this.ui.rename,
command : {
name : 'renameFiles',
seriesId : this.model.id,
seasonNumber : -1
}
});
},
onClose : function() {
if (this._backstrech) {
this._backstrech.destroy();
delete this._backstrech;
}
$('body').removeClass('backdrop');
reqres.removeHandler(reqres.Requests.GetEpisodeFileById);
},
_getImage : function(type) {
var image = _.where(this.model.get('images'), { coverType : type });
if (image && image[0]) {
return image[0].url;
}
return undefined;
},
_toggleMonitored : function() {
var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true });
this.ui.monitored.spinForPromise(savePromise);
},
_setMonitoredState : function() {
var monitored = this.model.get('monitored');
this.ui.monitored.removeAttr('data-idle-icon');
this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner');
if (monitored) {
this.ui.monitored.addClass('icon-lidarr-monitored');
this.ui.monitored.removeClass('icon-lidarr-unmonitored');
this.$el.removeClass('series-not-monitored');
} else {
this.ui.monitored.addClass('icon-lidarr-unmonitored');
this.ui.monitored.removeClass('icon-lidarr-monitored');
this.$el.addClass('series-not-monitored');
}
},
_editArtist : function() {
vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model });
},
_refreshArtist : function() {
CommandController.Execute('refreshArtist', {
name : 'refreshArtist',
seriesId : this.model.id
});
},
_artistRemoved : function() {
Backbone.history.navigate('/', { trigger : true });
},
_renameArtist : function() {
vent.trigger(vent.Commands.ShowRenamePreview, { artist : this.model });
},
_artistSearch : function() {
console.log('_artistSearch:', this.model);
CommandController.Execute('artistSearch', {
name : 'artistSearch',
artistId : this.model.id
});
},
_showAlbums : function() {
var self = this;
this.albums.show(new LoadingView());
this.albumCollection = new AlbumCollection(this.model.get('albums'));
this.trackCollection = new TrackCollection({ artistId : this.model.id }).bindSignalR();
this.trackFileCollection = new TrackFileCollection({ artistId : this.model.id }).bindSignalR();
reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(trackFileId) {
return self.trackFileCollection.get(trackFileId);
});
reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(artistId, seasonNumber, sceneSeasonNumber) {
if (self.model.get('id') !== artistId) {
return [];
}
if (sceneSeasonNumber === undefined) {
sceneSeasonNumber = seasonNumber;
}
return _.where(self.model.get('alternateTitles'),
function(alt) {
return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber;
});
});
$.when(this.trackCollection.fetch(), this.trackFileCollection.fetch()).done(function() {
var albumCollectionView = new AlbumCollectionView({
collection : self.albumCollection,
trackCollection : self.trackCollection,
artist : self.model
});
if (!self.isClosed) {
self.albums.show(albumCollectionView);
}
});
},
_showInfo : function() {
this.info.show(new InfoView({
model : this.model,
trackFileCollection : this.trackFileCollection
}));
},
_commandComplete : function(options) {
if (options.command.get('name') === 'renamefiles') {
if (options.command.get('artistId') === this.model.get('id')) {
this._refresh();
}
}
},
_refresh : function() {
this.albumCollection.add(this.model.get('albums'), { merge : true });
this.trackCollection.fetch();
this.trackFileCollection.fetch();
this._setMonitoredState();
this._showInfo();
},
_openTrackFileEditor : function() {
var view = new TrackFileEditorLayout({
artist : this.model,
trackCollection : this.trackCollection
});
vent.trigger(vent.Commands.OpenModalCommand, view);
},
_updateImages : function () {
var poster = this._getImage('poster');
if (poster) {
this.ui.poster.attr('src', poster);
}
this._showBackdrop();
},
_showBackdrop : function () {
$('body').addClass('backdrop');
var fanArt = this._getImage('fanart');
if (fanArt) {
this._backstrech = $.backstretch(fanArt);
} else {
$('body').removeClass('backdrop');
}
}
});

@ -0,0 +1,35 @@
<div class="row series-page-header">
<div class="visible-lg col-lg-2 poster">
{{poster}}
</div>
<div class="col-md-12 col-lg-10">
<div>
<h1 class="header-text">
<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-lidarr-episode-file" title="Modify episode files for series"/>
</div>
<div class="x-refresh">
<i class="icon-lidarr-refresh icon-can-spin" title="Update series info and scan disk"/>
</div>
<div class="x-rename">
<i class="icon-lidarr-rename" title="Preview rename for all episodes"/>
</div>
<div class="x-search">
<i class="icon-lidarr-search" title="Search for monitored episodes in this series"/>
</div>
<div class="x-edit">
<i class="icon-lidarr-edit" title="Edit series"/>
</div>
</div>
</h1>
</div>
<div class="series-detail-overview">
{{overview}}
</div>
<div id="info" class="series-info"></div>
</div>
</div>
<div id="seasons"></div>

@ -0,0 +1,18 @@
var Marionette = require('marionette');
module.exports = Marionette.ItemView.extend({
template : 'Series/Details/InfoViewTemplate',
initialize : function(options) {
this.episodeFileCollection = options.episodeFileCollection;
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.episodeFileCollection, 'sync', this.render);
},
templateHelpers : function() {
return {
fileCount : this.episodeFileCollection.length
};
}
});

@ -0,0 +1,73 @@
<div class="row">
<div class="col-md-9">
{{profile profileId}}
{{#if network}}
<span class="label label-info">{{network}}</span>
{{/if}}
<span class="label label-info">{{runtime}} minutes</span>
<span class="label label-info">{{path}}</span>
{{#if ratings}}
<span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span>
{{/if}}
<span class="label label-info">{{Bytes sizeOnDisk}}</span>
{{#if_eq fileCount compare="1"}}
<span class="label label-info"> 1 file</span>
{{else}}
<span class="label label-info"> {{fileCount}} files</span>
{{/if_eq}}
{{#if_eq status compare="continuing"}}
<span class="label label-info">Continuing</span>
{{else}}
<span class="label label-default">Ended</span>
{{/if_eq}}
</div>
<div class="col-md-3">
<span class="series-info-links">
<a href="{{traktUrl}}" class="label label-info">Trakt</a>
<a href="{{tvdbUrl}}" class="label label-info">The TVDB</a>
{{#if imdbId}}
<a href="{{imdbUrl}}" class="label label-info">IMDB</a>
{{/if}}
{{#if tvRageId}}
<a href="{{tvRageUrl}}" class="label label-info">TV Rage</a>
{{/if}}
{{#if tvMazeId}}
<a href="{{tvMazeUrl}}" class="label label-info">TV Maze</a>
{{/if}}
</span>
</div>
</div>
{{#if alternateTitles}}
<div class="row">
<div class="col-md-12">
{{#each alternateTitles}}
{{#if_eq seasonNumber compare="-1"}}
<span class="label label-default">{{title}}</span>
{{/if_eq}}
{{#if_eq sceneSeasonNumber compare="-1"}}
<span class="label label-default">{{title}}</span>
{{/if_eq}}
{{/each}}
</div>
</div>
{{/if}}
{{#if tags}}
<div class="row">
<div class="col-md-12">
{{tagDisplay tags}}
</div>
</div>
{{/if}}

@ -0,0 +1,47 @@
var Marionette = require('marionette');
var NzbDroneCell = require('../../Cells/NzbDroneCell');
var reqres = require('../../reqres');
var ArtistCollection = require('../ArtistCollection');
module.exports = NzbDroneCell.extend({
className : 'episode-number-cell',
template : 'Artist/Details/TrackNumberCellTemplate',
render : function() {
this.$el.empty();
this.$el.html(this.model.get('trackNumber'));
var series = ArtistCollection.get(this.model.get('seriesId'));
if (series.get('seriesType') === 'anime' && this.model.has('absoluteEpisodeNumber')) {
this.$el.html('{0} ({1})'.format(this.model.get('episodeNumber'), this.model.get('absoluteEpisodeNumber')));
}
var alternateTitles = [];
if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) {
alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber, this.model.get('seriesId'), this.model.get('seasonNumber'), this.model.get('sceneSeasonNumber'));
}
if (this.model.get('sceneSeasonNumber') > 0 || this.model.get('sceneEpisodeNumber') > 0 || this.model.has('sceneAbsoluteEpisodeNumber') || alternateTitles.length > 0) {
this.templateFunction = Marionette.TemplateCache.get(this.template);
var json = this.model.toJSON();
json.alternateTitles = alternateTitles;
var html = this.templateFunction(json);
this.$el.popover({
content : html,
html : true,
trigger : 'hover',
title : 'Scene Information',
placement : 'right',
container : this.$el
});
}
this.delegateEvents();
return this;
}
});

@ -0,0 +1,39 @@
<div class="scene-info">
{{#if sceneSeasonNumber}}
<div class="row">
<div class="key">Season</div>
<div class="value">{{sceneSeasonNumber}}</div>
</div>
{{/if}}
{{#if sceneEpisodeNumber}}
<div class="row">
<div class="key">Episode</div>
<div class="value">{{sceneEpisodeNumber}}</div>
</div>
{{/if}}
{{#if sceneAbsoluteEpisodeNumber}}
<div class="row">
<div class="key">Absolute</div>
<div class="value">{{sceneAbsoluteEpisodeNumber}}</div>
</div>
{{/if}}
{{#if alternateTitles}}
<div class="row">
{{#if_gt alternateTitles.length compare="1"}}
<div class="key">Titles</div>
{{else}}
<div class="key">Title</div>
{{/if_gt}}
<div class="value">
<ul>
{{#each alternateTitles}}
<li>{{title}}</li>
{{/each}}
</ul>
</div>
</div>
{{/if}}
</div>

@ -0,0 +1,21 @@
var NzbDroneCell = require('../../Cells/NzbDroneCell');
var ArtistCollection = require('../ArtistCollection');
module.exports = NzbDroneCell.extend({
className : 'track-warning-cell',
render : function() {
this.$el.empty();
if (this.model.get('unverifiedSceneNumbering')) {
this.$el.html('<i class="icon-lidarr-form-warning" title="Scene number hasn\'t been verified yet."></i>');
}
else if (ArtistCollection.get(this.model.get('artistId')).get('artistType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) {
this.$el.html('<i class="icon-lidarr-form-warning" title="Track does not have an absolute track number"></i>');
}
this.delegateEvents();
return this;
}
});

@ -0,0 +1,54 @@
var vent = require('vent');
var Marionette = require('marionette');
var Profiles = require('../../Profile/ProfileCollection');
var AsModelBoundView = require('../../Mixins/AsModelBoundView');
var AsValidatedView = require('../../Mixins/AsValidatedView');
var AsEditModalView = require('../../Mixins/AsEditModalView');
require('../../Mixins/TagInput');
require('../../Mixins/FileBrowser');
var view = Marionette.ItemView.extend({
template : 'Artist/Edit/EditArtistViewTemplate',
ui : {
profile : '.x-profile',
path : '.x-path',
tags : '.x-tags'
},
events : {
'click .x-remove' : '_removeArtist'
},
initialize : function() {
this.model.set('profiles', Profiles);
},
onRender : function() {
this.ui.path.fileBrowser();
this.ui.tags.tagInput({
model : this.model,
property : 'tags'
});
},
_onBeforeSave : function() {
var profileId = this.ui.profile.val();
this.model.set({ profileId : profileId });
},
_onAfterSave : function() {
this.trigger('saved');
vent.trigger(vent.Commands.CloseModalCommand);
},
_removeSeries : function() {
vent.trigger(vent.Commands.DeleteSeriesCommand, { series : this.model });
}
});
AsModelBoundView.call(view);
AsValidatedView.call(view);
AsEditModalView.call(view);
module.exports = view;

@ -0,0 +1,104 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>{{title}}</h3>
</div>
<div class="modal-body edit-artist-modal">
<div class="row">
<div class="col-sm-3 hidden-xs">
{{poster}}
</div>
<div class="col-sm-9">
<div class="form-horizontal">
<div class="form-group">
<label class="col-sm-4 control-label">Monitored</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="monitored"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-lidarr-form-info" title="Should Lidarr download tracks for this artist?"/>
</span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Use Album Folder</label>
<div class="col-sm-8">
<div class="input-group">
<label class="checkbox toggle well">
<input type="checkbox" name="seasonFolder"/>
<p>
<span>Yes</span>
<span>No</span>
</p>
<div class="btn btn-primary slide-button"/>
</label>
<span class="help-inline-checkbox">
<i class="icon-lidarr-form-info" title="Should downloaded tracks be stored in album folders?"/>
</span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Profile</label>
<div class="col-sm-4">
<select class="form-control x-profile" id="inputProfile" name="profileId">
{{#each profiles.models}}
<option value="{{id}}">{{attributes.name}}</option>
{{/each}}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Artist Type</label>
<div class="col-sm-4">
{{> ArtistTypeSelectionPartial}}
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Path</label>
<div class="col-sm-6">
<input type="text" class="form-control x-path" placeholder="Path" name="path">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">Tags</label>
<div class="col-sm-6">
<input type="text" class="form-control x-tags">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger pull-left x-remove">Delete</button>
<span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span>
<button class="btn" data-dismiss="modal">Cancel</button>
<button class="btn btn-primary x-save">Save</button>
</div>
</div>

@ -0,0 +1,126 @@
var _ = require('underscore');
var Marionette = require('marionette');
var vent = require('vent');
var Profiles = require('../../Profile/ProfileCollection');
var RootFolders = require('../../AddArtist/RootFolders/RootFolderCollection');
var RootFolderLayout = require('../../AddArtist/RootFolders/RootFolderLayout');
var UpdateFilesArtistView = require('./Organize/OrganizeFilesView');
var Config = require('../../Config');
module.exports = Marionette.ItemView.extend({
template : 'Artist/Editor/ArtistEditorFooterViewTemplate',
ui : {
monitored : '.x-monitored',
profile : '.x-profiles',
albumFolder : '.x-album-folder',
rootFolder : '.x-root-folder',
selectedCount : '.x-selected-count',
container : '.artist-editor-footer',
actions : '.x-action'
},
events : {
'click .x-save' : '_updateAndSave',
'change .x-root-folder' : '_rootFolderChanged',
'click .x-organize-files' : '_organizeFiles'
},
templateHelpers : function() {
return {
profiles : Profiles,
rootFolders : RootFolders.toJSON()
};
},
initialize : function(options) {
this.artistCollection = options.collection;
RootFolders.fetch().done(function() {
RootFolders.synced = true;
});
this.editorGrid = options.editorGrid;
this.listenTo(this.artistCollection, 'backgrid:selected', this._updateInfo);
this.listenTo(RootFolders, 'all', this.render);
},
onRender : function() {
this._updateInfo();
},
_updateAndSave : function() {
var selected = this.editorGrid.getSelectedModels();
var monitored = this.ui.monitored.val();
var profile = this.ui.profile.val();
var albumFolder = this.ui.albumFolder.val();
var rootFolder = this.ui.rootFolder.val();
_.each(selected, function(model) {
if (monitored === 'true') {
model.set('monitored', true);
} else if (monitored === 'false') {
model.set('monitored', false);
}
if (profile !== 'noChange') {
model.set('profileId', parseInt(profile, 10));
}
if (albumFolder === 'true') {
model.set('albumFolder', true);
} else if (albumFolder === 'false') {
model.set('albumFolder', false);
}
if (rootFolder !== 'noChange') {
var rootFolderPath = RootFolders.get(parseInt(rootFolder, 10));
model.set('rootFolderPath', rootFolderPath.get('path'));
}
model.edited = true;
});
this.artistCollection.save();
},
_updateInfo : function() {
var selected = this.editorGrid.getSelectedModels();
var selectedCount = selected.length;
this.ui.selectedCount.html('{0} artist selected'.format(selectedCount));
if (selectedCount === 0) {
this.ui.actions.attr('disabled', 'disabled');
} else {
this.ui.actions.removeAttr('disabled');
}
},
_rootFolderChanged : function() {
var rootFolderValue = this.ui.rootFolder.val();
if (rootFolderValue === 'addNew') {
var rootFolderLayout = new RootFolderLayout();
this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder);
vent.trigger(vent.Commands.OpenModalCommand, rootFolderLayout);
} else {
Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue);
}
},
_setRootFolder : function(options) {
vent.trigger(vent.Commands.CloseModalCommand);
this.ui.rootFolder.val(options.model.id);
this._rootFolderChanged();
},
_organizeFiles : function() {
var selected = this.editorGrid.getSelectedModels();
var updateFilesArtistView = new UpdateFilesArtistView({ artist : selected });
this.listenToOnce(updateFilesArtistView, 'updatingFiles', this._afterSave);
vent.trigger(vent.Commands.OpenModalCommand, updateFilesSeriesView);
}
});

@ -0,0 +1,54 @@
<div class="artist-editor-footer">
<div class="row">
<div class="form-group col-md-2">
<label>Monitored</label>
<select class="form-control x-action x-monitored">
<option value="noChange">No change</option>
<option value="true">Monitored</option>
<option value="false">Unmonitored</option>
</select>
</div>
<div class="form-group col-md-2">
<label>Profile</label>
<select class="form-control x-action x-profiles">
<option value="noChange">No change</option>
{{#each profiles.models}}
<option value="{{id}}">{{attributes.name}}</option>
{{/each}}
</select>
</div>
<div class="form-group col-md-2">
<label>Season Folder</label>
<select class="form-control x-action x-season-folder">
<option value="noChange">No change</option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<div class="form-group col-md-3">
<label>Root Folder</label>
<select class="form-control x-action x-root-folder" validation-name="RootFolderPath">
<option value="noChange">No change</option>
{{#each rootFolders}}
<option value="{{id}}">{{path}}</option>
{{/each}}
<option value="addNew">Add a different path</option>
</select>
</div>
<div class="form-group col-md-3 actions">
<label class="x-selected-count">0 artist selected</label>
<div>
<button class="btn btn-primary x-action x-save">Save</button>
<button class="btn btn-danger x-action x-organize-files" title="Organize and rename track files">Organize</button>
</div>
</div>
</div>
</div>

@ -0,0 +1,185 @@
var vent = require('vent');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
var EmptyView = require('../Index/EmptyView');
var ArtistCollection = require('../ArtistCollection');
var ArtistTitleCell = require('../../Cells/ArtistTitleCell');
var ProfileCell = require('../../Cells/ProfileCell');
var ArtistStatusCell = require('../../Cells/ArtistStatusCell');
var ArtistFolderCell = require('../../Cells/ArtistFolderCell');
var SelectAllCell = require('../../Cells/SelectAllCell');
var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout');
var FooterView = require('./ArtistEditorFooterView');
require('../../Mixins/backbone.signalr.mixin');
module.exports = Marionette.Layout.extend({
template : 'Artist/Editor/ArtistEditorLayoutTemplate',
regions : {
artistRegion : '#x-artist-editor',
toolbar : '#x-toolbar'
},
ui : {
monitored : '.x-monitored',
profiles : '.x-profiles',
rootFolder : '.x-root-folder',
selectedCount : '.x-selected-count'
},
events : {
'click .x-save' : '_updateAndSave',
'change .x-root-folder' : '_rootFolderChanged'
},
columns : [
{
name : '',
cell : SelectAllCell,
headerCell : 'select-all',
sortable : false
},
{
name : 'statusWeight',
label : '',
cell : ArtistStatusCell
},
{
name : 'artistName',
label : 'Artist',
cell : ArtistTitleCell,
cellValue : 'this'
},
{
name : 'profileId',
label : 'Profile',
cell : ProfileCell
},
{
name : 'artistFolder',
label : 'Artist Folder',
cell : ArtistFolderCell
},
{
name : 'path',
label : 'Path',
cell : 'string'
}
],
leftSideButtons : {
type : 'default',
storeState : false,
items : [
{
title : 'Season Pass',
icon : 'icon-lidarr-monitored',
route : 'seasonpass'
},
{
title : 'Update Library',
icon : 'icon-lidarr-refresh',
command : 'refreshartist',
successMessage : 'Library was updated!',
errorMessage : 'Library update failed!'
}
]
},
initialize : function() {
this.artistCollection = ArtistCollection.clone();
this.artistCollection.bindSignalR();
this.listenTo(this.artistCollection, 'save', this.render);
this.filteringOptions = {
type : 'radio',
storeState : true,
menuKey : 'artisteditor.filterMode',
defaultAction : 'all',
items : [
{
key : 'all',
title : '',
tooltip : 'All',
icon : 'icon-lidarr-all',
callback : this._setFilter
},
{
key : 'monitored',
title : '',
tooltip : 'Monitored Only',
icon : 'icon-lidarr-monitored',
callback : this._setFilter
},
{
key : 'continuing',
title : '',
tooltip : 'Continuing Only',
icon : 'icon-lidarr-artist-continuing',
callback : this._setFilter
},
{
key : 'ended',
title : '',
tooltip : 'Ended Only',
icon : 'icon-lidarr-artist-ended',
callback : this._setFilter
}
]
};
},
onRender : function() {
this._showToolbar();
this._showTable();
},
onClose : function() {
vent.trigger(vent.Commands.CloseControlPanelCommand);
},
_showTable : function() {
if (this.artistCollection.shadowCollection.length === 0) {
this.artistRegion.show(new EmptyView());
this.toolbar.close();
return;
}
this.columns[0].sortedCollection = this.artistCollection;
this.editorGrid = new Backgrid.Grid({
collection : this.artistCollection,
columns : this.columns,
className : 'table table-hover'
});
this.artistRegion.show(this.editorGrid);
this._showFooter();
},
_showToolbar : function() {
this.toolbar.show(new ToolbarLayout({
left : [
this.leftSideButtons
],
right : [
this.filteringOptions
],
context : this
}));
},
_showFooter : function() {
vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({
editorGrid : this.editorGrid,
collection : this.artistCollection
}));
},
_setFilter : function(buttonContext) {
var mode = buttonContext.model.get('key');
this.artistCollection.setFilterMode(mode);
}
});

@ -0,0 +1,7 @@
<div id="x-toolbar"></div>
<div class="row">
<div class="col-md-12">
<div id="x-artist-editor" class="table-responsive"></div>
</div>
</div>

@ -0,0 +1,33 @@
var _ = require('underscore');
var vent = require('vent');
var Backbone = require('backbone');
var Marionette = require('marionette');
var CommandController = require('../../../Commands/CommandController');
module.exports = Marionette.ItemView.extend({
template : 'Artist/Editor/Organize/OrganizeFilesViewTemplate',
events : {
'click .x-confirm-organize' : '_organize'
},
initialize : function(options) {
this.artist = options.artist;
this.templateHelpers = {
numberOfArtist : this.artist.length,
artist : new Backbone.Collection(this.artist).toJSON()
};
},
_organize : function() {
var artistIds = _.pluck(this.artist, 'id');
CommandController.Execute('renameArtist', {
name : 'renameArtist',
artistIds : artistIds
});
this.trigger('organizingFiles');
vent.trigger(vent.Commands.CloseModalCommand);
}
});

@ -0,0 +1,25 @@
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Organize of Selected Series</h3>
</div>
<div class="modal-body update-files-artist-modal">
<div class="alert alert-info">
<button type="button" class="close" data-dismiss="alert">&times;</button>
Tip: To preview a rename... select "Cancel" then any artist title and use the <i data-original-title="" class="icon-lidarr-rename" title=""></i>
</div>
Are you sure you want to update all files in the {{numberOfSeries}} selected artist?
{{debug}}
<ul class="selected-artist">
{{#each series}}
<li>{{title}}</li>
{{/each}}
</ul>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal">Cancel</button>
<button class="btn btn-danger x-confirm-organize">Organize</button>
</div>
</div>

@ -0,0 +1,35 @@
var vent = require('vent');
var Marionette = require('marionette');
var CommandController = require('../../Commands/CommandController');
module.exports = Marionette.ItemView.extend({
ui : {
refresh : '.x-refresh'
},
events : {
'click .x-edit' : '_editArtist',
'click .x-refresh' : '_refreshArtist'
},
onRender : function() {
CommandController.bindToCommand({
element : this.ui.refresh,
command : {
name : 'refreshArtist',
seriesId : this.model.get('id')
}
});
},
_editArtist : function() {
vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model });
},
_refreshArtist : function() {
CommandController.Execute('refreshArtist', {
name : 'refreshArtist',
seriesId : this.model.id
});
}
});

@ -0,0 +1,357 @@
var _ = require('underscore');
var Marionette = require('marionette');
var Backgrid = require('backgrid');
var PosterCollectionView = require('./Posters/ArtistPostersCollectionView');
var ListCollectionView = require('./Overview/ArtistOverviewCollectionView');
var EmptyView = require('./EmptyView');
var ArtistCollection = require('../ArtistCollection');
var RelativeDateCell = require('../../Cells/RelativeDateCell');
var ArtistTitleCell = require('../../Cells/ArtistTitleCell');
var TemplatedCell = require('../../Cells/TemplatedCell');
var ProfileCell = require('../../Cells/ProfileCell');
var EpisodeProgressCell = require('../../Cells/EpisodeProgressCell');
var ArtistActionsCell = require('../../Cells/ArtistActionsCell');
var ArtistStatusCell = require('../../Cells/ArtistStatusCell');
var FooterView = require('./FooterView');
var FooterModel = require('./FooterModel');
var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout');
require('../../Mixins/backbone.signalr.mixin');
module.exports = Marionette.Layout.extend({
template : 'Artist/Index/ArtistIndexLayoutTemplate',
regions : {
artistRegion : '#x-artist',
toolbar : '#x-toolbar',
toolbar2 : '#x-toolbar2',
footer : '#x-artist-footer'
},
columns : [
{
name : 'statusWeight',
label : '',
cell : ArtistStatusCell
},
{
name : 'title',
label : 'Title',
cell : ArtistTitleCell,
cellValue : 'this',
sortValue : 'sortTitle'
},
{
name : 'albumCount',
label : 'Albums',
cell : 'integer'
},
{
name : 'profileId',
label : 'Profile',
cell : ProfileCell
},
{
name : 'network',
label : 'Network',
cell : 'string'
},
{
name : 'nextAiring',
label : 'Next Airing',
cell : RelativeDateCell
},
{
name : 'percentOfEpisodes',
label : 'Tracks',
cell : EpisodeProgressCell,
className : 'episode-progress-cell'
},
{
name : 'this',
label : '',
sortable : false,
cell : ArtistActionsCell
}
],
leftSideButtons : {
type : 'default',
storeState : false,
collapse : true,
items : [
{
title : 'Add Artist',
icon : 'icon-lidarr-add',
route : 'addartist'
},
{
title : 'Season Pass',
icon : 'icon-lidarr-monitored',
route : 'seasonpass'
},
{
title : 'Artist Editor',
icon : 'icon-lidarr-edit',
route : 'artisteditor'
},
{
title : 'RSS Sync',
icon : 'icon-lidarr-rss',
command : 'rsssync',
errorMessage : 'RSS Sync Failed!'
},
{
title : 'Update Library',
icon : 'icon-lidarr-refresh',
command : 'refreshartist',
successMessage : 'Library was updated!',
errorMessage : 'Library update failed!'
}
]
},
initialize : function() {
this.artistCollection = ArtistCollection.clone();
this.artistCollection.bindSignalR();
this.listenTo(this.artistCollection, 'sync', function(model, collection, options) {
this.artistCollection.fullCollection.resetFiltered();
this._renderView();
});
this.listenTo(this.artistCollection, 'add', function(model, collection, options) {
this.artistCollection.fullCollection.resetFiltered();
this._renderView();
});
this.listenTo(this.artistCollection, 'remove', function(model, collection, options) {
this.artistCollection.fullCollection.resetFiltered();
this._renderView();
});
this.sortingOptions = {
type : 'sorting',
storeState : false,
viewCollection : this.artistCollection,
items : [
{
title : 'Title',
name : 'title'
},
{
title : 'Albums',
name : 'albumCount'
},
{
title : 'Quality',
name : 'profileId'
},
{
title : 'Network',
name : 'network'
},
{
title : 'Next Airing',
name : 'nextAiring'
},
{
title : 'Tracks',
name : 'percentOfEpisodes'
}
]
};
this.filteringOptions = {
type : 'radio',
storeState : true,
menuKey : 'series.filterMode',
defaultAction : 'all',
items : [
{
key : 'all',
title : '',
tooltip : 'All',
icon : 'icon-lidarr-all',
callback : this._setFilter
},
{
key : 'monitored',
title : '',
tooltip : 'Monitored Only',
icon : 'icon-lidarr-monitored',
callback : this._setFilter
},
{
key : 'continuing',
title : '',
tooltip : 'Continuing Only',
icon : 'icon-lidarr-artist-continuing',
callback : this._setFilter
},
{
key : 'ended',
title : '',
tooltip : 'Ended Only',
icon : 'icon-lidarr-artist-ended',
callback : this._setFilter
},
{
key : 'missing',
title : '',
tooltip : 'Missing',
icon : 'icon-lidarr-missing',
callback : this._setFilter
}
]
};
this.viewButtons = {
type : 'radio',
storeState : true,
menuKey : 'seriesViewMode',
defaultAction : 'listView',
items : [
{
key : 'posterView',
title : '',
tooltip : 'Posters',
icon : 'icon-lidarr-view-poster',
callback : this._showPosters
},
{
key : 'listView',
title : '',
tooltip : 'Overview List',
icon : 'icon-lidarr-view-list',
callback : this._showList
},
{
key : 'tableView',
title : '',
tooltip : 'Table',
icon : 'icon-lidarr-view-table',
callback : this._showTable
}
]
};
},
onShow : function() {
this._showToolbar();
this._fetchCollection();
},
_showTable : function() {
this.currentView = new Backgrid.Grid({
collection : this.artistCollection,
columns : this.columns,
className : 'table table-hover'
});
this._renderView();
},
_showList : function() {
this.currentView = new ListCollectionView({
collection : this.artistCollection
});
this._renderView();
},
_showPosters : function() {
this.currentView = new PosterCollectionView({
collection : this.artistCollection
});
this._renderView();
},
_renderView : function() {
// Problem is this is calling before artistCollection has updated. Where are the promises with backbone?
if (this.artistCollection.length === 0) {
this.artistRegion.show(new EmptyView());
this.toolbar.close();
this.toolbar2.close();
} else {
this.artistRegion.show(this.currentView);
this._showToolbar();
this._showFooter();
}
},
_fetchCollection : function() {
this.artistCollection.fetch();
console.log('index page, collection: ', this.artistCollection);
},
_setFilter : function(buttonContext) {
var mode = buttonContext.model.get('key');
this.artistCollection.setFilterMode(mode);
},
_showToolbar : function() {
if (this.toolbar.currentView) {
return;
}
this.toolbar2.show(new ToolbarLayout({
right : [
this.filteringOptions
],
context : this
}));
this.toolbar.show(new ToolbarLayout({
right : [
this.sortingOptions,
this.viewButtons
],
left : [
this.leftSideButtons
],
context : this
}));
},
_showFooter : function() {
var footerModel = new FooterModel();
var artist = this.artistCollection.models.length;
var episodes = 0;
var episodeFiles = 0;
var ended = 0;
var continuing = 0;
var monitored = 0;
_.each(this.artistCollection.models, function(model) {
episodes += model.get('episodeCount'); // TODO: Refactor to Seasons and Tracks
episodeFiles += model.get('episodeFileCount');
/*if (model.get('status').toLowerCase() === 'ended') {
ended++;
} else {
continuing++;
}*/
if (model.get('monitored')) {
monitored++;
}
});
footerModel.set({
artist : artist,
ended : ended,
continuing : continuing,
monitored : monitored,
unmonitored : artist - monitored,
episodes : episodes,
episodeFiles : episodeFiles
});
this.footer.show(new FooterView({ model : footerModel }));
}
});

@ -0,0 +1,12 @@
<div class="toolbars">
<div id="x-toolbar"></div>
<div id="x-toolbar2"></div>
</div>
<div class="row">
<div class="col-md-12">
<div id="x-artist" class="table-responsive"></div>
</div>
</div>
<div id="x-artist-footer"></div>

@ -0,0 +1,16 @@
<div class="no-artist">
<div class="row">
<div class="well col-md-12">
<i class="icon-lidarr-comment"/>
You must be new around here. You should add some music.
</div>
</div>
<div class="row">
<div class="col-md-4 col-md-offset-4">
<a href="/addartist" class='btn btn-lg btn-block btn-success x-add-artist'>
<i class='icon-lidarr-add'></i>
Add Music
</a>
</div>
</div>
</div>

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

@ -0,0 +1,4 @@
var Backbone = require('backbone');
var _ = require('underscore');
module.exports = Backbone.Model.extend({});

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

@ -0,0 +1,46 @@
<div class="row">
<div class="artist-legend legend col-xs-6 col-sm-4">
<ul class='legend-labels'>
<li><span class="progress-bar"></span>Continuing (All tracks downloaded)</li>
<li><span class="progress-bar-success"></span>Ended (All tracks downloaded)</li>
<li><span class="progress-bar-danger"></span>Missing Tracks (Artist monitored)</li>
<li><span class="progress-bar-warning"></span>Missing Tracks (Artist not monitored)</li>
</ul>
</div>
<div class="col-xs-5 col-sm-7">
<div class="row">
<div class="artist-stats col-sm-4">
<dl class="dl-horizontal">
<dt>Artists</dt>
<dd>{{artist}}</dd>
<dt>Ended</dt>
<dd>{{ended}}</dd>
<dt>Continuing</dt>
<dd>{{continuing}}</dd>
</dl>
</div>
<div class="artist-stats col-sm-4">
<dl class="dl-horizontal">
<dt>Monitored</dt>
<dd>{{monitored}}</dd>
<dt>Unmonitored</dt>
<dd>{{unmonitored}}</dd>
</dl>
</div>
<div class="artist-stats col-sm-4">
<dl class="dl-horizontal">
<dt>Tracks</dt>
<dd>{{episodes}}</dd>
<dt>Files</dt>
<dd>{{episodeFiles}}</dd>
</dl>
</div>
</div>
</div>
</div>

@ -0,0 +1,8 @@
var Marionette = require('marionette');
var ListItemView = require('./ArtistOverviewItemView');
module.exports = Marionette.CompositeView.extend({
itemView : ListItemView,
itemViewContainer : '#x-artist-list',
template : 'Artist/Index/Overview/ArtistOverviewCollectionViewTemplate'
});

@ -0,0 +1,7 @@
var vent = require('vent');
var Marionette = require('marionette');
var ArtistIndexItemView = require('../ArtistIndexItemView');
module.exports = ArtistIndexItemView.extend({
template : 'Artist/Index/Overview/ArtistOverviewItemViewTemplate'
});

@ -0,0 +1,59 @@
<div class="artist-item">
<div class="row">
<div class="col-md-2 col-xs-3">
<a href="{{route}}">
{{poster}}
</a>
</div>
<div class="col-md-10 col-xs-9">
<div class="row">
<div class="col-md-10 col-xs-10">
<a href="{{route}}" target="_blank">
<h2>{{artistName}}</h2>
</a>
</div>
<div class="col-md-2 col-xs-2">
<div class="pull-right artist-overview-list-actions">
<i class="icon-lidarr-refresh x-refresh" title="Update artist info and scan disk"/>
<i class="icon-lidarr-edit x-edit" title="Edit Artist"/>
</div>
</div>
</div>
<div class="row">
<div class="col-md-10 col-xs-12">
<div>
{{truncate overview 600}}
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
&nbsp;
</div>
</div>
<div class="row">
<div class="col-md-10 col-xs-8">
<!--{{#if_eq status compare="ended"}}
<span class="label label-danger">Ended</span>
{{/if_eq}}-->
<!--
NOTE: We can show next drop date of album in future
{{#if nextAiring}}
<span class="label label-default">{{RelativeDate nextAiring}}</span>
{{/if}}-->
{{albumCountHelper}}
{{profile profileId}}
</div>
<div class="col-md-2 col-xs-4">
{{> EpisodeProgressPartial }}
</div>
<div class="col-md-8 col-xs-10">
Path {{path}}
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,8 @@
var Marionette = require('marionette');
var PosterItemView = require('./ArtistPostersItemView');
module.exports = Marionette.CompositeView.extend({
itemView : PosterItemView,
itemViewContainer : '#x-artist-posters',
template : 'Artist/Index/Posters/ArtistPostersCollectionViewTemplate'
});

@ -0,0 +1 @@
<ul id="x-artist-posters" class="artist-posters"></ul>

@ -0,0 +1,19 @@
var ArtistIndexItemView = require('../ArtistIndexItemView');
module.exports = ArtistIndexItemView.extend({
tagName : 'li',
template : 'Artist/Index/Posters/ArtistPostersItemViewTemplate',
initialize : function() {
this.events['mouseenter .x-artist-poster-container'] = 'posterHoverAction';
this.events['mouseleave .x-artist-poster-container'] = 'posterHoverAction';
this.ui.controls = '.x-artist-controls';
this.ui.title = '.x-title';
},
posterHoverAction : function() {
this.ui.controls.slideToggle();
this.ui.title.slideToggle();
}
});

@ -0,0 +1,30 @@
<div class="artist-posters-item">
<div class="center">
<div class="artist-poster-container x-artist-poster-container">
<div class="artist-controls x-artist-controls">
<i class="icon-lidarr-refresh x-refresh" title="Refresh Artist"/>
<i class="icon-lidarr-edit x-edit" title="Edit Artist"/>
</div>
{{#unless_eq status compare="continuing"}}
<div class="ended-banner">Ended</div>
{{/unless_eq}}
<a href="{{route}}">
{{poster}}
<div class="center title">{{title}}</div>
</a>
<div class="hidden-title x-title">
{{title}}
</div>
</div>
</div>
<div class="center">
<div class="labels">
{{> EpisodeProgressPartial }}
{{#if nextAiring}}
<span class="label label-default">{{RelativeDate nextAiring}}</span>
{{/if}}
</div>
</div>
</div>

@ -0,0 +1,4 @@
<div class="progress episode-progress">
<span class="progressbar-back-text">{{episodeFileCount}} / {{episodeCount}}</span>
<div class="progress-bar {{EpisodeProgressClass}} episode-progress" style="width:{{percentOfEpisodes}}%"><span class="progressbar-front-text">{{episodeFileCount}} / {{episodeCount}}</span></div>
</div>

@ -0,0 +1,62 @@
var Backbone = require('backbone');
var PageableCollection = require('backbone.pageable');
var TrackModel = require('./TrackModel');
require('./TrackCollection');
module.exports = PageableCollection.extend({
url : window.NzbDrone.ApiRoot + '/episode',
model : TrackModel,
state : {
sortKey : 'episodeNumber',
order : 1,
pageSize : 100000
},
mode : 'client',
originalFetch : Backbone.Collection.prototype.fetch,
initialize : function(options) {
this.seriesId = options.seriesId;
},
bySeason : function(season) {
var filtered = this.filter(function(episode) {
return episode.get('seasonNumber') === season;
});
var EpisodeCollection = require('./TrackCollection');
return new EpisodeCollection(filtered);
},
comparator : function(model1, model2) {
var episode1 = model1.get('episodeNumber');
var episode2 = model2.get('episodeNumber');
if (episode1 < episode2) {
return 1;
}
if (episode1 > episode2) {
return -1;
}
return 0;
},
fetch : function(options) {
if (!this.seriesId) {
throw 'seriesId is required';
}
if (!options) {
options = {};
}
options.data = { seriesId : this.seriesId };
return this.originalFetch.call(this, options);
}
});

@ -0,0 +1,28 @@
var Backbone = require('backbone');
var TrackFileModel = require('./TrackFileModel');
module.exports = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/episodefile',
model : TrackFileModel,
originalFetch : Backbone.Collection.prototype.fetch,
initialize : function(options) {
this.artistId = options.artistId;
this.models = [];
},
fetch : function(options) {
if (!this.artistId) {
throw 'artistId is required';
}
if (!options) {
options = {};
}
options.data = { seriesId : this.seriesId };
return this.originalFetch.call(this, options);
}
});

@ -0,0 +1,3 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({});

@ -0,0 +1,20 @@
var Backbone = require('backbone');
module.exports = Backbone.Model.extend({
defaults : {
seasonNumber : 0,
status : 0
},
methodUrls : {
'update' : window.NzbDrone.ApiRoot + '/episode'
},
sync : function(method, model, options) {
if (model.methodUrls && model.methodUrls[method.toLowerCase()]) {
options = options || {};
options.url = model.methodUrls[method.toLowerCase()];
}
return Backbone.sync(method, model, options);
}
});

@ -0,0 +1,477 @@
@import "../Content/Bootstrap/variables";
@import "../Shared/Styles/card.less";
@import "../Shared/Styles/clickable.less";
@import "../Content/prefixer";
.artist-poster {
min-width: 56px;
max-width: 100%;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.edit-artist-modal, .delete-artist-modal {
overflow : visible;
.artist-poster {
padding-left : 20px;
width : 168px;
}
.form-horizontal {
margin-top : 10px;
}
.twitter-typeahead {
.form-control[disabled] {
background-color: #ffffff;
}
}
}
.delete-artist-modal {
.path {
margin-left : 30px;
}
.delete-files-info {
margin-top : 10px;
display : none;
}
}
.artist-item {
padding-bottom : 30px;
:hover {
text-decoration : none;
}
h2 {
margin-top : 0px;
}
a {
color : #000000;
}
}
.artist-page-header {
.card(black);
.opacity(0.9);
background : #000000;
color : #ffffff;
padding : 30px 15px;
margin : 50px 10px;
.poster {
margin-top : 4px;
}
.header-text {
margin-top : 0px;
}
}
.artist-album {
.card;
.opacity(0.9);
margin : 30px 10px;
padding : 10px 25px;
.show-hide-episodes {
.clickable();
text-align : center;
i {
.clickable();
}
}
}
.artist-posters {
list-style-type: none;
@media (max-width: @screen-xs-max) {
padding : 0px;
}
li {
display : inline-block;
vertical-align : top;
}
.artist-posters-item {
.card;
.clickable;
margin-bottom : 20px;
height : 315px;
.center {
display : block;
margin-left : auto;
margin-right : auto;
text-align : center;
.progress {
text-align : left;
margin-top : 5px;
left : 0px;
width : 170px;
.progressbar-front-text, .progressbar-back-text {
width : 170px;
}
}
}
.labels {
display : inline-block;
.opacity(0.75);
width : 170px;
:hover {
cursor : default;
}
.label {
margin-top : 3px;
display : block;
}
.tooltip {
.opacity(1);
}
}
@media (max-width: @screen-xs-max) {
height : 235px;
margin : 5px;
padding : 6px 5px;
.center {
.progress {
width : 125px;
.progressbar-front-text, .progressbar-back-text {
width : 125px
}
}
}
.labels {
width: 125px;
}
}
}
.artist-poster-container {
position : relative;
overflow : hidden;
display : inline-block;
.placeholder-image ~ .title {
opacity: 1.0;
}
.title {
position : absolute;
top : 25px;
color : #f5f5f5;
width : 100%;
font-size : 22px;
line-height: 24px;
opacity : 0.0;
font-weight: 100;
}
.ended-banner {
color : #eeeeee;
background-color : #b94a48;
.box-shadow(2px 2px 20px #888888);
-moz-transform-origin : 50% 50%;
-webkit-transform-origin : 50% 50%;
position : absolute;
width : 320px;
top : 200px;
left : -122px;
text-align : center;
.opacity(0.9);
.transform(rotate(45deg));
}
.artist-controls {
position : absolute;;
top : 0px;
overflow : hidden;
background-color : #eeeeee;
width : 100%;
text-align : right;
padding-right : 10px;
display : none;
.opacity(0.8);
i {
.clickable();
}
}
.hidden-title {
position : absolute;;
bottom : 0px;
overflow : hidden;
background-color : #eeeeee;
width : 100%;
text-align : center;
.opacity(0.8);
display : none;
}
.artist-poster {
width : 168px;
height : 247px;
display : block;
font-size : 34px;
line-height : 34px;
}
@media (max-width: @screen-xs-max) {
.artist-poster {
width : 120px;
height : 176px;
}
.ended-banner {
top : 145px;
left : -137px;
}
}
}
}
.artist-detail-overview {
margin-bottom : 50px;
}
.artist-album {
.episode-number-cell {
width : 40px;
white-space: nowrap;
}
.episode-air-date-cell {
width : 150px;
}
.episode-status-cell {
width : 100px;
}
.episode-title-cell {
cursor : pointer;
}
}
.episode-detail-modal {
.episode-info {
margin-bottom : 10px;
}
.episode-overview {
font-style : italic;
}
.episode-file-info {
margin-top : 30px;
font-size : 12px;
}
.episode-history-details-cell .popover {
max-width: 800px;
}
.hidden-artist-title {
display : none;
}
}
.album-grid {
.toggle-cell {
width : 28px;
text-align : center;
padding-left : 0px;
padding-right : 0px;
}
.toggle-cell {
i {
.clickable;
}
}
}
.album-actions {
width: 100px;
}
.album-actions, .artist-actions {
div {
display : inline-block
}
text-transform : none;
i {
.clickable();
font-size : 24px;
margin-left : 5px;
}
}
.artist-stats {
font-size : 11px;
}
.artist-legend {
padding-top : 5px;
}
.albumpass-artist {
.card;
margin : 20px 0px;
.title {
font-weight : 300;
font-size : 24px;
line-height : 30px;
margin-left : 5px;
}
.album-select {
margin-bottom : 0px;
}
.expander {
.clickable;
line-height : 30px;
margin-left : 8px;
width : 16px;
}
.album-grid {
margin-top : 10px;
}
.album-pass-button {
display : inline-block;
}
.artist-monitor-toggle {
font-size : 24px;
margin-top : 3px;
}
.help-inline {
margin-top : 7px;
display : inline-block;
}
}
.album-status {
font-size : 11px;
vertical-align : middle !important;
}
//Overview List
.artist-overview-list-actions {
min-width: 56px;
max-width: 56px;
i {
.clickable();
}
}
//Editor
.artist-editor-footer {
max-width: 1160px;
color: #f5f5f5;
margin-left: auto;
margin-right: auto;
.form-group {
padding-top: 0px;
}
}
.update-files-artist-modal {
.selected-artist {
margin-top: 15px;
}
}
//artist Details
.artist-not-monitored {
.album-monitored, .episode-monitored {
color: #888888;
cursor: not-allowed;
i {
cursor: not-allowed;
}
}
}
.artist-info {
.row {
margin-bottom : 3px;
.label {
display : inline-block;
margin-bottom : 2px;
padding : 4px 6px 3px 6px;
max-width : 100%;
white-space : normal;
word-wrap : break-word;
}
}
.artist-info-links {
@media (max-width: @screen-sm-max) {
display : inline-block;
margin-top : 5px;
}
}
}
.scene-info {
.key, .value {
display : inline-block;
}
.key {
width : 80px;
margin-left : 10px;
vertical-align : top;
}
.value {
margin-right : 10px;
max-width : 170px;
}
ul {
padding-left : 0px;
list-style-type : none;
}
}

@ -0,0 +1,45 @@
var vent = require('vent');
var NzbDroneCell = require('./NzbDroneCell');
var CommandController = require('../Commands/CommandController');
module.exports = NzbDroneCell.extend({
className : 'artist-actions-cell',
ui : {
refresh : '.x-refresh'
},
events : {
'click .x-edit' : '_editArtist',
'click .x-refresh' : '_refreshArtist'
},
render : function() {
this.$el.empty();
this.$el.html('<i class="icon-lidarr-refresh x-refresh hidden-xs" title="" data-original-title="Update artist info and scan disk"></i> ' +
'<i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit Artist"></i>');
CommandController.bindToCommand({
element : this.$el.find('.x-refresh'),
command : {
name : 'refreshArtist',
seriesId : this.model.get('id')
}
});
this.delegateEvents();
return this;
},
_editArtist : function() {
vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model });
},
_refreshArtist : function() {
CommandController.Execute('refreshArtist', {
name : 'refreshArtist',
artistId : this.model.id
});
}
});

@ -0,0 +1,14 @@
var Backgrid = require('backgrid');
module.exports = Backgrid.Cell.extend({
className : 'artist-folder-cell',
render : function() {
this.$el.empty();
var artistFolder = this.model.get(this.column.get('name'));
this.$el.html(artistFolder.toString());
return this;
}
});

@ -0,0 +1,32 @@
var NzbDroneCell = require('./NzbDroneCell');
module.exports = NzbDroneCell.extend({
className : 'artist-status-cell',
render : function() {
this.$el.empty();
var monitored = this.model.get('monitored');
var status = this.model.get('status');
if (status === 'ended') {
this.$el.html('<i class="icon-lidarr-artist-ended grid-icon" title="Ended"></i>');
this._setStatusWeight(3);
}
else if (!monitored) {
this.$el.html('<i class="icon-lidarr-artist-unmonitored grid-icon" title="Not Monitored"></i>');
this._setStatusWeight(2);
}
else {
this.$el.html('<i class="icon-lidarr-artist-continuing grid-icon" title="Continuing"></i>');
this._setStatusWeight(1);
}
return this;
},
_setStatusWeight : function(weight) {
this.model.set('statusWeight', weight, { silent : true });
}
});

@ -0,0 +1,6 @@
var TemplatedCell = require('./TemplatedCell');
module.exports = TemplatedCell.extend({
className : 'artist-title-cell',
template : 'Cells/ArtistTitleTemplate'
});

@ -0,0 +1 @@
<a href="{{route}}">{{artistName}}</a>

@ -5,7 +5,7 @@
@import "../Content/mixins";
@import "../Content/variables";
.series-title-cell {
.artist-title-cell {
.text-overflow();
max-width: 450px;
@ -141,7 +141,7 @@ td.episode-status-cell, td.quality-cell, td.history-quality-cell, td.progress-ce
}
}
.series-actions-cell {
.artist-actions-cell {
width : 56px;
min-width : 56px;
}
@ -184,7 +184,7 @@ td.delete-episode-file-cell {
}
}
.series-status-cell {
.artist-status-cell {
width: 16px;
}

@ -9,8 +9,8 @@ module.exports = {
Keys : {
DefaultProfileId : 'DefaultProfileId',
DefaultRootFolderId : 'DefaultRootFolderId',
UseSeasonFolder : 'UseSeasonFolder',
DefaultSeriesType : 'DefaultSeriesType',
UseAlbumFolder : 'UseAlbumFolder',
DefaultArtistType : 'DefaultArtistType',
MonitorEpisodes : 'MonitorEpisodes',
AdvancedSettings : 'advancedSettings'
},

@ -311,7 +311,7 @@
.fa-icon-content(@fa-var-bars);
}
.icon-lidarr-navbar-series {
.icon-lidarr-navbar-artist {
.fa-icon-content(@fa-var-play);
}
@ -376,15 +376,15 @@
.fa-icon-content(@fa-var-refresh);
}
.icon-lidarr-series-ended {
.icon-lidarr-artist-ended {
.fa-icon-content(@fa-var-stop);
}
.icon-lidarr-series-continuing {
.icon-lidarr-artist-continuing {
.fa-icon-content(@fa-var-play);
}
.icon-lidarr-series-unmonitored {
.icon-lidarr-artist-unmonitored {
.fa-icon-content(@fa-var-pause);
}

@ -3,18 +3,20 @@ var AppLayout = require('./AppLayout');
var Marionette = require('marionette');
var ActivityLayout = require('./Activity/ActivityLayout');
var SettingsLayout = require('./Settings/SettingsLayout');
var AddSeriesLayout = require('./AddSeries/AddSeriesLayout');
//var AddSeriesLayout = require('./AddSeries/AddSeriesLayout');
var AddArtistLayout = require('./AddArtist/AddArtistLayout');
var WantedLayout = require('./Wanted/WantedLayout');
var CalendarLayout = require('./Calendar/CalendarLayout');
var ReleaseLayout = require('./Release/ReleaseLayout');
var SystemLayout = require('./System/SystemLayout');
var SeasonPassLayout = require('./SeasonPass/SeasonPassLayout');
var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout');
//var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout');
var ArtistEditorLayout = require('./Artist/Editor/ArtistEditorLayout');
module.exports = NzbDroneController.extend({
addSeries : function(action) {
this.setTitle('Add Series');
this.showMainRegion(new AddSeriesLayout({ action : action }));
addArtist : function(action) {
this.setTitle('Add Artist');
this.showMainRegion(new AddArtistLayout({ action : action }));
},
calendar : function() {
@ -52,8 +54,8 @@ module.exports = NzbDroneController.extend({
this.showMainRegion(new SeasonPassLayout());
},
seriesEditor : function() {
this.setTitle('Series Editor');
this.showMainRegion(new SeriesEditorLayout());
artistEditor : function() {
this.setTitle('Artist Editor');
this.showMainRegion(new ArtistEditorLayout());
}
});

@ -0,0 +1,106 @@
var Handlebars = require('handlebars');
var StatusModel = require('../../System/StatusModel');
var _ = require('underscore');
Handlebars.registerHelper('poster', function() {
var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png';
var poster = _.where(this.images, { coverType : 'poster' });
if (poster[0]) {
if (!poster[0].url.match(/^https?:\/\//)) {
return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250)));
} else {
var url = poster[0].url.replace(/^https?\:/, '');
return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url)));
}
}
return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder));
});
Handlebars.registerHelper('traktUrl', function() {
return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show';
});
Handlebars.registerHelper('imdbUrl', function() {
return 'http://imdb.com/title/' + this.imdbId;
});
Handlebars.registerHelper('tvdbUrl', function() {
return 'http://www.thetvdb.com/?tab=series&id=' + this.tvdbId;
});
Handlebars.registerHelper('tvRageUrl', function() {
return 'http://www.tvrage.com/shows/id-' + this.tvRageId;
});
Handlebars.registerHelper('tvMazeUrl', function() {
return 'http://www.tvmaze.com/shows/' + this.tvMazeId + '/_';
});
Handlebars.registerHelper('route', function() {
return StatusModel.get('urlBase') + '/artist/' + this.titleSlug;
});
Handlebars.registerHelper('percentOfEpisodes', function() {
var episodeCount = this.episodeCount;
var episodeFileCount = this.episodeFileCount;
var percent = 100;
if (episodeCount > 0) {
percent = episodeFileCount / episodeCount * 100;
}
return percent;
});
Handlebars.registerHelper('seasonCountHelper', function() {
var seasonCount = this.seasonCount;
var continuing = this.status === 'continuing';
if (continuing) {
return new Handlebars.SafeString('<span class="label label-info">Season {0}</span>'.format(seasonCount));
}
if (seasonCount === 1) {
return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount));
}
return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount));
});
Handlebars.registerHelper ('truncate', function (str, len) {
if (str && str.length > len && str.length > 0) {
var new_str = str + " ";
new_str = str.substr (0, len);
new_str = str.substr (0, new_str.lastIndexOf(" "));
new_str = (new_str.length > 0) ? new_str : str.substr (0, len);
return new Handlebars.SafeString ( new_str +'...' );
}
return str;
});
Handlebars.registerHelper('albumCountHelper', function() {
var albumCount = this.albumCount;
if (albumCount === 1) {
return new Handlebars.SafeString('<span class="label label-info">{0} Albums</span>'.format(albumCount));
}
return new Handlebars.SafeString('<span class="label label-info">{0} Albums</span>'.format(albumCount));
});
/*Handlebars.registerHelper('titleWithYear', function() {
if (this.title.endsWith(' ({0})'.format(this.year))) {
return this.title;
}
if (!this.year) {
return this.title;
}
return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year));
});*/

@ -4,7 +4,8 @@ require('./Helpers/DateTime');
require('./Helpers/Html');
require('./Helpers/Numbers');
require('./Helpers/Episode');
require('./Helpers/Series');
//require('./Helpers/Series');
require('./Helpers/Artist');
require('./Helpers/Quality');
require('./Helpers/System');
require('./Helpers/EachReverse');

@ -19,7 +19,7 @@
</div>
<div class="navbar-collapse collapse x-navbar-collapse">
<ul class="nav navbar-nav">
<li><a href="{{UrlBase}}/" class="x-series-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-series"></i> Artists</a></li>
<li><a href="{{UrlBase}}/" class="x-series-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-artist"></i> Artists</a></li>
<li><a href="{{UrlBase}}/calendar" class="x-calendar-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-calendar"></i> Calendar</a></li>
<li><a href="{{UrlBase}}/activity" class="x-activity-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-activity"></i> Activity<span id="x-queue-count" class="navbar-info"></span></a></li>
<li><a href="{{UrlBase}}/wanted" class="x-wanted-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-wanted"></i> Wanted</a></li>
@ -37,7 +37,7 @@
<div class="col-md-6 col-md-offset-3">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" class="col-md-6 form-control x-series-search" placeholder="Search the series in your library">
<input type="text" class="col-md-6 form-control x-series-search" placeholder="Search the artist in your library">
</div>
</div>
</div>

@ -2,17 +2,18 @@ var _ = require('underscore');
var $ = require('jquery');
var vent = require('vent');
var Backbone = require('backbone');
var SeriesCollection = require('../Series/SeriesCollection');
var ArtistCollection = require('../Artist/ArtistCollection');
require('typeahead');
vent.on(vent.Hotkeys.NavbarSearch, function() {
$('.x-series-search').focus();
$('.x-artist-search').focus();
});
var substringMatcher = function() {
return function findMatches (q, cb) {
var matches = _.select(SeriesCollection.toJSON(), function(series) {
return series.title.toLowerCase().indexOf(q.toLowerCase()) > -1;
var matches = _.select(ArtistCollection.toJSON(), function(artist) {
return artist.artistName.toLowerCase().indexOf(q.toLowerCase()) > -1;
});
cb(matches);
};
@ -24,14 +25,14 @@ $.fn.bindSearch = function() {
highlight : true,
minLength : 1
}, {
name : 'series',
displayKey : 'title',
name : 'artist',
displayKey : 'artistName',
source : substringMatcher()
});
$(this).on('typeahead:selected typeahead:autocompleted', function(e, series) {
$(this).on('typeahead:selected typeahead:autocompleted', function(e, artist) {
this.blur();
$(this).val('');
Backbone.history.navigate('/series/{0}'.format(series.titleSlug), { trigger : true });
Backbone.history.navigate('/artist/{0}'.format(artist.artistSlug), { trigger : true });
});
};

@ -4,8 +4,8 @@ var Controller = require('./Controller');
module.exports = Marionette.AppRouter.extend({
controller : new Controller(),
appRoutes : {
'addseries' : 'addSeries',
'addseries/:action(/:query)' : 'addSeries',
'addartist' : 'addArtist',
'addartist/:action(/:query)' : 'addArtist',
'calendar' : 'calendar',
'settings' : 'settings',
'settings/:action(/:query)' : 'settings',
@ -19,7 +19,7 @@ module.exports = Marionette.AppRouter.extend({
'system' : 'system',
'system/:action' : 'system',
'seasonpass' : 'seasonPass',
'serieseditor' : 'seriesEditor',
'artisteditor' : 'artistEditor',
':whatever' : 'showNotFound'
}
});

@ -2,7 +2,7 @@ var _ = require('underscore');
var $ = require('jquery');
var Marionette = require('marionette');
var vent = require('vent');
var RootFolders = require('../AddSeries/RootFolders/RootFolderCollection');
var RootFolders = require('../AddArtist/RootFolders/RootFolderCollection');
module.exports = Marionette.ItemView.extend({
template : 'SeasonPass/SeasonPassFooterViewTemplate',

@ -2,8 +2,8 @@ var _ = require('underscore');
var Marionette = require('marionette');
var vent = require('vent');
var Profiles = require('../../Profile/ProfileCollection');
var RootFolders = require('../../AddSeries/RootFolders/RootFolderCollection');
var RootFolderLayout = require('../../AddSeries/RootFolders/RootFolderLayout');
var RootFolders = require('../../AddArtist/RootFolders/RootFolderCollection');
var RootFolderLayout = require('../../AddArtist/RootFolders/RootFolderLayout');
var UpdateFilesSeriesView = require('./Organize/OrganizeFilesView');
var Config = require('../../Config');

@ -7,7 +7,7 @@
</div>
<div class="row">
<div class="col-md-4 col-md-offset-4">
<a href="/addseries" class='btn btn-lg btn-block btn-success x-add-series'>
<a href="/addartist" class='btn btn-lg btn-block btn-success x-add-artist'>
<i class='icon-lidarr-add'></i>
Add Music
</a>

@ -81,9 +81,9 @@ module.exports = Marionette.Layout.extend({
collapse : true,
items : [
{
title : 'Add Series',
title : 'Add Artist',
icon : 'icon-lidarr-add',
route : 'addseries'
route : 'addartist'
},
{
title : 'Season Pass',

@ -1,8 +1,8 @@
var vent = require('vent');
var AppLayout = require('../../AppLayout');
var Marionette = require('marionette');
var EditSeriesView = require('../../Series/Edit/EditSeriesView');
var DeleteSeriesView = require('../../Series/Delete/DeleteSeriesView');
var EditArtistView = require('../../Artist/Edit/EditArtistView');
var DeleteArtistView = require('../../Artist/Delete/DeleteArtistView');
var EpisodeDetailsLayout = require('../../Episode/EpisodeDetailsLayout');
var HistoryDetailsLayout = require('../../Activity/History/Details/HistoryDetailsLayout');
var LogDetailsView = require('../../System/Logs/Table/Details/LogDetailsView');
@ -16,8 +16,8 @@ module.exports = Marionette.AppRouter.extend({
vent.on(vent.Commands.CloseModalCommand, this._closeModal, this);
vent.on(vent.Commands.OpenModal2Command, this._openModal2, this);
vent.on(vent.Commands.CloseModal2Command, this._closeModal2, this);
vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this);
vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this);
vent.on(vent.Commands.EditArtistCommand, this._editArtist, this);
vent.on(vent.Commands.DeleteArtistCommand, this._deleteArtist, this);
vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this);
vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this);
vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this);
@ -43,13 +43,13 @@ module.exports = Marionette.AppRouter.extend({
AppLayout.modalRegion2.closeModal();
},
_editSeries : function(options) {
var view = new EditSeriesView({ model : options.series });
_editArtist : function(options) {
var view = new EditArtistView({ model : options.artist });
AppLayout.modalRegion.show(view);
},
_deleteSeries : function(options) {
var view = new DeleteSeriesView({ model : options.series });
_deleteArtist : function(options) {
var view = new DeleteArtistView({ model : options.artist });
AppLayout.modalRegion.show(view);
},

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save