SubtitleManager.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #nullable disable
  2. #pragma warning disable CS1591
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using Jellyfin.Extensions;
  11. using MediaBrowser.Common.Extensions;
  12. using MediaBrowser.Controller.Entities;
  13. using MediaBrowser.Controller.Entities.Movies;
  14. using MediaBrowser.Controller.Entities.TV;
  15. using MediaBrowser.Controller.Library;
  16. using MediaBrowser.Controller.Persistence;
  17. using MediaBrowser.Controller.Providers;
  18. using MediaBrowser.Controller.Subtitles;
  19. using MediaBrowser.Model.Configuration;
  20. using MediaBrowser.Model.Entities;
  21. using MediaBrowser.Model.Globalization;
  22. using MediaBrowser.Model.IO;
  23. using MediaBrowser.Model.Providers;
  24. using Microsoft.Extensions.Logging;
  25. namespace MediaBrowser.Providers.Subtitles
  26. {
  27. public class SubtitleManager : ISubtitleManager
  28. {
  29. private readonly ILogger<SubtitleManager> _logger;
  30. private readonly IFileSystem _fileSystem;
  31. private readonly ILibraryMonitor _monitor;
  32. private readonly IMediaSourceManager _mediaSourceManager;
  33. private readonly ILocalizationManager _localization;
  34. private readonly ISubtitleProvider[] _subtitleProviders;
  35. public SubtitleManager(
  36. ILogger<SubtitleManager> logger,
  37. IFileSystem fileSystem,
  38. ILibraryMonitor monitor,
  39. IMediaSourceManager mediaSourceManager,
  40. ILocalizationManager localizationManager,
  41. IEnumerable<ISubtitleProvider> subtitleProviders)
  42. {
  43. _logger = logger;
  44. _fileSystem = fileSystem;
  45. _monitor = monitor;
  46. _mediaSourceManager = mediaSourceManager;
  47. _localization = localizationManager;
  48. _subtitleProviders = subtitleProviders
  49. .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
  50. .ToArray();
  51. }
  52. /// <inheritdoc />
  53. public event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure;
  54. /// <inheritdoc />
  55. public async Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken)
  56. {
  57. if (request.Language is not null)
  58. {
  59. var culture = _localization.FindLanguageInfo(request.Language);
  60. if (culture is not null)
  61. {
  62. request.TwoLetterISOLanguageName = culture.TwoLetterISOLanguageName;
  63. }
  64. }
  65. var contentType = request.ContentType;
  66. var providers = _subtitleProviders
  67. .Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparison.OrdinalIgnoreCase))
  68. .OrderBy(i =>
  69. {
  70. var index = request.SubtitleFetcherOrder.ToList().IndexOf(i.Name);
  71. return index == -1 ? int.MaxValue : index;
  72. })
  73. .ToArray();
  74. // If not searching all, search one at a time until something is found
  75. if (!request.SearchAllProviders)
  76. {
  77. foreach (var provider in providers)
  78. {
  79. try
  80. {
  81. var searchResults = await provider.Search(request, cancellationToken).ConfigureAwait(false);
  82. var list = searchResults.ToArray();
  83. if (list.Length > 0)
  84. {
  85. Normalize(list);
  86. return list;
  87. }
  88. }
  89. catch (Exception ex)
  90. {
  91. _logger.LogError(ex, "Error downloading subtitles from {Provider}", provider.Name);
  92. }
  93. }
  94. return Array.Empty<RemoteSubtitleInfo>();
  95. }
  96. var tasks = providers.Select(async i =>
  97. {
  98. try
  99. {
  100. var searchResults = await i.Search(request, cancellationToken).ConfigureAwait(false);
  101. var list = searchResults.ToArray();
  102. Normalize(list);
  103. return list;
  104. }
  105. catch (Exception ex)
  106. {
  107. _logger.LogError(ex, "Error downloading subtitles from {0}", i.Name);
  108. return Array.Empty<RemoteSubtitleInfo>();
  109. }
  110. });
  111. var results = await Task.WhenAll(tasks).ConfigureAwait(false);
  112. return results.SelectMany(i => i).ToArray();
  113. }
  114. /// <inheritdoc />
  115. public Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken)
  116. {
  117. var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
  118. return DownloadSubtitles(video, libraryOptions, subtitleId, cancellationToken);
  119. }
  120. /// <inheritdoc />
  121. public async Task DownloadSubtitles(
  122. Video video,
  123. LibraryOptions libraryOptions,
  124. string subtitleId,
  125. CancellationToken cancellationToken)
  126. {
  127. var parts = subtitleId.Split('_', 2);
  128. var provider = GetProvider(parts[0]);
  129. try
  130. {
  131. var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
  132. await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false);
  133. }
  134. catch (RateLimitExceededException)
  135. {
  136. throw;
  137. }
  138. catch (Exception ex)
  139. {
  140. SubtitleDownloadFailure?.Invoke(this, new SubtitleDownloadFailureEventArgs
  141. {
  142. Item = video,
  143. Exception = ex,
  144. Provider = provider.Name
  145. });
  146. throw;
  147. }
  148. }
  149. /// <inheritdoc />
  150. public Task UploadSubtitle(Video video, SubtitleResponse response)
  151. {
  152. var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
  153. return TrySaveSubtitle(video, libraryOptions, response);
  154. }
  155. private async Task TrySaveSubtitle(
  156. Video video,
  157. LibraryOptions libraryOptions,
  158. SubtitleResponse response)
  159. {
  160. var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
  161. var memoryStream = new MemoryStream();
  162. await using (memoryStream.ConfigureAwait(false))
  163. {
  164. var stream = response.Stream;
  165. await using (stream.ConfigureAwait(false))
  166. {
  167. await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
  168. memoryStream.Position = 0;
  169. }
  170. }
  171. var savePaths = new List<string>();
  172. var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
  173. if (response.IsForced)
  174. {
  175. saveFileName += ".forced";
  176. }
  177. saveFileName += "." + response.Format.ToLowerInvariant();
  178. if (saveInMediaFolder)
  179. {
  180. var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
  181. // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path.");
  182. if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal))
  183. {
  184. savePaths.Add(mediaFolderPath);
  185. }
  186. }
  187. var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
  188. // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path.");
  189. if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
  190. {
  191. savePaths.Add(internalPath);
  192. }
  193. if (savePaths.Count > 0)
  194. {
  195. await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
  196. }
  197. else
  198. {
  199. _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid.");
  200. }
  201. }
  202. private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
  203. {
  204. List<Exception> exs = null;
  205. foreach (var savePath in savePaths)
  206. {
  207. _logger.LogInformation("Saving subtitles to {SavePath}", savePath);
  208. _monitor.ReportFileSystemChangeBeginning(savePath);
  209. try
  210. {
  211. Directory.CreateDirectory(Path.GetDirectoryName(savePath));
  212. var fileOptions = AsyncFile.WriteOptions;
  213. fileOptions.Mode = FileMode.CreateNew;
  214. fileOptions.PreallocationSize = stream.Length;
  215. var fs = new FileStream(savePath, fileOptions);
  216. await using (fs.ConfigureAwait(false))
  217. {
  218. await stream.CopyToAsync(fs).ConfigureAwait(false);
  219. }
  220. return;
  221. }
  222. catch (Exception ex)
  223. {
  224. // Bug in analyzer -- https://github.com/dotnet/roslyn-analyzers/issues/5160
  225. #pragma warning disable CA1508
  226. (exs ??= new List<Exception>()).Add(ex);
  227. #pragma warning restore CA1508
  228. }
  229. finally
  230. {
  231. _monitor.ReportFileSystemChangeComplete(savePath, false);
  232. }
  233. stream.Position = 0;
  234. }
  235. if (exs is not null)
  236. {
  237. throw new AggregateException(exs);
  238. }
  239. }
  240. /// <inheritdoc />
  241. public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, bool isAutomated, CancellationToken cancellationToken)
  242. {
  243. if (video.VideoType != VideoType.VideoFile)
  244. {
  245. return Task.FromResult(Array.Empty<RemoteSubtitleInfo>());
  246. }
  247. VideoContentType mediaType;
  248. if (video is Episode)
  249. {
  250. mediaType = VideoContentType.Episode;
  251. }
  252. else if (video is Movie)
  253. {
  254. mediaType = VideoContentType.Movie;
  255. }
  256. else
  257. {
  258. // These are the only supported types
  259. return Task.FromResult(Array.Empty<RemoteSubtitleInfo>());
  260. }
  261. var request = new SubtitleSearchRequest
  262. {
  263. ContentType = mediaType,
  264. IndexNumber = video.IndexNumber,
  265. Language = language,
  266. MediaPath = video.Path,
  267. Name = video.Name,
  268. ParentIndexNumber = video.ParentIndexNumber,
  269. ProductionYear = video.ProductionYear,
  270. ProviderIds = video.ProviderIds,
  271. RuntimeTicks = video.RunTimeTicks,
  272. IsPerfectMatch = isPerfectMatch ?? false,
  273. IsAutomated = isAutomated
  274. };
  275. if (video is Episode episode)
  276. {
  277. request.IndexNumberEnd = episode.IndexNumberEnd;
  278. request.SeriesName = episode.SeriesName;
  279. }
  280. return SearchSubtitles(request, cancellationToken);
  281. }
  282. private void Normalize(IEnumerable<RemoteSubtitleInfo> subtitles)
  283. {
  284. foreach (var sub in subtitles)
  285. {
  286. sub.Id = GetProviderId(sub.ProviderName) + "_" + sub.Id;
  287. }
  288. }
  289. private string GetProviderId(string name)
  290. {
  291. return name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
  292. }
  293. private ISubtitleProvider GetProvider(string id)
  294. {
  295. return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name), StringComparison.Ordinal));
  296. }
  297. /// <inheritdoc />
  298. public Task DeleteSubtitles(BaseItem item, int index)
  299. {
  300. var stream = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
  301. {
  302. Index = index,
  303. ItemId = item.Id,
  304. Type = MediaStreamType.Subtitle
  305. })[0];
  306. var path = stream.Path;
  307. _monitor.ReportFileSystemChangeBeginning(path);
  308. try
  309. {
  310. _fileSystem.DeleteFile(path);
  311. }
  312. finally
  313. {
  314. _monitor.ReportFileSystemChangeComplete(path, false);
  315. }
  316. return item.RefreshMetadata(CancellationToken.None);
  317. }
  318. /// <inheritdoc />
  319. public Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken)
  320. {
  321. var parts = id.Split('_', 2);
  322. var provider = GetProvider(parts[0]);
  323. id = parts[^1];
  324. return provider.GetSubtitles(id, cancellationToken);
  325. }
  326. /// <inheritdoc />
  327. public SubtitleProviderInfo[] GetSupportedProviders(BaseItem item)
  328. {
  329. VideoContentType mediaType;
  330. if (item is Episode)
  331. {
  332. mediaType = VideoContentType.Episode;
  333. }
  334. else if (item is Movie)
  335. {
  336. mediaType = VideoContentType.Movie;
  337. }
  338. else
  339. {
  340. // These are the only supported types
  341. return Array.Empty<SubtitleProviderInfo>();
  342. }
  343. return _subtitleProviders
  344. .Where(i => i.SupportedMediaTypes.Contains(mediaType))
  345. .Select(i => new SubtitleProviderInfo
  346. {
  347. Name = i.Name,
  348. Id = GetProviderId(i.Name)
  349. }).ToArray();
  350. }
  351. }
  352. }