@ -1,3 +1,337 @@
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 < Episode > 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 < 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>[- ._)\]]*)\}" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
private static readonly Regex EpisodeRegex = new Regex ( @"(?<episode>\{episode(?:\:0+)?})" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
private static readonly Regex SeasonRegex = new Regex ( @"(?<season>\{season(?:\:0+)?})" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
private static readonly Regex AbsoluteEpisodeRegex = new Regex ( @"(?<absolute>\{absolute(?:\:0+)?})" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
public static readonly Regex SeasonEpisodePatternRegex = new Regex ( @"(?<separator>(?<=})[- ._]+?)?(?<seasonEpisode>s?{season(?:\:0+)?}(?<episodeSeparator>[- ._]?[ex])(?<episode>{episode(?:\:0+)?}))(?<separator>[- ._]+?(?={))?" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
public static readonly Regex AbsoluteEpisodePatternRegex = new Regex ( @"(?<separator>(?<=})[- ._]+?)?(?<absolute>{absolute(?:\:0+)?})(?<separator>[- ._]+?(?={))?" ,
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 ( @"(?<token>\{(?:Series)(?<separator>[- ._])(Clean)?Title\})" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
public static readonly Regex MovieTitleRegex = new Regex ( @"(?<token>\{((?:(Movie|Original))(?<separator>[- ._])(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<MovieFormat>(GetType(), "movieFormat");
_episodeFormatCache = cacheManager . GetCache < EpisodeFormat [ ] > ( GetType ( ) , "episodeFormat" ) ;
_absoluteEpisodeFormatCache = cacheManager . GetCache < AbsoluteEpisodeFormat [ ] > ( GetType ( ) , "absoluteEpisodeFormat" ) ;
_logger = logger ;
}
public string BuildFileName ( List < Episode > 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 < string , Func < TokenMatch , string > > ( 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 < string , Func < TokenMatch , string > > ( 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 < string , Func < TokenMatch , string > > ( 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 < string , Func < TokenMatch , string > > ( 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 < string , Func < TokenMatch , string > > ( 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 " } ;
foreach ( string prefix in prefixes )
{
int prefix_length = prefix . Length ;
if ( prefix . ToLower ( ) = = title . Substring ( 0 , prefix_length ) . ToLower ( ) )
{
title = title . Substring ( prefix_length ) + ", " + prefix . Trim ( ) ;
break ;
}
}
return title . Trim ( ) ;
}
using System ;
using System.Collections.Generic ;
using System.Globalization ;
@ -41,6 +375,9 @@ namespace NzbDrone.Core.Organizer
private static readonly Regex EpisodeRegex = new Regex ( @"(?<episode>\{episode(?:\:0+)?})" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
private static readonly Regex TagsRegex = new Regex ( @"(?<tags>\{tags(?:\:0+)?})" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
private static readonly Regex SeasonRegex = new Regex ( @"(?<season>\{season(?:\:0+)?})" ,
RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
@ -165,6 +502,7 @@ namespace NzbDrone.Core.Organizer
AddQualityTokens ( tokenHandlers , movie , movieFile ) ;
AddMediaInfoTokens ( tokenHandlers , movieFile ) ;
AddMovieFileTokens ( tokenHandlers , movieFile ) ;
AddTagsTokens ( tokenHandlers , movieFile ) ;
var fileName = ReplaceTokens ( pattern , tokenHandlers , namingConfig ) . Trim ( ) ;
fileName = FileNameCleanupRegex . Replace ( fileName , match = > match . Captures [ 0 ] . Value [ 0 ] . ToString ( ) ) ;
@ -316,20 +654,6 @@ namespace NzbDrone.Core.Organizer
return title ;
}
public static string TitleThe ( string title )
{
string [ ] prefixes = { "The " , "An " , "A " } ;
foreach ( string prefix in prefixes )
{
int prefix_length = prefix . Length ;
if ( prefix . ToLower ( ) = = title . Substring ( 0 , prefix_length ) . ToLower ( ) )
{
title = title . Substring ( prefix_length ) + ", " + prefix . Trim ( ) ;
break ;
}
}
return title . Trim ( ) ;
}
public static string CleanFileName ( string name , bool replace = true )
@ -491,6 +815,14 @@ namespace NzbDrone.Core.Organizer
tokenHandlers [ "{Movie Title The}" ] = m = > TitleThe ( movie . Title ) ;
}
private void AddTagsTokens ( Dictionary < string , Func < TokenMatch , string > > tokenHandlers , MovieFile movieFile )
{
if ( movieFile . Edition . IsNotNullOrWhiteSpace ( ) )
{
tokenHandlers [ "{Edition Tags}" ] = m = > CultureInfo . CurrentCulture . TextInfo . ToTitleCase ( movieFile . Edition . ToLower ( ) ) ;
}
}
private void AddReleaseDateTokens ( Dictionary < string , Func < TokenMatch , string > > tokenHandlers , int releaseYear )
{
tokenHandlers [ "{Release Year}" ] = m = > string . Format ( "{0}" , releaseYear . ToString ( ) ) ; //Do I need m.CustomFormat?
@ -1064,4 +1396,4 @@ namespace NzbDrone.Core.Organizer
Range = 4 ,
PrefixedRange = 5
}
}
}