EpisodePathParser.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. #pragma warning disable CS1591
  2. #nullable enable
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.Linq;
  7. using Emby.Naming.Common;
  8. namespace Emby.Naming.TV
  9. {
  10. public class EpisodePathParser
  11. {
  12. private readonly NamingOptions _options;
  13. public EpisodePathParser(NamingOptions options)
  14. {
  15. _options = options;
  16. }
  17. public EpisodePathParserResult Parse(
  18. string path,
  19. bool isDirectory,
  20. bool? isNamed = null,
  21. bool? isOptimistic = null,
  22. bool? supportsAbsoluteNumbers = null,
  23. bool fillExtendedInfo = true)
  24. {
  25. // Added to be able to use regex patterns which require a file extension.
  26. // There were no failed tests without this block, but to be safe, we can keep it until
  27. // the regex which require file extensions are modified so that they don't need them.
  28. if (isDirectory)
  29. {
  30. path += ".mp4";
  31. }
  32. EpisodePathParserResult? result = null;
  33. foreach (var expression in _options.EpisodeExpressions)
  34. {
  35. if (supportsAbsoluteNumbers.HasValue
  36. && expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value)
  37. {
  38. continue;
  39. }
  40. if (isNamed.HasValue && expression.IsNamed != isNamed.Value)
  41. {
  42. continue;
  43. }
  44. if (isOptimistic.HasValue && expression.IsOptimistic != isOptimistic.Value)
  45. {
  46. continue;
  47. }
  48. var currentResult = Parse(path, expression);
  49. if (currentResult.Success)
  50. {
  51. result = currentResult;
  52. break;
  53. }
  54. }
  55. if (result != null && fillExtendedInfo)
  56. {
  57. FillAdditional(path, result);
  58. if (!string.IsNullOrEmpty(result.SeriesName))
  59. {
  60. result.SeriesName = result.SeriesName
  61. .Trim()
  62. .Trim('_', '.', '-')
  63. .Trim();
  64. }
  65. }
  66. return result ?? new EpisodePathParserResult();
  67. }
  68. private static EpisodePathParserResult Parse(string name, EpisodeExpression expression)
  69. {
  70. var result = new EpisodePathParserResult();
  71. // This is a hack to handle wmc naming
  72. if (expression.IsByDate)
  73. {
  74. name = name.Replace('_', '-');
  75. }
  76. var match = expression.Regex.Match(name);
  77. // (Full)(Season)(Episode)(Extension)
  78. if (match.Success && match.Groups.Count >= 3)
  79. {
  80. if (expression.IsByDate)
  81. {
  82. DateTime date;
  83. if (expression.DateTimeFormats.Length > 0)
  84. {
  85. if (DateTime.TryParseExact(
  86. match.Groups[0].Value,
  87. expression.DateTimeFormats,
  88. CultureInfo.InvariantCulture,
  89. DateTimeStyles.None,
  90. out date))
  91. {
  92. result.Year = date.Year;
  93. result.Month = date.Month;
  94. result.Day = date.Day;
  95. result.Success = true;
  96. }
  97. }
  98. else if (DateTime.TryParse(match.Groups[0].Value, out date))
  99. {
  100. result.Year = date.Year;
  101. result.Month = date.Month;
  102. result.Day = date.Day;
  103. result.Success = true;
  104. }
  105. // TODO: Only consider success if date successfully parsed?
  106. result.Success = true;
  107. }
  108. else if (expression.IsNamed)
  109. {
  110. if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
  111. {
  112. result.SeasonNumber = num;
  113. }
  114. if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
  115. {
  116. result.EpisodeNumber = num;
  117. }
  118. var endingNumberGroup = match.Groups["endingepnumber"];
  119. if (endingNumberGroup.Success)
  120. {
  121. // Will only set EndingEpisodeNumber if the captured number is not followed by additional numbers
  122. // or a 'p' or 'i' as what you would get with a pixel resolution specification.
  123. // It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108
  124. int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length;
  125. if (nextIndex >= name.Length
  126. || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
  127. {
  128. if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
  129. {
  130. result.EndingEpsiodeNumber = num;
  131. }
  132. }
  133. }
  134. result.SeriesName = match.Groups["seriesname"].Value;
  135. result.Success = result.EpisodeNumber.HasValue;
  136. }
  137. else
  138. {
  139. if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
  140. {
  141. result.SeasonNumber = num;
  142. }
  143. if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
  144. {
  145. result.EpisodeNumber = num;
  146. }
  147. result.Success = result.EpisodeNumber.HasValue;
  148. }
  149. // Invalidate match when the season is 200 through 1927 or above 2500
  150. // because it is an error unless the TV show is intentionally using false season numbers.
  151. // It avoids erroneous parsing of something like "Series Special (1920x1080).mkv" as being season 1920 episode 1080.
  152. if ((result.SeasonNumber >= 200 && result.SeasonNumber < 1928)
  153. || result.SeasonNumber > 2500)
  154. {
  155. result.Success = false;
  156. }
  157. result.IsByDate = expression.IsByDate;
  158. }
  159. return result;
  160. }
  161. private void FillAdditional(string path, EpisodePathParserResult info)
  162. {
  163. var expressions = _options.MultipleEpisodeExpressions.ToList();
  164. if (string.IsNullOrEmpty(info.SeriesName))
  165. {
  166. expressions.InsertRange(0, _options.EpisodeExpressions.Where(i => i.IsNamed));
  167. }
  168. FillAdditional(path, info, expressions);
  169. }
  170. private void FillAdditional(string path, EpisodePathParserResult info, IEnumerable<EpisodeExpression> expressions)
  171. {
  172. foreach (var i in expressions)
  173. {
  174. if (!i.IsNamed)
  175. {
  176. continue;
  177. }
  178. var result = Parse(path, i);
  179. if (!result.Success)
  180. {
  181. continue;
  182. }
  183. if (string.IsNullOrEmpty(info.SeriesName))
  184. {
  185. info.SeriesName = result.SeriesName;
  186. }
  187. if (!info.EndingEpsiodeNumber.HasValue && info.EpisodeNumber.HasValue)
  188. {
  189. info.EndingEpsiodeNumber = result.EndingEpsiodeNumber;
  190. }
  191. if (!string.IsNullOrEmpty(info.SeriesName)
  192. && (!info.EpisodeNumber.HasValue || info.EndingEpsiodeNumber.HasValue))
  193. {
  194. break;
  195. }
  196. }
  197. }
  198. }
  199. }