From 0ec520c4d5305f4a232a0fc8c0bcbf0eeb43e8dd Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Nov 2013 20:20:45 -0800 Subject: [PATCH] Basic UI + Wizard for custom naming added --- src/NzbDrone.Api/Config/NamingModule.cs | 48 ++++-- .../Config/NamingSampleResource.cs | 1 + .../029_add_formats_to_naming_config.cs | 7 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/NzbDrone.Core/Organizer/Exception.cs | 15 ++ .../Organizer/FileNameBuilder.cs | 15 +- src/NzbDrone.Core/Organizer/NamingConfig.cs | 6 +- src/UI/Mixins/AsModelBoundView.js | 6 +- .../Naming/NamingSampleModel.js | 1 - .../MediaManagement/Naming/NamingView.js | 14 +- .../Naming/NamingViewTemplate.html | 104 +++---------- .../Naming/Wizard/NamingWizardModel.js | 17 +++ .../Naming/Wizard/NamingWizardView.js | 141 ++++++++++++++++++ .../Wizard/NamingWizardViewTemplate.html | 141 ++++++++++++++++++ .../MediaManagement/Sorting/ViewTemplate.html | 4 +- src/UI/Settings/settings.less | 6 +- src/UI/Shared/Modal/Controller.js | 11 +- src/UI/vent.js | 3 +- 18 files changed, 422 insertions(+), 119 deletions(-) create mode 100644 src/NzbDrone.Core/Organizer/Exception.cs create mode 100644 src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js create mode 100644 src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js create mode 100644 src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index 33afd046c..20ac48270 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingModule.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using FluentValidation; using Nancy.Responses; using NzbDrone.Core.MediaFiles; @@ -28,6 +29,8 @@ namespace NzbDrone.Api.Config Get["/samples"] = x => GetExamples(this.Bind()); SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 3); + SharedValidator.RuleFor(c => c.StandardEpisodeFormat).NotEmpty(); + SharedValidator.RuleFor(c => c.DailyEpisodeFormat).NotEmpty(); } private void UpdateNamingConfig(NamingConfigResource resource) @@ -59,7 +62,8 @@ namespace NzbDrone.Api.Config { SeasonNumber = 1, EpisodeNumber = 1, - Title = "Episode Title (1)" + Title = "Episode Title (1)", + AirDate = "2013-10-30" }; var episode2 = new Episode @@ -77,19 +81,43 @@ namespace NzbDrone.Api.Config var sampleResource = new NamingSampleResource(); - sampleResource.SingleEpisodeExample = _buildFileNames.BuildFilename(new List { episode1 }, - series, - episodeFile, - nameSpec); + sampleResource.SingleEpisodeExample = BuildSample(new List { episode1 }, + series, + episodeFile, + nameSpec); episodeFile.Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv"; - sampleResource.MultiEpisodeExample = _buildFileNames.BuildFilename(new List { episode1, episode2 }, - series, - episodeFile, - nameSpec); + sampleResource.MultiEpisodeExample = BuildSample(new List { episode1, episode2 }, + series, + episodeFile, + nameSpec); + + episodeFile.Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv"; + series.SeriesType = SeriesTypes.Daily; + + sampleResource.DailyEpisodeExample = BuildSample(new List { episode1 }, + series, + episodeFile, + nameSpec); return sampleResource.AsResponse(); } + + private string BuildSample(List episodes, Core.Tv.Series series, EpisodeFile episodeFile, NamingConfig nameSpec) + { + try + { + return _buildFileNames.BuildFilename(episodes, + series, + episodeFile, + nameSpec); + } + catch (NamingFormatException ex) + { + //Catching to avoid blowing up all samples + return String.Empty; + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/NamingSampleResource.cs b/src/NzbDrone.Api/Config/NamingSampleResource.cs index 60fba0b3c..7f6d0e99e 100644 --- a/src/NzbDrone.Api/Config/NamingSampleResource.cs +++ b/src/NzbDrone.Api/Config/NamingSampleResource.cs @@ -4,5 +4,6 @@ { public string SingleEpisodeExample { get; set; } public string MultiEpisodeExample { get; set; } + public string DailyEpisodeExample { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs index 1beb15cea..b723dca46 100644 --- a/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs +++ b/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Datastore.Migration //Output settings var seriesTitlePattern = ""; var episodeTitlePattern = ""; - var dailyEpisodePattern = "{Air Date}"; + var dailyEpisodePattern = "{Air-Date}"; var qualityFormat = " [{Quality Title}]"; if (includeSeriesTitle) @@ -61,11 +61,6 @@ namespace NzbDrone.Core.Datastore.Migration } } - if (replaceSpaces) - { - dailyEpisodePattern = "{Air.Date}"; - } - var standardEpisodeFormat = String.Format("{0}{1}{2}", seriesTitlePattern, GetNumberStyle(numberStyle).Pattern, episodeTitlePattern); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9217351e4..f88be54d4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -325,6 +325,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/Exception.cs b/src/NzbDrone.Core/Organizer/Exception.cs new file mode 100644 index 000000000..c4e80b17d --- /dev/null +++ b/src/NzbDrone.Core/Organizer/Exception.cs @@ -0,0 +1,15 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Organizer +{ + public class NamingFormatException : NzbDroneException + { + public NamingFormatException(string message, params object[] args) : base(message, args) + { + } + + public NamingFormatException(string message) : base(message) + { + } + } +} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 1941981ad..492573da1 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -96,6 +96,16 @@ namespace NzbDrone.Core.Organizer return episodeFile.SceneName; } + if (String.IsNullOrWhiteSpace(nameSpec.StandardEpisodeFormat) && series.SeriesType == SeriesTypes.Standard) + { + throw new NamingFormatException("Standard episode format cannot be null"); + } + + if (String.IsNullOrWhiteSpace(nameSpec.DailyEpisodeFormat) && series.SeriesType == SeriesTypes.Daily) + { + throw new NamingFormatException("Daily episode format cannot be null"); + } + var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber).ToList(); var pattern = nameSpec.StandardEpisodeFormat; var episodeTitles = new List @@ -112,7 +122,7 @@ namespace NzbDrone.Core.Organizer if (!String.IsNullOrWhiteSpace(episodes.First().AirDate)) { - tokenValues.Add("{Air Date}", episodes.First().AirDate); + tokenValues.Add("{Air Date}", episodes.First().AirDate.Replace('-', ' ')); } else { @@ -218,8 +228,9 @@ namespace NzbDrone.Core.Organizer { var separator = match.Groups["separator"].Value; var token = match.Groups["token"].Value; - var replacementText = tokenValues[token]; + var replacementText = ""; var patternTokenArray = token.ToCharArray(); + if (!tokenValues.TryGetValue(token, out replacementText)) return null; if (patternTokenArray.All(t => !char.IsLetter(t) || char.IsLower(t))) { diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index ea6784f03..5b28999c3 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -10,10 +10,10 @@ namespace NzbDrone.Core.Organizer { return new NamingConfig { - RenameEpisodes = true, + RenameEpisodes = false, MultiEpisodeStyle = 0, - StandardEpisodeFormat = "{Series Title} - {season}x{0episode} - {Episode Title} {Quality Title}", - DailyEpisodeFormat = "{Series Title} - {Air Date} - {Episode Title} {Quality Title}" + StandardEpisodeFormat = "", + DailyEpisodeFormat = "" }; } } diff --git a/src/UI/Mixins/AsModelBoundView.js b/src/UI/Mixins/AsModelBoundView.js index 7d1c961f7..d35f8a5a0 100644 --- a/src/UI/Mixins/AsModelBoundView.js +++ b/src/UI/Mixins/AsModelBoundView.js @@ -19,7 +19,11 @@ define( this._modelBinder = new ModelBinder(); } - this._modelBinder.bind(this.model, this.el); + var options = { + changeTriggers: {'': 'change', '[contenteditable]': 'blur', '[data-onkeyup]': 'keyup'} + }; + + this._modelBinder.bind(this.model, this.el, null, options); if (originalOnRender) { originalOnRender.call(this); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js b/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js index fd833e32f..55b167fc0 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js @@ -6,5 +6,4 @@ define( return Backbone.Model.extend({ url: window.NzbDrone.ApiRoot + '/config/naming/samples' }); - }); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 5ca680643..b94654287 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -1,10 +1,11 @@ 'use strict'; define( [ + 'vent', 'marionette', 'Settings/MediaManagement/Naming/NamingSampleModel', 'Mixins/AsModelBoundView' - ], function (Marionette, NamingSampleModel, AsModelBoundView) { + ], function (vent, Marionette, NamingSampleModel, AsModelBoundView) { var view = Marionette.ItemView.extend({ template: 'Settings/MediaManagement/Naming/NamingViewTemplate', @@ -13,11 +14,13 @@ define( namingOptions : '.x-naming-options', renameEpisodesCheckbox: '.x-rename-episodes', singleEpisodeExample : '.x-single-episode-example', - multiEpisodeExample : '.x-multi-episode-example' + multiEpisodeExample : '.x-multi-episode-example', + dailyEpisodeExample : '.x-daily-episode-example' }, events: { - 'change .x-rename-episodes': '_setFailedDownloadOptionsVisibility' + 'change .x-rename-episodes': '_setFailedDownloadOptionsVisibility', + 'click .x-show-wizard' : '_showWizard' }, onRender: function () { @@ -50,6 +53,11 @@ define( _showSamples: function () { this.ui.singleEpisodeExample.html(this.namingSampleModel.get('singleEpisodeExample')); this.ui.multiEpisodeExample.html(this.namingSampleModel.get('multiEpisodeExample')); + this.ui.dailyEpisodeExample.html(this.namingSampleModel.get('dailyEpisodeExample')); + }, + + _showWizard: function () { + vent.trigger(vent.Commands.ShowNamingWizard, { model: this.model }); } }); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index 657be7b83..037333fe0 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -24,108 +24,30 @@
- +
-
- -
- - -
-
- +
-
- -
- - -
-
- +
- -
-
- -
- - -
- -
-
- -
- - -
- +
@@ -145,4 +67,12 @@
+ +
+ + +
+ +
+
diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js new file mode 100644 index 000000000..e102e5cc4 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js @@ -0,0 +1,17 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + defaults: { + includeSeriesTitle : true, + includeEpisodeTitle: true, + includeQuality : true, + replaceSpaces : false, + separator : ' - ', + numberStyle : '2', + multiEpisodeStyle : 0 + } + }); + }); diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js new file mode 100644 index 000000000..8a6dc9d3e --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js @@ -0,0 +1,141 @@ +'use strict'; +define( + [ + 'vent', + 'marionette', + 'Settings/MediaManagement/Naming/NamingSampleModel', + 'Settings/MediaManagement/Naming/Wizard/NamingWizardModel', + 'Mixins/AsModelBoundView' + ], function (vent, Marionette, NamingSampleModel, NamingWizardModel, AsModelBoundView) { + + var view = Marionette.ItemView.extend({ + template: 'Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate', + + ui: { + namingOptions : '.x-naming-options', + singleEpisodeExample : '.x-single-episode-example', + multiEpisodeExample : '.x-multi-episode-example', + dailyEpisodeExample : '.x-daily-episode-example' + }, + + events: { + 'click .x-apply': '_applyNaming' + }, + + initialize: function (options) { + this.model = new NamingWizardModel(); + this.namingModel = options.model; + this.namingSampleModel = new NamingSampleModel(); + }, + + onRender: function () { + if (!this.model.get('renameEpisodes')) { + this.ui.namingOptions.hide(); + } + + this.listenTo(this.model, 'change', this._buildFormat); + this.listenTo(this.namingSampleModel, 'sync', this._showSamples); + this._buildFormat(); + }, + + _updateSamples: function () { + var data = { + renameEpisodes: true, + standardEpisodeFormat: this.standardEpisodeFormat, + dailyEpisodeFormat: this.dailyEpisodeFormat, + multiEpisodeStyle: this.model.get('multiEpisodeStyle') + }; + + this.namingSampleModel.fetch({data: data}); + }, + + _showSamples: function () { + this.ui.singleEpisodeExample.html(this.namingSampleModel.get('singleEpisodeExample')); + this.ui.multiEpisodeExample.html(this.namingSampleModel.get('multiEpisodeExample')); + this.ui.dailyEpisodeExample.html(this.namingSampleModel.get('dailyEpisodeExample')); + }, + + _applyNaming: function () { + this.namingModel.set('standardEpisodeFormat', this.standardEpisodeFormat); + this.namingModel.set('dailyEpisodeFormat', this.dailyEpisodeFormat); + this.namingModel.set('multiEpisodeStyle', this.model.get('multiEpisodeStyle')); + + vent.trigger(vent.Commands.CloseModalCommand); + }, + + _buildFormat: function () { + this.standardEpisodeFormat = ''; + this.dailyEpisodeFormat = ''; + + if (this.model.get('includeSeriesTitle')) { + if (this.model.get('replaceSpaces')) { + this.standardEpisodeFormat += '{Series.Title}'; + this.dailyEpisodeFormat += '{Series.Title}'; + } + + else { + this.standardEpisodeFormat += '{Series Title}'; + this.dailyEpisodeFormat += '{Series Title}'; + } + + this.standardEpisodeFormat += this.model.get('separator'); + this.dailyEpisodeFormat += this.model.get('separator'); + } + + switch (this.model.get('numberStyle')) { + case '0': + this.standardEpisodeFormat += '{season}x{0episode}'; + break; + case '1': + this.standardEpisodeFormat += '{0season}x{0episode}'; + break; + case '2': + this.standardEpisodeFormat += 'S{0season}E{0episode}'; + break; + case '3': + this.standardEpisodeFormat += 's{0season}e{0episode}'; + break; + default: + this.standardEpisodeFormat += 'Unknown Number Pattern'; + } + + this.dailyEpisodeFormat += '{Air-Date}'; + + if (this.model.get('includeEpisodeTitle')) { + this.standardEpisodeFormat += this.model.get('separator'); + this.dailyEpisodeFormat += this.model.get('separator'); + + if (this.model.get('replaceSpaces')) { + this.standardEpisodeFormat += '{Episode.Title}'; + this.dailyEpisodeFormat += '{Episode.Title}'; + } + + else { + this.standardEpisodeFormat += '{Episode Title}'; + this.dailyEpisodeFormat += '{Episode Title}'; + } + } + + if (this.model.get('includeQuality')) { + if (this.model.get('replaceSpaces')) { + this.standardEpisodeFormat += ' {Quality.Title}'; + this.dailyEpisodeFormat += ' {Quality.Title}'; + } + + else { + this.standardEpisodeFormat += ' {Quality Title}'; + this.dailyEpisodeFormat += ' {Quality Title}'; + } + } + + if (this.model.get('replaceSpaces')) { + this.standardEpisodeFormat = this.standardEpisodeFormat.replace(/\s/g, '.'); + this.dailyEpisodeFormat = this.dailyEpisodeFormat.replace(/\s/g, '.'); + } + + this._updateSamples(); + } + }); + + return AsModelBoundView.call(view); + }); diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html new file mode 100644 index 000000000..5857fb301 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html @@ -0,0 +1,141 @@ + +