mirror of
https://github.com/Radarr/Radarr
synced 2024-12-29 11:25:26 +00:00
Fixed: Multiple SeasonEpisode formats in the same pattern are now supported.
This commit is contained in:
parent
ec4dc89142
commit
49e2f26ffc
2 changed files with 151 additions and 117 deletions
|
@ -487,6 +487,26 @@ public void should_replace_absolute_numbering_when_series_is_anime()
|
||||||
.Should().Be("South.Park.100.City.Sushi");
|
.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<Episode> { _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<Episode> { _episode1, _episode2 }, _series, _episodeFile)
|
||||||
|
.Should().Be("South Park Season 0015 Episode 0006-0007\\South.Park.S15E06-07.100-101.City.Sushi");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_use_dash_as_separator_when_multi_episode_style_is_extend_for_anime()
|
public void should_use_dash_as_separator_when_multi_episode_style_is_extend_for_anime()
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,7 +26,8 @@ public class FileNameBuilder : IBuildFileNames
|
||||||
{
|
{
|
||||||
private readonly INamingConfigService _namingConfigService;
|
private readonly INamingConfigService _namingConfigService;
|
||||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
private readonly IQualityDefinitionService _qualityDefinitionService;
|
||||||
private readonly ICached<EpisodeFormat> _patternCache;
|
private readonly ICached<EpisodeFormat[]> _episodeFormatCache;
|
||||||
|
private readonly ICached<AbsoluteEpisodeFormat[]> _absoluteEpisodeFormatCache;
|
||||||
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(@"\{(?<prefix>[- ._]*)(?<token>(?:[a-z0-9]+)(?:(?<separator>[- ._]+)(?:[a-z0-9]+))?)(?::(?<customFormat>[a-z0-9]+))?(?<suffix>[- ._]*)\}",
|
||||||
|
@ -63,7 +64,8 @@ public FileNameBuilder(INamingConfigService namingConfigService,
|
||||||
{
|
{
|
||||||
_namingConfigService = namingConfigService;
|
_namingConfigService = namingConfigService;
|
||||||
_qualityDefinitionService = qualityDefinitionService;
|
_qualityDefinitionService = qualityDefinitionService;
|
||||||
_patternCache = cacheManager.GetCache<EpisodeFormat>(GetType());
|
_episodeFormatCache = cacheManager.GetCache<EpisodeFormat[]>(GetType(), "episodeFormat");
|
||||||
|
_absoluteEpisodeFormatCache = cacheManager.GetCache<AbsoluteEpisodeFormat[]>(GetType(), "absoluteEpisodeFormat");
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,14 +102,6 @@ public string BuildFileName(List<Episode> episodes, Series series, EpisodeFile e
|
||||||
|
|
||||||
episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList();
|
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)
|
if (series.SeriesType == SeriesTypes.Daily)
|
||||||
{
|
{
|
||||||
pattern = namingConfig.DailyEpisodeFormat;
|
pattern = namingConfig.DailyEpisodeFormat;
|
||||||
|
@ -118,85 +112,18 @@ public string BuildFileName(List<Episode> episodes, Series series, EpisodeFile e
|
||||||
pattern = namingConfig.AnimeEpisodeFormat;
|
pattern = namingConfig.AnimeEpisodeFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
var episodeFormat = GetEpisodeFormat(pattern);
|
pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig);
|
||||||
|
|
||||||
if (episodeFormat != null)
|
pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig);
|
||||||
{
|
|
||||||
pattern = pattern.Replace(episodeFormat.SeasonEpisodePattern, "{Season Episode}");
|
|
||||||
var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern;
|
|
||||||
|
|
||||||
foreach (var episode in episodes.Skip(1))
|
AddSeriesTokens(tokenHandlers, series);
|
||||||
{
|
|
||||||
switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle)
|
|
||||||
{
|
|
||||||
case MultiEpisodeStyle.Duplicate:
|
|
||||||
seasonEpisodePattern += episodeFormat.Separator + episodeFormat.SeasonEpisodePattern;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MultiEpisodeStyle.Repeat:
|
AddEpisodeTokens(tokenHandlers, episodes);
|
||||||
seasonEpisodePattern += episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MultiEpisodeStyle.Scene:
|
AddEpisodeFileTokens(tokenHandlers, episodeFile);
|
||||||
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);
|
|
||||||
|
|
||||||
if (absoluteEpisodeFormat != null)
|
|
||||||
{
|
|
||||||
if (series.SeriesType != SeriesTypes.Anime)
|
|
||||||
{
|
|
||||||
pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, "{Absolute Pattern}");
|
|
||||||
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);
|
|
||||||
tokenHandlers["{Absolute Pattern}"] = m => absoluteEpisodePattern;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
AddMediaInfoTokens(tokenHandlers, episodeFile);
|
||||||
|
|
||||||
var filename = ReplaceTokens(pattern, tokenHandlers).Trim();
|
var filename = ReplaceTokens(pattern, tokenHandlers).Trim();
|
||||||
filename = FileNameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString());
|
filename = FileNameCleanupRegex.Replace(filename, match => match.Captures[0].Value[0].ToString());
|
||||||
|
|
||||||
|
@ -234,7 +161,7 @@ public string BuildFilePath(Series series, int seasonNumber, string fileName, st
|
||||||
|
|
||||||
public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec)
|
public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec)
|
||||||
{
|
{
|
||||||
var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat);
|
var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault();
|
||||||
|
|
||||||
if (episodeFormat == null)
|
if (episodeFormat == null)
|
||||||
{
|
{
|
||||||
|
@ -347,9 +274,112 @@ private void AddSeriesTokens(Dictionary<String, Func<TokenMatch, String>> tokenH
|
||||||
tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title);
|
tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String AddSeasonEpisodeNumberingTokens(String pattern, Dictionary<String, Func<TokenMatch, String>> tokenHandlers, List<Episode> 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<String, Func<TokenMatch, String>> tokenHandlers, Series series, List<Episode> 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<String, Func<TokenMatch, String>> tokenHandlers, Int32 seasonNumber)
|
private void AddSeasonTokens(Dictionary<String, Func<TokenMatch, String>> tokenHandlers, Int32 seasonNumber)
|
||||||
{
|
{
|
||||||
tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat ?? "0");
|
tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddEpisodeTokens(Dictionary<String, Func<TokenMatch, String>> tokenHandlers, List<Episode> episodes)
|
private void AddEpisodeTokens(Dictionary<String, Func<TokenMatch, String>> tokenHandlers, List<Episode> episodes)
|
||||||
|
@ -579,42 +609,26 @@ private string ReplaceNumberToken(string token, int value)
|
||||||
return value.ToString(split[1]);
|
return value.ToString(split[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private EpisodeFormat GetEpisodeFormat(string pattern)
|
private EpisodeFormat[] GetEpisodeFormat(string pattern)
|
||||||
{
|
{
|
||||||
return _patternCache.Get(pattern, () =>
|
return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType<Match>()
|
||||||
{
|
.Select(match => new EpisodeFormat
|
||||||
var match = SeasonEpisodePatternRegex.Match(pattern);
|
|
||||||
|
|
||||||
if (match.Success)
|
|
||||||
{
|
{
|
||||||
return new EpisodeFormat
|
EpisodeSeparator = match.Groups["episodeSeparator"].Value,
|
||||||
{
|
Separator = match.Groups["separator"].Value,
|
||||||
EpisodeSeparator = match.Groups["episodeSeparator"].Value,
|
EpisodePattern = match.Groups["episode"].Value,
|
||||||
Separator = match.Groups["separator"].Value,
|
SeasonEpisodePattern = match.Groups["seasonEpisode"].Value,
|
||||||
EpisodePattern = match.Groups["episode"].Value,
|
}).ToArray());
|
||||||
SeasonEpisodePattern = match.Groups["seasonEpisode"].Value,
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AbsoluteEpisodeFormat GetAbsoluteFormat(string pattern)
|
private AbsoluteEpisodeFormat[] GetAbsoluteFormat(string pattern)
|
||||||
{
|
{
|
||||||
var match = AbsoluteEpisodePatternRegex.Match(pattern);
|
return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType<Match>()
|
||||||
|
.Select(match => new AbsoluteEpisodeFormat
|
||||||
if (match.Success)
|
{
|
||||||
{
|
Separator = match.Groups["separator"].Value,
|
||||||
return new AbsoluteEpisodeFormat
|
AbsoluteEpisodePattern = match.Groups["absolute"].Value
|
||||||
{
|
}).ToArray());
|
||||||
Separator = match.Groups["separator"].Value,
|
|
||||||
AbsoluteEpisodePattern = match.Groups["absolute"].Value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String GetEpisodeTitle(List<Episode> episodes)
|
private String GetEpisodeTitle(List<Episode> episodes)
|
||||||
|
|
Loading…
Reference in a new issue