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

fixes #3434

(cherry picked from commit 9aa89a0df97f7d2b31d89a31c9b25e64ccd5c056)
This commit is contained in:
Taloth Saldono 2019-12-24 10:58:47 +01:00 committed by Adam Shick
parent 5396dd3e8e
commit 615fadd4ff
2 changed files with 94 additions and 12 deletions

View file

@ -421,6 +421,28 @@ public void should_replace_all_contents_in_pattern()
.Should().Be("Linkin Park - Hybrid Theory - [MP3]");
}
[TestCase("Some Escaped {{ String", "Some Escaped { String")]
[TestCase("Some Escaped }} String", "Some Escaped } String")]
[TestCase("Some Escaped {{Book Title}} String", "Some Escaped {Book Title} String")]
[TestCase("Some Escaped {{{Book Title}}} String", "Some Escaped {Hybrid Theory} String")]
public void should_escape_token_in_format(string format, string expected)
{
_namingConfig.StandardBookFormat = format;
Subject.BuildBookFileName(_author, _edition, _trackFile, _namingConfig)
.Should().Be(expected);
}
[Test]
public void should_escape_token_in_title()
{
_namingConfig.StandardBookFormat = "Some Unescaped {Book Title} String";
_edition.Title = "My {Quality Full} Title";
Subject.BuildBookFileName(_author, _edition, _trackFile, _namingConfig)
.Should().Be("Some Unescaped My {Quality Full} Title String");
}
[Test]
public void use_file_name_when_sceneName_is_null()
{

View file

@ -33,7 +33,7 @@ public class FileNameBuilder : IBuildFileNames
private readonly ICached<BookFormat[]> _trackFormatCache;
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);
public static readonly Regex PartRegex = new Regex(@"\{(?<prefix>[^{]*?)(?<token1>PartNumber|PartCount)(?::(?<customFormat1>[a-z0-9]+))?(?<separator>.*(?=PartNumber|PartCount))?((?<token2>PartNumber|PartCount)(?::(?<customFormat2>[a-z0-9]+))?)?(?<suffix>[^}]*)\}",
@ -90,13 +90,6 @@ public string BuildBookFileName(Author author, Edition edition, BookFile bookFil
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
AddAuthorTokens(tokenHandlers, author);
AddBookTokens(tokenHandlers, edition);
AddBookFileTokens(tokenHandlers, bookFile);
AddQualityTokens(tokenHandlers, author, bookFile);
AddMediaInfoTokens(tokenHandlers, bookFile);
AddCustomFormats(tokenHandlers, author, bookFile, customFormats);
var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
var components = new List<string>();
@ -104,7 +97,17 @@ public string BuildBookFileName(Author author, Edition edition, BookFile bookFil
{
var splitPattern = s;
AddAuthorTokens(tokenHandlers, author);
AddBookPlaceholderTokens(tokenHandlers);
AddBookFileTokens(tokenHandlers, bookFile);
AddQualityTokens(tokenHandlers, author, bookFile);
AddMediaInfoTokens(tokenHandlers, bookFile);
AddCustomFormats(tokenHandlers, author, bookFile, customFormats);
var component = ReplacePartTokens(splitPattern, tokenHandlers, namingConfig).Trim();
component = ReplaceTokens(component, tokenHandlers, namingConfig, true).Trim();
AddBookTokens(tokenHandlers, edition);
component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim();
component = FileNameCleanupRegex.Replace(component, match => match.Captures[0].Value[0].ToString());
@ -249,6 +252,30 @@ private void AddAuthorTokens(Dictionary<string, Func<TokenMatch, string>> tokenH
}
}
private void AddBookPlaceholderTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers)
{
tokenHandlers["{Book Title}"] = m => null;
tokenHandlers["{Book CleanTitle}"] = m => null;
tokenHandlers["{Book TitleThe}"] = m => null;
tokenHandlers["{Book TitleNoSub}"] = m => null;
tokenHandlers["{Book CleanTitleNoSub}"] = m => null;
tokenHandlers["{Book TitleTheNoSub}"] = m => null;
tokenHandlers["{Book Subtitle}"] = m => null;
tokenHandlers["{Book CleanSubtitle}"] = m => null;
tokenHandlers["{Book SubtitleThe}"] = m => null;
tokenHandlers["{Book Series}"] = m => null;
tokenHandlers["{Book SeriesPosition}"] = m => null;
tokenHandlers["{Book SeriesTitle}"] = m => null;
tokenHandlers["{Book Disambiguation}"] = m => null;
tokenHandlers["{Release Year}"] = m => null;
tokenHandlers["{Edition Year}"] = m => null;
tokenHandlers["{Release YearFirst}"] = m => null;
}
private void AddBookTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Edition edition)
{
tokenHandlers["{Book Title}"] = m => edition.Title;
@ -372,13 +399,29 @@ private void AddCustomFormats(Dictionary<string, Func<TokenMatch, string>> token
tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming));
}
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
{
RegexMatch = match,
@ -396,7 +439,19 @@ private string ReplaceToken(Match match, Dictionary<string, Func<TokenMatch, str
var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty);
var replacementText = tokenHandler(tokenMatch).Trim();
var replacementText = tokenHandler(tokenMatch);
if (replacementText == null && escape)
{
// Preserve original token if handler returned null and escape is true
return match.Value;
}
else if (replacementText == null)
{
return "";
}
replacementText = replacementText.Trim();
if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t)))
{
@ -419,6 +474,11 @@ private string ReplaceToken(Match match, Dictionary<string, Func<TokenMatch, str
replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix;
}
if (escape)
{
replacementText = replacementText.Replace("{", "{{").Replace("}", "}}");
}
return replacementText;
}