using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
using Emby.Naming.Common ;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to parse information about episode from path.
/// </summary>
public class EpisodePathParser
{
private readonly NamingOptions _options ;
/// <summary>
/// Initializes a new instance of the <see cref="EpisodePathParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
public EpisodePathParser ( NamingOptions options )
{
_options = options ;
}
/// <summary>
/// Parses information about episode from path.
/// </summary>
/// <param name="path">Path.</param>
/// <param name="isDirectory">Is path for a directory or file.</param>
/// <param name="isNamed">Do we want to use IsNamed expressions.</param>
/// <param name="isOptimistic">Do we want to use Optimistic expressions.</param>
/// <param name="supportsAbsoluteNumbers">Do we want to use expressions supporting absolute episode numbers.</param>
/// <param name="fillExtendedInfo">Should we attempt to retrieve extended information.</param>
/// <returns>Returns <see cref="EpisodePathParserResult"/> object.</returns>
public EpisodePathParserResult Parse (
string path ,
bool isDirectory ,
bool? isNamed = null ,
bool? isOptimistic = null ,
bool? supportsAbsoluteNumbers = null ,
bool fillExtendedInfo = true )
{
// Added to be able to use regex patterns which require a file extension.
// There were no failed tests without this block, but to be safe, we can keep it until
// the regex which require file extensions are modified so that they don't need them.
if ( isDirectory )
{
path + = ".mp4" ;
}
EpisodePathParserResult ? result = null ;
foreach ( var expression in _options . EpisodeExpressions )
{
if ( supportsAbsoluteNumbers . HasValue
& & expression . SupportsAbsoluteEpisodeNumbers ! = supportsAbsoluteNumbers . Value )
{
continue ;
}
if ( isNamed . HasValue & & expression . IsNamed ! = isNamed . Value )
{
continue ;
}
if ( isOptimistic . HasValue & & expression . IsOptimistic ! = isOptimistic . Value )
{
continue ;
}
var currentResult = Parse ( path , expression ) ;
if ( currentResult . Success )
{
result = currentResult ;
break ;
}
}
if ( result is not null & & fillExtendedInfo )
{
FillAdditional ( path , result ) ;
if ( ! string . IsNullOrEmpty ( result . SeriesName ) )
{
result . SeriesName = result . SeriesName
. Trim ( )
. Trim ( '_' , '.' , '-' )
. Trim ( ) ;
}
}
return result ? ? new EpisodePathParserResult ( ) ;
}
private static EpisodePathParserResult Parse ( string name , EpisodeExpression expression )
{
var result = new EpisodePathParserResult ( ) ;
// This is a hack to handle wmc naming
if ( expression . IsByDate )
{
name = name . Replace ( '_' , '-' ) ;
}
var match = expression . Regex . Match ( name ) ;
// (Full)(Season)(Episode)(Extension)
if ( match . Success & & match . Groups . Count > = 3 )
{
if ( expression . IsByDate )
{
DateTime date ;
if ( expression . DateTimeFormats . Length > 0 )
{
if ( DateTime . TryParseExact (
match . Groups [ 0 ] . ValueSpan ,
expression . DateTimeFormats ,
CultureInfo . InvariantCulture ,
DateTimeStyles . None ,
out date ) )
{
result . Year = date . Year ;
result . Month = date . Month ;
result . Day = date . Day ;
result . Success = true ;
}
}
else if ( DateTime . TryParse ( match . Groups [ 0 ] . ValueSpan , out date ) )
{
result . Year = date . Year ;
result . Month = date . Month ;
result . Day = date . Day ;
result . Success = true ;
}
// TODO: Only consider success if date successfully parsed?
result . Success = true ;
}
else if ( expression . IsNamed )
{
if ( int . TryParse ( match . Groups [ "seasonnumber" ] . ValueSpan , NumberStyles . Integer , CultureInfo . InvariantCulture , out var num ) )
{
result . SeasonNumber = num ;
}
if ( int . TryParse ( match . Groups [ "epnumber" ] . ValueSpan , NumberStyles . Integer , CultureInfo . InvariantCulture , out num ) )
{
result . EpisodeNumber = num ;
}
var endingNumberGroup = match . Groups [ "endingepnumber" ] ;
if ( endingNumberGroup . Success )
{
// Will only set EndingEpisodeNumber if the captured number is not followed by additional numbers
// or a 'p' or 'i' as what you would get with a pixel resolution specification.
// It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108
int nextIndex = endingNumberGroup . Index + endingNumberGroup . Length ;
if ( nextIndex > = name . Length
| | ! "0123456789iIpP" . Contains ( name [ nextIndex ] , StringComparison . Ordinal ) )
{
if ( int . TryParse ( endingNumberGroup . ValueSpan , NumberStyles . Integer , CultureInfo . InvariantCulture , out num ) )
{
result . EndingEpisodeNumber = num ;
}
}
}
result . SeriesName = match . Groups [ "seriesname" ] . Value ;
result . Success = result . EpisodeNumber . HasValue ;
}
else
{
if ( int . TryParse ( match . Groups [ 1 ] . ValueSpan , NumberStyles . Integer , CultureInfo . InvariantCulture , out var num ) )
{
result . SeasonNumber = num ;
}
if ( int . TryParse ( match . Groups [ 2 ] . ValueSpan , NumberStyles . Integer , CultureInfo . InvariantCulture , out num ) )
{
result . EpisodeNumber = num ;
}
result . Success = result . EpisodeNumber . HasValue ;
}
// Invalidate match when the season is 200 through 1927 or above 2500
// because it is an error unless the TV show is intentionally using false season numbers.
// It avoids erroneous parsing of something like "Series Special (1920x1080).mkv" as being season 1920 episode 1080.
if ( ( result . SeasonNumber > = 200 & & result . SeasonNumber < 1928 )
| | result . SeasonNumber > 2500 )
{
result . Success = false ;
}
result . IsByDate = expression . IsByDate ;
}
return result ;
}
private void FillAdditional ( string path , EpisodePathParserResult info )
{
var expressions = _options . MultipleEpisodeExpressions . Where ( i = > i . IsNamed ) . ToList ( ) ;
if ( string . IsNullOrEmpty ( info . SeriesName ) )
{
expressions . InsertRange ( 0 , _options . EpisodeExpressions . Where ( i = > i . IsNamed ) ) ;
}
FillAdditional ( path , info , expressions ) ;
}
private void FillAdditional ( string path , EpisodePathParserResult info , IEnumerable < EpisodeExpression > expressions )
{
foreach ( var i in expressions )
{
var result = Parse ( path , i ) ;
if ( ! result . Success )
{
continue ;
}
if ( string . IsNullOrEmpty ( info . SeriesName ) )
{
info . SeriesName = result . SeriesName ;
}
if ( ! info . EndingEpisodeNumber . HasValue & & info . EpisodeNumber . HasValue )
{
info . EndingEpisodeNumber = result . EndingEpisodeNumber ;
}
if ( ! string . IsNullOrEmpty ( info . SeriesName )
& & ( ! info . EpisodeNumber . HasValue | | info . EndingEpisodeNumber . HasValue ) )
{
break ;
}
}
}
}
}