mirror of
https://github.com/Readarr/Readarr
synced 2026-05-08 21:13:58 +02:00
Fixed: Inserting literal { or } in renaming format using {{ or }}
fixes #3434 (cherry picked from commit 9aa89a0df97f7d2b31d89a31c9b25e64ccd5c056)
This commit is contained in:
parent
5396dd3e8e
commit
615fadd4ff
2 changed files with 94 additions and 12 deletions
|
|
@ -421,6 +421,28 @@ public void should_replace_all_contents_in_pattern()
|
||||||
.Should().Be("Linkin Park - Hybrid Theory - [MP3]");
|
.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]
|
[Test]
|
||||||
public void use_file_name_when_sceneName_is_null()
|
public void use_file_name_when_sceneName_is_null()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ public class FileNameBuilder : IBuildFileNames
|
||||||
private readonly ICached<BookFormat[]> _trackFormatCache;
|
private readonly ICached<BookFormat[]> _trackFormatCache;
|
||||||
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);
|
||||||
|
|
||||||
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>[^}]*)\}",
|
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);
|
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 splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var components = new List<string>();
|
var components = new List<string>();
|
||||||
|
|
||||||
|
|
@ -104,7 +97,17 @@ public string BuildBookFileName(Author author, Edition edition, BookFile bookFil
|
||||||
{
|
{
|
||||||
var splitPattern = s;
|
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();
|
var component = ReplacePartTokens(splitPattern, tokenHandlers, namingConfig).Trim();
|
||||||
|
component = ReplaceTokens(component, tokenHandlers, namingConfig, true).Trim();
|
||||||
|
|
||||||
|
AddBookTokens(tokenHandlers, edition);
|
||||||
component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim();
|
component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim();
|
||||||
|
|
||||||
component = FileNameCleanupRegex.Replace(component, match => match.Captures[0].Value[0].ToString());
|
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)
|
private void AddBookTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Edition edition)
|
||||||
{
|
{
|
||||||
tokenHandlers["{Book Title}"] = m => edition.Title;
|
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));
|
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
|
var tokenMatch = new TokenMatch
|
||||||
{
|
{
|
||||||
RegexMatch = match,
|
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 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)))
|
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;
|
replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (escape)
|
||||||
|
{
|
||||||
|
replacementText = replacementText.Replace("{", "{{").Replace("}", "}}");
|
||||||
|
}
|
||||||
|
|
||||||
return replacementText;
|
return replacementText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue