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;