EpisodePathParser.cs 9.3 KB

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