Files
jellyfin-jellyfin-1/Emby.Naming/TV/SeasonPathParser.cs

180 lines
6.9 KiB
C#
Raw Normal View History

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
{
[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();
[GeneratedRegex(@"^\s*(?:[[시즌]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
2025-03-23 17:05:40 +01:00
private static partial Regex ProcessPost();
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
{
2021-02-13 11:38:17 +01:00
string filename = Path.GetFileName(path);
2025-03-23 17:05:40 +01:00
filename = Regex.Replace(filename, "[ ._-]", string.Empty);
if (parentFolderName is not null)
{
parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
}
2018-09-12 19:26:21 +02:00
if (supportSpecialAliases)
{
if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
{
2019-05-10 20:37:42 +02:00
return (0, true);
2018-09-12 19:26:21 +02:00
}
2019-05-10 20:37:42 +02:00
2018-09-12 19:26:21 +02:00
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
{
2019-05-10 20:37:42 +02:00
return (0, true);
2018-09-12 19:26:21 +02:00
}
}
if (supportNumericSeasonFolders)
{
if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
2018-09-12 19:26:21 +02:00
{
2019-05-10 20:37:42 +02:00
return (val, true);
2018-09-12 19:26:21 +02:00
}
}
if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's'))
2018-09-12 19:26:21 +02:00
{
2025-03-23 17:05:40 +01:00
var testFilename = filename.AsSpan()[1..];
2018-09-12 19:26:21 +02:00
2025-03-23 17:05:40 +01:00
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
2018-09-12 19:26:21 +02:00
{
2025-03-23 17:05:40 +01: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
{
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
}
}
}