FFProbeVideoInfo.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. #nullable disable
  2. #pragma warning disable CA1068, 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 MediaBrowser.Common.Configuration;
  11. using MediaBrowser.Controller.Chapters;
  12. using MediaBrowser.Controller.Configuration;
  13. using MediaBrowser.Controller.Entities;
  14. using MediaBrowser.Controller.Entities.Movies;
  15. using MediaBrowser.Controller.Entities.TV;
  16. using MediaBrowser.Controller.Library;
  17. using MediaBrowser.Controller.MediaEncoding;
  18. using MediaBrowser.Controller.Persistence;
  19. using MediaBrowser.Controller.Providers;
  20. using MediaBrowser.Controller.Subtitles;
  21. using MediaBrowser.Model.Configuration;
  22. using MediaBrowser.Model.Dlna;
  23. using MediaBrowser.Model.Dto;
  24. using MediaBrowser.Model.Entities;
  25. using MediaBrowser.Model.Globalization;
  26. using MediaBrowser.Model.MediaInfo;
  27. using MediaBrowser.Model.Providers;
  28. using Microsoft.Extensions.Logging;
  29. namespace MediaBrowser.Providers.MediaInfo
  30. {
  31. public class FFProbeVideoInfo
  32. {
  33. private readonly ILogger<FFProbeVideoInfo> _logger;
  34. private readonly IMediaEncoder _mediaEncoder;
  35. private readonly IItemRepository _itemRepo;
  36. private readonly IBlurayExaminer _blurayExaminer;
  37. private readonly ILocalizationManager _localization;
  38. private readonly IEncodingManager _encodingManager;
  39. private readonly IServerConfigurationManager _config;
  40. private readonly ISubtitleManager _subtitleManager;
  41. private readonly IChapterManager _chapterManager;
  42. private readonly ILibraryManager _libraryManager;
  43. private readonly AudioResolver _audioResolver;
  44. private readonly SubtitleResolver _subtitleResolver;
  45. private readonly IMediaSourceManager _mediaSourceManager;
  46. public FFProbeVideoInfo(
  47. ILogger<FFProbeVideoInfo> logger,
  48. IMediaSourceManager mediaSourceManager,
  49. IMediaEncoder mediaEncoder,
  50. IItemRepository itemRepo,
  51. IBlurayExaminer blurayExaminer,
  52. ILocalizationManager localization,
  53. IEncodingManager encodingManager,
  54. IServerConfigurationManager config,
  55. ISubtitleManager subtitleManager,
  56. IChapterManager chapterManager,
  57. ILibraryManager libraryManager,
  58. AudioResolver audioResolver,
  59. SubtitleResolver subtitleResolver)
  60. {
  61. _logger = logger;
  62. _mediaSourceManager = mediaSourceManager;
  63. _mediaEncoder = mediaEncoder;
  64. _itemRepo = itemRepo;
  65. _blurayExaminer = blurayExaminer;
  66. _localization = localization;
  67. _encodingManager = encodingManager;
  68. _config = config;
  69. _subtitleManager = subtitleManager;
  70. _chapterManager = chapterManager;
  71. _libraryManager = libraryManager;
  72. _audioResolver = audioResolver;
  73. _subtitleResolver = subtitleResolver;
  74. }
  75. public async Task<ItemUpdateType> ProbeVideo<T>(
  76. T item,
  77. MetadataRefreshOptions options,
  78. CancellationToken cancellationToken)
  79. where T : Video
  80. {
  81. BlurayDiscInfo blurayDiscInfo = null;
  82. Model.MediaInfo.MediaInfo mediaInfoResult = null;
  83. if (!item.IsShortcut || options.EnableRemoteContentProbe)
  84. {
  85. if (item.VideoType == VideoType.Dvd)
  86. {
  87. // Get list of playable .vob files
  88. var vobs = _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, null);
  89. // Return if no playable .vob files are found
  90. if (vobs.Count == 0)
  91. {
  92. _logger.LogError("No playable .vob files found in DVD structure, skipping FFprobe.");
  93. return ItemUpdateType.MetadataImport;
  94. }
  95. // Fetch metadata of first .vob file
  96. mediaInfoResult = await GetMediaInfo(
  97. new Video
  98. {
  99. Path = vobs[0]
  100. },
  101. cancellationToken).ConfigureAwait(false);
  102. // Sum up the runtime of all .vob files skipping the first .vob
  103. for (var i = 1; i < vobs.Count; i++)
  104. {
  105. var tmpMediaInfo = await GetMediaInfo(
  106. new Video
  107. {
  108. Path = vobs[i]
  109. },
  110. cancellationToken).ConfigureAwait(false);
  111. mediaInfoResult.RunTimeTicks += tmpMediaInfo.RunTimeTicks;
  112. }
  113. }
  114. else if (item.VideoType == VideoType.BluRay)
  115. {
  116. // Get BD disc information
  117. blurayDiscInfo = GetBDInfo(item.Path);
  118. // Get playable .m2ts files
  119. var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path);
  120. // Return if no playable .m2ts files are found
  121. if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0)
  122. {
  123. _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe.");
  124. return ItemUpdateType.MetadataImport;
  125. }
  126. // Fetch metadata of first .m2ts file
  127. mediaInfoResult = await GetMediaInfo(
  128. new Video
  129. {
  130. Path = m2ts[0]
  131. },
  132. cancellationToken).ConfigureAwait(false);
  133. }
  134. else
  135. {
  136. mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
  137. }
  138. cancellationToken.ThrowIfCancellationRequested();
  139. }
  140. await Fetch(item, cancellationToken, mediaInfoResult, blurayDiscInfo, options).ConfigureAwait(false);
  141. return ItemUpdateType.MetadataImport;
  142. }
  143. private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(
  144. Video item,
  145. CancellationToken cancellationToken)
  146. {
  147. cancellationToken.ThrowIfCancellationRequested();
  148. var path = item.Path;
  149. var protocol = item.PathProtocol ?? MediaProtocol.File;
  150. if (item.IsShortcut)
  151. {
  152. path = item.ShortcutPath;
  153. protocol = _mediaSourceManager.GetPathProtocol(path);
  154. }
  155. return _mediaEncoder.GetMediaInfo(
  156. new MediaInfoRequest
  157. {
  158. ExtractChapters = true,
  159. MediaType = DlnaProfileType.Video,
  160. MediaSource = new MediaSourceInfo
  161. {
  162. Path = path,
  163. Protocol = protocol,
  164. VideoType = item.VideoType,
  165. IsoType = item.IsoType
  166. }
  167. },
  168. cancellationToken);
  169. }
  170. protected async Task Fetch(
  171. Video video,
  172. CancellationToken cancellationToken,
  173. Model.MediaInfo.MediaInfo mediaInfo,
  174. BlurayDiscInfo blurayInfo,
  175. MetadataRefreshOptions options)
  176. {
  177. List<MediaStream> mediaStreams;
  178. IReadOnlyList<MediaAttachment> mediaAttachments;
  179. ChapterInfo[] chapters;
  180. mediaStreams = new List<MediaStream>();
  181. // Add external streams before adding the streams from the file to preserve stream IDs on remote videos
  182. await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
  183. await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
  184. var startIndex = mediaStreams.Count == 0 ? 0 : (mediaStreams.Max(i => i.Index) + 1);
  185. if (mediaInfo is not null)
  186. {
  187. foreach (var mediaStream in mediaInfo.MediaStreams)
  188. {
  189. mediaStream.Index = startIndex++;
  190. mediaStreams.Add(mediaStream);
  191. }
  192. mediaAttachments = mediaInfo.MediaAttachments;
  193. video.TotalBitrate = mediaInfo.Bitrate;
  194. video.RunTimeTicks = mediaInfo.RunTimeTicks;
  195. video.Size = mediaInfo.Size;
  196. if (video.VideoType == VideoType.VideoFile)
  197. {
  198. var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.');
  199. video.Container = extension;
  200. }
  201. else
  202. {
  203. video.Container = null;
  204. }
  205. video.Container = mediaInfo.Container;
  206. chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
  207. if (blurayInfo is not null)
  208. {
  209. FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
  210. }
  211. }
  212. else
  213. {
  214. var currentMediaStreams = video.GetMediaStreams();
  215. foreach (var mediaStream in currentMediaStreams)
  216. {
  217. if (!mediaStream.IsExternal)
  218. {
  219. mediaStream.Index = startIndex++;
  220. mediaStreams.Add(mediaStream);
  221. }
  222. }
  223. mediaAttachments = Array.Empty<MediaAttachment>();
  224. chapters = Array.Empty<ChapterInfo>();
  225. }
  226. var libraryOptions = _libraryManager.GetLibraryOptions(video);
  227. if (mediaInfo is not null)
  228. {
  229. FetchEmbeddedInfo(video, mediaInfo, options, libraryOptions);
  230. FetchPeople(video, mediaInfo, options);
  231. video.Timestamp = mediaInfo.Timestamp;
  232. video.Video3DFormat ??= mediaInfo.Video3DFormat;
  233. }
  234. if (libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowText || libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowNone)
  235. {
  236. _logger.LogDebug("Disabling embedded image subtitles for {Path} due to DisableEmbeddedImageSubtitles setting", video.Path);
  237. mediaStreams.RemoveAll(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal && !i.IsTextSubtitleStream);
  238. }
  239. if (libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowImage || libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowNone)
  240. {
  241. _logger.LogDebug("Disabling embedded text subtitles for {Path} due to DisableEmbeddedTextSubtitles setting", video.Path);
  242. mediaStreams.RemoveAll(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal && i.IsTextSubtitleStream);
  243. }
  244. var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
  245. video.Height = videoStream?.Height ?? 0;
  246. video.Width = videoStream?.Width ?? 0;
  247. video.DefaultVideoStreamIndex = videoStream?.Index;
  248. video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
  249. _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
  250. if (mediaAttachments.Any())
  251. {
  252. _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
  253. }
  254. if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
  255. options.MetadataRefreshMode == MetadataRefreshMode.Default)
  256. {
  257. if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
  258. {
  259. chapters = CreateDummyChapters(video);
  260. }
  261. NormalizeChapterNames(chapters);
  262. var extractDuringScan = false;
  263. if (libraryOptions is not null)
  264. {
  265. extractDuringScan = libraryOptions.ExtractChapterImagesDuringLibraryScan;
  266. }
  267. await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false);
  268. _chapterManager.SaveChapters(video.Id, chapters);
  269. }
  270. }
  271. private void NormalizeChapterNames(ChapterInfo[] chapters)
  272. {
  273. for (int i = 0; i < chapters.Length; i++)
  274. {
  275. string name = chapters[i].Name;
  276. // Check if the name is empty and/or if the name is a time
  277. // Some ripping programs do that.
  278. if (string.IsNullOrWhiteSpace(name) ||
  279. TimeSpan.TryParse(name, out _))
  280. {
  281. chapters[i].Name = string.Format(
  282. CultureInfo.InvariantCulture,
  283. _localization.GetLocalizedString("ChapterNameValue"),
  284. (i + 1).ToString(CultureInfo.InvariantCulture));
  285. }
  286. }
  287. }
  288. private void FetchBdInfo(Video video, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
  289. {
  290. if (blurayInfo.Files.Length <= 1)
  291. {
  292. return;
  293. }
  294. // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output
  295. int? currentHeight = null;
  296. int? currentWidth = null;
  297. int? currentBitRate = null;
  298. var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
  299. // Grab the values that ffprobe recorded
  300. if (videoStream is not null)
  301. {
  302. currentBitRate = videoStream.BitRate;
  303. currentWidth = videoStream.Width;
  304. currentHeight = videoStream.Height;
  305. }
  306. // Fill video properties from the BDInfo result
  307. mediaStreams.Clear();
  308. mediaStreams.AddRange(blurayInfo.MediaStreams);
  309. if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0)
  310. {
  311. video.RunTimeTicks = blurayInfo.RunTimeTicks;
  312. }
  313. if (blurayInfo.Chapters is not null)
  314. {
  315. double[] brChapter = blurayInfo.Chapters;
  316. chapters = new ChapterInfo[brChapter.Length];
  317. for (int i = 0; i < brChapter.Length; i++)
  318. {
  319. chapters[i] = new ChapterInfo
  320. {
  321. StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks
  322. };
  323. }
  324. }
  325. videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
  326. // Use the ffprobe values if these are empty
  327. if (videoStream is not null)
  328. {
  329. videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
  330. videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
  331. videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
  332. }
  333. }
  334. private bool IsEmpty(int? num)
  335. {
  336. return !num.HasValue || num.Value == 0;
  337. }
  338. /// <summary>
  339. /// Gets information about the longest playlist on a bdrom.
  340. /// </summary>
  341. /// <param name="path">The path.</param>
  342. /// <returns>VideoStream.</returns>
  343. private BlurayDiscInfo GetBDInfo(string path)
  344. {
  345. ArgumentException.ThrowIfNullOrEmpty(path);
  346. try
  347. {
  348. return _blurayExaminer.GetDiscInfo(path);
  349. }
  350. catch (Exception ex)
  351. {
  352. _logger.LogError(ex, "Error getting BDInfo");
  353. return null;
  354. }
  355. }
  356. private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
  357. {
  358. var replaceData = refreshOptions.ReplaceAllMetadata;
  359. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.OfficialRating))
  360. {
  361. if (string.IsNullOrWhiteSpace(video.OfficialRating) || replaceData)
  362. {
  363. video.OfficialRating = data.OfficialRating;
  364. }
  365. }
  366. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Genres))
  367. {
  368. if (video.Genres.Length == 0 || replaceData)
  369. {
  370. video.Genres = Array.Empty<string>();
  371. foreach (var genre in data.Genres)
  372. {
  373. video.AddGenre(genre);
  374. }
  375. }
  376. }
  377. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Studios))
  378. {
  379. if (video.Studios.Length == 0 || replaceData)
  380. {
  381. video.SetStudios(data.Studios);
  382. }
  383. }
  384. if (!video.IsLocked && video is MusicVideo musicVideo)
  385. {
  386. if (string.IsNullOrEmpty(musicVideo.Album) || replaceData)
  387. {
  388. musicVideo.Album = data.Album;
  389. }
  390. if (musicVideo.Artists.Count == 0 || replaceData)
  391. {
  392. musicVideo.Artists = data.Artists;
  393. }
  394. }
  395. if (data.ProductionYear.HasValue)
  396. {
  397. if (!video.ProductionYear.HasValue || replaceData)
  398. {
  399. video.ProductionYear = data.ProductionYear;
  400. }
  401. }
  402. if (data.PremiereDate.HasValue)
  403. {
  404. if (!video.PremiereDate.HasValue || replaceData)
  405. {
  406. video.PremiereDate = data.PremiereDate;
  407. }
  408. }
  409. if (data.IndexNumber.HasValue)
  410. {
  411. if (!video.IndexNumber.HasValue || replaceData)
  412. {
  413. video.IndexNumber = data.IndexNumber;
  414. }
  415. }
  416. if (data.ParentIndexNumber.HasValue)
  417. {
  418. if (!video.ParentIndexNumber.HasValue || replaceData)
  419. {
  420. video.ParentIndexNumber = data.ParentIndexNumber;
  421. }
  422. }
  423. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Name))
  424. {
  425. if (!string.IsNullOrWhiteSpace(data.Name) && libraryOptions.EnableEmbeddedTitles)
  426. {
  427. // Separate option to use the embedded name for extras because it will often be the same name as the movie
  428. if (!video.ExtraType.HasValue || libraryOptions.EnableEmbeddedExtrasTitles)
  429. {
  430. video.Name = data.Name;
  431. }
  432. }
  433. if (!string.IsNullOrWhiteSpace(data.ForcedSortName))
  434. {
  435. video.ForcedSortName = data.ForcedSortName;
  436. }
  437. }
  438. // If we don't have a ProductionYear try and get it from PremiereDate
  439. if (video.PremiereDate.HasValue && !video.ProductionYear.HasValue)
  440. {
  441. video.ProductionYear = video.PremiereDate.Value.ToLocalTime().Year;
  442. }
  443. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Overview))
  444. {
  445. if (string.IsNullOrWhiteSpace(video.Overview) || replaceData)
  446. {
  447. video.Overview = data.Overview;
  448. }
  449. }
  450. }
  451. private void FetchPeople(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options)
  452. {
  453. var replaceData = options.ReplaceAllMetadata;
  454. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Cast))
  455. {
  456. if (replaceData || _libraryManager.GetPeople(video).Count == 0)
  457. {
  458. var people = new List<PersonInfo>();
  459. foreach (var person in data.People)
  460. {
  461. PeopleHelper.AddPerson(people, new PersonInfo
  462. {
  463. Name = person.Name,
  464. Type = person.Type,
  465. Role = person.Role
  466. });
  467. }
  468. _libraryManager.UpdatePeople(video, people);
  469. }
  470. }
  471. }
  472. private SubtitleOptions GetOptions()
  473. {
  474. return _config.GetConfiguration<SubtitleOptions>("subtitles");
  475. }
  476. /// <summary>
  477. /// Adds the external subtitles.
  478. /// </summary>
  479. /// <param name="video">The video.</param>
  480. /// <param name="currentStreams">The current streams.</param>
  481. /// <param name="options">The refreshOptions.</param>
  482. /// <param name="cancellationToken">The cancellation token.</param>
  483. /// <returns>Task.</returns>
  484. private async Task AddExternalSubtitlesAsync(
  485. Video video,
  486. List<MediaStream> currentStreams,
  487. MetadataRefreshOptions options,
  488. CancellationToken cancellationToken)
  489. {
  490. var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
  491. var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false);
  492. var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
  493. options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
  494. var subtitleOptions = GetOptions();
  495. var libraryOptions = _libraryManager.GetLibraryOptions(video);
  496. string[] subtitleDownloadLanguages;
  497. bool skipIfEmbeddedSubtitlesPresent;
  498. bool skipIfAudioTrackMatches;
  499. bool requirePerfectMatch;
  500. bool enabled;
  501. if (libraryOptions.SubtitleDownloadLanguages is null)
  502. {
  503. subtitleDownloadLanguages = subtitleOptions.DownloadLanguages;
  504. skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent;
  505. skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches;
  506. requirePerfectMatch = subtitleOptions.RequirePerfectMatch;
  507. enabled = (subtitleOptions.DownloadEpisodeSubtitles &&
  508. video is Episode) ||
  509. (subtitleOptions.DownloadMovieSubtitles &&
  510. video is Movie);
  511. }
  512. else
  513. {
  514. subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
  515. skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
  516. skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
  517. requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
  518. enabled = true;
  519. }
  520. if (enableSubtitleDownloading && enabled)
  521. {
  522. var downloadedLanguages = await new SubtitleDownloader(
  523. _logger,
  524. _subtitleManager).DownloadSubtitles(
  525. video,
  526. currentStreams.Concat(externalSubtitleStreams).ToList(),
  527. skipIfEmbeddedSubtitlesPresent,
  528. skipIfAudioTrackMatches,
  529. requirePerfectMatch,
  530. subtitleDownloadLanguages,
  531. libraryOptions.DisabledSubtitleFetchers,
  532. libraryOptions.SubtitleFetcherOrder,
  533. true,
  534. cancellationToken).ConfigureAwait(false);
  535. // Rescan
  536. if (downloadedLanguages.Count > 0)
  537. {
  538. externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken).ConfigureAwait(false);
  539. }
  540. }
  541. video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).Distinct().ToArray();
  542. currentStreams.AddRange(externalSubtitleStreams);
  543. }
  544. /// <summary>
  545. /// Adds the external audio.
  546. /// </summary>
  547. /// <param name="video">The video.</param>
  548. /// <param name="currentStreams">The current streams.</param>
  549. /// <param name="options">The refreshOptions.</param>
  550. /// <param name="cancellationToken">The cancellation token.</param>
  551. private async Task AddExternalAudioAsync(
  552. Video video,
  553. List<MediaStream> currentStreams,
  554. MetadataRefreshOptions options,
  555. CancellationToken cancellationToken)
  556. {
  557. var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
  558. var externalAudioStreams = await _audioResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false);
  559. video.AudioFiles = externalAudioStreams.Select(i => i.Path).Distinct().ToArray();
  560. currentStreams.AddRange(externalAudioStreams);
  561. }
  562. /// <summary>
  563. /// Creates dummy chapters.
  564. /// </summary>
  565. /// <param name="video">The video.</param>
  566. /// <returns>An array of dummy chapters.</returns>
  567. private ChapterInfo[] CreateDummyChapters(Video video)
  568. {
  569. var runtime = video.RunTimeTicks ?? 0;
  570. // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
  571. if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
  572. {
  573. throw new ArgumentException(
  574. string.Format(
  575. CultureInfo.InvariantCulture,
  576. "{0} has an invalid runtime of {1} minutes",
  577. video.Name,
  578. TimeSpan.FromTicks(runtime).Minutes));
  579. }
  580. long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
  581. if (runtime > dummyChapterDuration)
  582. {
  583. int chapterCount = (int)(runtime / dummyChapterDuration);
  584. var chapters = new ChapterInfo[chapterCount];
  585. long currentChapterTicks = 0;
  586. for (int i = 0; i < chapterCount; i++)
  587. {
  588. chapters[i] = new ChapterInfo
  589. {
  590. StartPositionTicks = currentChapterTicks
  591. };
  592. currentChapterTicks += dummyChapterDuration;
  593. }
  594. return chapters;
  595. }
  596. return Array.Empty<ChapterInfo>();
  597. }
  598. }
  599. }