MediaInfoResolver.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Emby.Naming.Common;
  8. using Emby.Naming.ExternalFiles;
  9. using MediaBrowser.Controller.Entities;
  10. using MediaBrowser.Controller.Entities.Audio;
  11. using MediaBrowser.Controller.MediaEncoding;
  12. using MediaBrowser.Controller.Providers;
  13. using MediaBrowser.Model.Dlna;
  14. using MediaBrowser.Model.Dto;
  15. using MediaBrowser.Model.Entities;
  16. using MediaBrowser.Model.Globalization;
  17. using MediaBrowser.Model.IO;
  18. using MediaBrowser.Model.MediaInfo;
  19. using Microsoft.Extensions.Logging;
  20. namespace MediaBrowser.Providers.MediaInfo
  21. {
  22. /// <summary>
  23. /// Resolves external files for <see cref="Video"/>.
  24. /// </summary>
  25. public abstract class MediaInfoResolver
  26. {
  27. /// <summary>
  28. /// The <see cref="ExternalPathParser"/> instance.
  29. /// </summary>
  30. private readonly ExternalPathParser _externalPathParser;
  31. /// <summary>
  32. /// The <see cref="IMediaEncoder"/> instance.
  33. /// </summary>
  34. private readonly IMediaEncoder _mediaEncoder;
  35. private readonly ILogger _logger;
  36. private readonly IFileSystem _fileSystem;
  37. /// <summary>
  38. /// The <see cref="NamingOptions"/> instance.
  39. /// </summary>
  40. private readonly NamingOptions _namingOptions;
  41. /// <summary>
  42. /// The <see cref="DlnaProfileType"/> of the files this resolver should resolve.
  43. /// </summary>
  44. private readonly DlnaProfileType _type;
  45. /// <summary>
  46. /// Initializes a new instance of the <see cref="MediaInfoResolver"/> class.
  47. /// </summary>
  48. /// <param name="logger">The logger.</param>
  49. /// <param name="localizationManager">The localization manager.</param>
  50. /// <param name="mediaEncoder">The media encoder.</param>
  51. /// <param name="fileSystem">The file system.</param>
  52. /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
  53. /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
  54. protected MediaInfoResolver(
  55. ILogger logger,
  56. ILocalizationManager localizationManager,
  57. IMediaEncoder mediaEncoder,
  58. IFileSystem fileSystem,
  59. NamingOptions namingOptions,
  60. DlnaProfileType type)
  61. {
  62. _logger = logger;
  63. _mediaEncoder = mediaEncoder;
  64. _fileSystem = fileSystem;
  65. _namingOptions = namingOptions;
  66. _type = type;
  67. _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type);
  68. }
  69. /// <summary>
  70. /// Retrieves the external streams for the provided video.
  71. /// </summary>
  72. /// <param name="video">The <see cref="Video"/> object to search external streams for.</param>
  73. /// <param name="startIndex">The stream index to start adding external streams at.</param>
  74. /// <param name="directoryService">The directory service to search for files.</param>
  75. /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
  76. /// <param name="cancellationToken">The cancellation token.</param>
  77. /// <returns>The external streams located.</returns>
  78. public async Task<IReadOnlyList<MediaStream>> GetExternalStreamsAsync(
  79. Video video,
  80. int startIndex,
  81. IDirectoryService directoryService,
  82. bool clearCache,
  83. CancellationToken cancellationToken)
  84. {
  85. if (!video.IsFileProtocol)
  86. {
  87. return Array.Empty<MediaStream>();
  88. }
  89. var pathInfos = GetExternalFiles(video, directoryService, clearCache);
  90. if (!pathInfos.Any())
  91. {
  92. return Array.Empty<MediaStream>();
  93. }
  94. var mediaStreams = new List<MediaStream>();
  95. foreach (var pathInfo in pathInfos)
  96. {
  97. if (!pathInfo.Path.AsSpan().EndsWith(".strm", StringComparison.OrdinalIgnoreCase))
  98. {
  99. try
  100. {
  101. var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false);
  102. if (mediaInfo.MediaStreams.Count == 1)
  103. {
  104. MediaStream mediaStream = mediaInfo.MediaStreams[0];
  105. if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
  106. || (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
  107. {
  108. mediaStream.Index = startIndex++;
  109. mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
  110. mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
  111. mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired;
  112. mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
  113. }
  114. }
  115. else
  116. {
  117. foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
  118. {
  119. if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
  120. || (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
  121. {
  122. mediaStream.Index = startIndex++;
  123. mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
  124. }
  125. }
  126. }
  127. }
  128. catch (Exception ex)
  129. {
  130. _logger.LogError(ex, "Error getting external streams from {Path}", pathInfo.Path);
  131. continue;
  132. }
  133. }
  134. }
  135. return mediaStreams;
  136. }
  137. /// <summary>
  138. /// Retrieves the external streams for the provided audio.
  139. /// </summary>
  140. /// <param name="audio">The <see cref="Audio"/> object to search external streams for.</param>
  141. /// <param name="startIndex">The stream index to start adding external streams at.</param>
  142. /// <param name="directoryService">The directory service to search for files.</param>
  143. /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
  144. /// <returns>The external streams located.</returns>
  145. public IReadOnlyList<MediaStream> GetExternalStreams(
  146. Audio audio,
  147. int startIndex,
  148. IDirectoryService directoryService,
  149. bool clearCache)
  150. {
  151. if (!audio.IsFileProtocol)
  152. {
  153. return Array.Empty<MediaStream>();
  154. }
  155. var pathInfos = GetExternalFiles(audio, directoryService, clearCache);
  156. if (pathInfos.Count == 0)
  157. {
  158. return Array.Empty<MediaStream>();
  159. }
  160. var mediaStreams = new MediaStream[pathInfos.Count];
  161. for (var i = 0; i < pathInfos.Count; i++)
  162. {
  163. mediaStreams[i] = new MediaStream
  164. {
  165. Type = MediaStreamType.Lyric,
  166. Path = pathInfos[i].Path,
  167. Language = pathInfos[i].Language,
  168. Index = startIndex++
  169. };
  170. }
  171. return mediaStreams;
  172. }
  173. /// <summary>
  174. /// Returns the external file infos for the given video.
  175. /// </summary>
  176. /// <param name="video">The <see cref="Video"/> object to search external files for.</param>
  177. /// <param name="directoryService">The directory service to search for files.</param>
  178. /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
  179. /// <returns>The external file paths located.</returns>
  180. public IReadOnlyList<ExternalPathParserResult> GetExternalFiles(
  181. Video video,
  182. IDirectoryService directoryService,
  183. bool clearCache)
  184. {
  185. if (!video.IsFileProtocol)
  186. {
  187. return Array.Empty<ExternalPathParserResult>();
  188. }
  189. // Check if video folder exists
  190. string folder = video.ContainingFolderPath;
  191. if (!_fileSystem.DirectoryExists(folder))
  192. {
  193. return Array.Empty<ExternalPathParserResult>();
  194. }
  195. var files = directoryService.GetFilePaths(folder, clearCache, true).ToList();
  196. files.Remove(video.Path);
  197. var internalMetadataPath = video.GetInternalMetadataPath();
  198. if (_fileSystem.DirectoryExists(internalMetadataPath))
  199. {
  200. files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true));
  201. }
  202. if (files.Count == 0)
  203. {
  204. return Array.Empty<ExternalPathParserResult>();
  205. }
  206. var externalPathInfos = new List<ExternalPathParserResult>();
  207. ReadOnlySpan<char> prefix = video.FileNameWithoutExtension;
  208. foreach (var file in files)
  209. {
  210. var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan());
  211. if (fileNameWithoutExtension.Length >= prefix.Length
  212. && prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase)
  213. && (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length])))
  214. {
  215. var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString());
  216. if (externalPathInfo is not null)
  217. {
  218. externalPathInfos.Add(externalPathInfo);
  219. }
  220. }
  221. }
  222. return externalPathInfos;
  223. }
  224. /// <summary>
  225. /// Returns the external file infos for the given audio.
  226. /// </summary>
  227. /// <param name="audio">The <see cref="Audio"/> object to search external files for.</param>
  228. /// <param name="directoryService">The directory service to search for files.</param>
  229. /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
  230. /// <returns>The external file paths located.</returns>
  231. public IReadOnlyList<ExternalPathParserResult> GetExternalFiles(
  232. Audio audio,
  233. IDirectoryService directoryService,
  234. bool clearCache)
  235. {
  236. if (!audio.IsFileProtocol)
  237. {
  238. return Array.Empty<ExternalPathParserResult>();
  239. }
  240. string folder = audio.ContainingFolderPath;
  241. var files = directoryService.GetFilePaths(folder, clearCache, true).ToList();
  242. files.Remove(audio.Path);
  243. var internalMetadataPath = audio.GetInternalMetadataPath();
  244. if (_fileSystem.DirectoryExists(internalMetadataPath))
  245. {
  246. files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true));
  247. }
  248. if (files.Count == 0)
  249. {
  250. return Array.Empty<ExternalPathParserResult>();
  251. }
  252. var externalPathInfos = new List<ExternalPathParserResult>();
  253. ReadOnlySpan<char> prefix = audio.FileNameWithoutExtension;
  254. foreach (var file in files)
  255. {
  256. var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan());
  257. if (fileNameWithoutExtension.Length >= prefix.Length
  258. && prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase)
  259. && (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length])))
  260. {
  261. var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString());
  262. if (externalPathInfo is not null)
  263. {
  264. externalPathInfos.Add(externalPathInfo);
  265. }
  266. }
  267. }
  268. return externalPathInfos;
  269. }
  270. /// <summary>
  271. /// Returns the media info of the given file.
  272. /// </summary>
  273. /// <param name="path">The path to the file.</param>
  274. /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
  275. /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
  276. /// <returns>The media info for the given file.</returns>
  277. private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, DlnaProfileType type, CancellationToken cancellationToken)
  278. {
  279. cancellationToken.ThrowIfCancellationRequested();
  280. return _mediaEncoder.GetMediaInfo(
  281. new MediaInfoRequest
  282. {
  283. MediaType = type,
  284. MediaSource = new MediaSourceInfo
  285. {
  286. Path = path,
  287. Protocol = MediaProtocol.File
  288. }
  289. },
  290. cancellationToken);
  291. }
  292. /// <summary>
  293. /// Merges path metadata into stream metadata.
  294. /// </summary>
  295. /// <param name="mediaStream">The <see cref="MediaStream"/> object.</param>
  296. /// <param name="pathInfo">The <see cref="ExternalPathParserResult"/> object.</param>
  297. /// <returns>The modified mediaStream.</returns>
  298. private MediaStream MergeMetadata(MediaStream mediaStream, ExternalPathParserResult pathInfo)
  299. {
  300. mediaStream.Path = pathInfo.Path;
  301. mediaStream.IsExternal = true;
  302. mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title;
  303. mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language;
  304. return mediaStream;
  305. }
  306. }
  307. }