From 7b54bca3c779aad4cd283b9a34730b9d0c620615 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 6 Nov 2013 17:48:51 -0800 Subject: [PATCH 01/21] Server side for custom naming is complete --- .../Config/NamingConfigResource.cs | 8 +- src/NzbDrone.Api/Config/NamingModule.cs | 2 - .../OrganizerTests/BuildFilePathFixture.cs | 12 +- .../OrganizerTests/GetNewFilenameFixture.cs | 859 +++++------------- .../Configuration/ConfigService.cs | 2 +- .../029_add_formats_to_naming_config.cs | 143 +++ .../030_update_series_folder_format.cs | 52 ++ .../031_delete_old_naming_config_columns.cs | 23 + src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + src/NzbDrone.Core/Organizer/EpisodeFormat.cs | 12 + .../Organizer/FileNameBuilder.cs | 240 ++--- .../FilenameBuilderTokenEqualityComparer.cs | 26 + src/NzbDrone.Core/Organizer/NamingConfig.cs | 23 +- src/NzbDrone.Core/Tv/QualityModel.cs | 2 +- 14 files changed, 613 insertions(+), 796 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/030_update_series_folder_format.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/031_delete_old_naming_config_columns.cs create mode 100644 src/NzbDrone.Core/Organizer/EpisodeFormat.cs create mode 100644 src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index af22a3f11..b57caa4cb 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -5,13 +5,9 @@ namespace NzbDrone.Api.Config { public class NamingConfigResource : RestResource { - public Boolean IncludeEpisodeTitle { get; set; } - public Boolean ReplaceSpaces { get; set; } public Boolean RenameEpisodes { get; set; } public Int32 MultiEpisodeStyle { get; set; } - public Int32 NumberStyle { get; set; } - public String Separator { get; set; } - public Boolean IncludeQuality { get; set; } - public Boolean IncludeSeriesTitle { get; set; } + public string StandardEpisodeFormat { get; set; } + public string DailyEpisodeFormat { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index 726a268b5..33afd046c 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingModule.cs @@ -28,8 +28,6 @@ namespace NzbDrone.Api.Config Get["/samples"] = x => GetExamples(this.Bind()); SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 3); - SharedValidator.RuleFor(c => c.NumberStyle).InclusiveBetween(0, 3); - SharedValidator.RuleFor(c => c.Separator).Matches(@"\s|\s\-\s|\."); } private void UpdateNamingConfig(NamingConfigResource resource) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs index 628bca46f..8013ebc07 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs @@ -26,12 +26,12 @@ namespace NzbDrone.Core.Test.OrganizerTests } [Test] - [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "Season %0s", @"C:\Test\30 Rock\Season 01\30 Rock - S01E05 - Episode Title.mkv")] - [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "Season %s", @"C:\Test\30 Rock\Season 1\30 Rock - S01E05 - Episode Title.mkv")] - [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season %0s", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] - [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season %s", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] - [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "ReallyUglySeasonFolder %s", @"C:\Test\30 Rock\ReallyUglySeasonFolder 1\30 Rock - S01E05 - Episode Title.mkv")] - [TestCase("30 Rock - S00E05 - Episode Title", 0, true, "Season %s", @"C:\Test\30 Rock\Specials\30 Rock - S00E05 - Episode Title.mkv")] + [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "Season {0season}", @"C:\Test\30 Rock\Season 01\30 Rock - S01E05 - Episode Title.mkv")] + [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "Season {season}", @"C:\Test\30 Rock\Season 1\30 Rock - S01E05 - Episode Title.mkv")] + [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {0season}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] + [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {season}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] + [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "ReallyUglySeasonFolder {season}", @"C:\Test\30 Rock\ReallyUglySeasonFolder 1\30 Rock - S01E05 - Episode Title.mkv")] + [TestCase("30 Rock - S00E05 - Episode Title", 0, true, "Season {season}", @"C:\Test\30 Rock\Specials\30 Rock - S00E05 - Episode Title.mkv")] public void CalculateFilePath_SeasonFolder_SingleNumber(string filename, int seasonNumber, bool useSeasonFolder, string seasonFolderFormat, string expectedPath) { var fakeSeries = Builder.CreateNew() diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs index 585fb10aa..1a94f38a2 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs @@ -16,8 +16,10 @@ namespace NzbDrone.Core.Test.OrganizerTests public class FileNameBuilderFixture : CoreTest { private Series _series; - - private NamingConfig namingConfig; + private Episode _episode1; + private Episode _episode2; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; [SetUp] public void Setup() @@ -28,571 +30,190 @@ namespace NzbDrone.Core.Test.OrganizerTests .Build(); - namingConfig = new NamingConfig(); - namingConfig.RenameEpisodes = true; + _namingConfig = new NamingConfig(); + _namingConfig.RenameEpisodes = true; Mocker.GetMock() - .Setup(c => c.GetConfig()).Returns(namingConfig); + .Setup(c => c.GetConfig()).Returns(_namingConfig); - - } - - [Test] - public void GetNewFilename_Series_Episode_Quality_S01E05_Dash() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - - var episode = Builder.CreateNew() + _episode1 = Builder.CreateNew() .With(e => e.Title = "City Sushi") .With(e => e.SeasonNumber = 15) .With(e => e.EpisodeNumber = 6) .Build(); - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("South Park - S15E06 - City Sushi [HDTV-720p]"); - } - - [Test] - public void GetNewFilename_Episode_Quality_1x05_Dash() - { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 0; - namingConfig.ReplaceSpaces = false; - - var episode = Builder.CreateNew() + _episode2 = Builder.CreateNew() .With(e => e.Title = "City Sushi") .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("15x06 - City Sushi [HDTV-720p]"); - } - - [Test] - public void GetNewFilename_Series_Quality_01x05_Space() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = false; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " "; - namingConfig.NumberStyle = 1; - namingConfig.ReplaceSpaces = false; - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 5) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("South Park 05x06 [HDTV-720p]"); - } - - [Test] - public void GetNewFilename_Series_s01e05_Space() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = false; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " "; - namingConfig.NumberStyle = 3; - namingConfig.ReplaceSpaces = false; - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 5) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("South Park s05e06"); - } - - [Test] - public void GetNewFilename_Series_Episode_s01e05_Periods() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " "; - namingConfig.NumberStyle = 3; - namingConfig.ReplaceSpaces = true; - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 5) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("South.Park.s05e06.City.Sushi"); - } - - [Test] - public void GetNewFilename_Series_Episode_s01e05_Dash_Periods_Quality() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 3; - namingConfig.ReplaceSpaces = true; - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 5) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("South.Park.-.s05e06.-.City.Sushi.[HDTV-720p]"); - } - - [Test] - public void GetNewFilename_S01E05_Dash() - { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = false; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("S15E06"); - } - - [Test] - public void GetNewFilename_multi_Series_Episode_Quality_S01E05_Scene_Dash() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - namingConfig.MultiEpisodeStyle = 3; - - var episodeOne = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (1)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 23) - .Build(); - - var episodeTwo = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (2)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 24) - .Build(); - - - string result = Subject.BuildFilename(new List { episodeOne, episodeTwo }, new Series { Title = "The Mentalist" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("The Mentalist - S03E23-E24 - Strawberries and Cream [HDTV-720p]"); - } - - [Test] - public void GetNewFilename_multi_Episode_Quality_1x05_Repeat_Dash() - { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 0; - namingConfig.ReplaceSpaces = false; - namingConfig.MultiEpisodeStyle = 2; - - var episodeOne = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (1)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 23) - .Build(); - - var episodeTwo = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (2)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 24) - .Build(); - - - string result = Subject.BuildFilename(new List { episodeOne, episodeTwo }, new Series { Title = "The Mentalist" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("3x23x24 - Strawberries and Cream [HDTV-720p]"); - } - - [Test] - public void GetNewFilename_multi_Episode_Quality_01x05_Repeat_Space() - { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " "; - namingConfig.NumberStyle = 0; - namingConfig.ReplaceSpaces = false; - namingConfig.MultiEpisodeStyle = 2; - - var episodeOne = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (1)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 23) - .Build(); - - var episodeTwo = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (2)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 24) - .Build(); - - - string result = Subject.BuildFilename(new List { episodeOne, episodeTwo }, new Series { Title = "The Mentalist" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("3x23x24 Strawberries and Cream [HDTV-720p]"); - } - - [Test] - public void GetNewFilename_multi_Series_Episode_s01e05_Duplicate_Period() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " "; - namingConfig.NumberStyle = 3; - namingConfig.ReplaceSpaces = true; - namingConfig.MultiEpisodeStyle = 1; - - var episodeOne = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (1)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 23) - .Build(); - - var episodeTwo = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (2)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 24) - .Build(); - - - string result = Subject.BuildFilename(new List { episodeOne, episodeTwo }, new Series { Title = "The Mentalist" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("The.Mentalist.s03e23.s03e24.Strawberries.and.Cream"); - } - - [Test] - public void GetNewFilename_multi_Series_S01E05_Extend_Dash_Period() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = false; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = true; - namingConfig.MultiEpisodeStyle = 0; - - var episodeOne = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (1)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 23) - .Build(); - - var episodeTwo = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (2)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 24) - .Build(); - - - string result = Subject.BuildFilename(new List { episodeOne, episodeTwo }, new Series { Title = "The Mentalist" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("The.Mentalist.-.S03E23-24"); - } - - [Test] - public void GetNewFilename_multi_1x05_Repeat_Dash_Period() - { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = false; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 0; - namingConfig.ReplaceSpaces = true; - namingConfig.MultiEpisodeStyle = 2; - - var episodeOne = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (1)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 23) - .Build(); - - var episodeTwo = Builder.CreateNew() - .With(e => e.Title = "Strawberries and Cream (2)") - .With(e => e.SeasonNumber = 3) - .With(e => e.EpisodeNumber = 24) - .Build(); - - - string result = Subject.BuildFilename(new List { episodeOne, episodeTwo }, new Series { Title = "The Mentalist" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("3x23x24"); - } - - [Test] - public void GetNewFilename_should_append_proper_when_proper_and_append_quality_is_true() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p, true) }); - - - result.Should().Be("South Park - S15E06 - City Sushi [HDTV-720p] [Proper]"); - } - - [Test] - public void GetNewFilename_should_not_append_proper_when_not_proper_and_append_quality_is_true() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("South Park - S15E06 - City Sushi [HDTV-720p]"); - } - - [Test] - public void GetNewFilename_should_not_append_proper_when_proper_and_append_quality_is_false() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p, true) }); - - - result.Should().Be("South Park - S15E06 - City Sushi"); - } - - [Test] - public void GetNewFilename_should_order_multiple_episode_files_in_numerical_order() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - namingConfig.MultiEpisodeStyle = 3; - - var episode = Builder.CreateNew() - .With(e => e.Title = "Hey, Baby, What's Wrong? (1)") - .With(e => e.SeasonNumber = 6) - .With(e => e.EpisodeNumber = 6) - .Build(); - - var episode2 = Builder.CreateNew() - .With(e => e.Title = "Hey, Baby, What's Wrong? (2)") - .With(e => e.SeasonNumber = 6) .With(e => e.EpisodeNumber = 7) .Build(); + _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }; + } - string result = Subject.BuildFilename(new List { episode2, episode }, new Series { Title = "30 Rock" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("30 Rock - S06E06-E07 - Hey, Baby, What's Wrong!"); + private void GivenProper() + { + _episodeFile.Quality.Proper = true; } [Test] - public void GetNewFilename_Series_Episode_Quality_S01E05_Period() + public void should_replace_Series_space_Title() { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = "."; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; + _namingConfig.StandardEpisodeFormat = "{Series Title}"; - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("South Park.S15E06.City Sushi [HDTV-720p]"); + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("South Park"); } [Test] - public void GetNewFilename_Episode_Quality_1x05_Period() + public void should_replace_Series_underscore_Title() { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = "."; ; - namingConfig.NumberStyle = 0; - namingConfig.ReplaceSpaces = false; + _namingConfig.StandardEpisodeFormat = "{Series_Title}"; - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("15x06.City Sushi [HDTV-720p]"); + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("South_Park"); } [Test] - public void GetNewFilename_UseSceneName_when_sceneName_isNull() + public void should_replace_Series_dot_Title() { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = "."; ; - namingConfig.NumberStyle = 0; - namingConfig.ReplaceSpaces = false; - namingConfig.RenameEpisodes = false; + _namingConfig.StandardEpisodeFormat = "{Series.Title}"; - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); - - var episodeFile = Builder.CreateNew() - .With(e => e.SceneName = null) - .With(e => e.Path = @"C:\Test\TV\30 Rock - S01E01 - Test") - .Build(); - - - string result = Subject.BuildFilename(new List { episode }, _series, episodeFile); - - - result.Should().Be(Path.GetFileNameWithoutExtension(episodeFile.Path)); + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("South.Park"); } [Test] - public void GetNewFilename_UseSceneName_when_sceneName_isNotNull() + public void should_replace_Series_dash_Title() { - namingConfig.IncludeSeriesTitle = false; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = "."; - namingConfig.NumberStyle = 0; - namingConfig.ReplaceSpaces = false; - namingConfig.RenameEpisodes = false; + _namingConfig.StandardEpisodeFormat = "{Series-Title}"; - var episode = Builder.CreateNew() - .With(e => e.Title = "City Sushi") - .With(e => e.SeasonNumber = 15) - .With(e => e.EpisodeNumber = 6) - .Build(); + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("South-Park"); + } - var episodeFile = Builder.CreateNew() - .With(e => e.SceneName = "30.Rock.S01E01.xvid-LOL") - .With(e => e.Path = @"C:\Test\TV\30 Rock - S01E01 - Test") - .Build(); + [Test] + public void should_replace_SERIES_TITLE_with_all_caps() + { + _namingConfig.StandardEpisodeFormat = "{SERIES TITLE}"; + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("SOUTH PARK"); + } - string result = Subject.BuildFilename(new List { episode }, _series, episodeFile); + [Test] + public void should_replace_series_title_with_all_lower_case() + { + _namingConfig.StandardEpisodeFormat = "{series title}"; + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("south park"); + } - result.Should().Be(episodeFile.SceneName); + [Test] + public void should_replace_episode_title() + { + _namingConfig.StandardEpisodeFormat = "{Episode Title}"; + + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("City Sushi"); + } + + [Test] + public void should_replace_season_number_with_single_digit() + { + _episode1.SeasonNumber = 1; + _namingConfig.StandardEpisodeFormat = "{season}x{episode}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("1x6"); + } + + [Test] + public void should_replace_0season_number_with_two_digits() + { + _episode1.SeasonNumber = 1; + _namingConfig.StandardEpisodeFormat = "{0season}x{episode}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("01x6"); + } + + [Test] + public void should_replace_episode_number_with_single_digit() + { + _episode1.SeasonNumber = 1; + _namingConfig.StandardEpisodeFormat = "{season}x{episode}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("1x6"); + } + + [Test] + public void should_replace_0episode_number_with_two_digits() + { + _episode1.SeasonNumber = 1; + _namingConfig.StandardEpisodeFormat = "{season}x{0episode}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("1x06"); + } + + [Test] + public void should_replace_quality_title() + { + _namingConfig.StandardEpisodeFormat = "{Quality Title}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("HDTV-720p"); + } + + [Test] + public void should_replace_quality_title_with_proper() + { + _namingConfig.StandardEpisodeFormat = "{Quality Title}"; + _episodeFile.Quality.Proper = true; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("HDTV-720p Proper"); + } + + [Test] + public void should_replace_all_contents_in_pattern() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title} [{Quality Title}]"; + + Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) + .Should().Be("South Park - S15E06 - City Sushi [HDTV-720p]"); + } + + [Test] + public void use_file_name_when_sceneName_is_null() + { + _namingConfig.RenameEpisodes = false; + _episodeFile.Path = @"C:\Test\TV\30 Rock - S01E01 - Test"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be(Path.GetFileNameWithoutExtension(_episodeFile.Path)); + } + + [Test] + public void use_file_name_when_sceneName_is_not_null() + { + _namingConfig.RenameEpisodes = false; + _episodeFile.SceneName = "30.Rock.S01E01.xvid-LOL"; + _episodeFile.Path = @"C:\Test\TV\30 Rock - S01E01 - Test"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("30.Rock.S01E01.xvid-LOL"); } [Test] public void should_only_have_one_episodeTitle_when_episode_titles_are_the_same() { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - namingConfig.MultiEpisodeStyle = 3; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.MultiEpisodeStyle = 3; var episode = Builder.CreateNew() .With(e => e.Title = "Hey, Baby, What's Wrong? (1)") @@ -607,163 +228,91 @@ namespace NzbDrone.Core.Test.OrganizerTests .Build(); - string result = Subject.BuildFilename(new List { episode2, episode }, new Series { Title = "30 Rock" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("30 Rock - S06E06-E07 - Hey, Baby, What's Wrong!"); + Subject.BuildFilename(new List {episode2, episode}, new Series {Title = "30 Rock"}, _episodeFile) + .Should().Be("30 Rock - S06E06-E07 - Hey, Baby, What's Wrong!"); } [Test] public void should_have_two_episodeTitles_when_episode_titles_are_not_the_same() { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - namingConfig.MultiEpisodeStyle = 3; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.MultiEpisodeStyle = 3; - var episode = Builder.CreateNew() - .With(e => e.Title = "Hello") - .With(e => e.SeasonNumber = 6) - .With(e => e.EpisodeNumber = 6) - .Build(); + _episode1.Title = "Hello"; + _episode2.Title = "World"; - var episode2 = Builder.CreateNew() - .With(e => e.Title = "World") - .With(e => e.SeasonNumber = 6) - .With(e => e.EpisodeNumber = 7) - .Build(); - - - string result = Subject.BuildFilename(new List { episode2, episode }, new Series { Title = "30 Rock" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("30 Rock - S06E06-E07 - Hello + World"); - } - - [Test] - public void should_have_two_episodeTitles_when_distinct_count_is_two() - { - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - namingConfig.MultiEpisodeStyle = 3; - - var episode = Builder.CreateNew() - .With(e => e.Title = "Hello (3)") - .With(e => e.SeasonNumber = 6) - .With(e => e.EpisodeNumber = 6) - .Build(); - - var episode2 = Builder.CreateNew() - .With(e => e.Title = "Hello (2)") - .With(e => e.SeasonNumber = 6) - .With(e => e.EpisodeNumber = 7) - .Build(); - - var episode3 = Builder.CreateNew() - .With(e => e.Title = "World") - .With(e => e.SeasonNumber = 6) - .With(e => e.EpisodeNumber = 8) - .Build(); - - - string result = Subject.BuildFilename(new List { episode, episode2, episode3 }, new Series { Title = "30 Rock" }, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - - - result.Should().Be("30 Rock - S06E06-E07-E08 - Hello + World"); + Subject.BuildFilename(new List {_episode1, _episode2}, _series, _episodeFile) + .Should().Be("South Park - S15E06-E07 - Hello + World"); } [Test] public void should_use_airDate_if_series_isDaily() { + _namingConfig.DailyEpisodeFormat = "{Series Title} - {air-date} - {Episode Title}"; - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = true; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; + _series.Title = "The Daily Show with Jon Stewart"; + _series.SeriesType = SeriesTypes.Daily; - var series = Builder - .CreateNew() - .With(s => s.SeriesType = SeriesTypes.Daily) - .With(s => s.Title = "The Daily Show with Jon Stewart") - .Build(); + _episode1.AirDate = "2012-12-13"; + _episode1.Title = "Kristen Stewart"; - var episodes = Builder - .CreateListOfSize(1) - .All() - .With(e => e.AirDate = "2012-12-13") - .With(e => e.Title = "Kristen Stewart") - .Build(); - - var result = Subject - .BuildFilename(episodes, series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - result.Should().Be("The Daily Show with Jon Stewart - 2012-12-13 - Kristen Stewart [HDTV-720p]"); - } - - [Test] - public void should_use_airDate_if_series_isDaily_no_episode_title() - { - - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = false; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; - - var series = Builder - .CreateNew() - .With(s => s.SeriesType = SeriesTypes.Daily) - .With(s => s.Title = "The Daily Show with Jon Stewart") - .Build(); - - var episodes = Builder - .CreateListOfSize(1) - .All() - .With(e => e.AirDate = "2012-12-13") - .With(e => e.Title = "Kristen Stewart") - .Build(); - - var result = Subject - .BuildFilename(episodes, series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - result.Should().Be("The Daily Show with Jon Stewart - 2012-12-13"); + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("The Daily Show with Jon Stewart - 2012-12-13 - Kristen Stewart"); } [Test] public void should_set_airdate_to_unknown_if_not_available() { + _namingConfig.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}"; - namingConfig.IncludeSeriesTitle = true; - namingConfig.IncludeEpisodeTitle = true; - namingConfig.IncludeQuality = false; - namingConfig.Separator = " - "; - namingConfig.NumberStyle = 2; - namingConfig.ReplaceSpaces = false; + _series.Title = "The Daily Show with Jon Stewart"; + _series.SeriesType = SeriesTypes.Daily; - var series = Builder - .CreateNew() - .With(s => s.SeriesType = SeriesTypes.Daily) - .With(s => s.Title = "The Daily Show with Jon Stewart") - .Build(); + _episode1.AirDate = null; + _episode1.Title = "Kristen Stewart"; - var episodes = Builder - .CreateListOfSize(1) - .All() - .With(e => e.AirDate = null) - .With(e => e.Title = "Kristen Stewart") - .Build(); + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("The Daily Show with Jon Stewart - Unknown - Kristen Stewart"); + } - var result = Subject - .BuildFilename(episodes, series, new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p) }); - result.Should().Be("The Daily Show with Jon Stewart - Unknown - Kristen Stewart"); + [Test] + public void should_format_extend_multi_episode_properly() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.MultiEpisodeStyle = 0; + + Subject.BuildFilename(new List {_episode1, _episode2}, _series, _episodeFile) + .Should().Be("South Park - S15E06-07 - City Sushi"); + } + + [Test] + public void should_format_duplicate_multi_episode_properly() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.MultiEpisodeStyle = 1; + + Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + .Should().Be("South Park - S15E06 - S15E07 - City Sushi"); + } + + [Test] + public void should_format_repeat_multi_episode_properly() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.MultiEpisodeStyle = 2; + + Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + .Should().Be("South Park - S15E06E07 - City Sushi"); + } + + [Test] + public void should_format_scene_multi_episode_properly() + { + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.MultiEpisodeStyle = 3; + + Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) + .Should().Be("South Park - S15E06-E07 - City Sushi"); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 341605d68..498731511 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -151,7 +151,7 @@ namespace NzbDrone.Core.Configuration public string SeasonFolderFormat { - get { return GetValue("SeasonFolderFormat", "Season %s"); } + get { return GetValue("SeasonFolderFormat", "Season {season}"); } set { SetValue("SeasonFolderFormat", value); } } 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 new file mode 100644 index 000000000..1beb15cea --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/029_add_formats_to_naming_config.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(29)] + public class add_formats_to_naming_config : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("NamingConfig").AddColumn("StandardEpisodeFormat").AsString().Nullable(); + Alter.Table("NamingConfig").AddColumn("DailyEpisodeFormat").AsString().Nullable(); + + Execute.WithConnection(ConvertConfig); + } + + private void ConvertConfig(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand namingConfigCmd = conn.CreateCommand()) + { + namingConfigCmd.Transaction = tran; + namingConfigCmd.CommandText = @"SELECT * FROM NamingConfig LIMIT 1"; + using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) + { + while (namingConfigReader.Read()) + { + var separator = namingConfigReader.GetString(1); + var numberStyle = namingConfigReader.GetInt32(2); + var includeSeriesTitle = namingConfigReader.GetBoolean(3); + var includeEpisodeTitle = namingConfigReader.GetBoolean(5); + var includeQuality = namingConfigReader.GetBoolean(6); + var replaceSpaces = namingConfigReader.GetBoolean(7); + + //Output settings + var seriesTitlePattern = ""; + var episodeTitlePattern = ""; + var dailyEpisodePattern = "{Air Date}"; + var qualityFormat = " [{Quality Title}]"; + + if (includeSeriesTitle) + { + seriesTitlePattern = "{Series Title}" + separator; + + if (replaceSpaces) + { + seriesTitlePattern = "{Series.Title}" + separator; + } + } + + if (includeEpisodeTitle) + { + episodeTitlePattern = separator + "{Episode Title}"; + + if (replaceSpaces) + { + episodeTitlePattern = separator + "{Episode.Title}"; + } + } + + if (replaceSpaces) + { + dailyEpisodePattern = "{Air.Date}"; + } + + var standardEpisodeFormat = String.Format("{0}{1}{2}", seriesTitlePattern, + GetNumberStyle(numberStyle).Pattern, + episodeTitlePattern); + + var dailyEpisodeFormat = String.Format("{0}{1}{2}", seriesTitlePattern, + dailyEpisodePattern, + episodeTitlePattern); + + if (includeQuality) + { + if (replaceSpaces) + { + qualityFormat = " [{Quality.Title}]"; + } + + standardEpisodeFormat += qualityFormat; + dailyEpisodeFormat += qualityFormat; + } + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + var text = String.Format("UPDATE NamingConfig " + + "SET StandardEpisodeFormat = '{0}', " + + "DailyEpisodeFormat = '{1}'", + standardEpisodeFormat, + dailyEpisodeFormat); + + updateCmd.Transaction = tran; + updateCmd.CommandText = text; + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + + private static readonly List NumberStyles = new List + { + new + { + Id = 0, + Name = "1x05", + Pattern = "{season}x{0episode}", + EpisodeSeparator = "x" + + }, + new + { + Id = 1, + Name = "01x05", + Pattern = "{0season}x{0episode}", + EpisodeSeparator = "x" + }, + new + { + Id = 2, + Name = "S01E05", + Pattern = "S{0season}E{0episode}", + EpisodeSeparator = "E" + }, + new + { + Id = 3, + Name = "s01e05", + Pattern = "s{0season}e{0episode}", + EpisodeSeparator = "e" + } + }; + + private static dynamic GetNumberStyle(int id) + { + return NumberStyles.Single(s => s.Id == id); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/030_update_series_folder_format.cs b/src/NzbDrone.Core/Datastore/Migration/030_update_series_folder_format.cs new file mode 100644 index 000000000..9f0b2fdc0 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/030_update_series_folder_format.cs @@ -0,0 +1,52 @@ +using System; +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(30)] + public class update_series_folder_format : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ConvertConfig); + } + + private void ConvertConfig(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand namingConfigCmd = conn.CreateCommand()) + { + namingConfigCmd.Transaction = tran; + namingConfigCmd.CommandText = @"SELECT * FROM Config WHERE [Key] = 'SeasonFolderFormat'"; + using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) + { + while (namingConfigReader.Read()) + { + var value = namingConfigReader.GetString(2); + + value = value.Replace("%sn", "{Series Title}") + .Replace("%s.n", "{Series.Title}") + .Replace("%s", "{season}") + .Replace("%0s", "{0season}") + .Replace("%e", "{episode}") + .Replace("%0e", "{0episode}"); + + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + var text = String.Format("UPDATE Config " + + "SET [VALUE] = '{0}'" + + "WHERE [Key] = 'SeasonFolderFormat'", + value); + + updateCmd.Transaction = tran; + updateCmd.CommandText = text; + updateCmd.ExecuteNonQuery(); + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/031_delete_old_naming_config_columns.cs b/src/NzbDrone.Core/Datastore/Migration/031_delete_old_naming_config_columns.cs new file mode 100644 index 000000000..c570fd574 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/031_delete_old_naming_config_columns.cs @@ -0,0 +1,23 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(31)] + public class delete_old_naming_config_columns : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + SqLiteAlter.DropColumns("NamingConfig", + new[] + { + "Separator", + "NumberStyle", + "IncludeSeriesTitle", + "IncludeEpisodeTitle", + "IncludeQuality", + "ReplaceSpaces" + }); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b4916da94..9217351e4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -185,6 +185,9 @@ Code + + + @@ -321,6 +324,8 @@ + + diff --git a/src/NzbDrone.Core/Organizer/EpisodeFormat.cs b/src/NzbDrone.Core/Organizer/EpisodeFormat.cs new file mode 100644 index 000000000..c5d8fe5db --- /dev/null +++ b/src/NzbDrone.Core/Organizer/EpisodeFormat.cs @@ -0,0 +1,12 @@ +using System; + +namespace NzbDrone.Core.Organizer +{ + public class EpisodeFormat + { + public String Separator { get; set; } + public String EpisodePattern { get; set; } + public String EpisodeSeparator { get; set; } + public String SeasonEpisodePattern { get; set; } + } +} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 837fbae3b..1941981ad 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; @@ -57,6 +58,18 @@ namespace NzbDrone.Core.Organizer private readonly INamingConfigService _namingConfigService; private readonly Logger _logger; + private static readonly Regex TitleRegex = new Regex(@"(?\{(?:\w+)(?\s|\W|_)\w+\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex EpisodeRegex = new Regex(@"(?\{0*(?:episode)})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex SeasonRegex = new Regex(@"(?\{0*(?:season)})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=}).+?)?(?s?{0?season}(?e|x)?(?{0?episode}))(?.+?(?={))?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + public FileNameBuilder(INamingConfigService namingConfigService, IConfigService configService, Logger logger) { _namingConfigService = namingConfigService; @@ -83,86 +96,78 @@ namespace NzbDrone.Core.Organizer return episodeFile.SceneName; } - var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber); - - var numberStyle = GetNumberStyle(nameSpec.NumberStyle); - - var episodeNames = new List - { - Parser.Parser.CleanupEpisodeTitle(sortedEpisodes.First().Title) - }; - - var result = String.Empty; - - if (nameSpec.IncludeSeriesTitle) + var sortedEpisodes = episodes.OrderBy(e => e.EpisodeNumber).ToList(); + var pattern = nameSpec.StandardEpisodeFormat; + var episodeTitles = new List { - result += series.Title + nameSpec.Separator; - } + Parser.Parser.CleanupEpisodeTitle(sortedEpisodes.First().Title) + }; - if (series.SeriesType == SeriesTypes.Standard) + var tokenValues = new Dictionary(new FilenameBuilderTokenEqualityComparer()); + tokenValues.Add("{Series Title}", series.Title); + + if (series.SeriesType == SeriesTypes.Daily) { - result += numberStyle.Pattern.Replace("%0e", - String.Format("{0:00}", sortedEpisodes.First().EpisodeNumber)); + pattern = nameSpec.DailyEpisodeFormat; - if (episodes.Count > 1) + if (!String.IsNullOrWhiteSpace(episodes.First().AirDate)) { - var multiEpisodeStyle = - GetMultiEpisodeStyle(nameSpec.MultiEpisodeStyle); - - foreach (var episode in sortedEpisodes.Skip(1)) - { - if (multiEpisodeStyle.Name == "Duplicate") - { - result += nameSpec.Separator + numberStyle.Pattern; - } - else - { - result += multiEpisodeStyle.Pattern; - } - - result = result.Replace("%0e", String.Format("{0:00}", episode.EpisodeNumber)); - episodeNames.Add(Parser.Parser.CleanupEpisodeTitle(episode.Title)); - } + tokenValues.Add("{Air Date}", episodes.First().AirDate); } - result = result - .Replace("%s", String.Format("{0}", episodes.First().SeasonNumber)) - .Replace("%0s", String.Format("{0:00}", episodes.First().SeasonNumber)) - .Replace("%x", numberStyle.EpisodeSeparator) - .Replace("%p", nameSpec.Separator); + else { + tokenValues.Add("{Air Date}", "Unknown"); + } } - else + var seasonEpisode = SeasonEpisodePatternRegex.Match(pattern); + if (seasonEpisode.Success) { - if (!String.IsNullOrEmpty(episodes.First().AirDate)) - result += episodes.First().AirDate; + var episodeFormat = new EpisodeFormat + { + EpisodeSeparator = seasonEpisode.Groups["episodeSeparator"].Value, + Separator = seasonEpisode.Groups["separator"].Value, + EpisodePattern = seasonEpisode.Groups["episode"].Value, + SeasonEpisodePattern = seasonEpisode.Groups["seasonEpisode"].Value, + }; - else - result += "Unknown"; - } + pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, "{Season Episode}"); + var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; - if (nameSpec.IncludeEpisodeTitle) - { - if (episodeNames.Distinct().Count() == 1) - result += nameSpec.Separator + episodeNames.First(); + foreach (var episode in sortedEpisodes.Skip(1)) + { + switch ((MultiEpisodeStyle)nameSpec.MultiEpisodeStyle) + { + case MultiEpisodeStyle.Duplicate: + seasonEpisodePattern += episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; + break; - else - result += nameSpec.Separator + String.Join(" + ", episodeNames.Distinct()); - } + case MultiEpisodeStyle.Repeat: + seasonEpisodePattern += episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + break; - if (nameSpec.IncludeQuality) - { - result += String.Format(" [{0}]", episodeFile.Quality.Quality); + case MultiEpisodeStyle.Scene: + seasonEpisodePattern += "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + break; - if (episodeFile.Quality.Proper) - result += " [Proper]"; - } + //MultiEpisodeStyle.Extend + default: + seasonEpisodePattern += "-" + episodeFormat.EpisodePattern; + break; + } - if (nameSpec.ReplaceSpaces) - result = result.Replace(' ', '.'); + episodeTitles.Add(Parser.Parser.CleanupEpisodeTitle(episode.Title)); + } - _logger.Trace("New File Name is: [{0}]", result.Trim()); - return CleanFilename(result.Trim()); + seasonEpisodePattern = ReplaceNumberTokens(seasonEpisodePattern, sortedEpisodes); + tokenValues.Add("{Season Episode}", seasonEpisodePattern); + } + + tokenValues.Add("{Episode Title}", String.Join(" + ", episodeTitles.Distinct())); + tokenValues.Add("{Quality Title}", episodeFile.Quality.ToString()); + + + return CleanFilename(ReplaceTokens(pattern, tokenValues).Trim()); } public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) @@ -179,12 +184,11 @@ namespace NzbDrone.Core.Organizer else { - seasonFolder = _configService.SeasonFolderFormat - .Replace("%sn", series.Title) - .Replace("%s.n", series.Title.Replace(' ', '.')) - .Replace("%s_n", series.Title.Replace(' ', '_')) - .Replace("%0s", seasonNumber.ToString("00")) - .Replace("%s", seasonNumber.ToString()); + var tokenValues = new Dictionary(new FilenameBuilderTokenEqualityComparer()); + tokenValues.Add("{Series Title}", series.Title); + + seasonFolder = ReplaceSeasonTokens(_configService.SeasonFolderFormat, seasonNumber); + seasonFolder = ReplaceTokens(seasonFolder, tokenValues); } path = Path.Combine(path, seasonFolder); @@ -205,38 +209,60 @@ namespace NzbDrone.Core.Organizer return result.Trim(); } - private static readonly List NumberStyles = new List - { - new EpisodeSortingType - { - Id = 0, - Name = "1x05", - Pattern = "%sx%0e", - EpisodeSeparator = "x" + private string ReplaceTokens(string pattern, Dictionary tokenValues) + { + return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenValues)); + } - }, - new EpisodeSortingType - { - Id = 1, - Name = "01x05", - Pattern = "%0sx%0e", - EpisodeSeparator = "x" - }, - new EpisodeSortingType - { - Id = 2, - Name = "S01E05", - Pattern = "S%0sE%0e", - EpisodeSeparator = "E" - }, - new EpisodeSortingType - { - Id = 3, - Name = "s01e05", - Pattern = "s%0se%0e", - EpisodeSeparator = "e" - } - }; + private string ReplaceToken(Match match, Dictionary tokenValues) + { + var separator = match.Groups["separator"].Value; + var token = match.Groups["token"].Value; + var replacementText = tokenValues[token]; + var patternTokenArray = token.ToCharArray(); + + if (patternTokenArray.All(t => !char.IsLetter(t) || char.IsLower(t))) + { + replacementText = replacementText.ToLowerInvariant(); + } + + else if (patternTokenArray.All(t => !char.IsLetter(t) || char.IsUpper(t))) + { + replacementText = replacementText.ToUpper(); + } + + if (!separator.Equals(" ")) + { + replacementText = replacementText.Replace(" ", separator); + } + + return replacementText; + } + + private string ReplaceNumberTokens(string pattern, List episodes) + { + var episodeIndex = 0; + pattern = EpisodeRegex.Replace(pattern, match => + { + var episode = episodes[episodeIndex].EpisodeNumber; + episodeIndex++; + + return ReplaceNumberToken(match.Groups["episode"].Value, episode); + }); + + return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); + } + + private string ReplaceSeasonTokens(string pattern, int seasonNumber) + { + return SeasonRegex.Replace(pattern, match => ReplaceNumberToken(match.Groups["season"].Value, seasonNumber)); + } + + private string ReplaceNumberToken(string token, int value) + { + var zeroCount = token.Count(z => z == '0'); + return value.ToString().PadLeft(zeroCount + 1, '0'); + } private static readonly List MultiEpisodeStyles = new List { @@ -266,15 +292,17 @@ namespace NzbDrone.Core.Organizer } }; - - private static EpisodeSortingType GetNumberStyle(int id) - { - return NumberStyles.Single(s => s.Id == id); - } - private static EpisodeSortingType GetMultiEpisodeStyle(int id) { return MultiEpisodeStyles.Single(s => s.Id == id); } } + + public enum MultiEpisodeStyle + { + Extend = 0, + Duplicate = 1, + Repeat = 2, + Scene = 3 + } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs b/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs new file mode 100644 index 000000000..a8ba17a23 --- /dev/null +++ b/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.Organizer +{ + public class FilenameBuilderTokenEqualityComparer : IEqualityComparer + { + private static readonly Regex SimpleTokenRegex = new Regex(@"\s|_|\W", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public bool Equals(String s1, String s2) + { + return SimplifyToken(s1).Equals(SimplifyToken(s2)); + } + + public int GetHashCode(String str) + { + return SimplifyToken(str).GetHashCode(); + } + + private string SimplifyToken(string token) + { + return SimpleTokenRegex.Replace(token, String.Empty).ToLower(); + } + } +} diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 962fd5777..ea6784f03 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -11,31 +11,16 @@ namespace NzbDrone.Core.Organizer return new NamingConfig { RenameEpisodes = true, - Separator = " - ", - NumberStyle = 0, - IncludeSeriesTitle = true, MultiEpisodeStyle = 0, - IncludeEpisodeTitle = true, - IncludeQuality = true, - ReplaceSpaces = false + StandardEpisodeFormat = "{Series Title} - {season}x{0episode} - {Episode Title} {Quality Title}", + DailyEpisodeFormat = "{Series Title} - {Air Date} - {Episode Title} {Quality Title}" }; } } public bool RenameEpisodes { get; set; } - - public string Separator { get; set; } - - public int NumberStyle { get; set; } - - public bool IncludeSeriesTitle { get; set; } - - public bool IncludeEpisodeTitle { get; set; } - - public bool IncludeQuality { get; set; } - public int MultiEpisodeStyle { get; set; } - - public bool ReplaceSpaces { get; set; } + public string StandardEpisodeFormat { get; set; } + public string DailyEpisodeFormat { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/QualityModel.cs b/src/NzbDrone.Core/Tv/QualityModel.cs index d5097c82d..eaa7f7884 100644 --- a/src/NzbDrone.Core/Tv/QualityModel.cs +++ b/src/NzbDrone.Core/Tv/QualityModel.cs @@ -85,7 +85,7 @@ namespace NzbDrone.Core.Tv string result = Quality.ToString(); if (Proper) { - result += " [proper]"; + result += " Proper"; } return result; From 0ec520c4d5305f4a232a0fc8c0bcbf0eeb43e8dd Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Nov 2013 20:20:45 -0800 Subject: [PATCH 02/21] 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 @@ + + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.html new file mode 100644 index 000000000..ed845e2c0 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.html @@ -0,0 +1,9 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.html new file mode 100644 index 000000000..c56795719 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.html @@ -0,0 +1,7 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.html new file mode 100644 index 000000000..d4ae003d6 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.html @@ -0,0 +1,8 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/QualityTitleNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/QualityTitleNamingPartial.html new file mode 100644 index 000000000..4fe8dc65e --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/QualityTitleNamingPartial.html @@ -0,0 +1,8 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.html new file mode 100644 index 000000000..1174a055d --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.html @@ -0,0 +1,7 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.html new file mode 100644 index 000000000..eb2abe61a --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.html @@ -0,0 +1,10 @@ + diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.html new file mode 100644 index 000000000..f2cd5fd81 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.html @@ -0,0 +1,8 @@ + From 795f78296c0556f4a9b4f9c45ee815f98bd2d602 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Nov 2013 22:30:58 -0800 Subject: [PATCH 04/21] Added tooltips and link to wiki, also made it an advanced option New: Fully customized naming now available as an advanced setting --- src/UI/Content/icons.less | 2 +- .../Naming/NamingViewTemplate.html | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index 734245993..849830f91 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -89,7 +89,7 @@ .icon-nd-form-info-link:before { .clickable; - .icon(@question-sign); + .icon(@info-sign); } .icon-nd-donate:before { diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index 8dc1d9c36..f4beac4ba 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -35,7 +35,7 @@
-
+
@@ -56,12 +56,13 @@
+
-
+
@@ -83,10 +84,24 @@
+
+ +
+ + +
+ +
+
From f2aec932a452951e1627dcc3e32d3c415d185b52 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 9 Nov 2013 23:29:19 -0800 Subject: [PATCH 05/21] Fixed naming config integration test --- src/NzbDrone.Integration.Test/NamingConfigTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Integration.Test/NamingConfigTests.cs b/src/NzbDrone.Integration.Test/NamingConfigTests.cs index 07592193e..5006809b6 100644 --- a/src/NzbDrone.Integration.Test/NamingConfigTests.cs +++ b/src/NzbDrone.Integration.Test/NamingConfigTests.cs @@ -26,8 +26,13 @@ namespace NzbDrone.Integration.Test { var config = NamingConfig.GetSingle(); config.RenameEpisodes = false; + config.StandardEpisodeFormat = "{Series Title} - {season}x{0episode} - {Episode Title}"; + config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}"; - NamingConfig.Put(config).RenameEpisodes.Should().BeFalse(); + var result = NamingConfig.Put(config); + result.RenameEpisodes.Should().BeFalse(); + result.StandardEpisodeFormat.Should().Be(config.StandardEpisodeFormat); + result.DailyEpisodeFormat.Should().Be(config.DailyEpisodeFormat); } } } \ No newline at end of file From 21af4bbdfa03598c3d69cd49f38bb2734dac587d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 10 Nov 2013 23:46:20 -0800 Subject: [PATCH 06/21] Season folder format is lowercase --- ...es_folder_format.cs => 030_update_season_folder_format.cs} | 4 ++-- src/NzbDrone.Core/NzbDrone.Core.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/NzbDrone.Core/Datastore/Migration/{030_update_series_folder_format.cs => 030_update_season_folder_format.cs} (96%) diff --git a/src/NzbDrone.Core/Datastore/Migration/030_update_series_folder_format.cs b/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs similarity index 96% rename from src/NzbDrone.Core/Datastore/Migration/030_update_series_folder_format.cs rename to src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs index 9f0b2fdc0..6b57fd00c 100644 --- a/src/NzbDrone.Core/Datastore/Migration/030_update_series_folder_format.cs +++ b/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore.Migration using (IDbCommand namingConfigCmd = conn.CreateCommand()) { namingConfigCmd.Transaction = tran; - namingConfigCmd.CommandText = @"SELECT * FROM Config WHERE [Key] = 'SeasonFolderFormat'"; + namingConfigCmd.CommandText = @"SELECT * FROM Config WHERE [Key] = 'seasonfolderformat'"; using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) { while (namingConfigReader.Read()) @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Datastore.Migration { var text = String.Format("UPDATE Config " + "SET [VALUE] = '{0}'" + - "WHERE [Key] = 'SeasonFolderFormat'", + "WHERE [Key] = 'seasonfolderformat'", value); updateCmd.Transaction = tran; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index f88be54d4..3985392e1 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -186,7 +186,7 @@ - + From 4cfb2f271d9ce1e81a0df59aeebcbbb5627fcbaf Mon Sep 17 00:00:00 2001 From: kayone Date: Wed, 13 Nov 2013 18:02:27 -0800 Subject: [PATCH 07/21] minor cleanup. --- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Organizer/FileNameBuilder.cs | 87 +++---------------- .../FilenameBuilderTokenEqualityComparer.cs | 9 +- .../Organizer/NamingConfigService.cs | 38 ++++++++ 4 files changed, 59 insertions(+), 76 deletions(-) create mode 100644 src/NzbDrone.Core/Organizer/NamingConfigService.cs diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 3985392e1..7f852ed5d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -327,6 +327,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 492573da1..51076b228 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Text.RegularExpressions; using NLog; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; @@ -18,39 +17,7 @@ namespace NzbDrone.Core.Organizer string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); } - public interface INamingConfigService - { - NamingConfig GetConfig(); - NamingConfig Save(NamingConfig namingConfig); - } - public class NamingConfigService : INamingConfigService - { - private readonly IBasicRepository _repository; - - public NamingConfigService(IBasicRepository repository) - { - _repository = repository; - } - - public NamingConfig GetConfig() - { - var config = _repository.SingleOrDefault(); - - if (config == null) - { - _repository.Insert(NamingConfig.Default); - config = _repository.Single(); - } - - return config; - } - - public NamingConfig Save(NamingConfig namingConfig) - { - return _repository.Upsert(namingConfig); - } - } public class FileNameBuilder : IBuildFileNames { @@ -84,9 +51,9 @@ namespace NzbDrone.Core.Organizer return BuildFilename(episodes, series, episodeFile, nameSpec); } - public string BuildFilename(IList episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) + public string BuildFilename(IList episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig) { - if (!nameSpec.RenameEpisodes) + if (!namingConfig.RenameEpisodes) { if (String.IsNullOrWhiteSpace(episodeFile.SceneName)) { @@ -96,29 +63,31 @@ namespace NzbDrone.Core.Organizer return episodeFile.SceneName; } - if (String.IsNullOrWhiteSpace(nameSpec.StandardEpisodeFormat) && series.SeriesType == SeriesTypes.Standard) + if (String.IsNullOrWhiteSpace(namingConfig.StandardEpisodeFormat) && series.SeriesType == SeriesTypes.Standard) { throw new NamingFormatException("Standard episode format cannot be null"); } - if (String.IsNullOrWhiteSpace(nameSpec.DailyEpisodeFormat) && series.SeriesType == SeriesTypes.Daily) + if (String.IsNullOrWhiteSpace(namingConfig.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 pattern = namingConfig.StandardEpisodeFormat; var episodeTitles = new List { Parser.Parser.CleanupEpisodeTitle(sortedEpisodes.First().Title) }; - var tokenValues = new Dictionary(new FilenameBuilderTokenEqualityComparer()); - tokenValues.Add("{Series Title}", series.Title); + var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance) + { + {"{Series Title}", series.Title} + }; if (series.SeriesType == SeriesTypes.Daily) { - pattern = nameSpec.DailyEpisodeFormat; + pattern = namingConfig.DailyEpisodeFormat; if (!String.IsNullOrWhiteSpace(episodes.First().AirDate)) { @@ -146,7 +115,7 @@ namespace NzbDrone.Core.Organizer foreach (var episode in sortedEpisodes.Skip(1)) { - switch ((MultiEpisodeStyle)nameSpec.MultiEpisodeStyle) + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) { case MultiEpisodeStyle.Duplicate: seasonEpisodePattern += episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; @@ -194,7 +163,7 @@ namespace NzbDrone.Core.Organizer else { - var tokenValues = new Dictionary(new FilenameBuilderTokenEqualityComparer()); + var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); tokenValues.Add("{Series Title}", series.Title); seasonFolder = ReplaceSeasonTokens(_configService.SeasonFolderFormat, seasonNumber); @@ -275,38 +244,6 @@ namespace NzbDrone.Core.Organizer return value.ToString().PadLeft(zeroCount + 1, '0'); } - private static readonly List MultiEpisodeStyles = new List - { - new EpisodeSortingType - { - Id = 0, - Name = "Extend", - Pattern = "-%0e" - }, - new EpisodeSortingType - { - Id = 1, - Name = "Duplicate", - Pattern = "%p%0s%x%0e" - }, - new EpisodeSortingType - { - Id = 2, - Name = "Repeat", - Pattern = "%x%0e" - }, - new EpisodeSortingType - { - Id = 3, - Name = "Scene", - Pattern = "-%x%0e" - } - }; - - private static EpisodeSortingType GetMultiEpisodeStyle(int id) - { - return MultiEpisodeStyles.Single(s => s.Id == id); - } } public enum MultiEpisodeStyle diff --git a/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs b/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs index a8ba17a23..16b225131 100644 --- a/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs +++ b/src/NzbDrone.Core/Organizer/FilenameBuilderTokenEqualityComparer.cs @@ -6,8 +6,15 @@ namespace NzbDrone.Core.Organizer { public class FilenameBuilderTokenEqualityComparer : IEqualityComparer { + public static readonly FilenameBuilderTokenEqualityComparer Instance = new FilenameBuilderTokenEqualityComparer(); + private static readonly Regex SimpleTokenRegex = new Regex(@"\s|_|\W", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private FilenameBuilderTokenEqualityComparer() + { + + } + public bool Equals(String s1, String s2) { return SimplifyToken(s1).Equals(SimplifyToken(s2)); @@ -18,7 +25,7 @@ namespace NzbDrone.Core.Organizer return SimplifyToken(str).GetHashCode(); } - private string SimplifyToken(string token) + private static string SimplifyToken(string token) { return SimpleTokenRegex.Replace(token, String.Empty).ToLower(); } diff --git a/src/NzbDrone.Core/Organizer/NamingConfigService.cs b/src/NzbDrone.Core/Organizer/NamingConfigService.cs new file mode 100644 index 000000000..113ef030a --- /dev/null +++ b/src/NzbDrone.Core/Organizer/NamingConfigService.cs @@ -0,0 +1,38 @@ +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Organizer +{ + public interface INamingConfigService + { + NamingConfig GetConfig(); + void Save(NamingConfig namingConfig); + } + + public class NamingConfigService : INamingConfigService + { + private readonly IBasicRepository _repository; + + public NamingConfigService(IBasicRepository repository) + { + _repository = repository; + } + + public NamingConfig GetConfig() + { + var config = _repository.SingleOrDefault(); + + if (config == null) + { + _repository.Insert(NamingConfig.Default); + config = _repository.Single(); + } + + return config; + } + + public void Save(NamingConfig namingConfig) + { + _repository.Upsert(namingConfig); + } + } +} \ No newline at end of file From 41cb5c02e8a8090d36175220253e1ec6eb1c2f67 Mon Sep 17 00:00:00 2001 From: kayone Date: Wed, 13 Nov 2013 18:08:20 -0800 Subject: [PATCH 08/21] added tests for when patter doesn't match our 'expected casing' --- .../OrganizerTests/GetNewFilenameFixture.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs index 1a94f38a2..44efa406c 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs @@ -102,6 +102,15 @@ namespace NzbDrone.Core.Test.OrganizerTests .Should().Be("SOUTH PARK"); } + [Test] + public void should_replace_SERIES_TITLE_with_random_casing_should_keep_original_casing() + { + _namingConfig.StandardEpisodeFormat = "{sErIES-tItLE}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be(_series.Title.Replace(' ', '-')); + } + [Test] public void should_replace_series_title_with_all_lower_case() { @@ -111,6 +120,8 @@ namespace NzbDrone.Core.Test.OrganizerTests .Should().Be("south park"); } + + [Test] public void should_replace_episode_title() { @@ -120,6 +131,15 @@ namespace NzbDrone.Core.Test.OrganizerTests .Should().Be("City Sushi"); } + [Test] + public void should_replace_episode_title_if_pattern_has_random_casing() + { + _namingConfig.StandardEpisodeFormat = "{ePisOde-TitLe}"; + + Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) + .Should().Be("City-Sushi"); + } + [Test] public void should_replace_season_number_with_single_digit() { From 48ece3d36771d585581d4bbb1bba008aaa8237d3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 14 Nov 2013 20:40:56 -0800 Subject: [PATCH 09/21] Using season:00 instead of 0season --- .../OrganizerTests/BuildFilePathFixture.cs | 4 ++-- .../OrganizerTests/GetNewFilenameFixture.cs | 24 +++++++++---------- .../029_add_formats_to_naming_config.cs | 8 +++---- .../030_update_season_folder_format.cs | 4 ++-- .../Organizer/FileNameBuilder.cs | 13 +++++----- .../NamingConfigTests.cs | 2 +- .../Naming/Partials/EpisodeNamingPartial.html | 2 +- .../Naming/Partials/SeasonNamingPartial.html | 2 +- .../Naming/Wizard/NamingWizardView.js | 8 +++---- 9 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs index 8013ebc07..2bc2b2a49 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs @@ -26,9 +26,9 @@ namespace NzbDrone.Core.Test.OrganizerTests } [Test] - [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "Season {0season}", @"C:\Test\30 Rock\Season 01\30 Rock - S01E05 - Episode Title.mkv")] + [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "Season {season:00}", @"C:\Test\30 Rock\Season 01\30 Rock - S01E05 - Episode Title.mkv")] [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "Season {season}", @"C:\Test\30 Rock\Season 1\30 Rock - S01E05 - Episode Title.mkv")] - [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {0season}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] + [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {season:00}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {season}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "ReallyUglySeasonFolder {season}", @"C:\Test\30 Rock\ReallyUglySeasonFolder 1\30 Rock - S01E05 - Episode Title.mkv")] [TestCase("30 Rock - S00E05 - Episode Title", 0, true, "Season {season}", @"C:\Test\30 Rock\Specials\30 Rock - S00E05 - Episode Title.mkv")] diff --git a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs index 44efa406c..373a4aa3c 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/GetNewFilenameFixture.cs @@ -120,8 +120,6 @@ namespace NzbDrone.Core.Test.OrganizerTests .Should().Be("south park"); } - - [Test] public void should_replace_episode_title() { @@ -151,10 +149,10 @@ namespace NzbDrone.Core.Test.OrganizerTests } [Test] - public void should_replace_0season_number_with_two_digits() + public void should_replace_season00_number_with_two_digits() { _episode1.SeasonNumber = 1; - _namingConfig.StandardEpisodeFormat = "{0season}x{episode}"; + _namingConfig.StandardEpisodeFormat = "{season:00}x{episode}"; Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) .Should().Be("01x6"); @@ -171,10 +169,10 @@ namespace NzbDrone.Core.Test.OrganizerTests } [Test] - public void should_replace_0episode_number_with_two_digits() + public void should_replace_episode00_number_with_two_digits() { _episode1.SeasonNumber = 1; - _namingConfig.StandardEpisodeFormat = "{season}x{0episode}"; + _namingConfig.StandardEpisodeFormat = "{season}x{episode:00}"; Subject.BuildFilename(new List { _episode1 }, _series, _episodeFile) .Should().Be("1x06"); @@ -202,7 +200,7 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_replace_all_contents_in_pattern() { - _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title} [{Quality Title}]"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} [{Quality Title}]"; Subject.BuildFilename(new List {_episode1}, _series, _episodeFile) .Should().Be("South Park - S15E06 - City Sushi [HDTV-720p]"); @@ -232,7 +230,7 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_only_have_one_episodeTitle_when_episode_titles_are_the_same() { - _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 3; var episode = Builder.CreateNew() @@ -255,7 +253,7 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_have_two_episodeTitles_when_episode_titles_are_not_the_same() { - _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 3; _episode1.Title = "Hello"; @@ -298,7 +296,7 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_format_extend_multi_episode_properly() { - _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 0; Subject.BuildFilename(new List {_episode1, _episode2}, _series, _episodeFile) @@ -308,7 +306,7 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_format_duplicate_multi_episode_properly() { - _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 1; Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) @@ -318,7 +316,7 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_format_repeat_multi_episode_properly() { - _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 2; Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) @@ -328,7 +326,7 @@ namespace NzbDrone.Core.Test.OrganizerTests [Test] public void should_format_scene_multi_episode_properly() { - _namingConfig.StandardEpisodeFormat = "{Series Title} - S{0season}E{0episode} - {Episode Title}"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title}"; _namingConfig.MultiEpisodeStyle = 3; Subject.BuildFilename(new List { _episode1, _episode2 }, _series, _episodeFile) 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 b723dca46..f3bbba37f 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 @@ -103,7 +103,7 @@ namespace NzbDrone.Core.Datastore.Migration { Id = 0, Name = "1x05", - Pattern = "{season}x{0episode}", + Pattern = "{season}x{episode:00}", EpisodeSeparator = "x" }, @@ -111,21 +111,21 @@ namespace NzbDrone.Core.Datastore.Migration { Id = 1, Name = "01x05", - Pattern = "{0season}x{0episode}", + Pattern = "{season:00}x{episode:00}", EpisodeSeparator = "x" }, new { Id = 2, Name = "S01E05", - Pattern = "S{0season}E{0episode}", + Pattern = "S{season:00}E{episode:00}", EpisodeSeparator = "E" }, new { Id = 3, Name = "s01e05", - Pattern = "s{0season}e{0episode}", + Pattern = "s{season:00}e{episode:00}", EpisodeSeparator = "e" } }; diff --git a/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs b/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs index 6b57fd00c..133e5146a 100644 --- a/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs +++ b/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs @@ -28,9 +28,9 @@ namespace NzbDrone.Core.Datastore.Migration value = value.Replace("%sn", "{Series Title}") .Replace("%s.n", "{Series.Title}") .Replace("%s", "{season}") - .Replace("%0s", "{0season}") + .Replace("%0s", "{season:00}") .Replace("%e", "{episode}") - .Replace("%0e", "{0episode}"); + .Replace("%0e", "{episode:00}"); using (IDbCommand updateCmd = conn.CreateCommand()) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 51076b228..e94d7b0ac 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -28,13 +28,13 @@ namespace NzbDrone.Core.Organizer private static readonly Regex TitleRegex = new Regex(@"(?\{(?:\w+)(?\s|\W|_)\w+\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex EpisodeRegex = new Regex(@"(?\{0*(?:episode)})", + private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonRegex = new Regex(@"(?\{0*(?:season)})", + private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=}).+?)?(?s?{0?season}(?e|x)?(?{0?episode}))(?.+?(?={))?", + private static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=}).+?)?(?s?{season(?:\:0+)?}(?e|x)?(?{episode(?:\:0+)?}))(?.+?(?={))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); public FileNameBuilder(INamingConfigService namingConfigService, IConfigService configService, Logger logger) @@ -240,10 +240,11 @@ namespace NzbDrone.Core.Organizer private string ReplaceNumberToken(string token, int value) { - var zeroCount = token.Count(z => z == '0'); - return value.ToString().PadLeft(zeroCount + 1, '0'); - } + var split = token.Trim('{', '}').Split(':'); + if (split.Length == 1) return value.ToString("0"); + return value.ToString(split[1]); + } } public enum MultiEpisodeStyle diff --git a/src/NzbDrone.Integration.Test/NamingConfigTests.cs b/src/NzbDrone.Integration.Test/NamingConfigTests.cs index 5006809b6..e2280cbb5 100644 --- a/src/NzbDrone.Integration.Test/NamingConfigTests.cs +++ b/src/NzbDrone.Integration.Test/NamingConfigTests.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Integration.Test { var config = NamingConfig.GetSingle(); config.RenameEpisodes = false; - config.StandardEpisodeFormat = "{Series Title} - {season}x{0episode} - {Episode Title}"; + config.StandardEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}"; config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}"; var result = NamingConfig.Put(config); diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.html index c56795719..4c20f4ffa 100644 --- a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.html +++ b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.html @@ -2,6 +2,6 @@ Episode diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.html b/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.html index 1174a055d..2c56024da 100644 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.html +++ b/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.html @@ -2,6 +2,6 @@ Season diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js index 8a6dc9d3e..d98bfb6db 100644 --- a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js +++ b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js @@ -84,16 +84,16 @@ define( switch (this.model.get('numberStyle')) { case '0': - this.standardEpisodeFormat += '{season}x{0episode}'; + this.standardEpisodeFormat += '{season}x{episode:00}'; break; case '1': - this.standardEpisodeFormat += '{0season}x{0episode}'; + this.standardEpisodeFormat += '{season:00}x{episode:00}'; break; case '2': - this.standardEpisodeFormat += 'S{0season}E{0episode}'; + this.standardEpisodeFormat += 'S{season:00}E{episode:00}'; break; case '3': - this.standardEpisodeFormat += 's{0season}e{0episode}'; + this.standardEpisodeFormat += 's{season:00}e{episode:00}'; break; default: this.standardEpisodeFormat += 'Unknown Number Pattern'; From 9d5c1aa0a45730d1fe987c8990f37aef65505e86 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 15 Nov 2013 00:53:12 -0800 Subject: [PATCH 10/21] Validate that we can parse the chosen scheme before saving --- src/NzbDrone.Api/Config/NamingModule.cs | 124 +++++++++++++++++- .../ParserTests/ParserFixture.cs | 2 + src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Organizer/FileNameBuilder.cs | 10 +- .../Organizer/FileNameValidation.cs | 43 ++++++ src/NzbDrone.Core/Parser/Parser.cs | 19 +-- src/UI/Mixins/AsValidatedView.js | 2 - .../MediaManagement/Naming/NamingView.js | 10 +- 8 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 src/NzbDrone.Core/Organizer/FileNameValidation.cs diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index 20ac48270..a6b5881aa 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingModule.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentValidation; +using FluentValidation.Results; using Nancy.Responses; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Organizer; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using Nancy.ModelBinding; @@ -29,13 +33,16 @@ 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(); + SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); + SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); } private void UpdateNamingConfig(NamingConfigResource resource) { - _namingConfigService.Save(resource.InjectTo()); + var nameSpec = resource.InjectTo(); + ValidateFormatResult(nameSpec); + + _namingConfigService.Save(nameSpec); } private NamingConfigResource GetNamingConfig() @@ -108,6 +115,7 @@ namespace NzbDrone.Api.Config { try { + //TODO: Validate the result is parsable return _buildFileNames.BuildFilename(episodes, series, episodeFile, @@ -116,8 +124,118 @@ namespace NzbDrone.Api.Config catch (NamingFormatException ex) { //Catching to avoid blowing up all samples + //TODO: Use validation to report error to client + return String.Empty; } } + + private void ValidateFormatResult(NamingConfig nameSpec) + { + if (!nameSpec.RenameEpisodes) + { + return; + } + + var series = new Core.Tv.Series + { + SeriesType = SeriesTypes.Standard, + Title = "Series Title" + }; + + var episode1 = new Episode + { + SeasonNumber = 1, + EpisodeNumber = 1, + Title = "Episode Title (1)", + AirDate = "2013-10-30" + }; + + var episode2 = new Episode + { + SeasonNumber = 1, + EpisodeNumber = 2, + Title = "Episode Title (2)", + AirDate = "2013-10-30" + }; + + var episodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.HDTV720p) + }; + + if (!ValidateStandardFormat(nameSpec, series, new List { episode1 }, episodeFile)) + { + throw new ValidationException(new List + { + new ValidationFailure("StandardEpisodeFormat", "Results in unparsable filenames") + }.ToArray()); + } + + if (!ValidateStandardFormat(nameSpec, series, new List { episode1, episode2 }, episodeFile)) + { + throw new ValidationException(new List + { + new ValidationFailure("StandardEpisodeFormat", "Results in unparsable multi-episode filenames") + }.ToArray()); + } + + if (!ValidateDailyFormat(nameSpec, series, episode1, episodeFile)) + { + throw new ValidationException(new List + { + new ValidationFailure("DailyEpisodeFormat", "Results in unparsable filenames") + }.ToArray()); + } + } + + private bool ValidateStandardFormat(NamingConfig nameSpec, Core.Tv.Series series, List episodes, EpisodeFile episodeFile) + { + var filename = _buildFileNames.BuildFilename(episodes, series, episodeFile, nameSpec); + var parsedEpisodeInfo = Parser.ParseTitle(filename); + + if (parsedEpisodeInfo == null) + { + return false; + } + + return ValidateSeasonAndEpisodeNumbers(episodes, parsedEpisodeInfo); + } + + private bool ValidateDailyFormat(NamingConfig nameSpec, Core.Tv.Series series, Episode episode, EpisodeFile episodeFile) + { + series.SeriesType = SeriesTypes.Daily; + + var filename = _buildFileNames.BuildFilename(new List { episode }, series, episodeFile, nameSpec); + var parsedEpisodeInfo = Parser.ParseTitle(filename); + + if (parsedEpisodeInfo == null) + { + return false; + } + + if (parsedEpisodeInfo.IsDaily()) + { + if (!parsedEpisodeInfo.AirDate.Equals(episode.AirDate)) + { + return false; + } + + return true; + } + + return ValidateSeasonAndEpisodeNumbers(new List {episode}, parsedEpisodeInfo); + } + + private bool ValidateSeasonAndEpisodeNumbers(List episodes, ParsedEpisodeInfo parsedEpisodeInfo) + { + if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber || + !parsedEpisodeInfo.EpisodeNumbers.OrderBy(e => e).SequenceEqual(episodes.Select(e => e.EpisodeNumber).OrderBy(e => e))) + { + return false; + } + + return true; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index f9d6d1ac3..e68ac3ca9 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -84,6 +84,8 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("House.Hunters.International.S05E607.720p.hdtv.x264", "House.Hunters.International", 5, 607)] [TestCase("Adventure.Time.With.Finn.And.Jake.S01E20.720p.BluRay.x264-DEiMOS", "Adventure.Time.With.Finn.And.Jake", 1, 20)] [TestCase("Hostages.S01E04.2-45.PM.[HDTV-720p].mkv", "Hostages", 1, 4)] + [TestCase("S01E04", "", 1, 4)] + [TestCase("1x04", "", 1, 4)] public void ParseTitle_single(string postTitle, string title, int seasonNumber, int episodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 7f852ed5d..a066a603a 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -327,6 +327,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index e94d7b0ac..22e484a8d 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -17,8 +17,6 @@ namespace NzbDrone.Core.Organizer string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); } - - public class FileNameBuilder : IBuildFileNames { private readonly IConfigService _configService; @@ -34,9 +32,11 @@ namespace NzbDrone.Core.Organizer private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=}).+?)?(?s?{season(?:\:0+)?}(?e|x)?(?{episode(?:\:0+)?}))(?.+?(?={))?", + public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=}).+?)?(?s?{season(?:\:0+)?}(?e|x)?(?{episode(?:\:0+)?}))(?.+?(?={))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public FileNameBuilder(INamingConfigService namingConfigService, IConfigService configService, Logger logger) { _namingConfigService = namingConfigService; @@ -201,12 +201,12 @@ namespace NzbDrone.Core.Organizer var patternTokenArray = token.ToCharArray(); if (!tokenValues.TryGetValue(token, out replacementText)) return null; - if (patternTokenArray.All(t => !char.IsLetter(t) || char.IsLower(t))) + if (patternTokenArray.All(t => !Char.IsLetter(t) || Char.IsLower(t))) { replacementText = replacementText.ToLowerInvariant(); } - else if (patternTokenArray.All(t => !char.IsLetter(t) || char.IsUpper(t))) + else if (patternTokenArray.All(t => !Char.IsLetter(t) || Char.IsUpper(t))) { replacementText = replacementText.ToUpper(); } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs new file mode 100644 index 000000000..0eceb9564 --- /dev/null +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -0,0 +1,43 @@ +using System; +using FluentValidation; +using FluentValidation.Validators; + +namespace NzbDrone.Core.Organizer +{ + public static class FileNameValidation + { + public static IRuleBuilderOptions ValidEpisodeFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.SeasonEpisodePatternRegex)).WithMessage("Must contain season and episode numbers"); + } + + public static IRuleBuilderOptions ValidDailyEpisodeFormat(this IRuleBuilder ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new ValidDailyEpisodeFormatValidator()); + } + } + + public class ValidDailyEpisodeFormatValidator : PropertyValidator + { + public ValidDailyEpisodeFormatValidator() + : base("Must contain Air Date or Season and Episode") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var value = context.PropertyValue as String; + + if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) && + !FileNameBuilder.AirDateRegex.IsMatch(value)) + { + return false; + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index ef62d9034..e771db42f 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -18,7 +18,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex[] ReportTitleRegex = new[] { //Episodes with airdate - new Regex(@"^(?.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Anime - Absolute Episode Number + Title + Season+Episode @@ -38,15 +38,15 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Multi-Part episodes without a title (S01E05.S01E06) - new Regex(@"^(?:\W*S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}(\W+|_|$)(?!\\)", + new Regex(@"^(?:\W*S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Multi-episode Repeated (S01E05 - S01E06, 1x05 - 1x06, etc) - new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]){1,2}(?<episode>\d{1,3}(?!\d+)))+){2,}", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) - new Regex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+)(\W+|_|$)(?!\\)", + new Regex(@"^(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+)))+)(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with single digit episode number (S01E1, S01E5E6, etc) @@ -66,11 +66,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Supports 103/113 naming - new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>\d{2}(?!p|i|\d+)))+(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.+?)?(?:\W?(?<season>(?<!\d+)\d{1})(?<episode>\d{2}(?!\w|\d+)))+", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Mini-Series, treated as season 1, episodes are labelled as Part01, Part 01, Part.1 - new Regex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)(\W+|_|$)(?!\\)", + new Regex(@"^(?<title>.+?)(?:\W+(?:(?:Part\W?|(?<!\d+\W+)e)(?<episode>\d{1,2}(?!\d+)))+)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Supports 1103/1113 naming @@ -96,7 +96,7 @@ namespace NzbDrone.Core.Parser //Anime - Title Absolute Episode Number new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+e(?<absoluteepisode>\d{2,3}))+", - RegexOptions.IgnoreCase | RegexOptions.Compiled), + RegexOptions.IgnoreCase | RegexOptions.Compiled) }; private static readonly Regex NormalizeRegex = new Regex(@"((^|\W|_)(a|an|the|and|or|of)($|\W|_))|\W|_|(?:(?<=[^0-9]+)|\b)(?!(?:19\d{2}|20\d{2}))\d+(?=[^0-9ip]+|\b)", @@ -144,6 +144,7 @@ namespace NzbDrone.Core.Parser foreach (var regex in ReportTitleRegex) { + var regexString = regex.ToString(); var match = regex.Matches(simpleTitle); if (match.Count != 0) @@ -435,7 +436,7 @@ namespace NzbDrone.Core.Parser return false; } - if (!title.Any(Char.IsLetterOrDigit) || (!title.Any(Char.IsPunctuation) && !title.Any(Char.IsWhiteSpace))) + if (!title.Any(Char.IsLetterOrDigit)) { return false; } diff --git a/src/UI/Mixins/AsValidatedView.js b/src/UI/Mixins/AsValidatedView.js index cf4ef661e..952c1da69 100644 --- a/src/UI/Mixins/AsValidatedView.js +++ b/src/UI/Mixins/AsValidatedView.js @@ -23,7 +23,6 @@ define( } }; - var validatedSync = function (method, model,options) { this.$el.removeAllErrors(); arguments[2].isValidatedCall = true; @@ -52,7 +51,6 @@ define( } }; - this.prototype.onBeforeClose = function () { if (this.model) { diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 27e5d5945..f6355e24b 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -5,8 +5,9 @@ define( 'vent', 'marionette', 'Settings/MediaManagement/Naming/NamingSampleModel', - 'Mixins/AsModelBoundView' - ], function ($, vent, Marionette, NamingSampleModel, AsModelBoundView) { + 'Mixins/AsModelBoundView', + 'Mixins/AsValidatedView' + ], function ($, vent, Marionette, NamingSampleModel, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ template: 'Settings/MediaManagement/Naming/NamingViewTemplate', @@ -86,5 +87,8 @@ define( } }); - return AsModelBoundView.call(view); + AsModelBoundView.call(view); + AsValidatedView.call(view); + + return view; }); From 2e694485fea026db7a50801f6d9f9c5991f296c0 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Fri, 15 Nov 2013 21:03:42 -0800 Subject: [PATCH 11/21] Validation for samples and saving --- src/NzbDrone.Api/Config/NamingModule.cs | 211 ++++-------------- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 + .../Organizer/FileNameBuilder.cs | 2 +- .../Organizer/FilenameSampleService.cs | 135 +++++++++++ .../Organizer/FilenameValidationService.cs | 76 +++++++ src/NzbDrone.Core/Organizer/SampleResult.cs | 17 ++ 6 files changed, 272 insertions(+), 172 deletions(-) create mode 100644 src/NzbDrone.Core/Organizer/FilenameSampleService.cs create mode 100644 src/NzbDrone.Core/Organizer/FilenameValidationService.cs create mode 100644 src/NzbDrone.Core/Organizer/SampleResult.cs diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index a6b5881aa..b2dfb0c9f 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingModule.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using FluentValidation; using FluentValidation.Results; using Nancy.Responses; -using NzbDrone.Core.MediaFiles; +using NzbDrone.Api.REST; using NzbDrone.Core.Organizer; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; using Nancy.ModelBinding; using NzbDrone.Api.Mapping; using NzbDrone.Api.Extensions; @@ -19,13 +13,17 @@ namespace NzbDrone.Api.Config public class NamingModule : NzbDroneRestModule<NamingConfigResource> { private readonly INamingConfigService _namingConfigService; - private readonly IBuildFileNames _buildFileNames; + private readonly IFilenameSampleService _filenameSampleService; + private readonly IFilenameValidationService _filenameValidationService; - public NamingModule(INamingConfigService namingConfigService, IBuildFileNames buildFileNames) + public NamingModule(INamingConfigService namingConfigService, + IFilenameSampleService filenameSampleService, + IFilenameValidationService filenameValidationService) : base("config/naming") { _namingConfigService = namingConfigService; - _buildFileNames = buildFileNames; + _filenameSampleService = filenameSampleService; + _filenameValidationService = filenameValidationService; GetResourceSingle = GetNamingConfig; GetResourceById = GetNamingConfig; UpdateResource = UpdateNamingConfig; @@ -57,185 +55,56 @@ namespace NzbDrone.Api.Config private JsonResponse<NamingSampleResource> GetExamples(NamingConfigResource config) { + //TODO: Validate that the format is valid var nameSpec = config.InjectTo<NamingConfig>(); - - var series = new Core.Tv.Series - { - SeriesType = SeriesTypes.Standard, - Title = "Series Title" - }; - - var episode1 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 1, - Title = "Episode Title (1)", - AirDate = "2013-10-30" - }; - - var episode2 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 2, - Title = "Episode Title (2)" - }; - - var episodeFile = new EpisodeFile - { - Quality = new QualityModel(Quality.HDTV720p), - Path = @"C:\Test\Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv" - }; - var sampleResource = new NamingSampleResource(); + + var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); + var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); - sampleResource.SingleEpisodeExample = BuildSample(new List<Episode> { episode1 }, - series, - episodeFile, - nameSpec); + sampleResource.SingleEpisodeExample = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult) != null + ? "Invalid format" + : singleEpisodeSampleResult.Filename; - episodeFile.Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv"; + sampleResource.MultiEpisodeExample = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult) != null + ? "Invalid format" + : multiEpisodeSampleResult.Filename; - sampleResource.MultiEpisodeExample = BuildSample(new List<Episode> { 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<Episode> { episode1 }, - series, - episodeFile, - nameSpec); + sampleResource.DailyEpisodeExample = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult) != null + ? "Invalid format" + : dailyEpisodeSampleResult.Filename; return sampleResource.AsResponse(); } - private string BuildSample(List<Episode> episodes, Core.Tv.Series series, EpisodeFile episodeFile, NamingConfig nameSpec) - { - try - { - //TODO: Validate the result is parsable - return _buildFileNames.BuildFilename(episodes, - series, - episodeFile, - nameSpec); - } - catch (NamingFormatException ex) - { - //Catching to avoid blowing up all samples - //TODO: Use validation to report error to client - - return String.Empty; - } - } - private void ValidateFormatResult(NamingConfig nameSpec) { - if (!nameSpec.RenameEpisodes) + var singleEpisodeSampleResult = _filenameSampleService.GetStandardSample(nameSpec); + var multiEpisodeSampleResult = _filenameSampleService.GetMultiEpisodeSample(nameSpec); + var dailyEpisodeSampleResult = _filenameSampleService.GetDailySample(nameSpec); + var singleEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(singleEpisodeSampleResult); + var multiEpisodeValidationResult = _filenameValidationService.ValidateStandardFilename(multiEpisodeSampleResult); + var dailyEpisodeValidationResult = _filenameValidationService.ValidateDailyFilename(dailyEpisodeSampleResult); + + var validationFailures = new List<ValidationFailure>(); + + if (singleEpisodeValidationResult != null) { - return; + validationFailures.Add(singleEpisodeValidationResult); } - var series = new Core.Tv.Series + if (multiEpisodeValidationResult != null) { - SeriesType = SeriesTypes.Standard, - Title = "Series Title" - }; - - var episode1 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 1, - Title = "Episode Title (1)", - AirDate = "2013-10-30" - }; - - var episode2 = new Episode - { - SeasonNumber = 1, - EpisodeNumber = 2, - Title = "Episode Title (2)", - AirDate = "2013-10-30" - }; - - var episodeFile = new EpisodeFile - { - Quality = new QualityModel(Quality.HDTV720p) - }; - - if (!ValidateStandardFormat(nameSpec, series, new List<Episode> { episode1 }, episodeFile)) - { - throw new ValidationException(new List<ValidationFailure> - { - new ValidationFailure("StandardEpisodeFormat", "Results in unparsable filenames") - }.ToArray()); + validationFailures.Add(multiEpisodeValidationResult); } - if (!ValidateStandardFormat(nameSpec, series, new List<Episode> { episode1, episode2 }, episodeFile)) + if (dailyEpisodeValidationResult != null) { - throw new ValidationException(new List<ValidationFailure> - { - new ValidationFailure("StandardEpisodeFormat", "Results in unparsable multi-episode filenames") - }.ToArray()); + validationFailures.Add(dailyEpisodeValidationResult); } - if (!ValidateDailyFormat(nameSpec, series, episode1, episodeFile)) - { - throw new ValidationException(new List<ValidationFailure> - { - new ValidationFailure("DailyEpisodeFormat", "Results in unparsable filenames") - }.ToArray()); - } - } - - private bool ValidateStandardFormat(NamingConfig nameSpec, Core.Tv.Series series, List<Episode> episodes, EpisodeFile episodeFile) - { - var filename = _buildFileNames.BuildFilename(episodes, series, episodeFile, nameSpec); - var parsedEpisodeInfo = Parser.ParseTitle(filename); - - if (parsedEpisodeInfo == null) - { - return false; - } - - return ValidateSeasonAndEpisodeNumbers(episodes, parsedEpisodeInfo); - } - - private bool ValidateDailyFormat(NamingConfig nameSpec, Core.Tv.Series series, Episode episode, EpisodeFile episodeFile) - { - series.SeriesType = SeriesTypes.Daily; - - var filename = _buildFileNames.BuildFilename(new List<Episode> { episode }, series, episodeFile, nameSpec); - var parsedEpisodeInfo = Parser.ParseTitle(filename); - - if (parsedEpisodeInfo == null) - { - return false; - } - - if (parsedEpisodeInfo.IsDaily()) - { - if (!parsedEpisodeInfo.AirDate.Equals(episode.AirDate)) - { - return false; - } - - return true; - } - - return ValidateSeasonAndEpisodeNumbers(new List<Episode> {episode}, parsedEpisodeInfo); - } - - private bool ValidateSeasonAndEpisodeNumbers(List<Episode> episodes, ParsedEpisodeInfo parsedEpisodeInfo) - { - if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber || - !parsedEpisodeInfo.EpisodeNumbers.OrderBy(e => e).SequenceEqual(episodes.Select(e => e.EpisodeNumber).OrderBy(e => e))) - { - return false; - } - - return true; + throw new ValidationException(validationFailures.ToArray()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index a066a603a..8e32b557d 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -324,11 +324,14 @@ <Compile Include="Notifications\Xbmc\Model\VersionResult.cs" /> <Compile Include="Notifications\Xbmc\Model\XbmcJsonResult.cs" /> <Compile Include="Notifications\Xbmc\Model\XbmcVersion.cs" /> + <Compile Include="Organizer\FilenameValidationService.cs" /> <Compile Include="Organizer\EpisodeFormat.cs" /> <Compile Include="Organizer\Exception.cs" /> <Compile Include="Organizer\FilenameBuilderTokenEqualityComparer.cs" /> <Compile Include="Organizer\FileNameValidation.cs" /> <Compile Include="Organizer\NamingConfigService.cs" /> + <Compile Include="Organizer\FilenameSampleService.cs" /> + <Compile Include="Organizer\SampleResult.cs" /> <Compile Include="Parser\InvalidDateException.cs" /> <Compile Include="Parser\Model\SeriesTitleInfo.cs" /> <Compile Include="ProgressMessaging\CommandUpdatedEvent.cs" /> diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 22e484a8d..eb38719d2 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Organizer private static readonly Regex SeasonRegex = new Regex(@"(?<season>\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?<separator>(?<=}).+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>e|x)?(?<episode>{episode(?:\:0+)?}))(?<separator>.+?(?={))?", + public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?<separator>(?<=}).+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>e|x)(?<episode>{episode(?:\:0+)?}))(?<separator>.+?(?={))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); diff --git a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs new file mode 100644 index 000000000..2ea499b9b --- /dev/null +++ b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public interface IFilenameSampleService + { + SampleResult GetStandardSample(NamingConfig nameSpec); + SampleResult GetMultiEpisodeSample(NamingConfig nameSpec); + SampleResult GetDailySample(NamingConfig nameSpec); + } + + public class FilenameSampleService : IFilenameSampleService + { + private readonly IBuildFileNames _buildFileNames; + private static Series _standardSeries; + private static Series _dailySeries; + private static Episode _episode1; + private static Episode _episode2; + private static List<Episode> _singleEpisode; + private static List<Episode> _multiEpisodes; + private static EpisodeFile _singleEpisodeFile; + private static EpisodeFile _multiEpisodeFile; + private static EpisodeFile _dailyEpisodeFile; + + public FilenameSampleService(IBuildFileNames buildFileNames) + { + _buildFileNames = buildFileNames; + _standardSeries = new Series + { + SeriesType = SeriesTypes.Standard, + Title = "Series Title" + }; + + _dailySeries = new Series + { + SeriesType = SeriesTypes.Daily, + Title = "Series Title" + }; + + _episode1 = new Episode + { + SeasonNumber = 1, + EpisodeNumber = 1, + Title = "Episode Title (1)", + AirDate = "2013-10-30" + }; + + _episode2 = new Episode + { + SeasonNumber = 1, + EpisodeNumber = 2, + Title = "Episode Title (2)" + }; + + _singleEpisode = new List<Episode> { _episode1 }; + _multiEpisodes = new List<Episode> { _episode1, _episode2 }; + + _singleEpisodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.HDTV720p), + Path = @"C:\Test\Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv" + }; + + _multiEpisodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.HDTV720p), + Path = @"C:\Test\Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv" + }; + + _dailyEpisodeFile = new EpisodeFile + { + Quality = new QualityModel(Quality.HDTV720p), + Path = @"C:\Test\Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv" + }; + } + + public SampleResult GetStandardSample(NamingConfig nameSpec) + { + var result = new SampleResult + { + Filename = BuildSample(_singleEpisode, _standardSeries, _singleEpisodeFile, nameSpec), + Series = _standardSeries, + Episodes = _singleEpisode, + EpisodeFile = _singleEpisodeFile + }; + + return result; + } + + public SampleResult GetMultiEpisodeSample(NamingConfig nameSpec) + { + var result = new SampleResult + { + Filename = BuildSample(_multiEpisodes, _standardSeries, _multiEpisodeFile, nameSpec), + Series = _standardSeries, + Episodes = _multiEpisodes, + EpisodeFile = _multiEpisodeFile + }; + + return result; + } + + public SampleResult GetDailySample(NamingConfig nameSpec) + { + var result = new SampleResult + { + Filename = BuildSample(_singleEpisode, _dailySeries, _dailyEpisodeFile, nameSpec), + Series = _dailySeries, + Episodes = _singleEpisode, + EpisodeFile = _dailyEpisodeFile + }; + + return result; + } + + private string BuildSample(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) + { + try + { + return _buildFileNames.BuildFilename(episodes, series, episodeFile, nameSpec); + } + catch (NamingFormatException ex) + { + return String.Empty; + } + } + } +} diff --git a/src/NzbDrone.Core/Organizer/FilenameValidationService.cs b/src/NzbDrone.Core/Organizer/FilenameValidationService.cs new file mode 100644 index 000000000..53f64bf86 --- /dev/null +++ b/src/NzbDrone.Core/Organizer/FilenameValidationService.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation.Results; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public interface IFilenameValidationService + { + ValidationFailure ValidateStandardFilename(SampleResult sampleResult); + ValidationFailure ValidateDailyFilename(SampleResult sampleResult); + } + + public class FilenameValidationService : IFilenameValidationService + { + private const string ERROR_MESSAGE = "Produces invalid file names"; + + public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) + { + var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); + var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.Filename); + + if (parsedEpisodeInfo == null) + { + return validationFailure; + } + + if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo)) + { + return validationFailure; + } + + return null; + } + + public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) + { + var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); + var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.Filename); + + if (parsedEpisodeInfo == null) + { + return validationFailure; + } + + if (parsedEpisodeInfo.IsDaily()) + { + if (!parsedEpisodeInfo.AirDate.Equals(sampleResult.Episodes.Single().AirDate)) + { + return validationFailure; + } + + return null; + } + + if (!ValidateSeasonAndEpisodeNumbers(sampleResult.Episodes, parsedEpisodeInfo)) + { + return validationFailure; + } + + return null; + } + + private bool ValidateSeasonAndEpisodeNumbers(List<Episode> episodes, ParsedEpisodeInfo parsedEpisodeInfo) + { + if (parsedEpisodeInfo.SeasonNumber != episodes.First().SeasonNumber || + !parsedEpisodeInfo.EpisodeNumbers.OrderBy(e => e).SequenceEqual(episodes.Select(e => e.EpisodeNumber).OrderBy(e => e))) + { + return false; + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/Organizer/SampleResult.cs b/src/NzbDrone.Core/Organizer/SampleResult.cs new file mode 100644 index 000000000..928438e8c --- /dev/null +++ b/src/NzbDrone.Core/Organizer/SampleResult.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public class SampleResult + { + public string Filename { get; set; } + public Series Series { get; set; } + public List<Episode> Episodes { get; set; } + public EpisodeFile EpisodeFile { get; set; } + } +} From fbf91fe8f54dae4bbea93014148d3b135e65fa0d Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sat, 16 Nov 2013 00:54:51 -0800 Subject: [PATCH 12/21] Added naming integration tests --- .../Client/ClientBase.cs | 7 ++++ .../NamingConfigTests.cs | 40 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index a93bfad43..228ac98eb 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -87,6 +87,13 @@ namespace NzbDrone.Integration.Test.Client return Post<List<dynamic>>(request, HttpStatusCode.BadRequest); } + public List<dynamic> InvalidPut(TResource body) + { + var request = BuildRequest(); + request.AddBody(body); + return Put<List<dynamic>>(request, HttpStatusCode.BadRequest); + } + public RestRequest BuildRequest(string command = "") { var request = new RestRequest(_resource + "/" + command.Trim('/')) diff --git a/src/NzbDrone.Integration.Test/NamingConfigTests.cs b/src/NzbDrone.Integration.Test/NamingConfigTests.cs index e2280cbb5..578654a35 100644 --- a/src/NzbDrone.Integration.Test/NamingConfigTests.cs +++ b/src/NzbDrone.Integration.Test/NamingConfigTests.cs @@ -1,5 +1,7 @@ -using FluentAssertions; +using System.Net; +using FluentAssertions; using NUnit.Framework; +using NzbDrone.Api.Config; namespace NzbDrone.Integration.Test { @@ -34,5 +36,41 @@ namespace NzbDrone.Integration.Test result.StandardEpisodeFormat.Should().Be(config.StandardEpisodeFormat); result.DailyEpisodeFormat.Should().Be(config.DailyEpisodeFormat); } + + [Test] + public void should_get_bad_request_if_standard_format_is_empty() + { + var config = NamingConfig.GetSingle(); + config.RenameEpisodes = false; + config.StandardEpisodeFormat = ""; + config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}"; + + var errors = NamingConfig.InvalidPut(config); + errors.Should().NotBeEmpty(); + } + + [Test] + public void should_get_bad_request_if_standard_format_doesnt_contain_season_and_episode() + { + var config = NamingConfig.GetSingle(); + config.RenameEpisodes = false; + config.StandardEpisodeFormat = "{season}"; + config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}"; + + var errors = NamingConfig.InvalidPut(config); + errors.Should().NotBeEmpty(); + } + + [Test] + public void should_get_bad_request_if_daily_format_doesnt_contain_season_and_episode_or_air_date() + { + var config = NamingConfig.GetSingle(); + config.RenameEpisodes = false; + config.StandardEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}"; + config.DailyEpisodeFormat = "{Series Title} - {season} - {Episode Title}"; + + var errors = NamingConfig.InvalidPut(config); + errors.Should().NotBeEmpty(); + } } } \ No newline at end of file From 7a5cee5b8e488db7638ef49622512fac95ad1202 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sat, 16 Nov 2013 01:22:31 -0800 Subject: [PATCH 13/21] Fixed issue with validation when rename episodes is false --- src/NzbDrone.Api/Config/NamingModule.cs | 14 ++++++-- .../NamingConfigTests.cs | 32 +++++++++++++++++-- .../RootFolderIntegrationTest.cs | 6 ---- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index b2dfb0c9f..d72d97483 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.Linq; using FluentValidation; using FluentValidation.Results; using Nancy.Responses; @@ -31,8 +32,12 @@ namespace NzbDrone.Api.Config Get["/samples"] = x => GetExamples(this.Bind<NamingConfigResource>()); SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 3); - SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); - SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); + + SharedValidator.When(spec => spec.RenameEpisodes, () => + { + SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); + SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); + }); } private void UpdateNamingConfig(NamingConfigResource resource) @@ -104,7 +109,10 @@ namespace NzbDrone.Api.Config validationFailures.Add(dailyEpisodeValidationResult); } - throw new ValidationException(validationFailures.ToArray()); + if (validationFailures.Any()) + { + throw new ValidationException(validationFailures.ToArray()); + } } } } diff --git a/src/NzbDrone.Integration.Test/NamingConfigTests.cs b/src/NzbDrone.Integration.Test/NamingConfigTests.cs index 578654a35..eed8373ba 100644 --- a/src/NzbDrone.Integration.Test/NamingConfigTests.cs +++ b/src/NzbDrone.Integration.Test/NamingConfigTests.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Integration.Test public void should_get_bad_request_if_standard_format_is_empty() { var config = NamingConfig.GetSingle(); - config.RenameEpisodes = false; + config.RenameEpisodes = true; config.StandardEpisodeFormat = ""; config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}"; @@ -53,7 +53,7 @@ namespace NzbDrone.Integration.Test public void should_get_bad_request_if_standard_format_doesnt_contain_season_and_episode() { var config = NamingConfig.GetSingle(); - config.RenameEpisodes = false; + config.RenameEpisodes = true; config.StandardEpisodeFormat = "{season}"; config.DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title}"; @@ -65,12 +65,38 @@ namespace NzbDrone.Integration.Test public void should_get_bad_request_if_daily_format_doesnt_contain_season_and_episode_or_air_date() { var config = NamingConfig.GetSingle(); - config.RenameEpisodes = false; + config.RenameEpisodes = true; config.StandardEpisodeFormat = "{Series Title} - {season}x{episode:00} - {Episode Title}"; config.DailyEpisodeFormat = "{Series Title} - {season} - {Episode Title}"; var errors = NamingConfig.InvalidPut(config); errors.Should().NotBeEmpty(); } + + [Test] + public void should_not_require_format_when_rename_episodes_is_false() + { + var config = NamingConfig.GetSingle(); + config.RenameEpisodes = false; + config.StandardEpisodeFormat = ""; + config.DailyEpisodeFormat = ""; + + var result = NamingConfig.Put(config); + result.RenameEpisodes.Should().BeFalse(); + result.StandardEpisodeFormat.Should().Be(config.StandardEpisodeFormat); + result.DailyEpisodeFormat.Should().Be(config.DailyEpisodeFormat); + } + + [Test] + public void should_require_format_when_rename_episodes_is_true() + { + var config = NamingConfig.GetSingle(); + config.RenameEpisodes = true; + config.StandardEpisodeFormat = ""; + config.DailyEpisodeFormat = ""; + + var errors = NamingConfig.InvalidPut(config); + errors.Should().NotBeEmpty(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Integration.Test/RootFolderIntegrationTest.cs b/src/NzbDrone.Integration.Test/RootFolderIntegrationTest.cs index c7984e940..216507465 100644 --- a/src/NzbDrone.Integration.Test/RootFolderIntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/RootFolderIntegrationTest.cs @@ -8,9 +8,6 @@ namespace NzbDrone.Integration.Test [TestFixture] public class RootFolderIntegrationTest : IntegrationTest { - - - [Test] public void should_have_no_root_folder_initially() { @@ -20,7 +17,6 @@ namespace NzbDrone.Integration.Test [Test] public void should_add_and_delete_root_folders() { - ConnectSignalR(); var rootFolder = new RootFolderResource @@ -42,8 +38,6 @@ namespace NzbDrone.Integration.Test SignalRMessages.Should().Contain(c => c.Name == "rootfolder"); - - } [Test] From 9d94c4490f2de0a3a4845903303f1202ef6a5aa6 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 20 Nov 2013 17:44:32 -0800 Subject: [PATCH 14/21] Cleanup! --- .../029_add_formats_to_naming_config.cs | 39 +++++++++++++------ .../030_update_season_folder_format.cs | 4 +- src/NzbDrone.Core/Organizer/NamingConfig.cs | 4 +- .../MediaManagement/Naming/NamingView.js | 24 ++++++++---- .../Naming/Wizard/NamingWizardModel.js | 2 +- .../Naming/Wizard/NamingWizardView.js | 38 ++++++------------ .../Wizard/NamingWizardViewTemplate.html | 8 ++-- src/UI/Shared/Modal/Controller.js | 14 +++---- src/UI/vent.js | 1 + 9 files changed, 74 insertions(+), 60 deletions(-) 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 f3bbba37f..fbcd4cb2b 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 @@ -26,14 +26,21 @@ namespace NzbDrone.Core.Datastore.Migration namingConfigCmd.CommandText = @"SELECT * FROM NamingConfig LIMIT 1"; using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) { + var separatorIndex = namingConfigReader.GetOrdinal("Separator"); + var numberStyleIndex = namingConfigReader.GetOrdinal("NumberStyle"); + var includeSeriesTitleIndex = namingConfigReader.GetOrdinal("IncludeSeriesTitle"); + var includeEpisodeTitleIndex = namingConfigReader.GetOrdinal("IncludeEpisodeTitle"); + var includeQualityIndex = namingConfigReader.GetOrdinal("IncludeQuality"); + var replaceSpacesIndex = namingConfigReader.GetOrdinal("ReplaceSpaces"); + while (namingConfigReader.Read()) { - var separator = namingConfigReader.GetString(1); - var numberStyle = namingConfigReader.GetInt32(2); - var includeSeriesTitle = namingConfigReader.GetBoolean(3); - var includeEpisodeTitle = namingConfigReader.GetBoolean(5); - var includeQuality = namingConfigReader.GetBoolean(6); - var replaceSpaces = namingConfigReader.GetBoolean(7); + var separator = namingConfigReader.GetString(separatorIndex); + var numberStyle = namingConfigReader.GetInt32(numberStyleIndex); + var includeSeriesTitle = namingConfigReader.GetBoolean(includeSeriesTitleIndex); + var includeEpisodeTitle = namingConfigReader.GetBoolean(includeEpisodeTitleIndex); + var includeQuality = namingConfigReader.GetBoolean(includeQualityIndex); + var replaceSpaces = namingConfigReader.GetBoolean(replaceSpacesIndex); //Output settings var seriesTitlePattern = ""; @@ -43,21 +50,31 @@ namespace NzbDrone.Core.Datastore.Migration if (includeSeriesTitle) { - seriesTitlePattern = "{Series Title}" + separator; - if (replaceSpaces) { - seriesTitlePattern = "{Series.Title}" + separator; + seriesTitlePattern = "{Series.Title}"; } + + else + { + seriesTitlePattern = "{Series Title}"; + } + + seriesTitlePattern += separator; } if (includeEpisodeTitle) { - episodeTitlePattern = separator + "{Episode Title}"; + episodeTitlePattern = separator; if (replaceSpaces) { - episodeTitlePattern = separator + "{Episode.Title}"; + episodeTitlePattern += "{Episode.Title}"; + } + + else + { + episodeTitlePattern += "{Episode Title}"; } } diff --git a/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs b/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs index 133e5146a..b6ba5e686 100644 --- a/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs +++ b/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs @@ -21,9 +21,11 @@ namespace NzbDrone.Core.Datastore.Migration namingConfigCmd.CommandText = @"SELECT * FROM Config WHERE [Key] = 'seasonfolderformat'"; using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) { + var valueIndex = namingConfigReader.GetOrdinal("[Value]"); + while (namingConfigReader.Read()) { - var value = namingConfigReader.GetString(2); + var value = namingConfigReader.GetString(valueIndex); value = value.Replace("%sn", "{Series Title}") .Replace("%s.n", "{Series.Title}") diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 5b28999c3..a7b7c6b8b 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -12,8 +12,8 @@ namespace NzbDrone.Core.Organizer { RenameEpisodes = false, MultiEpisodeStyle = 0, - StandardEpisodeFormat = "", - DailyEpisodeFormat = "" + StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Title}", + DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Title}" }; } } diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index f6355e24b..8592aa87e 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -1,13 +1,13 @@ 'use strict'; define( [ - 'jquery', 'vent', 'marionette', 'Settings/MediaManagement/Naming/NamingSampleModel', + 'Settings/MediaManagement/Naming/Wizard/NamingWizardView', 'Mixins/AsModelBoundView', 'Mixins/AsValidatedView' - ], function ($, vent, Marionette, NamingSampleModel, AsModelBoundView, AsValidatedView) { + ], function (vent, Marionette, NamingSampleModel, NamingWizardView, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ template: 'Settings/MediaManagement/Naming/NamingViewTemplate', @@ -28,7 +28,7 @@ define( }, onRender: function () { - if (!this.model.get('renameEpisodes')) { + if (!this.model.has('renameEpisodes')) { this.ui.namingOptions.hide(); } @@ -61,6 +61,10 @@ define( }, _showWizard: function () { + var modalView = new NamingWizardView(); + vent.trigger(vent.Commands.OpenModalCommand, modalView); + this.listenTo(modalView, modalView.formatsUpdated, this._updateFormats); + vent.trigger(vent.Commands.ShowNamingWizard, { model: this.model }); }, @@ -70,20 +74,26 @@ define( var target = e.target; var token = ''; - var input = $(target).closest('.x-helper-input').children('input'); + var input = this.$(target).closest('.x-helper-input').children('input'); - if ($(target).attr('data-token')) { - token = '{{0}}'.format($(target).attr('data-token')); + if (this.$(target).attr('data-token')) { + token = '{{0}}'.format(this.$(target).attr('data-token')); } else { - token = $(target).attr('data-separator'); + token = this.$(target).attr('data-separator'); } input.val(input.val() + token); this.ui.namingTokenHelper.removeClass('open'); input.focus(); + }, + + _updateFormats: function (options) { + this.model.set('standardEpisodeFormat', options.standardEpisodeFormat); + this.model.set('dailyEpisodeFormat', options.dailyEpisodeFormat); + this.model.set('multiEpisodeStyle', options.multiEpisodeStyle); } }); diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js index e102e5cc4..6d6806538 100644 --- a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js +++ b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js @@ -10,7 +10,7 @@ define( includeQuality : true, replaceSpaces : false, separator : ' - ', - numberStyle : '2', + numberStyle : 'S{season:00}E{episode:00}', multiEpisodeStyle : 0 } }); diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js index d98bfb6db..eb48dd081 100644 --- a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js +++ b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js @@ -22,17 +22,14 @@ define( 'click .x-apply': '_applyNaming' }, - initialize: function (options) { + formatsUpdated: 'formatsUpdated', + + initialize: function () { 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(); @@ -56,9 +53,14 @@ define( }, _applyNaming: function () { - this.namingModel.set('standardEpisodeFormat', this.standardEpisodeFormat); - this.namingModel.set('dailyEpisodeFormat', this.dailyEpisodeFormat); - this.namingModel.set('multiEpisodeStyle', this.model.get('multiEpisodeStyle')); + var options = { + standardEpisodeFormat: this.standardEpisodeFormat, + dailyEpisodeFormat: this.dailyEpisodeFormat, + multiEpisodeStyle: this.model.get('multiEpisodeStyle') + }; + + this.trigger(this.formatsUpdated, options); + vent.trigger(vent.Commands.CloseModalCommand); }, @@ -82,23 +84,7 @@ define( this.dailyEpisodeFormat += this.model.get('separator'); } - switch (this.model.get('numberStyle')) { - case '0': - this.standardEpisodeFormat += '{season}x{episode:00}'; - break; - case '1': - this.standardEpisodeFormat += '{season:00}x{episode:00}'; - break; - case '2': - this.standardEpisodeFormat += 'S{season:00}E{episode:00}'; - break; - case '3': - this.standardEpisodeFormat += 's{season:00}e{episode:00}'; - break; - default: - this.standardEpisodeFormat += 'Unknown Number Pattern'; - } - + this.standardEpisodeFormat += this.model.get('numberStyle'); this.dailyEpisodeFormat += '{Air-Date}'; if (this.model.get('includeEpisodeTitle')) { diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html index 5857fb301..484ad6b5a 100644 --- a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html @@ -89,10 +89,10 @@ <div class="controls"> <select class="inputClass" name="numberStyle"> - <option value="0">1x05</option> - <option value="1">01x05</option> - <option value="2">S01E05</option> - <option value="3">s01e05</option> + <option value="{season}x{episode:00}">1x05</option> + <option value="{season:00}x{episode:00}">01x05</option> + <option value="S{season:00}E{episode:00}">S01E05</option> + <option value="s{season:00}e{episode:00}">s01e05</option> </select> </div> </div> diff --git a/src/UI/Shared/Modal/Controller.js b/src/UI/Shared/Modal/Controller.js index 7295c6e1c..199aed4e6 100644 --- a/src/UI/Shared/Modal/Controller.js +++ b/src/UI/Shared/Modal/Controller.js @@ -9,19 +9,22 @@ define( 'Episode/EpisodeDetailsLayout', 'History/Details/HistoryDetailsView', 'System/Logs/Table/Details/LogDetailsView', - 'Settings/MediaManagement/Naming/Wizard/NamingWizardView' - ], function (vent, AppLayout, Marionette, EditSeriesView, DeleteSeriesView, EpisodeDetailsLayout, HistoryDetailsView, LogDetailsView, NamingWizardView) { + ], function (vent, AppLayout, Marionette, EditSeriesView, DeleteSeriesView, EpisodeDetailsLayout, HistoryDetailsView, LogDetailsView) { return Marionette.AppRouter.extend({ initialize: function () { + vent.on(vent.Commands.OpenModalCommand, this._openModal, this); vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); vent.on(vent.Commands.EditSeriesCommand, this._editSeries, this); vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, 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); - vent.on(vent.Commands.ShowNamingWizard, this._showNamingWizard, this); + }, + + _openModal: function (view) { + AppLayout.modalRegion.show(view); }, _closeModal: function () { @@ -51,11 +54,6 @@ define( _showLogDetails: function (options) { var view = new LogDetailsView({ model: options.model }); AppLayout.modalRegion.show(view); - }, - - _showNamingWizard: function (options) { - var view = new NamingWizardView({ model: options.model }); - AppLayout.modalRegion.show(view); } }); }); diff --git a/src/UI/vent.js b/src/UI/vent.js index d1405ccd4..348e9c461 100644 --- a/src/UI/vent.js +++ b/src/UI/vent.js @@ -18,6 +18,7 @@ define( vent.Commands = { EditSeriesCommand : 'EditSeriesCommand', DeleteSeriesCommand: 'DeleteSeriesCommand', + OpenModalCommand : 'OpenModalCommand', CloseModalCommand : 'CloseModalCommand', ShowEpisodeDetails : 'ShowEpisodeDetails', ShowHistoryDetails : 'ShowHistoryDetails', From 3db97e9d11760f1efc4b1e535e06525843bd260b Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 20 Nov 2013 21:34:38 -0800 Subject: [PATCH 15/21] Moved SeasonFolderFormat to NamingConfig Moved UseSeasonFolder to UI only (add series) --- .../Config/NamingConfigResource.cs | 1 + .../Configuration/ConfigService.cs | 13 ----- .../Configuration/IConfigService.cs | 2 - ...d_season_folder_format_to_naming_config.cs | 56 +++++++++++++++++++ .../030_update_season_folder_format.cs | 54 ------------------ src/NzbDrone.Core/NzbDrone.Core.csproj | 2 +- .../Organizer/FileNameBuilder.cs | 5 +- src/NzbDrone.Core/Organizer/NamingConfig.cs | 4 +- src/NzbDrone.Core/Tv/SeriesService.cs | 2 - .../StartingSeasonSelectionPartial.html | 2 +- src/UI/AddSeries/SearchResultView.js | 20 ++++++- .../AddSeries/SearchResultViewTemplate.html | 8 +++ src/UI/AddSeries/addSeries.less | 14 ++++- src/UI/Config.js | 3 +- src/UI/Content/checkbox-button.less | 28 ++++++++++ src/UI/Content/theme.less | 1 + .../Naming/NamingViewTemplate.html | 11 ++++ .../MediaManagement/Sorting/ViewTemplate.html | 33 +---------- 18 files changed, 147 insertions(+), 112 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/030_add_season_folder_format_to_naming_config.cs delete mode 100644 src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs create mode 100644 src/UI/Content/checkbox-button.less diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index b57caa4cb..c37fffaa9 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -9,5 +9,6 @@ namespace NzbDrone.Api.Config public Int32 MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; } + public string SeasonFolderFormat { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 498731511..89f542670 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -142,19 +142,6 @@ namespace NzbDrone.Core.Configuration set { SetValue(ConfigKey.DownloadedEpisodesFolder.ToString(), value); } } - public bool UseSeasonFolder - { - get { return GetValueBoolean("UseSeasonFolder", true); } - - set { SetValue("UseSeasonFolder", value); } - } - - public string SeasonFolderFormat - { - get { return GetValue("SeasonFolderFormat", "Season {season}"); } - set { SetValue("SeasonFolderFormat", value); } - } - public bool AutoUnmonitorPreviouslyDownloadedEpisodes { get { return GetValueBoolean("AutoUnmonitorPreviouslyDownloadedEpisodes"); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 654f07f73..60f98aada 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -20,8 +20,6 @@ namespace NzbDrone.Core.Configuration SabPriorityType SabOlderTvPriority { get; set; } Boolean SabUseSsl { get; set; } String DownloadedEpisodesFolder { get; set; } - bool UseSeasonFolder { get; set; } - string SeasonFolderFormat { get; set; } bool AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } int Retention { get; set; } DownloadClientType DownloadClient { get; set; } diff --git a/src/NzbDrone.Core/Datastore/Migration/030_add_season_folder_format_to_naming_config.cs b/src/NzbDrone.Core/Datastore/Migration/030_add_season_folder_format_to_naming_config.cs new file mode 100644 index 000000000..7e7a2ab87 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/030_add_season_folder_format_to_naming_config.cs @@ -0,0 +1,56 @@ +using System; +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(30)] + public class add_season_folder_format_to_naming_config : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("NamingConfig").AddColumn("SeasonFolderFormat").AsString().Nullable(); + Execute.WithConnection(ConvertConfig); + Execute.Sql("DELETE FROM Config WHERE [Key] = 'seasonfolderformat'"); + Execute.Sql("DELETE FROM Config WHERE [Key] = 'useseasonfolder'"); + } + + private void ConvertConfig(IDbConnection conn, IDbTransaction tran) + { + using (IDbCommand namingConfigCmd = conn.CreateCommand()) + { + namingConfigCmd.Transaction = tran; + namingConfigCmd.CommandText = @"SELECT [Value] FROM Config WHERE [Key] = 'seasonfolderformat'"; + var seasonFormat = "Season {season}"; + + using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) + { + while (namingConfigReader.Read()) + { + //only getting one column, so its index is 0 + seasonFormat = namingConfigReader.GetString(0); + + seasonFormat = seasonFormat.Replace("%sn", "{Series Title}") + .Replace("%s.n", "{Series.Title}") + .Replace("%s", "{season}") + .Replace("%0s", "{season:00}") + .Replace("%e", "{episode}") + .Replace("%0e", "{episode:00}"); + } + } + + using (IDbCommand updateCmd = conn.CreateCommand()) + { + var text = String.Format("UPDATE NamingConfig " + + "SET SeasonFolderFormat = '{0}'", + seasonFormat); + + updateCmd.Transaction = tran; + updateCmd.CommandText = text; + updateCmd.ExecuteNonQuery(); + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs b/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs deleted file mode 100644 index b6ba5e686..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/030_update_season_folder_format.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Data; -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(30)] - public class update_series_folder_format : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.WithConnection(ConvertConfig); - } - - private void ConvertConfig(IDbConnection conn, IDbTransaction tran) - { - using (IDbCommand namingConfigCmd = conn.CreateCommand()) - { - namingConfigCmd.Transaction = tran; - namingConfigCmd.CommandText = @"SELECT * FROM Config WHERE [Key] = 'seasonfolderformat'"; - using (IDataReader namingConfigReader = namingConfigCmd.ExecuteReader()) - { - var valueIndex = namingConfigReader.GetOrdinal("[Value]"); - - while (namingConfigReader.Read()) - { - var value = namingConfigReader.GetString(valueIndex); - - value = value.Replace("%sn", "{Series Title}") - .Replace("%s.n", "{Series.Title}") - .Replace("%s", "{season}") - .Replace("%0s", "{season:00}") - .Replace("%e", "{episode}") - .Replace("%0e", "{episode:00}"); - - - using (IDbCommand updateCmd = conn.CreateCommand()) - { - var text = String.Format("UPDATE Config " + - "SET [VALUE] = '{0}'" + - "WHERE [Key] = 'seasonfolderformat'", - value); - - updateCmd.Transaction = tran; - updateCmd.CommandText = text; - updateCmd.ExecuteNonQuery(); - } - } - } - } - } - } -} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 8e32b557d..315176686 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -186,8 +186,8 @@ </Compile> <Compile Include="Datastore\Migration\028_add_blacklist_table.cs" /> <Compile Include="Datastore\Migration\029_add_formats_to_naming_config.cs" /> - <Compile Include="Datastore\Migration\030_update_season_folder_format.cs" /> <Compile Include="Datastore\Migration\031_delete_old_naming_config_columns.cs" /> + <Compile Include="Datastore\Migration\030_add_season_folder_format_to_naming_config.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationContext.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationController.cs" /> <Compile Include="Datastore\Migration\Framework\MigrationExtension.cs" /> diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index eb38719d2..3089fb51e 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -163,10 +163,11 @@ namespace NzbDrone.Core.Organizer else { + var nameSpec = _namingConfigService.GetConfig(); var tokenValues = new Dictionary<string, string>(FilenameBuilderTokenEqualityComparer.Instance); tokenValues.Add("{Series Title}", series.Title); - - seasonFolder = ReplaceSeasonTokens(_configService.SeasonFolderFormat, seasonNumber); + + seasonFolder = ReplaceSeasonTokens(nameSpec.SeasonFolderFormat, seasonNumber); seasonFolder = ReplaceTokens(seasonFolder, tokenValues); } diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index a7b7c6b8b..7a537e3ce 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -13,7 +13,8 @@ namespace NzbDrone.Core.Organizer RenameEpisodes = false, MultiEpisodeStyle = 0, StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Title}", - DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Title}" + DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Title}", + SeasonFolderFormat = "Season {season}" }; } } @@ -22,5 +23,6 @@ namespace NzbDrone.Core.Organizer public int MultiEpisodeStyle { get; set; } public string StandardEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; } + public string SeasonFolderFormat { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 6f94faa84..9e1da54cd 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -71,8 +71,6 @@ namespace NzbDrone.Core.Tv newSeries.Monitored = true; newSeries.CleanTitle = Parser.Parser.CleanSeriesTitle(newSeries.Title); - newSeries.SeasonFolder = _configService.UseSeasonFolder; - _seriesRepository.Insert(newSeries); _eventAggregator.PublishEvent(new SeriesAddedEvent(newSeries)); diff --git a/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html b/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html index 0827ba052..82d28119c 100644 --- a/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html +++ b/src/UI/AddSeries/RootFolders/StartingSeasonSelectionPartial.html @@ -1,4 +1,4 @@ -<select class="span2 x-starting-season"> +<select class="starting-season x-starting-season"> {{#each this}} {{#if_eq seasonNumber compare="0"}} <option value="{{seasonNumber}}">Specials</option> diff --git a/src/UI/AddSeries/SearchResultView.js b/src/UI/AddSeries/SearchResultView.js index bb81a5f84..e8b24435c 100644 --- a/src/UI/AddSeries/SearchResultView.js +++ b/src/UI/AddSeries/SearchResultView.js @@ -22,6 +22,7 @@ define( ui: { qualityProfile: '.x-quality-profile', rootFolder : '.x-root-folder', + seasonFolder : '.x-season-folder', addButton : '.x-add', overview : '.x-overview', startingSeason: '.x-starting-season' @@ -30,7 +31,8 @@ define( events: { 'click .x-add' : '_addSeries', 'change .x-quality-profile': '_qualityProfileChanged', - 'change .x-root-folder' : '_rootFolderChanged' + 'change .x-root-folder' : '_rootFolderChanged', + 'change .x-season-folder' : '_seasonFolderChanged' }, initialize: function () { @@ -51,6 +53,7 @@ define( var defaultQuality = Config.getValue(Config.Keys.DefaultQualityProfileId); var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); + var useSeasonFolder = Config.getValueBoolean(Config.Keys.UseSeasonFolder, true); if (QualityProfiles.get(defaultQuality)) { this.ui.qualityProfile.val(defaultQuality); @@ -60,6 +63,8 @@ define( this.ui.rootFolder.val(defaultRoot); } + this.ui.seasonFolder.prop('checked', useSeasonFolder); + var minSeasonNotZero = _.min(_.reject(this.model.get('seasons'), { seasonNumber: 0 }), 'seasonNumber'); if (minSeasonNotZero) { @@ -91,15 +96,24 @@ define( if (options.key === Config.Keys.DefaultQualityProfileId) { this.ui.qualityProfile.val(options.value); } + else if (options.key === Config.Keys.DefaultRootFolderId) { this.ui.rootFolder.val(options.value); } + + else if (options.key === Config.Keys.UseSeasonFolder) { + this.ui.seasonFolder.prop('checked', options.value); + } }, _qualityProfileChanged: function () { Config.setValue(Config.Keys.DefaultQualityProfileId, this.ui.qualityProfile.val()); }, + _seasonFolderChanged: function () { + Config.setValue(Config.Keys.UseSeasonFolder, this.ui.seasonFolder.prop('checked')); + }, + _rootFolderChanged: function () { var rootFolderValue = this.ui.rootFolder.val(); if (rootFolderValue === 'addNew') { @@ -125,16 +139,17 @@ define( var quality = this.ui.qualityProfile.val(); var rootFolderPath = this.ui.rootFolder.children(':selected').text(); var startingSeason = this.ui.startingSeason.val(); + var seasonFolder = this.ui.seasonFolder.prop('checked'); this.model.set('qualityProfileId', quality); this.model.set('rootFolderPath', rootFolderPath); this.model.setSeasonPass(startingSeason); + this.model.set('seasonFolder', seasonFolder); var self = this; SeriesCollection.add(this.model); - var promise = this.model.save(); promise.done(function () { @@ -159,7 +174,6 @@ define( } }); - AsValidatedView.apply(view); return view; diff --git a/src/UI/AddSeries/SearchResultViewTemplate.html b/src/UI/AddSeries/SearchResultViewTemplate.html index 5b64b09a9..04747d8cb 100644 --- a/src/UI/AddSeries/SearchResultViewTemplate.html +++ b/src/UI/AddSeries/SearchResultViewTemplate.html @@ -32,6 +32,14 @@ {{> StartingSeasonSelectionPartial seasons}} {{> QualityProfileSelectionPartial qualityProfiles}} + + <label class="checkbox-button" title="Use season folders"> + <input type="checkbox" class="x-season-folder"/> + <div class="btn btn-primary btn-icon-only"> + <i class="icon-folder-close"></i> + </div> + </label> + <span class="btn btn-success x-add add-series pull-right"> Add <i class="icon-plus"></i> </span> diff --git a/src/UI/AddSeries/addSeries.less b/src/UI/AddSeries/addSeries.less index 1d36d9906..ee04cb289 100644 --- a/src/UI/AddSeries/addSeries.less +++ b/src/UI/AddSeries/addSeries.less @@ -91,6 +91,18 @@ .add-series { margin-left : 20px; } + + .checkbox { + width : 100px; + margin-left : 0px; + display : inline-block; + padding-top : 0px; + margin-bottom : 0px; + } + + .starting-season { + width: 140px; + } } } @@ -129,4 +141,4 @@ li.add-new:hover { overflow: auto; max-height: 300px; } -} \ No newline at end of file +} diff --git a/src/UI/Config.js b/src/UI/Config.js index 6c44d8857..e826e90cf 100644 --- a/src/UI/Config.js +++ b/src/UI/Config.js @@ -9,7 +9,8 @@ define( }, Keys : { DefaultQualityProfileId: 'DefaultQualityProfileId', - DefaultRootFolderId: 'DefaultRootFolderId' + DefaultRootFolderId: 'DefaultRootFolderId', + UseSeasonFolder: 'UseSeasonFolder' }, getValueBoolean: function (key, defaultValue) { diff --git a/src/UI/Content/checkbox-button.less b/src/UI/Content/checkbox-button.less new file mode 100644 index 000000000..4d12317fb --- /dev/null +++ b/src/UI/Content/checkbox-button.less @@ -0,0 +1,28 @@ +@import "Bootstrap/variables"; +@import "Bootstrap/mixins"; + +.checkbox-button div { + display: none; +} + +@media only screen { + .checkbox-button { + input { + position: absolute; + opacity: 0; + z-index: 5; + } + + div { + display: block; + } + + .btn { + .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); + } + + input:first-of-type:checked ~ .btn { + .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight); + } + } +} diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 3cf7edb0a..7c1decd1f 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -7,6 +7,7 @@ @import "Backgrid/backgrid"; @import "prefixer"; @import "icons"; +@import "checkbox-button"; @import "spinner"; @import "legend"; @import "../Shared/Styles/clickable"; diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index f4beac4ba..6eb23c4ef 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -104,6 +104,17 @@ </div> </div> + <div class="control-group"> + <label class="control-label">Season Folder Format</label> + + <div class="controls"> + <input type="text" placeholder="Season {season}" name="seasonFolderFormat"/> + <span class="help-inline"> + <i class="icon-question-sign" title="How should season folders be named? Please see the wiki for customization options"/> + </span> + </div> + </div> + <div class="control-group"> <label class="control-label">Single Episode Example</label> diff --git a/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html b/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html index 322a7446c..dde9a2d84 100644 --- a/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Sorting/ViewTemplate.html @@ -1,7 +1,7 @@ -<fieldset> +<fieldset class="advanced-setting"> <legend>Folders</legend> - <div class="control-group advanced-setting"> + <div class="control-group"> <label class="control-label">Create empty series folders</label> <div class="controls"> @@ -21,33 +21,4 @@ </span> </div> </div> - - <!--TODO: Remove this and move it to Add Series--> - <div class="control-group"> - <label class="control-label">Use Season Folder</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="useSeasonFolder"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Season Folder Format</label> - - <div class="controls"> - <input type="text" placeholder="Season {season}" name="seasonFolderFormat"/> - <span class="help-inline"> - <i class="icon-question-sign" title="How should season folders be named? Please see the wiki for customization options"/> - </span> - </div> - </div> </fieldset> From 2b682a493616e09ff1e93d40e9f20f070dda9560 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 20 Nov 2013 22:10:00 -0800 Subject: [PATCH 16/21] Added caching to seasonEpisodePattern matching --- .../Organizer/FileNameBuilder.cs | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 3089fb51e..db540b5c4 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Tv; @@ -21,6 +22,7 @@ namespace NzbDrone.Core.Organizer { private readonly IConfigService _configService; private readonly INamingConfigService _namingConfigService; + private readonly ICached<EpisodeFormat> _patternCache; private readonly Logger _logger; private static readonly Regex TitleRegex = new Regex(@"(?<token>\{(?:\w+)(?<separator>\s|\W|_)\w+\})", @@ -37,10 +39,14 @@ namespace NzbDrone.Core.Organizer public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public FileNameBuilder(INamingConfigService namingConfigService, IConfigService configService, Logger logger) + public FileNameBuilder(INamingConfigService namingConfigService, + IConfigService configService, + ICacheManger cacheManger, + Logger logger) { _namingConfigService = namingConfigService; _configService = configService; + _patternCache = cacheManger.GetCache<EpisodeFormat>(GetType()); _logger = logger; } @@ -99,17 +105,10 @@ namespace NzbDrone.Core.Organizer } } - var seasonEpisode = SeasonEpisodePatternRegex.Match(pattern); - if (seasonEpisode.Success) - { - var episodeFormat = new EpisodeFormat - { - EpisodeSeparator = seasonEpisode.Groups["episodeSeparator"].Value, - Separator = seasonEpisode.Groups["separator"].Value, - EpisodePattern = seasonEpisode.Groups["episode"].Value, - SeasonEpisodePattern = seasonEpisode.Groups["seasonEpisode"].Value, - }; + var episodeFormat = GetSeasonEpisodePattern(pattern); + if (episodeFormat != null) + { pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, "{Season Episode}"); var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; @@ -246,6 +245,28 @@ namespace NzbDrone.Core.Organizer return value.ToString(split[1]); } + + private EpisodeFormat GetSeasonEpisodePattern(string pattern) + { + return _patternCache.Get(pattern, () => + { + var match = SeasonEpisodePatternRegex.Match(pattern); + + if (match.Success) + { + return new EpisodeFormat + { + EpisodeSeparator = match.Groups["episodeSeparator"].Value, + Separator = match.Groups["separator"].Value, + EpisodePattern = match.Groups["episode"].Value, + SeasonEpisodePattern = match.Groups["seasonEpisode"].Value, + }; + + } + + return null; + }); + } } public enum MultiEpisodeStyle From 3d23ac9234d2b18d0069b7643ff5281df1ec2eaf Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 20 Nov 2013 22:41:53 -0800 Subject: [PATCH 17/21] Fixed compilation issue --- src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs index 2bc2b2a49..11866ea54 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/BuildFilePathFixture.cs @@ -20,7 +20,6 @@ namespace NzbDrone.Core.Test.OrganizerTests { namingConfig = new NamingConfig(); - Mocker.GetMock<INamingConfigService>() .Setup(c => c.GetConfig()).Returns(namingConfig); } @@ -40,7 +39,7 @@ namespace NzbDrone.Core.Test.OrganizerTests .With(s => s.SeasonFolder = useSeasonFolder) .Build(); - Mocker.GetMock<IConfigService>().Setup(e => e.SeasonFolderFormat).Returns(seasonFolderFormat); + namingConfig.SeasonFolderFormat = seasonFolderFormat; Subject.BuildFilePath(fakeSeries, seasonNumber, filename, ".mkv").Should().Be(expectedPath.AsOsAgnostic()); } From d9b7dd257e3b641677daf263c15473aa727d39ee Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 21 Nov 2013 18:29:49 -0800 Subject: [PATCH 18/21] Couple touch ups --- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 4 ++-- src/UI/Content/checkbox-button.less | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index db540b5c4..9d10eb608 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Organizer } } - var episodeFormat = GetSeasonEpisodePattern(pattern); + var episodeFormat = GetEpisodeFormat(pattern); if (episodeFormat != null) { @@ -246,7 +246,7 @@ namespace NzbDrone.Core.Organizer return value.ToString(split[1]); } - private EpisodeFormat GetSeasonEpisodePattern(string pattern) + private EpisodeFormat GetEpisodeFormat(string pattern) { return _patternCache.Get(pattern, () => { diff --git a/src/UI/Content/checkbox-button.less b/src/UI/Content/checkbox-button.less index 4d12317fb..becb5a7df 100644 --- a/src/UI/Content/checkbox-button.less +++ b/src/UI/Content/checkbox-button.less @@ -18,9 +18,14 @@ } .btn { - .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight); + .buttonBackground(@btnBackground, @btnBackgroundHighlight); + color: #333333; } + .btn:hover { + color: #333333; + } + input:first-of-type:checked ~ .btn { .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight); } From 5659a3c496a44547e54993a3ee2978cf76c7da47 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 21 Nov 2013 23:44:43 -0800 Subject: [PATCH 19/21] No more wizard, now only show when advanced settings are off --- .../Config/NamingConfigResource.cs | 7 + src/NzbDrone.Api/Config/NamingModule.cs | 13 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Organizer/BasicNamingConfig.cs | 17 +++ .../Organizer/FileNameBuilder.cs | 44 +++++- .../Naming/Basic/BasicNamingView.js | 102 +++++++++++++ .../Naming/Basic/BasicNamingViewTemplate.html | 92 ++++++++++++ .../Naming/{Model.js => NamingModel.js} | 0 .../MediaManagement/Naming/NamingView.js | 35 ++--- .../Naming/NamingViewTemplate.html | 31 ++-- .../Naming/Wizard/NamingWizardModel.js | 17 --- .../Naming/Wizard/NamingWizardView.js | 127 ---------------- .../Wizard/NamingWizardViewTemplate.html | 141 ------------------ src/UI/Settings/SettingsLayout.js | 2 +- src/UI/Settings/settings.less | 8 + 15 files changed, 313 insertions(+), 324 deletions(-) create mode 100644 src/NzbDrone.Core/Organizer/BasicNamingConfig.cs create mode 100644 src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js create mode 100644 src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html rename src/UI/Settings/MediaManagement/Naming/{Model.js => NamingModel.js} (100%) delete mode 100644 src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index c37fffaa9..9ca779dac 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -10,5 +10,12 @@ namespace NzbDrone.Api.Config public string StandardEpisodeFormat { get; set; } public string DailyEpisodeFormat { get; set; } public string SeasonFolderFormat { get; set; } + + public bool IncludeSeriesTitle { get; set; } + public bool IncludeEpisodeTitle { get; set; } + public bool IncludeQuality { get; set; } + public bool ReplaceSpaces { get; set; } + public string Separator { get; set; } + public string NumberStyle { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index d72d97483..4daad949e 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingModule.cs @@ -8,6 +8,7 @@ using NzbDrone.Core.Organizer; using Nancy.ModelBinding; using NzbDrone.Api.Mapping; using NzbDrone.Api.Extensions; +using Omu.ValueInjecter; namespace NzbDrone.Api.Config { @@ -16,15 +17,18 @@ namespace NzbDrone.Api.Config private readonly INamingConfigService _namingConfigService; private readonly IFilenameSampleService _filenameSampleService; private readonly IFilenameValidationService _filenameValidationService; + private readonly IBuildFileNames _filenameBuilder; public NamingModule(INamingConfigService namingConfigService, IFilenameSampleService filenameSampleService, - IFilenameValidationService filenameValidationService) + IFilenameValidationService filenameValidationService, + IBuildFileNames filenameBuilder) : base("config/naming") { _namingConfigService = namingConfigService; _filenameSampleService = filenameSampleService; _filenameValidationService = filenameValidationService; + _filenameBuilder = filenameBuilder; GetResourceSingle = GetNamingConfig; GetResourceById = GetNamingConfig; UpdateResource = UpdateNamingConfig; @@ -50,7 +54,12 @@ namespace NzbDrone.Api.Config private NamingConfigResource GetNamingConfig() { - return _namingConfigService.GetConfig().InjectTo<NamingConfigResource>(); + var nameSpec = _namingConfigService.GetConfig(); + var resource = nameSpec.InjectTo<NamingConfigResource>(); + var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); + resource.InjectFrom(basicConfig); + + return resource; } private NamingConfigResource GetNamingConfig(int id) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 315176686..142c9d9e8 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -324,6 +324,7 @@ <Compile Include="Notifications\Xbmc\Model\VersionResult.cs" /> <Compile Include="Notifications\Xbmc\Model\XbmcJsonResult.cs" /> <Compile Include="Notifications\Xbmc\Model\XbmcVersion.cs" /> + <Compile Include="Organizer\BasicNamingConfig.cs" /> <Compile Include="Organizer\FilenameValidationService.cs" /> <Compile Include="Organizer\EpisodeFormat.cs" /> <Compile Include="Organizer\Exception.cs" /> diff --git a/src/NzbDrone.Core/Organizer/BasicNamingConfig.cs b/src/NzbDrone.Core/Organizer/BasicNamingConfig.cs new file mode 100644 index 000000000..62d476701 --- /dev/null +++ b/src/NzbDrone.Core/Organizer/BasicNamingConfig.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Organizer +{ + public class BasicNamingConfig + { + public bool IncludeSeriesTitle { get; set; } + public bool IncludeEpisodeTitle { get; set; } + public bool IncludeQuality { get; set; } + public bool ReplaceSpaces { get; set; } + public string Separator { get; set; } + public string NumberStyle { get; set; } + } +} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 9d10eb608..618f22ea5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Organizer string BuildFilename(IList<Episode> episodes, Series series, EpisodeFile episodeFile); string BuildFilename(IList<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig); string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); + BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); } public class FileNameBuilder : IBuildFileNames @@ -25,7 +26,7 @@ namespace NzbDrone.Core.Organizer private readonly ICached<EpisodeFormat> _patternCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?<token>\{(?:\w+)(?<separator>\s|\W|_)\w+\})", + private static readonly Regex TitleRegex = new Regex(@"(?<token>\{(?:\w+)(?<separator>\s|\.|-|_)\w+\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", @@ -176,6 +177,47 @@ namespace NzbDrone.Core.Organizer return Path.Combine(path, fileName + extension); } + public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) + { + var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat); + + var basicNamingConfig = new BasicNamingConfig + { + Separator = episodeFormat.Separator, + NumberStyle = episodeFormat.SeasonEpisodePattern + }; + + var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); + + foreach (Match match in titleTokens) + { + var separator = match.Groups["separator"].Value; + var token = match.Groups["token"].Value; + + if (!separator.Equals(" ")) + { + basicNamingConfig.ReplaceSpaces = true; + } + + if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeSeriesTitle = true; + } + + if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeEpisodeTitle = true; + } + + if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeQuality = true; + } + } + + return basicNamingConfig; + } + public static string CleanFilename(string name) { string result = name; diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js new file mode 100644 index 000000000..616a20483 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js @@ -0,0 +1,102 @@ +'use strict'; +define( + [ + 'underscore', + '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/Basic/BasicNamingViewTemplate', + + ui: { + namingOptions : '.x-naming-options', + singleEpisodeExample : '.x-single-episode-example', + multiEpisodeExample : '.x-multi-episode-example', + dailyEpisodeExample : '.x-daily-episode-example' + }, + + onRender: function () { + this.listenTo(this.model, 'change', this._buildFormat); + this._buildFormat(); + }, + + _updateSamples: function () { + var data = { + renameEpisodes: true, + standardEpisodeFormat: this.standardEpisodeFormat, + dailyEpisodeFormat: this.dailyEpisodeFormat, + multiEpisodeStyle: this.model.get('multiEpisodeStyle') + }; + + this.namingSampleModel.fetch({data: data}); + }, + + _buildFormat: function () { + if (_.has(this.model.changed, 'standardEpisodeFormat') || _.has(this.model.changed, 'dailyEpisodeFormat')) { + return; + } + + var standardEpisodeFormat = ''; + var dailyEpisodeFormat = ''; + + if (this.model.get('includeSeriesTitle')) { + if (this.model.get('replaceSpaces')) { + standardEpisodeFormat += '{Series.Title}'; + dailyEpisodeFormat += '{Series.Title}'; + } + + else { + standardEpisodeFormat += '{Series Title}'; + dailyEpisodeFormat += '{Series Title}'; + } + + standardEpisodeFormat += this.model.get('separator'); + dailyEpisodeFormat += this.model.get('separator'); + } + + standardEpisodeFormat += this.model.get('numberStyle'); + dailyEpisodeFormat += '{Air-Date}'; + + if (this.model.get('includeEpisodeTitle')) { + standardEpisodeFormat += this.model.get('separator'); + dailyEpisodeFormat += this.model.get('separator'); + + if (this.model.get('replaceSpaces')) { + standardEpisodeFormat += '{Episode.Title}'; + dailyEpisodeFormat += '{Episode.Title}'; + } + + else { + standardEpisodeFormat += '{Episode Title}'; + dailyEpisodeFormat += '{Episode Title}'; + } + } + + if (this.model.get('includeQuality')) { + if (this.model.get('replaceSpaces')) { + standardEpisodeFormat += ' {Quality.Title}'; + dailyEpisodeFormat += ' {Quality.Title}'; + } + + else { + standardEpisodeFormat += ' {Quality Title}'; + dailyEpisodeFormat += ' {Quality Title}'; + } + } + + if (this.model.get('replaceSpaces')) { + standardEpisodeFormat = standardEpisodeFormat.replace(/\s/g, '.'); + dailyEpisodeFormat = dailyEpisodeFormat.replace(/\s/g, '.'); + } + + this.model.set('standardEpisodeFormat', standardEpisodeFormat); + this.model.set('dailyEpisodeFormat', dailyEpisodeFormat); + } + }); + + return AsModelBoundView.call(view); + }); diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html new file mode 100644 index 000000000..53cddefa4 --- /dev/null +++ b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.html @@ -0,0 +1,92 @@ +<div class="control-group"> + <label class="control-label">Include Series Title</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="includeSeriesTitle"/> + + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> +</div> + +<div class="control-group"> + <label class="control-label">Include Episode Title</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="includeEpisodeTitle"/> + + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> +</div> + +<div class="control-group"> + <label class="control-label">Include Quality</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="includeQuality"/> + + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> +</div> + +<div class="control-group"> + <label class="control-label">Replace Spaces</label> + + <div class="controls"> + <label class="checkbox toggle well"> + <input type="checkbox" name="replaceSpaces"/> + + <p> + <span>Yes</span> + <span>No</span> + </p> + + <div class="btn btn-primary slide-button"/> + </label> + </div> +</div> + +<div class="control-group"> + <label class="control-label">Separator</label> + + <div class="controls"> + <select class="inputClass" name="separator"> + <option value=" - ">Dash</option> + <option value=" ">Space</option> + <option value=".">Period</option> + </select> + </div> +</div> + +<div class="control-group"> + <label class="control-label">Numbering Style</label> + + <div class="controls"> + <select class="inputClass" name="numberStyle"> + <option value="{season}x{episode:00}">1x05</option> + <option value="{season:00}x{episode:00}">01x05</option> + <option value="S{season:00}E{episode:00}">S01E05</option> + <option value="s{season:00}e{episode:00}">s01e05</option> + </select> + </div> +</div> diff --git a/src/UI/Settings/MediaManagement/Naming/Model.js b/src/UI/Settings/MediaManagement/Naming/NamingModel.js similarity index 100% rename from src/UI/Settings/MediaManagement/Naming/Model.js rename to src/UI/Settings/MediaManagement/Naming/NamingModel.js diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 8592aa87e..7d50f9cf8 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -1,15 +1,16 @@ 'use strict'; define( [ - 'vent', + 'underscore', 'marionette', + 'Config', 'Settings/MediaManagement/Naming/NamingSampleModel', - 'Settings/MediaManagement/Naming/Wizard/NamingWizardView', + 'Settings/MediaManagement/Naming/Basic/BasicNamingView', 'Mixins/AsModelBoundView', 'Mixins/AsValidatedView' - ], function (vent, Marionette, NamingSampleModel, NamingWizardView, AsModelBoundView, AsValidatedView) { + ], function (_, Marionette, Config, NamingSampleModel, BasicNamingView, AsModelBoundView, AsValidatedView) { - var view = Marionette.ItemView.extend({ + var view = Marionette.Layout.extend({ template: 'Settings/MediaManagement/Naming/NamingViewTemplate', ui: { @@ -27,11 +28,17 @@ define( 'click .x-naming-token-helper a' : '_addToken' }, + regions: { + basicNamingRegion: '.x-basic-naming' + }, + onRender: function () { - if (!this.model.has('renameEpisodes')) { + if (!this.model.get('renameEpisodes')) { this.ui.namingOptions.hide(); } + this.basicNamingView = new BasicNamingView({ model: this.model }); + this.basicNamingRegion.show(this.basicNamingView); this.namingSampleModel = new NamingSampleModel(); this.listenTo(this.model, 'change', this._updateSamples); @@ -51,6 +58,10 @@ define( }, _updateSamples: function () { + if (!_.has(this.model.changed, 'standardEpisodeFormat') && !_.has(this.model.changed, 'dailyEpisodeFormat')) { + return; + } + this.namingSampleModel.fetch({ data: this.model.toJSON() }); }, @@ -60,14 +71,6 @@ define( this.ui.dailyEpisodeExample.html(this.namingSampleModel.get('dailyEpisodeExample')); }, - _showWizard: function () { - var modalView = new NamingWizardView(); - vent.trigger(vent.Commands.OpenModalCommand, modalView); - this.listenTo(modalView, modalView.formatsUpdated, this._updateFormats); - - vent.trigger(vent.Commands.ShowNamingWizard, { model: this.model }); - }, - _addToken: function (e) { e.preventDefault(); e.stopPropagation(); @@ -88,12 +91,6 @@ define( this.ui.namingTokenHelper.removeClass('open'); input.focus(); - }, - - _updateFormats: function (options) { - this.model.set('standardEpisodeFormat', options.standardEpisodeFormat); - this.model.set('dailyEpisodeFormat', options.dailyEpisodeFormat); - this.model.set('multiEpisodeStyle', options.multiEpisodeStyle); } }); diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index 6eb23c4ef..a8eb890d2 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -23,17 +23,7 @@ </div> <div class="x-naming-options"> - <div class="control-group"> - <label class="control-label">Wizard</label> - - <div class="controls"> - <button class="btn x-show-wizard">Show Wizard</button> - - <span class="help-inline"> - <i class="icon-nd-form-info" title="Wizard to setup preferred naming style"/> - </span> - </div> - </div> + <div class="basic-setting x-basic-naming"></div> <div class="control-group advanced-setting"> <label class="control-label">Standard Episode Format</label> @@ -90,7 +80,7 @@ </div> </div> - <div class="control-group advanced-setting"> + <div class="control-group"> <label class="control-label">Multi-Episode Style</label> <div class="controls"> @@ -108,10 +98,19 @@ <label class="control-label">Season Folder Format</label> <div class="controls"> - <input type="text" placeholder="Season {season}" name="seasonFolderFormat"/> - <span class="help-inline"> - <i class="icon-question-sign" title="How should season folders be named? Please see the wiki for customization options"/> - </span> + <div class="input-append x-helper-input"> + <input type="text" class="naming-format" name="seasonFolderFormat"/> + <div class="btn-group x-naming-token-helper"> + <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> + <i class="icon-plus"></i> + </button> + <ul class="dropdown-menu"> + {{> SeriesTitleNamingPartial}} + {{> SeasonNamingPartial}} + {{> SeparatorNamingPartial}} + </ul> + </div> + </div> </div> </div> diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js deleted file mode 100644 index 6d6806538..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardModel.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; -define( - [ - 'backbone' - ], function (Backbone) { - return Backbone.Model.extend({ - defaults: { - includeSeriesTitle : true, - includeEpisodeTitle: true, - includeQuality : true, - replaceSpaces : false, - separator : ' - ', - numberStyle : 'S{season:00}E{episode:00}', - multiEpisodeStyle : 0 - } - }); - }); diff --git a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js b/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js deleted file mode 100644 index eb48dd081..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardView.js +++ /dev/null @@ -1,127 +0,0 @@ -'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' - }, - - formatsUpdated: 'formatsUpdated', - - initialize: function () { - this.model = new NamingWizardModel(); - this.namingSampleModel = new NamingSampleModel(); - }, - - onRender: function () { - 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 () { - var options = { - standardEpisodeFormat: this.standardEpisodeFormat, - dailyEpisodeFormat: this.dailyEpisodeFormat, - multiEpisodeStyle: this.model.get('multiEpisodeStyle') - }; - - this.trigger(this.formatsUpdated, options); - - - 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'); - } - - this.standardEpisodeFormat += this.model.get('numberStyle'); - 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 deleted file mode 100644 index 484ad6b5a..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Wizard/NamingWizardViewTemplate.html +++ /dev/null @@ -1,141 +0,0 @@ -<div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Naming Wizard</h3> -</div> -<div class="modal-body"> - <div class="form-horizontal"> - <div class="control-group"> - <label class="control-label">Include Series Title</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeSeriesTitle"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Include Episode Title</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeEpisodeTitle"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Include Quality</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeQuality"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Replace Spaces</label> - - <div class="controls"> - <label class="checkbox toggle well"> - <input type="checkbox" name="replaceSpaces"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Separator</label> - - <div class="controls"> - <select class="inputClass" name="separator"> - <option value=" - ">Dash</option> - <option value=" ">Space</option> - <option value=".">Period</option> - </select> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Numbering Style</label> - - <div class="controls"> - <select class="inputClass" name="numberStyle"> - <option value="{season}x{episode:00}">1x05</option> - <option value="{season:00}x{episode:00}">01x05</option> - <option value="S{season:00}E{episode:00}">S01E05</option> - <option value="s{season:00}e{episode:00}">s01e05</option> - </select> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Multi-Episode Style</label> - - <div class="controls"> - <select class="inputClass" name="multiEpisodeStyle"> - <option value="0">Extend</option> - <option value="1">Duplicate</option> - <option value="2">Repeat</option> - <option value="3">Scene</option> - </select> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Single Episode Example</label> - - <div class="controls"> - <span class="x-single-episode-example naming-example"></span> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Multi-Episode Example</label> - - <div class="controls"> - <span class="x-multi-episode-example naming-example"></span> - </div> - </div> - - <div class="control-group"> - <label class="control-label">Daily-Episode Example</label> - - <div class="controls"> - <span class="x-daily-episode-example naming-example"></span> - </div> - </div> - </div> -</div> -<div class="modal-footer"> - <button class="btn" data-dismiss="modal">cancel</button> - <button class="btn btn-primary x-apply">apply</button> -</div> diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js index 20ae6912d..a8b281cf8 100644 --- a/src/UI/Settings/SettingsLayout.js +++ b/src/UI/Settings/SettingsLayout.js @@ -6,7 +6,7 @@ define( 'backbone', 'Settings/SettingsModel', 'Settings/General/GeneralSettingsModel', - 'Settings/MediaManagement/Naming/Model', + 'Settings/MediaManagement/Naming/NamingModel', 'Settings/MediaManagement/MediaManagementLayout', 'Settings/Quality/QualityLayout', 'Settings/Indexers/IndexerLayout', diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less index e771bc7cc..528a7f37b 100644 --- a/src/UI/Settings/settings.less +++ b/src/UI/Settings/settings.less @@ -77,8 +77,16 @@ li.save-and-add:hover { } } +.basic-setting { + display: block; +} + .show-advanced-settings { .advanced-setting { display: block; } + + .basic-setting { + display: none; + } } From c90d010cd806d41e469a03b910803e3bb72ad4d8 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Fri, 22 Nov 2013 08:59:03 -0800 Subject: [PATCH 20/21] Fixed the broken tests --- src/NzbDrone.Api/Config/NamingModule.cs | 9 ++++++++- src/NzbDrone.Core/Organizer/FileNameBuilder.cs | 5 +++++ .../MediaManagement/Naming/Basic/BasicNamingView.js | 3 +-- src/UI/Settings/MediaManagement/Naming/NamingView.js | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index 4daad949e..3e6b8efe0 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 System.Linq; using FluentValidation; using FluentValidation.Results; @@ -56,6 +57,12 @@ namespace NzbDrone.Api.Config { var nameSpec = _namingConfigService.GetConfig(); var resource = nameSpec.InjectTo<NamingConfigResource>(); + + if (String.IsNullOrWhiteSpace(resource.StandardEpisodeFormat)) + { + return resource; + } + var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); resource.InjectFrom(basicConfig); diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 618f22ea5..776f138df 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -181,6 +181,11 @@ namespace NzbDrone.Core.Organizer { var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat); + if (episodeFormat == null) + { + return new BasicNamingConfig(); + } + var basicNamingConfig = new BasicNamingConfig { Separator = episodeFormat.Separator, diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js index 616a20483..29f14a74a 100644 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js @@ -5,9 +5,8 @@ define( 'vent', 'marionette', 'Settings/MediaManagement/Naming/NamingSampleModel', - 'Settings/MediaManagement/Naming/Wizard/NamingWizardModel', 'Mixins/AsModelBoundView' - ], function (_, vent, Marionette, NamingSampleModel, NamingWizardModel, AsModelBoundView) { + ], function (_, vent, Marionette, NamingSampleModel, AsModelBoundView) { var view = Marionette.ItemView.extend({ template: 'Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate', diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index 7d50f9cf8..a9ad9e7e9 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -37,8 +37,8 @@ define( this.ui.namingOptions.hide(); } - this.basicNamingView = new BasicNamingView({ model: this.model }); - this.basicNamingRegion.show(this.basicNamingView); + var basicNamingView = new BasicNamingView({ model: this.model }); + this.basicNamingRegion.show(basicNamingView); this.namingSampleModel = new NamingSampleModel(); this.listenTo(this.model, 'change', this._updateSamples); From 767586304a35b40700228762794158fe30cd9e90 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Fri, 22 Nov 2013 09:17:04 -0800 Subject: [PATCH 21/21] Never allow empty episode formats to be saved --- src/NzbDrone.Api/Config/NamingModule.cs | 8 ++------ src/NzbDrone.Integration.Test/NamingConfigTests.cs | 6 ++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Api/Config/NamingModule.cs b/src/NzbDrone.Api/Config/NamingModule.cs index 3e6b8efe0..1f0b39fac 100644 --- a/src/NzbDrone.Api/Config/NamingModule.cs +++ b/src/NzbDrone.Api/Config/NamingModule.cs @@ -37,12 +37,8 @@ namespace NzbDrone.Api.Config Get["/samples"] = x => GetExamples(this.Bind<NamingConfigResource>()); SharedValidator.RuleFor(c => c.MultiEpisodeStyle).InclusiveBetween(0, 3); - - SharedValidator.When(spec => spec.RenameEpisodes, () => - { - SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); - SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); - }); + SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); + SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); } private void UpdateNamingConfig(NamingConfigResource resource) diff --git a/src/NzbDrone.Integration.Test/NamingConfigTests.cs b/src/NzbDrone.Integration.Test/NamingConfigTests.cs index eed8373ba..3f351bb68 100644 --- a/src/NzbDrone.Integration.Test/NamingConfigTests.cs +++ b/src/NzbDrone.Integration.Test/NamingConfigTests.cs @@ -81,10 +81,8 @@ namespace NzbDrone.Integration.Test config.StandardEpisodeFormat = ""; config.DailyEpisodeFormat = ""; - var result = NamingConfig.Put(config); - result.RenameEpisodes.Should().BeFalse(); - result.StandardEpisodeFormat.Should().Be(config.StandardEpisodeFormat); - result.DailyEpisodeFormat.Should().Be(config.DailyEpisodeFormat); + var errors = NamingConfig.InvalidPut(config); + errors.Should().NotBeEmpty(); } [Test]