EpisodePathParser.cs 8.3 KB

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