SeasonPathParser.cs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. using System;
  2. using System.Globalization;
  3. using System.IO;
  4. using System.Text.RegularExpressions;
  5. namespace Emby.Naming.TV
  6. {
  7. /// <summary>
  8. /// Class to parse season paths.
  9. /// </summary>
  10. public static partial class SeasonPathParser
  11. {
  12. [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>.*)$")]
  13. private static partial Regex ProcessPre();
  14. [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>.*)$")]
  15. private static partial Regex ProcessPost();
  16. /// <summary>
  17. /// Attempts to parse season number from path.
  18. /// </summary>
  19. /// <param name="path">Path to season.</param>
  20. /// <param name="parentPath">Folder name of the parent.</param>
  21. /// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
  22. /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
  23. /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
  24. public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
  25. {
  26. var result = new SeasonPathParserResult();
  27. var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
  28. var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
  29. result.SeasonNumber = seasonNumber;
  30. if (result.SeasonNumber.HasValue)
  31. {
  32. result.Success = true;
  33. result.IsSeasonFolder = isSeasonFolder;
  34. }
  35. return result;
  36. }
  37. /// <summary>
  38. /// Gets the season number from path.
  39. /// </summary>
  40. /// <param name="path">The path.</param>
  41. /// <param name="parentFolderName">The parent folder name.</param>
  42. /// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
  43. /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
  44. /// <returns>System.Nullable{System.Int32}.</returns>
  45. private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
  46. string path,
  47. string? parentFolderName,
  48. bool supportSpecialAliases,
  49. bool supportNumericSeasonFolders)
  50. {
  51. string filename = Path.GetFileName(path);
  52. filename = Regex.Replace(filename, "[ ._-]", string.Empty);
  53. if (parentFolderName is not null)
  54. {
  55. parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
  56. filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
  57. }
  58. if (supportSpecialAliases)
  59. {
  60. if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
  61. {
  62. return (0, true);
  63. }
  64. if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
  65. {
  66. return (0, true);
  67. }
  68. }
  69. if (supportNumericSeasonFolders)
  70. {
  71. if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
  72. {
  73. return (val, true);
  74. }
  75. }
  76. if (filename.StartsWith('s'))
  77. {
  78. var testFilename = filename.AsSpan()[1..];
  79. if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
  80. {
  81. return (val, true);
  82. }
  83. }
  84. var preMatch = ProcessPre().Match(filename);
  85. if (preMatch.Success)
  86. {
  87. return CheckMatch(preMatch);
  88. }
  89. else
  90. {
  91. var postMatch = ProcessPost().Match(filename);
  92. return CheckMatch(postMatch);
  93. }
  94. }
  95. private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
  96. {
  97. var numberString = match.Groups["seasonnumber"];
  98. if (numberString.Success)
  99. {
  100. var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
  101. return (seasonNumber, true);
  102. }
  103. return (null, false);
  104. }
  105. /// <summary>
  106. /// Extracts the season number from the second half of the Season folder name (everything after "Season", or "Staffel").
  107. /// </summary>
  108. /// <param name="path">The path.</param>
  109. /// <returns>System.Nullable{System.Int32}.</returns>
  110. private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPathSubstring(ReadOnlySpan<char> path)
  111. {
  112. var numericStart = -1;
  113. var length = 0;
  114. var hasOpenParenthesis = false;
  115. var isSeasonFolder = true;
  116. // Find out where the numbers start, and then keep going until they end
  117. for (var i = 0; i < path.Length; i++)
  118. {
  119. if (char.IsNumber(path[i]))
  120. {
  121. if (!hasOpenParenthesis)
  122. {
  123. if (numericStart == -1)
  124. {
  125. numericStart = i;
  126. }
  127. length++;
  128. }
  129. }
  130. else if (numericStart != -1)
  131. {
  132. // There's other stuff after the season number, e.g. episode number
  133. isSeasonFolder = false;
  134. break;
  135. }
  136. var currentChar = path[i];
  137. if (currentChar == '(')
  138. {
  139. hasOpenParenthesis = true;
  140. }
  141. else if (currentChar == ')')
  142. {
  143. hasOpenParenthesis = false;
  144. }
  145. }
  146. if (numericStart == -1)
  147. {
  148. return (null, isSeasonFolder);
  149. }
  150. return (int.Parse(path.Slice(numericStart, length), provider: CultureInfo.InvariantCulture), isSeasonFolder);
  151. }
  152. }
  153. }