From 12e74aa38b4fd98b7048df42cffae5203fa56e1e Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Sun, 22 Jan 2017 18:18:15 +0100 Subject: [PATCH] Change Forms Auth Cookie. Fixes #285 --- .../Authentication/EnableAuthInNancy.cs | 2 + .../Organizer/FileNameBuilder.cs | 2106 ++++++++--------- 2 files changed, 1055 insertions(+), 1053 deletions(-) diff --git a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs index f6efc16ce..7aa1bce2a 100644 --- a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs +++ b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs @@ -64,6 +64,8 @@ namespace NzbDrone.Api.Authentication new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))) ); + FormsAuthentication.FormsAuthenticationCookieName = "_ncfaradarr"; //For those people that both have sonarr and radarr. + FormsAuthentication.Enable(pipelines, new FormsAuthenticationConfiguration { RedirectUrl = _configFileProvider.UrlBase + "/login", diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 535a89071..df327bee5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -1,321 +1,321 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using NLog; -using NzbDrone.Common.Cache; -using NzbDrone.Common.EnsureThat; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; - -namespace NzbDrone.Core.Organizer -{ - public interface IBuildFileNames - { - string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); - string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); - string BuildFilePath(Movie movie, string fileName, string extension); - string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); - string BuildSeasonPath(Series series, int seasonNumber); - BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); - string GetSeriesFolder(Series series, NamingConfig namingConfig = null); - string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); - string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); - } - - public class FileNameBuilder : IBuildFileNames - { - private readonly INamingConfigService _namingConfigService; - private readonly IQualityDefinitionService _qualityDefinitionService; - private readonly ICached _episodeFormatCache; - private readonly ICached _absoluteEpisodeFormatCache; - private readonly Logger _logger; - - private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?{absolute(?:\:0+)?})(?[- ._]+?(?={))?", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?Title(The)?)\})", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); - private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); - - private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) - private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; - - public FileNameBuilder(INamingConfigService namingConfigService, - IQualityDefinitionService qualityDefinitionService, - ICacheManager cacheManager, - Logger logger) - { - _namingConfigService = namingConfigService; - _qualityDefinitionService = qualityDefinitionService; - //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); - _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); - _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); - _logger = logger; - } - - public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(episodeFile); - } - - if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) - { - throw new NamingFormatException("Standard episode format cannot be empty"); - } - - if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) - { - throw new NamingFormatException("Daily episode format cannot be empty"); - } - - if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) - { - throw new NamingFormatException("Anime episode format cannot be empty"); - } - - var pattern = namingConfig.StandardEpisodeFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); - - if (series.SeriesType == SeriesTypes.Daily) - { - pattern = namingConfig.DailyEpisodeFormat; - } - - if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) - { - pattern = namingConfig.AnimeEpisodeFormat; - } - - pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); - pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); - - AddSeriesTokens(tokenHandlers, series); - AddEpisodeTokens(tokenHandlers, episodes); - AddEpisodeFileTokens(tokenHandlers, episodeFile); - AddQualityTokens(tokenHandlers, series, episodeFile); - AddMediaInfoTokens(tokenHandlers, episodeFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - if (!namingConfig.RenameEpisodes) - { - return GetOriginalTitle(movieFile); - } - - //TODO: Update namingConfig for Movies! - var pattern = namingConfig.StandardMovieFormat; - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - AddQualityTokens(tokenHandlers, movie, movieFile); - AddMediaInfoTokens(tokenHandlers, movieFile); - AddMovieFileTokens(tokenHandlers, movieFile); - - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); - fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); - fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); - - return fileName; - } - - public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = BuildSeasonPath(series, seasonNumber); - - return Path.Combine(path, fileName + extension); - } - - public string BuildFilePath(Movie movie, string fileName, string extension) - { - Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); - - var path = movie.Path; - - return Path.Combine(path, fileName + extension); - } - - public string BuildSeasonPath(Series series, int seasonNumber) - { - var path = series.Path; - - if (series.SeasonFolder) - { - if (seasonNumber == 0) - { - path = Path.Combine(path, "Specials"); - } - else - { - var seasonFolder = GetSeasonFolder(series, seasonNumber); - - seasonFolder = CleanFileName(seasonFolder); - - path = Path.Combine(path, seasonFolder); - } - } - - return path; - } - - public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) - { - return new BasicNamingConfig(); //For now let's be lazy - - var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); - - if (episodeFormat == null) - { - return new BasicNamingConfig(); - } - - var basicNamingConfig = new BasicNamingConfig - { - Separator = episodeFormat.Separator, - NumberStyle = episodeFormat.SeasonEpisodePattern - }; - - var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); - - foreach (Match match in titleTokens) - { - var separator = match.Groups["separator"].Value; - var token = match.Groups["token"].Value; - - if (!separator.Equals(" ")) - { - basicNamingConfig.ReplaceSpaces = true; - } - - if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeSeriesTitle = true; - } - - if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeEpisodeTitle = true; - } - - if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) - { - basicNamingConfig.IncludeQuality = true; - } - } - - return basicNamingConfig; - } - - public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - - return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) - { - if (namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddSeriesTokens(tokenHandlers, series); - AddSeasonTokens(tokenHandlers, seasonNumber); - - return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); - } - - public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) - { - if(namingConfig == null) - { - namingConfig = _namingConfigService.GetConfig(); - } - - var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - - AddMovieTokens(tokenHandlers, movie); - AddReleaseDateTokens(tokenHandlers, movie.Year); - AddImdbIdTokens(tokenHandlers, movie.ImdbId); - - return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); - } - - public static string CleanTitle(string title) - { - title = title.Replace("&", "and"); - title = ScenifyReplaceChars.Replace(title, " "); - title = ScenifyRemoveChars.Replace(title, string.Empty); - - return title; - } - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Organizer +{ + public interface IBuildFileNames + { + string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null); + string BuildFilePath(Movie movie, string fileName, string extension); + string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); + string BuildSeasonPath(Series series, int seasonNumber); + BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); + string GetSeriesFolder(Series series, NamingConfig namingConfig = null); + string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null); + string GetMovieFolder(Movie movie, NamingConfig namingConfig = null); + } + + public class FileNameBuilder : IBuildFileNames + { + private readonly INamingConfigService _namingConfigService; + private readonly IQualityDefinitionService _qualityDefinitionService; + private readonly ICached _episodeFormatCache; + private readonly ICached _absoluteEpisodeFormatCache; + private readonly Logger _logger; + + private static readonly Regex TitleRegex = new Regex(@"\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9]+))?(?[- ._)\]]*)\}", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex SeasonEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?s?{season(?:\:0+)?}(?[- ._]?[ex])(?{episode(?:\:0+)?}))(?[- ._]+?(?={))?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AbsoluteEpisodePatternRegex = new Regex(@"(?(?<=})[- ._]+?)?(?{absolute(?:\:0+)?})(?[- ._]+?(?={))?", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex AirDateRegex = new Regex(@"\{Air(\s|\W|_)Date\}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex SeriesTitleRegex = new Regex(@"(?\{(?:Series)(?[- ._])(Clean)?Title\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?Title(The)?)\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); + private static readonly Regex TrimSeparatorsRegex = new Regex(@"[- ._]$", RegexOptions.Compiled); + + private static readonly Regex ScenifyRemoveChars = new Regex(@"(?<=\s)(,|<|>|\/|\\|;|:|'|""|\||`|~|!|\?|@|$|%|^|\*|-|_|=){1}(?=\s)|('|:|\?|,)(?=(?:(?:s|m)\s)|\s|$)|(\(|\)|\[|\]|\{|\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ScenifyReplaceChars = new Regex(@"[\/]", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) + private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; + + public FileNameBuilder(INamingConfigService namingConfigService, + IQualityDefinitionService qualityDefinitionService, + ICacheManager cacheManager, + Logger logger) + { + _namingConfigService = namingConfigService; + _qualityDefinitionService = qualityDefinitionService; + //_movieFormatCache = cacheManager.GetCache(GetType(), "movieFormat"); + _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); + _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); + _logger = logger; + } + + public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(episodeFile); + } + + if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard) + { + throw new NamingFormatException("Standard episode format cannot be empty"); + } + + if (namingConfig.DailyEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Daily) + { + throw new NamingFormatException("Daily episode format cannot be empty"); + } + + if (namingConfig.AnimeEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Anime) + { + throw new NamingFormatException("Anime episode format cannot be empty"); + } + + var pattern = namingConfig.StandardEpisodeFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + episodes = episodes.OrderBy(e => e.SeasonNumber).ThenBy(e => e.EpisodeNumber).ToList(); + + if (series.SeriesType == SeriesTypes.Daily) + { + pattern = namingConfig.DailyEpisodeFormat; + } + + if (series.SeriesType == SeriesTypes.Anime && episodes.All(e => e.AbsoluteEpisodeNumber.HasValue)) + { + pattern = namingConfig.AnimeEpisodeFormat; + } + + pattern = AddSeasonEpisodeNumberingTokens(pattern, tokenHandlers, episodes, namingConfig); + pattern = AddAbsoluteNumberingTokens(pattern, tokenHandlers, series, episodes, namingConfig); + + AddSeriesTokens(tokenHandlers, series); + AddEpisodeTokens(tokenHandlers, episodes); + AddEpisodeFileTokens(tokenHandlers, episodeFile); + AddQualityTokens(tokenHandlers, series, episodeFile); + AddMediaInfoTokens(tokenHandlers, episodeFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + + public string BuildFileName(Movie movie, MovieFile movieFile, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + if (!namingConfig.RenameEpisodes) + { + return GetOriginalTitle(movieFile); + } + + //TODO: Update namingConfig for Movies! + var pattern = namingConfig.StandardMovieFormat; + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); //In case we want to separate the year + AddImdbIdTokens(tokenHandlers, movie.ImdbId); + AddQualityTokens(tokenHandlers, movie, movieFile); + AddMediaInfoTokens(tokenHandlers, movieFile); + AddMovieFileTokens(tokenHandlers, movieFile); + + var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); + fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); + + return fileName; + } + + public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = BuildSeasonPath(series, seasonNumber); + + return Path.Combine(path, fileName + extension); + } + + public string BuildFilePath(Movie movie, string fileName, string extension) + { + Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); + + var path = movie.Path; + + return Path.Combine(path, fileName + extension); + } + + public string BuildSeasonPath(Series series, int seasonNumber) + { + var path = series.Path; + + if (series.SeasonFolder) + { + if (seasonNumber == 0) + { + path = Path.Combine(path, "Specials"); + } + else + { + var seasonFolder = GetSeasonFolder(series, seasonNumber); + + seasonFolder = CleanFileName(seasonFolder); + + path = Path.Combine(path, seasonFolder); + } + } + + return path; + } + + public BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec) + { + return new BasicNamingConfig(); //For now let's be lazy + + var episodeFormat = GetEpisodeFormat(nameSpec.StandardEpisodeFormat).LastOrDefault(); + + if (episodeFormat == null) + { + return new BasicNamingConfig(); + } + + var basicNamingConfig = new BasicNamingConfig + { + Separator = episodeFormat.Separator, + NumberStyle = episodeFormat.SeasonEpisodePattern + }; + + var titleTokens = TitleRegex.Matches(nameSpec.StandardEpisodeFormat); + + foreach (Match match in titleTokens) + { + var separator = match.Groups["separator"].Value; + var token = match.Groups["token"].Value; + + if (!separator.Equals(" ")) + { + basicNamingConfig.ReplaceSpaces = true; + } + + if (token.StartsWith("{Series", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeSeriesTitle = true; + } + + if (token.StartsWith("{Episode", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeEpisodeTitle = true; + } + + if (token.StartsWith("{Quality", StringComparison.InvariantCultureIgnoreCase)) + { + basicNamingConfig.IncludeQuality = true; + } + } + + return basicNamingConfig; + } + + public string GetSeriesFolder(Series series, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddSeriesTokens(tokenHandlers, series); + + return CleanFolderName(ReplaceTokens(namingConfig.SeriesFolderFormat, tokenHandlers, namingConfig)); + } + + public string GetSeasonFolder(Series series, int seasonNumber, NamingConfig namingConfig = null) + { + if (namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddSeriesTokens(tokenHandlers, series); + AddSeasonTokens(tokenHandlers, seasonNumber); + + return CleanFolderName(ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig)); + } + + public string GetMovieFolder(Movie movie, NamingConfig namingConfig = null) + { + if(namingConfig == null) + { + namingConfig = _namingConfigService.GetConfig(); + } + + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); + + AddMovieTokens(tokenHandlers, movie); + AddReleaseDateTokens(tokenHandlers, movie.Year); + AddImdbIdTokens(tokenHandlers, movie.ImdbId); + + return CleanFolderName(ReplaceTokens(namingConfig.MovieFolderFormat, tokenHandlers, namingConfig)); + } + + public static string CleanTitle(string title) + { + title = title.Replace("&", "and"); + title = ScenifyReplaceChars.Replace(title, " "); + title = ScenifyRemoveChars.Replace(title, string.Empty); + + return title; + } + public static string TitleThe(string title) { string[] prefixes = { "The ", "An ", "A " }; @@ -330,738 +330,738 @@ namespace NzbDrone.Core.Organizer } return title.Trim(); - } - - public static string CleanFileName(string name, bool replace = true) - { - string result = name; - string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; - string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "", "" }; - - for (int i = 0; i < badCharacters.Length; i++) - { - result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty); - } - - return result.Trim(); - } - - public static string CleanFolderName(string name) - { - name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); - name = name.Trim(' ', '.'); - - return CleanFileName(name); - } - - private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) - { - tokenHandlers["{Series Title}"] = m => series.Title; - tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); - } - - private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) - { - var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); - - int index = 1; - foreach (var episodeFormat in episodeFormats) - { - var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; - string formatPattern; - - switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) - { - case MultiEpisodeStyle.Duplicate: - formatPattern = episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Repeat: - formatPattern = episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Scene: - formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Range: - formatPattern = "-" + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.PrefixedRange: - formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - - //MultiEpisodeStyle.Extend - default: - formatPattern = "-" + episodeFormat.EpisodePattern; - seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); - break; - } - - 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> tokenHandlers, Series series, List 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; - string formatPattern; - - switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) - { - - case MultiEpisodeStyle.Duplicate: - formatPattern = absoluteEpisodeFormat.Separator + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Repeat: - var repeatSeparator = absoluteEpisodeFormat.Separator.Trim().IsNullOrWhiteSpace() ? " " : absoluteEpisodeFormat.Separator.Trim(); - - formatPattern = repeatSeparator + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Scene: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - - case MultiEpisodeStyle.Range: - case MultiEpisodeStyle.PrefixedRange: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - var eps = new List {episodes.First()}; - - if (episodes.Count > 1) eps.Add(episodes.Last()); - - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); - break; - - //MultiEpisodeStyle.Extend - default: - formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; - absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); - break; - } - - var token = string.Format("{{Absolute Pattern{0}}}", index++); - pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token); - tokenHandlers[token] = m => absoluteEpisodePattern; - } - - return pattern; - } - - private void AddMovieTokens(Dictionary> tokenHandlers, Movie movie) - { - tokenHandlers["{Movie Title}"] = m => movie.Title; - tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); - tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); - } - - private void AddReleaseDateTokens(Dictionary> tokenHandlers, int releaseYear) - { - tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? - } - - private void AddImdbIdTokens(Dictionary> tokenHandlers, string imdbId) - { - tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; - } - - private void AddSeasonTokens(Dictionary> tokenHandlers, int seasonNumber) - { - tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); - } - - private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) - { - if (!episodes.First().AirDate.IsNullOrWhiteSpace()) - { - tokenHandlers["{Air Date}"] = m => episodes.First().AirDate.Replace('-', ' '); - } - else - { - tokenHandlers["{Air Date}"] = m => "Unknown"; - } - - tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(episodes, "+"); - tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); - } - - private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) - { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); - } - - private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) - { - tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); - tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); - //tokenHandlers["{IMDb Id}"] = m => - tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); - } - - private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) - { - var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; - var qualityProper = GetQualityProper(series, episodeFile.Quality); - var qualityReal = GetQualityReal(series, episodeFile.Quality); - - tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); - tokenHandlers["{Quality Title}"] = m => qualityTitle; - tokenHandlers["{Quality Proper}"] = m => qualityProper; - tokenHandlers["{Quality Real}"] = m => qualityReal; - } - - private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) - { - var qualityTitle = _qualityDefinitionService.Get(movieFile.Quality.Quality).Title; - var qualityProper = GetQualityProper(movie, movieFile.Quality); - var qualityReal = GetQualityReal(movie, movieFile.Quality); - - tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); - tokenHandlers["{Quality Title}"] = m => qualityTitle; - tokenHandlers["{Quality Proper}"] = m => qualityProper; - tokenHandlers["{Quality Real}"] = m => qualityReal; - } - - private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) - { - if (episodeFile.MediaInfo == null) return; - - string videoCodec; - switch (episodeFile.MediaInfo.VideoCodec) - { - case "AVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = episodeFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (episodeFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "MPEG Audio": - if (episodeFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = episodeFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - - default: - audioCodec = episodeFile.MediaInfo.AudioFormat; - break; - } - - var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); - if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) - { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); - } - - if (mediaInfoAudioLanguages == "[EN]") - { - mediaInfoAudioLanguages = string.Empty; - } - - var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); - if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) - { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); - } - - var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = episodeFile.MediaInfo.FormattedAudioChannels > 0 ? - episodeFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; - - tokenHandlers["{MediaInfo Video}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; - - tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); - } - - private void AddMediaInfoTokens(Dictionary> tokenHandlers, MovieFile movieFile) - { - if (movieFile.MediaInfo == null) return; - - string videoCodec; - switch (movieFile.MediaInfo.VideoCodec) - { - case "AVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) - { - videoCodec = "h264"; - } - else - { - videoCodec = "x264"; - } - break; - - case "V_MPEGH/ISO/HEVC": - if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) - { - videoCodec = "h265"; - } - else - { - videoCodec = "x265"; - } - break; - - case "MPEG-2 Video": - videoCodec = "MPEG2"; - break; - - default: - videoCodec = movieFile.MediaInfo.VideoCodec; - break; - } - - string audioCodec; - switch (movieFile.MediaInfo.AudioFormat) - { - case "AC-3": - audioCodec = "AC3"; - break; - - case "E-AC-3": - audioCodec = "EAC3"; - break; - - case "MPEG Audio": - if (movieFile.MediaInfo.AudioProfile == "Layer 3") - { - audioCodec = "MP3"; - } - else - { - audioCodec = movieFile.MediaInfo.AudioFormat; - } - break; - - case "DTS": - audioCodec = movieFile.MediaInfo.AudioFormat; - break; - - default: - audioCodec = movieFile.MediaInfo.AudioFormat; - break; - } - - var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); - if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) - { - mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); - } - - if (mediaInfoAudioLanguages == "[EN]") - { - mediaInfoAudioLanguages = string.Empty; - } - - var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); - if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) - { - mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); - } - - var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; - var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? - movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : - string.Empty; - - tokenHandlers["{MediaInfo Video}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; - tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; - - tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; - tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; - - tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); - - tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); - } - - private string GetLanguagesToken(string mediaInfoLanguages) - { - List tokens = new List(); - foreach (var item in mediaInfoLanguages.Split('/')) - { - if (!string.IsNullOrWhiteSpace(item)) - tokens.Add(item.Trim()); - } - - var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); - for (int i = 0; i < tokens.Count; i++) - { - try - { - var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); - - if (cultureInfo != null) - tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); - } - catch - { - } - } - - return string.Join("+", tokens.Distinct()); - } - - private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) - { - return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig)); - } - - private string ReplaceToken(Match match, Dictionary> tokenHandlers, NamingConfig namingConfig) - { - var tokenMatch = new TokenMatch - { - RegexMatch = match, - Prefix = match.Groups["prefix"].Value, - Separator = match.Groups["separator"].Value, - Suffix = match.Groups["suffix"].Value, - Token = match.Groups["token"].Value, - CustomFormat = match.Groups["customFormat"].Value - }; - - if (tokenMatch.CustomFormat.IsNullOrWhiteSpace()) - { - tokenMatch.CustomFormat = null; - } - - var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty); - - var replacementText = tokenHandler(tokenMatch).Trim(); - - if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) - { - replacementText = replacementText.ToLower(); - } - else if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsUpper(t))) - { - replacementText = replacementText.ToUpper(); - } - - if (!tokenMatch.Separator.IsNullOrWhiteSpace()) - { - replacementText = replacementText.Replace(" ", tokenMatch.Separator); - } - - replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); - - if (!replacementText.IsNullOrWhiteSpace()) - { - replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; - } - - return replacementText; - } - - private string FormatNumberTokens(string basePattern, string formatPattern, List episodes) - { - var pattern = string.Empty; - - for (int i = 0; i < episodes.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += EpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["episode"].Value, episodes[i].EpisodeNumber)); - } - - return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); - } - - private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) - { - var pattern = string.Empty; - - for (int i = 0; i < episodes.Count; i++) - { - var patternToReplace = i == 0 ? basePattern : formatPattern; - - pattern += AbsoluteEpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["absolute"].Value, episodes[i].AbsoluteEpisodeNumber.Value)); - } - - return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); - } - - private string FormatRangeNumberTokens(string seasonEpisodePattern, string formatPattern, List episodes) - { - var eps = new List { episodes.First() }; - - if (episodes.Count > 1) eps.Add(episodes.Last()); - - return FormatNumberTokens(seasonEpisodePattern, formatPattern, eps); - } - - private string ReplaceSeasonTokens(string pattern, int seasonNumber) - { - return SeasonRegex.Replace(pattern, match => ReplaceNumberToken(match.Groups["season"].Value, seasonNumber)); - } - - private string ReplaceNumberToken(string token, int value) - { - var split = token.Trim('{', '}').Split(':'); - if (split.Length == 1) return value.ToString("0"); - - return value.ToString(split[1]); - } - - private EpisodeFormat[] GetEpisodeFormat(string pattern) - { - return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() - .Select(match => new EpisodeFormat - { - 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) - { - return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() - .Select(match => new AbsoluteEpisodeFormat - { - Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", - AbsoluteEpisodePattern = match.Groups["absolute"].Value - }).ToArray()); - } - - private string GetEpisodeTitle(List episodes, string separator) - { - separator = string.Format(" {0} ", separator.Trim()); - - if (episodes.Count == 1) - { - return episodes.First().Title.TrimEnd(EpisodeTitleTrimCharacters); - } - - var titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) - .Select(CleanupEpisodeTitle) - .Distinct() - .ToList(); - - if (titles.All(t => t.IsNullOrWhiteSpace())) - { - titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) - .Distinct() - .ToList(); - } - - return string.Join(separator, titles); - } - - private string CleanupEpisodeTitle(string title) - { - //this will remove (1),(2) from the end of multi part episodes. - return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); - } - - private string GetQualityProper(Movie movie, QualityModel quality) - { - if (quality.Revision.Version > 1) - { - return "Proper"; - } - - return String.Empty; - } - - private string GetQualityProper(Series series, QualityModel quality) - { - if (quality.Revision.Version > 1) - { - if (series.SeriesType == SeriesTypes.Anime) - { - return "v" + quality.Revision.Version; - } - - return "Proper"; - } - - return String.Empty; - } - - private string GetQualityReal(Series series, QualityModel quality) - { - if (quality.Revision.Real > 0) - { - return "REAL"; - } - - return string.Empty; - } - - private string GetQualityReal(Movie movie, QualityModel quality) - { - if (quality.Revision.Real > 0) - { - return "REAL"; - } - - return string.Empty; - } - - private string GetOriginalTitle(EpisodeFile episodeFile) - { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - return GetOriginalFileName(episodeFile); - } - - return episodeFile.SceneName; - } - - private string GetOriginalFileName(EpisodeFile episodeFile) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - - private string GetOriginalTitle(MovieFile episodeFile) - { - if (episodeFile.SceneName.IsNullOrWhiteSpace()) - { - return GetOriginalFileName(episodeFile); - } - - return episodeFile.SceneName; - } - - private string GetOriginalFileName(MovieFile episodeFile) - { - if (episodeFile.RelativePath.IsNullOrWhiteSpace()) - { - return Path.GetFileNameWithoutExtension(episodeFile.Path); - } - - return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); - } - } - - internal sealed class TokenMatch - { - public Match RegexMatch { get; set; } - public string Prefix { get; set; } - public string Separator { get; set; } - public string Suffix { get; set; } - public string Token { get; set; } - public string CustomFormat { get; set; } - - public string DefaultValue(string defaultValue) - { - if (string.IsNullOrEmpty(Prefix) && string.IsNullOrEmpty(Suffix)) - { - return defaultValue; - } - else - { - return string.Empty; - } - } - } - - public enum MultiEpisodeStyle - { - Extend = 0, - Duplicate = 1, - Repeat = 2, - Scene = 3, - Range = 4, - PrefixedRange = 5 - } -} + } + + public static string CleanFileName(string name, bool replace = true) + { + string result = name; + string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" }; + string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "", "" }; + + for (int i = 0; i < badCharacters.Length; i++) + { + result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty); + } + + return result.Trim(); + } + + public static string CleanFolderName(string name) + { + name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString()); + name = name.Trim(' ', '.'); + + return CleanFileName(name); + } + + private void AddSeriesTokens(Dictionary> tokenHandlers, Series series) + { + tokenHandlers["{Series Title}"] = m => series.Title; + tokenHandlers["{Series CleanTitle}"] = m => CleanTitle(series.Title); + } + + private string AddSeasonEpisodeNumberingTokens(string pattern, Dictionary> tokenHandlers, List episodes, NamingConfig namingConfig) + { + var episodeFormats = GetEpisodeFormat(pattern).DistinctBy(v => v.SeasonEpisodePattern).ToList(); + + int index = 1; + foreach (var episodeFormat in episodeFormats) + { + var seasonEpisodePattern = episodeFormat.SeasonEpisodePattern; + string formatPattern; + + switch ((MultiEpisodeStyle)namingConfig.MultiEpisodeStyle) + { + case MultiEpisodeStyle.Duplicate: + formatPattern = episodeFormat.Separator + episodeFormat.SeasonEpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Repeat: + formatPattern = episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Scene: + formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Range: + formatPattern = "-" + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.PrefixedRange: + formatPattern = "-" + episodeFormat.EpisodeSeparator + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatRangeNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + + //MultiEpisodeStyle.Extend + default: + formatPattern = "-" + episodeFormat.EpisodePattern; + seasonEpisodePattern = FormatNumberTokens(seasonEpisodePattern, formatPattern, episodes); + break; + } + + 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> tokenHandlers, Series series, List 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; + string formatPattern; + + switch ((MultiEpisodeStyle) namingConfig.MultiEpisodeStyle) + { + + case MultiEpisodeStyle.Duplicate: + formatPattern = absoluteEpisodeFormat.Separator + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Repeat: + var repeatSeparator = absoluteEpisodeFormat.Separator.Trim().IsNullOrWhiteSpace() ? " " : absoluteEpisodeFormat.Separator.Trim(); + + formatPattern = repeatSeparator + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Scene: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + + case MultiEpisodeStyle.Range: + case MultiEpisodeStyle.PrefixedRange: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + var eps = new List {episodes.First()}; + + if (episodes.Count > 1) eps.Add(episodes.Last()); + + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, eps); + break; + + //MultiEpisodeStyle.Extend + default: + formatPattern = "-" + absoluteEpisodeFormat.AbsoluteEpisodePattern; + absoluteEpisodePattern = FormatAbsoluteNumberTokens(absoluteEpisodePattern, formatPattern, episodes); + break; + } + + var token = string.Format("{{Absolute Pattern{0}}}", index++); + pattern = pattern.Replace(absoluteEpisodeFormat.AbsoluteEpisodePattern, token); + tokenHandlers[token] = m => absoluteEpisodePattern; + } + + return pattern; + } + + private void AddMovieTokens(Dictionary> tokenHandlers, Movie movie) + { + tokenHandlers["{Movie Title}"] = m => movie.Title; + tokenHandlers["{Movie CleanTitle}"] = m => CleanTitle(movie.Title); + tokenHandlers["{Movie Title The}"] = m => TitleThe(movie.Title); + } + + private void AddReleaseDateTokens(Dictionary> tokenHandlers, int releaseYear) + { + tokenHandlers["{Release Year}"] = m => string.Format("{0}", releaseYear.ToString()); //Do I need m.CustomFormat? + } + + private void AddImdbIdTokens(Dictionary> tokenHandlers, string imdbId) + { + tokenHandlers["{IMDb Id}"] = m => $"{imdbId}"; + } + + private void AddSeasonTokens(Dictionary> tokenHandlers, int seasonNumber) + { + tokenHandlers["{Season}"] = m => seasonNumber.ToString(m.CustomFormat); + } + + private void AddEpisodeTokens(Dictionary> tokenHandlers, List episodes) + { + if (!episodes.First().AirDate.IsNullOrWhiteSpace()) + { + tokenHandlers["{Air Date}"] = m => episodes.First().AirDate.Replace('-', ' '); + } + else + { + tokenHandlers["{Air Date}"] = m => "Unknown"; + } + + tokenHandlers["{Episode Title}"] = m => GetEpisodeTitle(episodes, "+"); + tokenHandlers["{Episode CleanTitle}"] = m => CleanTitle(GetEpisodeTitle(episodes, "and")); + } + + private void AddEpisodeFileTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); + } + + private void AddMovieFileTokens(Dictionary> tokenHandlers, MovieFile episodeFile) + { + tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile); + tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile); + //tokenHandlers["{IMDb Id}"] = m => + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup ?? m.DefaultValue("Sonarr"); + } + + private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) + { + var qualityTitle = _qualityDefinitionService.Get(episodeFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(series, episodeFile.Quality); + var qualityReal = GetQualityReal(series, episodeFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Real}"] = m => qualityReal; + } + + private void AddQualityTokens(Dictionary> tokenHandlers, Movie movie, MovieFile movieFile) + { + var qualityTitle = _qualityDefinitionService.Get(movieFile.Quality.Quality).Title; + var qualityProper = GetQualityProper(movie, movieFile.Quality); + var qualityReal = GetQualityReal(movie, movieFile.Quality); + + tokenHandlers["{Quality Full}"] = m => String.Format("{0} {1} {2}", qualityTitle, qualityProper, qualityReal); + tokenHandlers["{Quality Title}"] = m => qualityTitle; + tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Real}"] = m => qualityReal; + } + + private void AddMediaInfoTokens(Dictionary> tokenHandlers, EpisodeFile episodeFile) + { + if (episodeFile.MediaInfo == null) return; + + string videoCodec; + switch (episodeFile.MediaInfo.VideoCodec) + { + case "AVC": + if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h264")) + { + videoCodec = "h264"; + } + else + { + videoCodec = "x264"; + } + break; + + case "V_MPEGH/ISO/HEVC": + if (episodeFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(episodeFile.SceneName).Contains("h265")) + { + videoCodec = "h265"; + } + else + { + videoCodec = "x265"; + } + break; + + case "MPEG-2 Video": + videoCodec = "MPEG2"; + break; + + default: + videoCodec = episodeFile.MediaInfo.VideoCodec; + break; + } + + string audioCodec; + switch (episodeFile.MediaInfo.AudioFormat) + { + case "AC-3": + audioCodec = "AC3"; + break; + + case "E-AC-3": + audioCodec = "EAC3"; + break; + + case "MPEG Audio": + if (episodeFile.MediaInfo.AudioProfile == "Layer 3") + { + audioCodec = "MP3"; + } + else + { + audioCodec = episodeFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + audioCodec = episodeFile.MediaInfo.AudioFormat; + break; + + default: + audioCodec = episodeFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(episodeFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = string.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(episodeFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + var videoBitDepth = episodeFile.MediaInfo.VideoBitDepth > 0 ? episodeFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var audioChannels = episodeFile.MediaInfo.FormattedAudioChannels > 0 ? + episodeFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + tokenHandlers["{MediaInfo Video}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; + + tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + + tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + + tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + + private void AddMediaInfoTokens(Dictionary> tokenHandlers, MovieFile movieFile) + { + if (movieFile.MediaInfo == null) return; + + string videoCodec; + switch (movieFile.MediaInfo.VideoCodec) + { + case "AVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h264")) + { + videoCodec = "h264"; + } + else + { + videoCodec = "x264"; + } + break; + + case "V_MPEGH/ISO/HEVC": + if (movieFile.SceneName.IsNotNullOrWhiteSpace() && Path.GetFileNameWithoutExtension(movieFile.SceneName).Contains("h265")) + { + videoCodec = "h265"; + } + else + { + videoCodec = "x265"; + } + break; + + case "MPEG-2 Video": + videoCodec = "MPEG2"; + break; + + default: + videoCodec = movieFile.MediaInfo.VideoCodec; + break; + } + + string audioCodec; + switch (movieFile.MediaInfo.AudioFormat) + { + case "AC-3": + audioCodec = "AC3"; + break; + + case "E-AC-3": + audioCodec = "EAC3"; + break; + + case "MPEG Audio": + if (movieFile.MediaInfo.AudioProfile == "Layer 3") + { + audioCodec = "MP3"; + } + else + { + audioCodec = movieFile.MediaInfo.AudioFormat; + } + break; + + case "DTS": + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + + default: + audioCodec = movieFile.MediaInfo.AudioFormat; + break; + } + + var mediaInfoAudioLanguages = GetLanguagesToken(movieFile.MediaInfo.AudioLanguages); + if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace()) + { + mediaInfoAudioLanguages = string.Format("[{0}]", mediaInfoAudioLanguages); + } + + if (mediaInfoAudioLanguages == "[EN]") + { + mediaInfoAudioLanguages = string.Empty; + } + + var mediaInfoSubtitleLanguages = GetLanguagesToken(movieFile.MediaInfo.Subtitles); + if (!mediaInfoSubtitleLanguages.IsNullOrWhiteSpace()) + { + mediaInfoSubtitleLanguages = string.Format("[{0}]", mediaInfoSubtitleLanguages); + } + + var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty; + var audioChannels = movieFile.MediaInfo.FormattedAudioChannels > 0 ? + movieFile.MediaInfo.FormattedAudioChannels.ToString("F1", CultureInfo.InvariantCulture) : + string.Empty; + + tokenHandlers["{MediaInfo Video}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoCodec}"] = m => videoCodec; + tokenHandlers["{MediaInfo VideoBitDepth}"] = m => videoBitDepth; + + tokenHandlers["{MediaInfo Audio}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioCodec}"] = m => audioCodec; + tokenHandlers["{MediaInfo AudioChannels}"] = m => audioChannels; + + tokenHandlers["{MediaInfo Simple}"] = m => string.Format("{0} {1}", videoCodec, audioCodec); + + tokenHandlers["{MediaInfo Full}"] = m => string.Format("{0} {1}{2} {3}", videoCodec, audioCodec, mediaInfoAudioLanguages, mediaInfoSubtitleLanguages); + } + + private string GetLanguagesToken(string mediaInfoLanguages) + { + List tokens = new List(); + foreach (var item in mediaInfoLanguages.Split('/')) + { + if (!string.IsNullOrWhiteSpace(item)) + tokens.Add(item.Trim()); + } + + var cultures = System.Globalization.CultureInfo.GetCultures(System.Globalization.CultureTypes.NeutralCultures); + for (int i = 0; i < tokens.Count; i++) + { + try + { + var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName == tokens[i]); + + if (cultureInfo != null) + tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper(); + } + catch + { + } + } + + return string.Join("+", tokens.Distinct()); + } + + private string ReplaceTokens(string pattern, Dictionary> tokenHandlers, NamingConfig namingConfig) + { + return TitleRegex.Replace(pattern, match => ReplaceToken(match, tokenHandlers, namingConfig)); + } + + private string ReplaceToken(Match match, Dictionary> tokenHandlers, NamingConfig namingConfig) + { + var tokenMatch = new TokenMatch + { + RegexMatch = match, + Prefix = match.Groups["prefix"].Value, + Separator = match.Groups["separator"].Value, + Suffix = match.Groups["suffix"].Value, + Token = match.Groups["token"].Value, + CustomFormat = match.Groups["customFormat"].Value + }; + + if (tokenMatch.CustomFormat.IsNullOrWhiteSpace()) + { + tokenMatch.CustomFormat = null; + } + + var tokenHandler = tokenHandlers.GetValueOrDefault(tokenMatch.Token, m => string.Empty); + + var replacementText = tokenHandler(tokenMatch).Trim(); + + if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsLower(t))) + { + replacementText = replacementText.ToLower(); + } + else if (tokenMatch.Token.All(t => !char.IsLetter(t) || char.IsUpper(t))) + { + replacementText = replacementText.ToUpper(); + } + + if (!tokenMatch.Separator.IsNullOrWhiteSpace()) + { + replacementText = replacementText.Replace(" ", tokenMatch.Separator); + } + + replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters); + + if (!replacementText.IsNullOrWhiteSpace()) + { + replacementText = tokenMatch.Prefix + replacementText + tokenMatch.Suffix; + } + + return replacementText; + } + + private string FormatNumberTokens(string basePattern, string formatPattern, List episodes) + { + var pattern = string.Empty; + + for (int i = 0; i < episodes.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += EpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["episode"].Value, episodes[i].EpisodeNumber)); + } + + return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); + } + + private string FormatAbsoluteNumberTokens(string basePattern, string formatPattern, List episodes) + { + var pattern = string.Empty; + + for (int i = 0; i < episodes.Count; i++) + { + var patternToReplace = i == 0 ? basePattern : formatPattern; + + pattern += AbsoluteEpisodeRegex.Replace(patternToReplace, match => ReplaceNumberToken(match.Groups["absolute"].Value, episodes[i].AbsoluteEpisodeNumber.Value)); + } + + return ReplaceSeasonTokens(pattern, episodes.First().SeasonNumber); + } + + private string FormatRangeNumberTokens(string seasonEpisodePattern, string formatPattern, List episodes) + { + var eps = new List { episodes.First() }; + + if (episodes.Count > 1) eps.Add(episodes.Last()); + + return FormatNumberTokens(seasonEpisodePattern, formatPattern, eps); + } + + private string ReplaceSeasonTokens(string pattern, int seasonNumber) + { + return SeasonRegex.Replace(pattern, match => ReplaceNumberToken(match.Groups["season"].Value, seasonNumber)); + } + + private string ReplaceNumberToken(string token, int value) + { + var split = token.Trim('{', '}').Split(':'); + if (split.Length == 1) return value.ToString("0"); + + return value.ToString(split[1]); + } + + private EpisodeFormat[] GetEpisodeFormat(string pattern) + { + return _episodeFormatCache.Get(pattern, () => SeasonEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new EpisodeFormat + { + 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) + { + return _absoluteEpisodeFormatCache.Get(pattern, () => AbsoluteEpisodePatternRegex.Matches(pattern).OfType() + .Select(match => new AbsoluteEpisodeFormat + { + Separator = match.Groups["separator"].Value.IsNotNullOrWhiteSpace() ? match.Groups["separator"].Value : "-", + AbsoluteEpisodePattern = match.Groups["absolute"].Value + }).ToArray()); + } + + private string GetEpisodeTitle(List episodes, string separator) + { + separator = string.Format(" {0} ", separator.Trim()); + + if (episodes.Count == 1) + { + return episodes.First().Title.TrimEnd(EpisodeTitleTrimCharacters); + } + + var titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Select(CleanupEpisodeTitle) + .Distinct() + .ToList(); + + if (titles.All(t => t.IsNullOrWhiteSpace())) + { + titles = episodes.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + .Distinct() + .ToList(); + } + + return string.Join(separator, titles); + } + + private string CleanupEpisodeTitle(string title) + { + //this will remove (1),(2) from the end of multi part episodes. + return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); + } + + private string GetQualityProper(Movie movie, QualityModel quality) + { + if (quality.Revision.Version > 1) + { + return "Proper"; + } + + return String.Empty; + } + + private string GetQualityProper(Series series, QualityModel quality) + { + if (quality.Revision.Version > 1) + { + if (series.SeriesType == SeriesTypes.Anime) + { + return "v" + quality.Revision.Version; + } + + return "Proper"; + } + + return String.Empty; + } + + private string GetQualityReal(Series series, QualityModel quality) + { + if (quality.Revision.Real > 0) + { + return "REAL"; + } + + return string.Empty; + } + + private string GetQualityReal(Movie movie, QualityModel quality) + { + if (quality.Revision.Real > 0) + { + return "REAL"; + } + + return string.Empty; + } + + private string GetOriginalTitle(EpisodeFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(EpisodeFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } + + private string GetOriginalTitle(MovieFile episodeFile) + { + if (episodeFile.SceneName.IsNullOrWhiteSpace()) + { + return GetOriginalFileName(episodeFile); + } + + return episodeFile.SceneName; + } + + private string GetOriginalFileName(MovieFile episodeFile) + { + if (episodeFile.RelativePath.IsNullOrWhiteSpace()) + { + return Path.GetFileNameWithoutExtension(episodeFile.Path); + } + + return Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + } + } + + internal sealed class TokenMatch + { + public Match RegexMatch { get; set; } + public string Prefix { get; set; } + public string Separator { get; set; } + public string Suffix { get; set; } + public string Token { get; set; } + public string CustomFormat { get; set; } + + public string DefaultValue(string defaultValue) + { + if (string.IsNullOrEmpty(Prefix) && string.IsNullOrEmpty(Suffix)) + { + return defaultValue; + } + else + { + return string.Empty; + } + } + } + + public enum MultiEpisodeStyle + { + Extend = 0, + Duplicate = 1, + Repeat = 2, + Scene = 3, + Range = 4, + PrefixedRange = 5 + } +}