|
|
|
@ -26,7 +26,8 @@ namespace NzbDrone.Core.Organizer
|
|
|
|
|
{
|
|
|
|
|
private readonly INamingConfigService _namingConfigService;
|
|
|
|
|
private readonly IQualityDefinitionService _qualityDefinitionService;
|
|
|
|
|
private readonly ICached<EpisodeFormat> _patternCache;
|
|
|
|
|
private readonly ICached<EpisodeFormat[]> _episodeFormatCache;
|
|
|
|
|
private readonly ICached<AbsoluteEpisodeFormat[]> _absoluteEpisodeFormatCache;
|
|
|
|
|
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>[- ._]*)\}",
|
|
|
|
@ -63,7 +64,8 @@ namespace NzbDrone.Core.Organizer
|
|
|
|
|
{
|
|
|
|
|
_namingConfigService = namingConfigService;
|
|
|
|
|
_qualityDefinitionService = qualityDefinitionService;
|
|
|
|
|
_patternCache = cacheManager.GetCache<EpisodeFormat>(GetType());
|
|
|
|
|
_episodeFormatCache = cacheManager.GetCache<EpisodeFormat[]>(GetType(), "episodeFormat");
|
|
|
|
|
_absoluteEpisodeFormatCache = cacheManager.GetCache<AbsoluteEpisodeFormat[]>(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<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)
|
|
|
|
|
{
|
|
|
|
|
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)
|
|
|
|
@ -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<Match>()
|
|
|
|
|
.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<Match>()
|
|
|
|
|
.Select(match => new AbsoluteEpisodeFormat
|
|
|
|
|
{
|
|
|
|
|
Separator = match.Groups["separator"].Value,
|
|
|
|
|
AbsoluteEpisodePattern = match.Groups["absolute"].Value
|
|
|
|
|
}).ToArray());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String GetEpisodeTitle(List<Episode> episodes)
|
|
|
|
|