Fixed: Inserting literal { or } in renaming format using {{ or }}

fixes #3434
This commit is contained in:
Taloth Saldono 2019-12-24 10:58:47 +01:00
parent 556bd11725
commit 9aa89a0df9
2 changed files with 53 additions and 8 deletions

View File

@ -233,6 +233,28 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be("South Park - S15E06 - City Sushi [HDTV-720p]"); .Should().Be("South Park - S15E06 - City Sushi [HDTV-720p]");
} }
[TestCase("Some Escaped {{ String", "Some Escaped { String")]
[TestCase("Some Escaped }} String", "Some Escaped } String")]
[TestCase("Some Escaped {{Series Title}} String", "Some Escaped {Series Title} String")]
[TestCase("Some Escaped {{{Series Title}}} String", "Some Escaped {South Park} String")]
public void should_escape_token_in_format(string format, string expected)
{
_namingConfig.StandardEpisodeFormat = format;
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be(expected);
}
[Test]
public void should_escape_token_in_title()
{
_namingConfig.StandardEpisodeFormat = "Some Unescaped {Series Title} String";
_series.Title = "My {Quality Full} Title";
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be("Some Unescaped My {Quality Full} Title String");
}
[Test] [Test]
public void use_file_name_when_sceneName_is_null() public void use_file_name_when_sceneName_is_null()
{ {

View File

@ -40,7 +40,7 @@ namespace NzbDrone.Core.Organizer
private readonly ICached<bool> _requiresAbsoluteEpisodeNumberCache; private readonly ICached<bool> _requiresAbsoluteEpisodeNumberCache;
private readonly Logger _logger; private readonly Logger _logger;
private static readonly Regex TitleRegex = new Regex(@"\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9]+))?(?<suffix>[- ._)\]]*)\}", private static readonly Regex TitleRegex = new Regex(@"(?<escaped>\{\{|\}\})|\{(?<prefix>[- ._\[(]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9]+))?(?<suffix>[- ._)\]]*)\}",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})", private static readonly Regex EpisodeRegex = new Regex(@"(?<episode>\{episode(?:\:0+)?})",
@ -158,7 +158,7 @@ namespace NzbDrone.Core.Organizer
AddMediaInfoTokens(tokenHandlers, episodeFile); AddMediaInfoTokens(tokenHandlers, episodeFile);
AddPreferredWords(tokenHandlers, series, episodeFile, preferredWords); AddPreferredWords(tokenHandlers, series, episodeFile, preferredWords);
var component = ReplaceTokens(splitPattern, tokenHandlers, namingConfig).Trim(); var component = ReplaceTokens(splitPattern, tokenHandlers, namingConfig, true).Trim();
var maxEpisodeTitleLength = 255 - GetLengthWithoutEpisodeTitle(component, namingConfig); var maxEpisodeTitleLength = 255 - GetLengthWithoutEpisodeTitle(component, namingConfig);
AddEpisodeTitleTokens(tokenHandlers, episodes, maxEpisodeTitleLength); AddEpisodeTitleTokens(tokenHandlers, episodes, maxEpisodeTitleLength);
@ -548,8 +548,8 @@ namespace NzbDrone.Core.Organizer
private void AddEpisodeTitlePlaceholderTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers) private void AddEpisodeTitlePlaceholderTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers)
{ {
tokenHandlers["{Episode Title}"] = m => m.RegexMatch.Value; tokenHandlers["{Episode Title}"] = m => null;
tokenHandlers["{Episode CleanTitle}"] = m => m.RegexMatch.Value; tokenHandlers["{Episode CleanTitle}"] = m => null;
} }
private void AddEpisodeTitleTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, List<Episode> episodes, int maxLength) private void AddEpisodeTitleTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, List<Episode> episodes, int maxLength)
@ -709,13 +709,23 @@ namespace NzbDrone.Core.Organizer
} }
} }
private string ReplaceTokens(string pattern, Dictionary<string, Func<TokenMatch, string>> tokenHandlers, NamingConfig namingConfig) private string ReplaceTokens(string pattern, Dictionary<string, Func<TokenMatch, string>> tokenHandlers, NamingConfig namingConfig, bool escape = false)
{ {
return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig)); return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig, escape));
} }
private string ReplaceToken(Match match, Dictionary<string, Func<TokenMatch, string>> tokenHandlers, NamingConfig namingConfig) private string ReplaceToken(Match match, Dictionary<string, Func<TokenMatch, string>> tokenHandlers, NamingConfig namingConfig, bool escape)
{ {
if (match.Groups["escaped"].Success)
{
if (escape)
return match.Value;
else if (match.Value == "{{")
return "{";
else if (match.Value == "}}")
return "}";
}
var tokenMatch = new TokenMatch var tokenMatch = new TokenMatch
{ {
RegexMatch = match, RegexMatch = match,
@ -733,7 +743,15 @@ namespace NzbDrone.Core.Organizer
var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty); var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty);
var replacementText = tokenHandler(tokenMatch).Trim(); var replacementText = tokenHandler(tokenMatch);
if (replacementText == null)
{
// Preserve original token if handler returned null
return match.Value;
}
replacementText = replacementText.Trim();
if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t)))
{ {
@ -756,6 +774,11 @@ namespace NzbDrone.Core.Organizer
replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix;
} }
if (escape)
{
replacementText = replacementText.Replace("{", "{{").Replace("}", "}}");
}
return replacementText; return replacementText;
} }