VideoListResolver.cs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text.RegularExpressions;
  6. using Emby.Naming.Common;
  7. using MediaBrowser.Model.IO;
  8. namespace Emby.Naming.Video
  9. {
  10. /// <summary>
  11. /// Resolves alternative versions and extras from list of video files.
  12. /// </summary>
  13. public static class VideoListResolver
  14. {
  15. /// <summary>
  16. /// Resolves alternative versions and extras from list of video files.
  17. /// </summary>
  18. /// <param name="videoInfos">List of related video files.</param>
  19. /// <param name="namingOptions">The naming options.</param>
  20. /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
  21. /// <param name="parseName">Whether to parse the name or use the filename.</param>
  22. /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
  23. public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
  24. {
  25. // Filter out all extras, otherwise they could cause stacks to not be resolved
  26. // See the unit test TestStackedWithTrailer
  27. var nonExtras = videoInfos
  28. .Where(i => i.ExtraType is null)
  29. .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
  30. var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
  31. var remainingFiles = new List<VideoFileInfo>();
  32. var standaloneMedia = new List<VideoFileInfo>();
  33. for (var i = 0; i < videoInfos.Count; i++)
  34. {
  35. var current = videoInfos[i];
  36. if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
  37. {
  38. continue;
  39. }
  40. if (current.ExtraType is null)
  41. {
  42. standaloneMedia.Add(current);
  43. }
  44. else
  45. {
  46. remainingFiles.Add(current);
  47. }
  48. }
  49. var list = new List<VideoInfo>();
  50. foreach (var stack in stackResult)
  51. {
  52. var info = new VideoInfo(stack.Name)
  53. {
  54. Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
  55. .OfType<VideoFileInfo>()
  56. .ToList()
  57. };
  58. info.Year = info.Files[0].Year;
  59. list.Add(info);
  60. }
  61. foreach (var media in standaloneMedia)
  62. {
  63. var info = new VideoInfo(media.Name) { Files = new[] { media } };
  64. info.Year = info.Files[0].Year;
  65. list.Add(info);
  66. }
  67. if (supportMultiVersion)
  68. {
  69. list = GetVideosGroupedByVersion(list, namingOptions);
  70. }
  71. // Whatever files are left, just add them
  72. list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
  73. {
  74. Files = new[] { i },
  75. Year = i.Year,
  76. ExtraType = i.ExtraType
  77. }));
  78. return list;
  79. }
  80. private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
  81. {
  82. if (videos.Count == 0)
  83. {
  84. return videos;
  85. }
  86. var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
  87. if (folderName.Length <= 1 || !HaveSameYear(videos))
  88. {
  89. return videos;
  90. }
  91. // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
  92. for (var i = 0; i < videos.Count; i++)
  93. {
  94. var video = videos[i];
  95. if (video.ExtraType is not null)
  96. {
  97. continue;
  98. }
  99. if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
  100. {
  101. return videos;
  102. }
  103. }
  104. // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
  105. videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
  106. var list = new List<VideoInfo>
  107. {
  108. videos[0]
  109. };
  110. var alternateVersionsLen = videos.Count - 1;
  111. var alternateVersions = new VideoFileInfo[alternateVersionsLen];
  112. for (int i = 0; i < alternateVersionsLen; i++)
  113. {
  114. var video = videos[i + 1];
  115. alternateVersions[i] = video.Files[0];
  116. }
  117. list[0].AlternateVersions = alternateVersions;
  118. list[0].Name = folderName.ToString();
  119. return list;
  120. }
  121. private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
  122. {
  123. if (videos.Count == 1)
  124. {
  125. return true;
  126. }
  127. var firstYear = videos[0].Year ?? -1;
  128. for (var i = 1; i < videos.Count; i++)
  129. {
  130. if ((videos[i].Year ?? -1) != firstYear)
  131. {
  132. return false;
  133. }
  134. }
  135. return true;
  136. }
  137. private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
  138. {
  139. var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
  140. if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
  141. {
  142. return false;
  143. }
  144. // Remove the folder name before cleaning as we don't care about cleaning that part
  145. if (folderName.Length <= testFilename.Length)
  146. {
  147. testFilename = testFilename[folderName.Length..].Trim();
  148. }
  149. // There are no span overloads for regex unfortunately
  150. var tmpTestFilename = testFilename.ToString();
  151. if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
  152. {
  153. tmpTestFilename = cleanName.Trim();
  154. }
  155. // The CleanStringParser should have removed common keywords etc.
  156. return string.IsNullOrEmpty(tmpTestFilename)
  157. || testFilename[0] == '-'
  158. || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
  159. }
  160. }
  161. }