EpisodePathParser.cs 8.2 KB

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