SubtitleResolver.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Providers;
  6. using MediaBrowser.Model.Entities;
  7. using MediaBrowser.Model.Globalization;
  8. namespace MediaBrowser.Providers.MediaInfo
  9. {
  10. /// <summary>
  11. /// Resolves external subtitles for videos.
  12. /// </summary>
  13. public class SubtitleResolver
  14. {
  15. private readonly ILocalizationManager _localization;
  16. /// <summary>
  17. /// Initializes a new instance of the <see cref="SubtitleResolver"/> class.
  18. /// </summary>
  19. /// <param name="localization">The localization manager.</param>
  20. public SubtitleResolver(ILocalizationManager localization)
  21. {
  22. _localization = localization;
  23. }
  24. /// <summary>
  25. /// Retrieves the external subtitle streams for the provided video.
  26. /// </summary>
  27. /// <param name="video">The video to search from.</param>
  28. /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
  29. /// <param name="directoryService">The directory service to search for files.</param>
  30. /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
  31. /// <returns>The external subtitle streams located.</returns>
  32. public List<MediaStream> GetExternalSubtitleStreams(
  33. Video video,
  34. int startIndex,
  35. IDirectoryService directoryService,
  36. bool clearCache)
  37. {
  38. var streams = new List<MediaStream>();
  39. if (!video.IsFileProtocol)
  40. {
  41. return streams;
  42. }
  43. AddExternalSubtitleStreams(streams, video.ContainingFolderPath, video.Path, startIndex, directoryService, clearCache);
  44. startIndex += streams.Count;
  45. string folder = video.GetInternalMetadataPath();
  46. if (!Directory.Exists(folder))
  47. {
  48. return streams;
  49. }
  50. try
  51. {
  52. AddExternalSubtitleStreams(streams, folder, video.Path, startIndex, directoryService, clearCache);
  53. }
  54. catch (IOException)
  55. {
  56. }
  57. return streams;
  58. }
  59. /// <summary>
  60. /// Locates the external subtitle files for the provided video.
  61. /// </summary>
  62. /// <param name="video">The video to search from.</param>
  63. /// <param name="directoryService">The directory service to search for files.</param>
  64. /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
  65. /// <returns>The external subtitle file paths located.</returns>
  66. public IEnumerable<string> GetExternalSubtitleFiles(
  67. Video video,
  68. IDirectoryService directoryService,
  69. bool clearCache)
  70. {
  71. if (!video.IsFileProtocol)
  72. {
  73. yield break;
  74. }
  75. var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache);
  76. foreach (var stream in streams)
  77. {
  78. yield return stream.Path;
  79. }
  80. }
  81. /// <summary>
  82. /// Extracts the subtitle files from the provided list and adds them to the list of streams.
  83. /// </summary>
  84. /// <param name="streams">The list of streams to add external subtitles to.</param>
  85. /// <param name="videoPath">The path to the video file.</param>
  86. /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
  87. /// <param name="files">The files to add if they are subtitles.</param>
  88. public void AddExternalSubtitleStreams(
  89. List<MediaStream> streams,
  90. string videoPath,
  91. int startIndex,
  92. IReadOnlyList<string> files)
  93. {
  94. var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath);
  95. for (var i = 0; i < files.Count; i++)
  96. {
  97. var fullName = files[i];
  98. var extension = Path.GetExtension(fullName.AsSpan());
  99. if (!IsSubtitleExtension(extension))
  100. {
  101. continue;
  102. }
  103. var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName);
  104. MediaStream mediaStream;
  105. // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot
  106. if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
  107. {
  108. mediaStream = new MediaStream
  109. {
  110. Index = startIndex++,
  111. Type = MediaStreamType.Subtitle,
  112. IsExternal = true,
  113. Path = fullName
  114. };
  115. }
  116. else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
  117. && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
  118. && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
  119. {
  120. var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase)
  121. || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase);
  122. var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase);
  123. // Support xbmc naming conventions - 300.spanish.srt
  124. var languageSpan = fileNameWithoutExtension;
  125. while (languageSpan.Length > 0)
  126. {
  127. var lastDot = languageSpan.LastIndexOf('.');
  128. if (lastDot < videoFileNameWithoutExtension.Length)
  129. {
  130. languageSpan = ReadOnlySpan<char>.Empty;
  131. break;
  132. }
  133. var currentSlice = languageSpan[lastDot..];
  134. if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase)
  135. || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase)
  136. || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase))
  137. {
  138. languageSpan = languageSpan[..lastDot];
  139. continue;
  140. }
  141. languageSpan = languageSpan[(lastDot + 1)..];
  142. break;
  143. }
  144. var language = languageSpan.ToString();
  145. if (string.IsNullOrWhiteSpace(language))
  146. {
  147. language = null;
  148. }
  149. else
  150. {
  151. // Try to translate to three character code
  152. // Be flexible and check against both the full and three character versions
  153. var culture = _localization.FindLanguageInfo(language);
  154. language = culture == null ? language : culture.ThreeLetterISOLanguageName;
  155. }
  156. mediaStream = new MediaStream
  157. {
  158. Index = startIndex++,
  159. Type = MediaStreamType.Subtitle,
  160. IsExternal = true,
  161. Path = fullName,
  162. Language = language,
  163. IsForced = isForced,
  164. IsDefault = isDefault
  165. };
  166. }
  167. else
  168. {
  169. continue;
  170. }
  171. mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant();
  172. streams.Add(mediaStream);
  173. }
  174. }
  175. private static bool IsSubtitleExtension(ReadOnlySpan<char> extension)
  176. {
  177. return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase)
  178. || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase)
  179. || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase)
  180. || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
  181. || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase)
  182. || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase)
  183. || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase);
  184. }
  185. private static ReadOnlySpan<char> NormalizeFilenameForSubtitleComparison(string filename)
  186. {
  187. // Try to account for sloppy file naming
  188. filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
  189. filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
  190. return Path.GetFileNameWithoutExtension(filename.AsSpan());
  191. }
  192. private void AddExternalSubtitleStreams(
  193. List<MediaStream> streams,
  194. string folder,
  195. string videoPath,
  196. int startIndex,
  197. IDirectoryService directoryService,
  198. bool clearCache)
  199. {
  200. var files = directoryService.GetFilePaths(folder, clearCache, true);
  201. AddExternalSubtitleStreams(streams, videoPath, startIndex, files);
  202. }
  203. }
  204. }