mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
243 lines
9.3 KiB
C#
243 lines
9.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|