diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs index ebe3edfd1..e1b1843d8 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderFixture.cs @@ -487,6 +487,26 @@ namespace NzbDrone.Core.Test.OrganizerTests .Should().Be("South.Park.100.City.Sushi"); } + [Test] + public void should_replace_duplicate_numbering_individually() + { + _series.SeriesType = SeriesTypes.Anime; + _namingConfig.AnimeEpisodeFormat = "{Series.Title}.{season}x{episode:00}.{absolute:000}\\{Series.Title}.S{season:00}E{episode:00}.{absolute:00}.{Episode.Title}"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("South.Park.15x06.100\\South.Park.S15E06.100.City.Sushi"); + } + + [Test] + public void should_replace_individual_season_episode_tokens() + { + _series.SeriesType = SeriesTypes.Anime; + _namingConfig.AnimeEpisodeFormat = "{Series Title} Season {season:0000} Episode {episode:0000}\\{Series.Title}.S{season:00}E{episode:00}.{absolute:00}.{Episode.Title}"; + + Subject.BuildFileName(new List { _episode1, _episode2 }, _series, _episodeFile) + .Should().Be("South Park Season 0015 Episode 0006-0007\\South.Park.S15E06-07.100-101.City.Sushi"); + } + [Test] public void should_use_dash_as_separator_when_multi_episode_style_is_extend_for_anime() { diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 453ce4c0d..d8cc5fc94 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -26,7 +26,8 @@ namespace NzbDrone.Core.Organizer { private readonly INamingConfigService _namingConfigService; private readonly IQualityDefinitionService _qualityDefinitionService; - private readonly ICached _patternCache; + private readonly ICached _episodeFormatCache; + private readonly ICached _absoluteEpisodeFormatCache; private readonly Logger _logger; private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._]*)\}", @@ -63,7 +64,8 @@ namespace NzbDrone.Core.Organizer { _namingConfigService = namingConfigService; _qualityDefinitionService = qualityDefinitionService; - _patternCache = cacheManager.GetCache(GetType()); + _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); + _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); _logger = logger; } @@ -100,14 +102,6 @@ namespace NzbDrone.Core.Organizer episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); - AddSeriesTokens(tokenHandlers, series); - - AddEpisodeTokens(tokenHandlers, episodes); - - AddEpisodeFileTokens(tokenHandlers, episodeFile); - - AddMediaInfoTokens(tokenHandlers, episodeFile); - if (series.SeriesType == SeriesTypes.Daily) { pattern = namingConfig.DailyEpisodeFormat; @@ -118,85 +112,18 @@ namespace NzbDrone.Core.Organizer pattern = namingConfig.AnimeEpisodeFormat; } - var episodeFormat = GetEpisodeFormat(pattern); - - if (episodeFormat != null) - { - pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, "{Season Episode}"); - var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; - - foreach (var episode in episodes.Skip(1)) - { - switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) - { - case MultiEpisodeStyle.Duplicate: - seasonEpisodePattern += episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; - break; - - case MultiEpisodeStyle.Repeat: - seasonEpisodePattern += episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - break; - - case MultiEpisodeStyle.Scene: - seasonEpisodePattern += "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - break; - - //MultiEpisodeStyle.Extend - default: - seasonEpisodePattern += "-" + episodeFormat.EpisodePattern; - break; - } - } - - seasonEpisodePattern = ReplaceNumberTokens(seasonEpisodePattern, episodes); - tokenHandlers["{Season Episode}"] = m => seasonEpisodePattern; - } - - //TODO: Extract to another method - var absoluteEpisodeFormat = GetAbsoluteFormat(pattern); + pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); - if (absoluteEpisodeFormat != null) - { - if (series.SeriesType != SeriesTypes.Anime) - { - pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, ""); - } + pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); - else - { - pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, "{Absolute Pattern}"); - var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; + AddSeriesTokens(tokenHandlers, series); - foreach (var episode in episodes.Skip(1)) - { - switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) - { - case MultiEpisodeStyle.Duplicate: - absoluteEpisodePattern += absoluteEpisodeFormat.Separator + - absoluteEpisodeFormat.AbsoluteEpisodePattern; - break; - - case MultiEpisodeStyle.Repeat: - absoluteEpisodePattern += absoluteEpisodeFormat.Separator + - absoluteEpisodeFormat.AbsoluteEpisodePattern; - break; - - case MultiEpisodeStyle.Scene: - absoluteEpisodePattern += "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - break; - - //MultiEpisodeStyle.Extend - default: - absoluteEpisodePattern += "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - break; - } - } + AddEpisodeTokens(tokenHandlers, episodes); - absoluteEpisodePattern = ReplaceAbsoluteNumberTokens(absoluteEpisodePattern, episodes); - tokenHandlers["{Absolute Pattern}"] = m => absoluteEpisodePattern; - } - } + AddEpisodeFileTokens(tokenHandlers, episodeFile); + AddMediaInfoTokens(tokenHandlers, episodeFile); + var filename = ReplaceTokens(pattern, tokenHandlers).Trim(); filename = FileNameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString()); @@ -234,7 +161,7 @@ namespace NzbDrone.Core.Organizer public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) { - var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat); + var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); if (episodeFormat == null) { @@ -347,9 +274,112 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); } + private String AddSeasonEpisodeNumberingTokens(String pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) + { + var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); + + int index = 1; + foreach (var episodeFormat in episodeFormats) + { + var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; + + foreach (var episode in episodes.Skip(1)) + { + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) + { + case MultiEpisodeStyle.Duplicate: + seasonEpisodePattern += episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; + break; + + case MultiEpisodeStyle.Repeat: + seasonEpisodePattern += episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + break; + + case MultiEpisodeStyle.Scene: + seasonEpisodePattern += "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + break; + + //MultiEpisodeStyle.Extend + default: + seasonEpisodePattern += "-" + episodeFormat.EpisodePattern; + break; + } + } + + seasonEpisodePattern = ReplaceNumberTokens(seasonEpisodePattern, episodes); + + var token = String.Format("{{Season Episode{0}}}", index++); + pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, token); + tokenHandlers[token] = m => seasonEpisodePattern; + } + + AddSeasonTokens(tokenHandlers, episodes.First().SeasonNumber); + + if (episodes.Count > 1) + { + tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat) + "-" + episodes.Last().EpisodeNumber.ToString(m.CustomFormat); + } + else + { + tokenHandlers["{Episode}"] = m => episodes.First().EpisodeNumber.ToString(m.CustomFormat); + } + + return pattern; + } + + private String AddAbsoluteNumberingTokens(String pattern, Dictionary> tokenHandlers, Series series, List episodes, NamingConfig namingConfig) + { + var absoluteEpisodeFormats = GetAbsoluteFormat(pattern).DistinctBy(v => v.AbsoluteEpisodePattern).ToList(); + + int index = 1; + foreach (var absoluteEpisodeFormat in absoluteEpisodeFormats) + { + if (series.SeriesType != SeriesTypes.Anime) + { + pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, ""); + continue; + } + + var absoluteEpisodePattern = absoluteEpisodeFormat.AbsoluteEpisodePattern; + + foreach (var episode in episodes.Skip(1)) + { + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) + { + case MultiEpisodeStyle.Duplicate: + absoluteEpisodePattern += absoluteEpisodeFormat.Separator + + absoluteEpisodeFormat.AbsoluteEpisodePattern; + break; + + case MultiEpisodeStyle.Repeat: + absoluteEpisodePattern += absoluteEpisodeFormat.Separator + + absoluteEpisodeFormat.AbsoluteEpisodePattern; + break; + + case MultiEpisodeStyle.Scene: + absoluteEpisodePattern += "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + break; + + //MultiEpisodeStyle.Extend + default: + absoluteEpisodePattern += "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + break; + } + } + + absoluteEpisodePattern = ReplaceAbsoluteNumberTokens(absoluteEpisodePattern, episodes); + + var token = String.Format("{{Absolute Pattern{0}}}", index++); + pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token); + tokenHandlers[token] = m => absoluteEpisodePattern; + } + + return pattern; + } + private void AddSeasonTokens(Dictionary> tokenHandlers, Int32 seasonNumber) { - tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat ?? "0"); + tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); } private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) @@ -579,42 +609,26 @@ namespace NzbDrone.Core.Organizer return value.ToString(split[1]); } - private EpisodeFormat GetEpisodeFormat(string pattern) + private EpisodeFormat[] GetEpisodeFormat(string pattern) { - return _patternCache.Get(pattern, () => - { - var match = SeasonEpisodePatternRegex.Match(pattern); - - if (match.Success) + return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new EpisodeFormat { - 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; - }); + EpisodeSeparator = match.Groups["episodeSeparator"].Value, + Separator = match.Groups["separator"].Value, + EpisodePattern = match.Groups["episode"].Value, + SeasonEpisodePattern = match.Groups["seasonEpisode"].Value, + }).ToArray()); } - private AbsoluteEpisodeFormat GetAbsoluteFormat(string pattern) + private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern) { - var match = AbsoluteEpisodePatternRegex.Match(pattern); - - if (match.Success) - { - return new AbsoluteEpisodeFormat - { - Separator = match.Groups["separator"].Value, - AbsoluteEpisodePattern = match.Groups["absolute"].Value - }; - } - - return null; + return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new AbsoluteEpisodeFormat + { + Separator = match.Groups["separator"].Value, + AbsoluteEpisodePattern = match.Groups["absolute"].Value + }).ToArray()); } private String GetEpisodeTitle(List episodes)