2019-01-13 20:54:44 +01:00
|
|
|
|
using System;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.IO;
|
2025-03-23 17:05:40 +01:00
|
|
|
|
using System.Text.RegularExpressions;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
|
|
|
|
|
|
namespace Emby.Naming.TV
|
|
|
|
|
|
{
|
2020-11-10 19:23:10 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Class to parse season paths.
|
|
|
|
|
|
/// </summary>
|
2025-03-23 17:05:40 +01:00
|
|
|
|
public static partial class SeasonPathParser
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2025-11-17 14:08:54 -05:00
|
|
|
|
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
|
|
|
|
|
|
|
2025-10-27 15:43:12 -04:00
|
|
|
|
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
2025-03-23 17:05:40 +01:00
|
|
|
|
private static partial Regex ProcessPre();
|
|
|
|
|
|
|
2025-11-17 14:08:54 -05:00
|
|
|
|
[GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
|
2025-03-23 17:05:40 +01:00
|
|
|
|
private static partial Regex ProcessPost();
|
2024-08-01 17:17:10 +02:00
|
|
|
|
|
2025-11-17 14:08:54 -05:00
|
|
|
|
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
|
|
|
|
|
|
private static partial Regex SeasonPrefix();
|
|
|
|
|
|
|
2020-11-10 19:23:10 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Attempts to parse season number from path.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="path">Path to season.</param>
|
2025-03-23 17:05:40 +01:00
|
|
|
|
/// <param name="parentPath">Folder name of the parent.</param>
|
2020-11-10 19:23:10 +01:00
|
|
|
|
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
|
|
|
|
|
|
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
|
|
|
|
|
|
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
|
2025-03-23 17:05:40 +01:00
|
|
|
|
public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
|
|
|
|
|
var result = new SeasonPathParserResult();
|
2025-03-23 17:05:40 +01:00
|
|
|
|
var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
|
2025-03-23 17:05:40 +01:00
|
|
|
|
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
|
2020-02-19 21:56:35 +01:00
|
|
|
|
result.SeasonNumber = seasonNumber;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
|
|
|
|
|
|
if (result.SeasonNumber.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Success = true;
|
2020-02-19 21:56:35 +01:00
|
|
|
|
result.IsSeasonFolder = isSeasonFolder;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the season number from path.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="path">The path.</param>
|
2025-03-23 17:05:40 +01:00
|
|
|
|
/// <param name="parentFolderName">The parent folder name.</param>
|
2018-09-12 19:26:21 +02:00
|
|
|
|
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
|
|
|
|
|
|
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
|
|
|
|
|
|
/// <returns>System.Nullable{System.Int32}.</returns>
|
2021-12-24 14:18:24 -07:00
|
|
|
|
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
|
2019-05-10 20:37:42 +02:00
|
|
|
|
string path,
|
2025-03-23 17:05:40 +01:00
|
|
|
|
string? parentFolderName,
|
2019-05-10 20:37:42 +02:00
|
|
|
|
bool supportSpecialAliases,
|
|
|
|
|
|
bool supportNumericSeasonFolders)
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2025-11-17 14:08:54 -05:00
|
|
|
|
var fileName = Path.GetFileName(path);
|
2025-03-23 17:05:40 +01:00
|
|
|
|
|
2025-11-17 14:08:54 -05:00
|
|
|
|
var seasonPrefixMatch = SeasonPrefix().Match(fileName);
|
|
|
|
|
|
if (seasonPrefixMatch.Success &&
|
|
|
|
|
|
int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
2025-03-23 17:05:40 +01:00
|
|
|
|
{
|
2025-11-17 14:08:54 -05:00
|
|
|
|
return (val, true);
|
2025-03-23 17:05:40 +01:00
|
|
|
|
}
|
2018-09-12 19:26:21 +02:00
|
|
|
|
|
2025-11-17 14:08:54 -05:00
|
|
|
|
string filename = CleanNameRegex.Replace(fileName, string.Empty);
|
|
|
|
|
|
|
|
|
|
|
|
if (parentFolderName is not null)
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2025-11-17 14:08:54 -05:00
|
|
|
|
var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty);
|
|
|
|
|
|
filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 14:08:54 -05:00
|
|
|
|
if (supportSpecialAliases &&
|
|
|
|
|
|
(filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
|
filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2025-11-17 14:08:54 -05:00
|
|
|
|
return (0, true);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 14:08:54 -05:00
|
|
|
|
if (supportNumericSeasonFolders &&
|
|
|
|
|
|
int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2025-11-17 14:08:54 -05:00
|
|
|
|
return (val, true);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-23 17:05:40 +01:00
|
|
|
|
var preMatch = ProcessPre().Match(filename);
|
|
|
|
|
|
if (preMatch.Success)
|
2020-02-19 21:56:35 +01:00
|
|
|
|
{
|
2025-03-23 17:05:40 +01:00
|
|
|
|
return CheckMatch(preMatch);
|
2020-02-19 21:56:35 +01:00
|
|
|
|
}
|
2025-03-23 17:05:40 +01:00
|
|
|
|
else
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2025-03-23 17:05:40 +01:00
|
|
|
|
var postMatch = ProcessPost().Match(filename);
|
|
|
|
|
|
return CheckMatch(postMatch);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
2025-03-23 17:05:40 +01:00
|
|
|
|
}
|
2018-09-12 19:26:21 +02:00
|
|
|
|
|
2025-03-23 17:05:40 +01:00
|
|
|
|
private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
|
|
|
|
|
|
{
|
|
|
|
|
|
var numberString = match.Groups["seasonnumber"];
|
|
|
|
|
|
if (numberString.Success)
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2025-11-02 21:58:45 -05:00
|
|
|
|
if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
|
|
|
|
|
|
{
|
|
|
|
|
|
return (seasonNumber, true);
|
|
|
|
|
|
}
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-23 17:05:40 +01:00
|
|
|
|
return (null, false);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2019-10-25 12:47:20 +02:00
|
|
|
|
/// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel").
|
2018-09-12 19:26:21 +02:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="path">The path.</param>
|
|
|
|
|
|
/// <returns>System.Nullable{System.Int32}.</returns>
|
2021-12-24 14:18:24 -07:00
|
|
|
|
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
|
|
|
|
|
var numericStart = -1;
|
|
|
|
|
|
var length = 0;
|
|
|
|
|
|
|
2020-11-01 11:19:22 +01:00
|
|
|
|
var hasOpenParenthesis = false;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
var isSeasonFolder = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Find out where the numbers start, and then keep going until they end
|
|
|
|
|
|
for (var i = 0; i < path.Length; i++)
|
|
|
|
|
|
{
|
2020-02-19 21:56:35 +01:00
|
|
|
|
if (char.IsNumber(path[i]))
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2020-11-01 11:19:22 +01:00
|
|
|
|
if (!hasOpenParenthesis)
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
|
|
|
|
|
if (numericStart == -1)
|
|
|
|
|
|
{
|
|
|
|
|
|
numericStart = i;
|
|
|
|
|
|
}
|
2020-01-22 22:18:56 +01:00
|
|
|
|
|
2018-09-12 19:26:21 +02:00
|
|
|
|
length++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (numericStart != -1)
|
|
|
|
|
|
{
|
|
|
|
|
|
// There's other stuff after the season number, e.g. episode number
|
|
|
|
|
|
isSeasonFolder = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var currentChar = path[i];
|
2020-01-22 22:18:56 +01:00
|
|
|
|
if (currentChar == '(')
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2020-11-01 11:19:22 +01:00
|
|
|
|
hasOpenParenthesis = true;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
2020-01-22 22:18:56 +01:00
|
|
|
|
else if (currentChar == ')')
|
2018-09-12 19:26:21 +02:00
|
|
|
|
{
|
2020-11-01 11:19:22 +01:00
|
|
|
|
hasOpenParenthesis = false;
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (numericStart == -1)
|
|
|
|
|
|
{
|
2019-05-10 20:37:42 +02:00
|
|
|
|
return (null, isSeasonFolder);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-02-19 21:56:35 +01:00
|
|
|
|
return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder);
|
2018-09-12 19:26:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|