From 8711c41649059431d612d42f1fc1359328e2cf10 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 25 Oct 2014 23:43:18 -0700 Subject: [PATCH] New: Ability to use Original Title only in naming settings --- .../NzbDrone.Core.Test.csproj | 1 - .../OrganizerTests/FileNameBuilderFixture.cs | 20 +++++- .../Organizer/FileNameBuilder.cs | 68 +++++++++++-------- .../Organizer/FileNameValidation.cs | 37 ++++++++-- .../Organizer/FilenameSampleService.cs | 8 ++- 5 files changed, 95 insertions(+), 39 deletions(-) diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 32bbf520f..8948394c8 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -327,7 +327,6 @@ Always - App.config diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs index 1d25ca1a5..bd1809135 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs @@ -384,7 +384,7 @@ namespace NzbDrone.Core.Test.OrganizerTests } [Test] - public void should_be_able_to_use_orginal_title() + public void should_be_able_to_use_original_title() { _series.Title = "30 Rock"; _namingConfig.StandardEpisodeFormat = "{Series Title} - {Original Title}"; @@ -618,15 +618,16 @@ namespace NzbDrone.Core.Test.OrganizerTests } [Test] - public void should_use_empty_string_instead_of_null_when_scene_name_is_not_available() + public void should_use_existing_filename_when_scene_name_is_not_available() { _namingConfig.RenameEpisodes = true; _namingConfig.StandardEpisodeFormat = "{Original Title}"; _episodeFile.SceneName = null; + _episodeFile.RelativePath = "existing.file.mkv"; Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) - .Should().Be(String.Empty); + .Should().Be(Path.GetFileNameWithoutExtension(_episodeFile.RelativePath)); } [Test] @@ -640,6 +641,19 @@ namespace NzbDrone.Core.Test.OrganizerTests .Should().Be("South Park - S15E06 - S15E07 - (HDTV-720p, , DRONE) - City Sushi"); } + [Test] + public void should_be_able_to_use_only_original_title() + { + _series.Title = "30 Rock"; + _namingConfig.StandardEpisodeFormat = "{Original Title}"; + + _episodeFile.SceneName = "30.Rock.S01E01.xvid-LOL"; + _episodeFile.RelativePath = "30 Rock - S01E01 - Test"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("30.Rock.S01E01.xvid-LOL"); + } + [Test] public void should_allow_period_between_season_and_episode() { diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 147f0187a..f40aa28bf 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -53,6 +53,9 @@ namespace NzbDrone.Core.Organizer public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex OriginalTitleRegex = new Regex(@"(\^{original[- ._]title\}$)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex FileNameCleanupRegex = new Regex(@"\.{2,}", RegexOptions.Compiled); private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; @@ -78,27 +81,22 @@ namespace NzbDrone.Core.Organizer if (!namingConfig.RenameEpisodes) { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - - return episodeFile.SceneName; + return GetOriginalTitle(episodeFile); } if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) { - throw new NamingFormatException("Standard episode format cannot be null"); + throw new NamingFormatException("Standard episode format cannot be empty"); } if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) { - throw new NamingFormatException("Daily episode format cannot be null"); + throw new NamingFormatException("Daily episode format cannot be empty"); + } + + if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) + { + throw new NamingFormatException("Anime episode format cannot be empty"); } var pattern = namingConfig.StandardEpisodeFormat; @@ -124,10 +122,10 @@ namespace NzbDrone.Core.Organizer AddEpisodeFileTokens(tokenHandlers, series, episodeFile); AddMediaInfoTokens(tokenHandlers, episodeFile); - var filename = ReplaceTokens(pattern, tokenHandlers).Trim(); - filename = FileNameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString()); + var fileName = ReplaceTokens(pattern, tokenHandlers).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - return filename; + return fileName; } public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) @@ -398,7 +396,7 @@ namespace NzbDrone.Core.Organizer private void AddEpisodeFileTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) { - tokenHandlers["{Original Title}"] = m => episodeFile.SceneName ?? String.Empty; + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? "DRONE"; tokenHandlers["{Quality Title}"] = m => GetQualityTitle(series, episodeFile.Quality); } @@ -556,17 +554,6 @@ namespace NzbDrone.Core.Organizer return replacementText; } - - private sealed class TokenMatch - { - public Match RegexMatch { get; set; } - public String Prefix { get; set; } - public String Separator { get; set; } - public String Suffix { get; set; } - public String Token { get; set; } - public String CustomFormat { get; set; } - } - private string ReplaceNumberTokens(string pattern, List episodes) { var episodeIndex = 0; @@ -665,6 +652,31 @@ namespace NzbDrone.Core.Organizer return _qualityDefinitionService.Get(quality.Quality).Title + qualitySuffix; } + + private String GetOriginalTitle(EpisodeFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } + + return episodeFile.SceneName; + } + } + + internal sealed class TokenMatch + { + public Match RegexMatch { get; set; } + public String Prefix { get; set; } + public String Separator { get; set; } + public String Suffix { get; set; } + public String Token { get; set; } + public String CustomFormat { get; set; } } public enum MultiEpisodeStyle diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index 6546ac29d..52bc92eb6 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -10,10 +10,13 @@ namespace NzbDrone.Core.Organizer private static readonly Regex SeasonFolderRegex = new Regex(@"(\{season(\:\d+)?\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + internal static readonly Regex OriginalTitleRegex = new Regex(@"(\{original[- ._]title\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + 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"); + return ruleBuilder.SetValidator(new ValidStandardEpisodeFormatValidator()); } public static IRuleBuilderOptions ValidDailyEpisodeFormat(this IRuleBuilder ruleBuilder) @@ -41,10 +44,32 @@ namespace NzbDrone.Core.Organizer } } + public class ValidStandardEpisodeFormatValidator : PropertyValidator + { + public ValidStandardEpisodeFormatValidator() + : base("Must contain season and episode numbers OR Original Title") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var value = context.PropertyValue as String; + + if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) && + !FileNameValidation.OriginalTitleRegex.IsMatch(value)) + { + return false; + } + + return true; + } + } + public class ValidDailyEpisodeFormatValidator : PropertyValidator { public ValidDailyEpisodeFormatValidator() - : base("Must contain Air Date or Season and Episode") + : base("Must contain Air Date OR Season and Episode OR Original Title") { } @@ -54,7 +79,8 @@ namespace NzbDrone.Core.Organizer var value = context.PropertyValue as String; if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) && - !FileNameBuilder.AirDateRegex.IsMatch(value)) + !FileNameBuilder.AirDateRegex.IsMatch(value) && + !FileNameValidation.OriginalTitleRegex.IsMatch(value)) { return false; } @@ -66,7 +92,7 @@ namespace NzbDrone.Core.Organizer public class ValidAnimeEpisodeFormatValidator : PropertyValidator { public ValidAnimeEpisodeFormatValidator() - : base("Must contain Absolute Episode number or Season and Episode") + : base("Must contain Absolute Episode number OR Season and Episode OR Original Title") { } @@ -76,7 +102,8 @@ namespace NzbDrone.Core.Organizer var value = context.PropertyValue as String; if (!FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) && - !FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value)) + !FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) && + !FileNameValidation.OriginalTitleRegex.IsMatch(value)) { return false; } diff --git a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs index e0a495bca..af4efac85 100644 --- a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs @@ -60,7 +60,7 @@ namespace NzbDrone.Core.Organizer EpisodeNumber = 1, Title = "Episode Title (1)", AirDate = "2013-10-30", - AbsoluteEpisodeNumber = 1 + AbsoluteEpisodeNumber = 1, }; _episode2 = new Episode @@ -94,6 +94,7 @@ namespace NzbDrone.Core.Organizer { Quality = new QualityModel(Quality.HDTV720p), RelativePath = "Series.Title.S01E01.720p.HDTV.x264-EVOLVE.mkv", + SceneName = "Series.Title.S01E01.720p.HDTV.x264-EVOLVE", ReleaseGroup = "RlsGrp", MediaInfo = mediaInfo }; @@ -102,14 +103,16 @@ namespace NzbDrone.Core.Organizer { Quality = new QualityModel(Quality.HDTV720p), RelativePath = "Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE.mkv", + SceneName = "Series.Title.S01E01-E02.720p.HDTV.x264-EVOLVE", ReleaseGroup = "RlsGrp", - MediaInfo = mediaInfo + MediaInfo = mediaInfo, }; _dailyEpisodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), RelativePath = "Series.Title.2013.10.30.HDTV.x264-EVOLVE.mkv", + SceneName = "Series.Title.2013.10.30.HDTV.x264-EVOLVE", ReleaseGroup = "RlsGrp", MediaInfo = mediaInfo }; @@ -118,6 +121,7 @@ namespace NzbDrone.Core.Organizer { Quality = new QualityModel(Quality.HDTV720p), RelativePath = "Series.Title.001.HDTV.x264-EVOLVE.mkv", + SceneName = "Series.Title.001.HDTV.x264-EVOLVE", ReleaseGroup = "RlsGrp", MediaInfo = mediaInfoAnime };