FFProbeVideoInfo.cs 28 KB

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