FFProbeVideoInfo.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  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 ILocalizationManager _localization;
  37. private readonly IEncodingManager _encodingManager;
  38. private readonly IServerConfigurationManager _config;
  39. private readonly ISubtitleManager _subtitleManager;
  40. private readonly IChapterManager _chapterManager;
  41. private readonly ILibraryManager _libraryManager;
  42. private readonly AudioResolver _audioResolver;
  43. private readonly SubtitleResolver _subtitleResolver;
  44. private readonly IMediaSourceManager _mediaSourceManager;
  45. public FFProbeVideoInfo(
  46. ILogger<FFProbeVideoInfo> logger,
  47. IMediaSourceManager mediaSourceManager,
  48. IMediaEncoder mediaEncoder,
  49. IItemRepository itemRepo,
  50. ILocalizationManager localization,
  51. IEncodingManager encodingManager,
  52. IServerConfigurationManager config,
  53. ISubtitleManager subtitleManager,
  54. IChapterManager chapterManager,
  55. ILibraryManager libraryManager,
  56. AudioResolver audioResolver,
  57. SubtitleResolver subtitleResolver)
  58. {
  59. _logger = logger;
  60. _mediaSourceManager = mediaSourceManager;
  61. _mediaEncoder = mediaEncoder;
  62. _itemRepo = itemRepo;
  63. _localization = localization;
  64. _encodingManager = encodingManager;
  65. _config = config;
  66. _subtitleManager = subtitleManager;
  67. _chapterManager = chapterManager;
  68. _libraryManager = libraryManager;
  69. _audioResolver = audioResolver;
  70. _subtitleResolver = subtitleResolver;
  71. }
  72. public async Task<ItemUpdateType> ProbeVideo<T>(
  73. T item,
  74. MetadataRefreshOptions options,
  75. CancellationToken cancellationToken)
  76. where T : Video
  77. {
  78. Model.MediaInfo.MediaInfo mediaInfoResult = null;
  79. if (!item.IsShortcut || options.EnableRemoteContentProbe)
  80. {
  81. mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
  82. cancellationToken.ThrowIfCancellationRequested();
  83. }
  84. await Fetch(item, cancellationToken, mediaInfoResult, options).ConfigureAwait(false);
  85. return ItemUpdateType.MetadataImport;
  86. }
  87. private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(
  88. Video item,
  89. CancellationToken cancellationToken)
  90. {
  91. cancellationToken.ThrowIfCancellationRequested();
  92. var path = item.Path;
  93. var protocol = item.PathProtocol ?? MediaProtocol.File;
  94. if (item.IsShortcut)
  95. {
  96. path = item.ShortcutPath;
  97. protocol = _mediaSourceManager.GetPathProtocol(path);
  98. }
  99. return _mediaEncoder.GetMediaInfo(
  100. new MediaInfoRequest
  101. {
  102. ExtractChapters = true,
  103. MediaType = DlnaProfileType.Video,
  104. MediaSource = new MediaSourceInfo
  105. {
  106. Path = path,
  107. Protocol = protocol,
  108. VideoType = item.VideoType,
  109. IsoType = item.IsoType
  110. }
  111. },
  112. cancellationToken);
  113. }
  114. protected async Task Fetch(
  115. Video video,
  116. CancellationToken cancellationToken,
  117. Model.MediaInfo.MediaInfo mediaInfo,
  118. MetadataRefreshOptions options)
  119. {
  120. List<MediaStream> mediaStreams;
  121. IReadOnlyList<MediaAttachment> mediaAttachments;
  122. ChapterInfo[] chapters;
  123. mediaStreams = new List<MediaStream>();
  124. // Add external streams before adding the streams from the file to preserve stream IDs on remote videos
  125. await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
  126. await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
  127. var startIndex = mediaStreams.Count == 0 ? 0 : (mediaStreams.Max(i => i.Index) + 1);
  128. if (mediaInfo is not null)
  129. {
  130. foreach (var mediaStream in mediaInfo.MediaStreams)
  131. {
  132. mediaStream.Index = startIndex++;
  133. mediaStreams.Add(mediaStream);
  134. }
  135. mediaAttachments = mediaInfo.MediaAttachments;
  136. video.TotalBitrate = mediaInfo.Bitrate;
  137. // video.FormatName = (mediaInfo.Container ?? string.Empty)
  138. // .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
  139. // For DVDs this may not always be accurate, so don't set the runtime if the item already has one
  140. var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks is null || video.RunTimeTicks.Value == 0;
  141. if (needToSetRuntime)
  142. {
  143. video.RunTimeTicks = mediaInfo.RunTimeTicks;
  144. }
  145. video.Size = mediaInfo.Size;
  146. if (video.VideoType == VideoType.VideoFile)
  147. {
  148. var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.');
  149. video.Container = extension;
  150. }
  151. else
  152. {
  153. video.Container = null;
  154. }
  155. video.Container = mediaInfo.Container;
  156. chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
  157. }
  158. else
  159. {
  160. var currentMediaStreams = video.GetMediaStreams();
  161. foreach (var mediaStream in currentMediaStreams)
  162. {
  163. if (!mediaStream.IsExternal)
  164. {
  165. mediaStream.Index = startIndex++;
  166. mediaStreams.Add(mediaStream);
  167. }
  168. }
  169. mediaAttachments = Array.Empty<MediaAttachment>();
  170. chapters = Array.Empty<ChapterInfo>();
  171. }
  172. var libraryOptions = _libraryManager.GetLibraryOptions(video);
  173. if (mediaInfo is not null)
  174. {
  175. FetchEmbeddedInfo(video, mediaInfo, options, libraryOptions);
  176. FetchPeople(video, mediaInfo, options);
  177. video.Timestamp = mediaInfo.Timestamp;
  178. video.Video3DFormat ??= mediaInfo.Video3DFormat;
  179. }
  180. if (libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowText || libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowNone)
  181. {
  182. _logger.LogDebug("Disabling embedded image subtitles for {Path} due to DisableEmbeddedImageSubtitles setting", video.Path);
  183. mediaStreams.RemoveAll(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal && !i.IsTextSubtitleStream);
  184. }
  185. if (libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowImage || libraryOptions.AllowEmbeddedSubtitles == EmbeddedSubtitleOptions.AllowNone)
  186. {
  187. _logger.LogDebug("Disabling embedded text subtitles for {Path} due to DisableEmbeddedTextSubtitles setting", video.Path);
  188. mediaStreams.RemoveAll(i => i.Type == MediaStreamType.Subtitle && !i.IsExternal && i.IsTextSubtitleStream);
  189. }
  190. var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
  191. video.Height = videoStream?.Height ?? 0;
  192. video.Width = videoStream?.Width ?? 0;
  193. video.DefaultVideoStreamIndex = videoStream?.Index;
  194. video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
  195. _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
  196. if (mediaAttachments.Any())
  197. {
  198. _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
  199. }
  200. if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
  201. options.MetadataRefreshMode == MetadataRefreshMode.Default)
  202. {
  203. if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
  204. {
  205. chapters = CreateDummyChapters(video);
  206. }
  207. NormalizeChapterNames(chapters);
  208. var extractDuringScan = false;
  209. if (libraryOptions is not null)
  210. {
  211. extractDuringScan = libraryOptions.ExtractChapterImagesDuringLibraryScan;
  212. }
  213. await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false);
  214. _chapterManager.SaveChapters(video.Id, chapters);
  215. }
  216. }
  217. private void NormalizeChapterNames(ChapterInfo[] chapters)
  218. {
  219. for (int i = 0; i < chapters.Length; i++)
  220. {
  221. string name = chapters[i].Name;
  222. // Check if the name is empty and/or if the name is a time
  223. // Some ripping programs do that.
  224. if (string.IsNullOrWhiteSpace(name) ||
  225. TimeSpan.TryParse(name, out _))
  226. {
  227. chapters[i].Name = string.Format(
  228. CultureInfo.InvariantCulture,
  229. _localization.GetLocalizedString("ChapterNameValue"),
  230. (i + 1).ToString(CultureInfo.InvariantCulture));
  231. }
  232. }
  233. }
  234. private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
  235. {
  236. var replaceData = refreshOptions.ReplaceAllMetadata;
  237. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.OfficialRating))
  238. {
  239. if (string.IsNullOrWhiteSpace(video.OfficialRating) || replaceData)
  240. {
  241. video.OfficialRating = data.OfficialRating;
  242. }
  243. }
  244. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Genres))
  245. {
  246. if (video.Genres.Length == 0 || replaceData)
  247. {
  248. video.Genres = Array.Empty<string>();
  249. foreach (var genre in data.Genres)
  250. {
  251. video.AddGenre(genre);
  252. }
  253. }
  254. }
  255. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Studios))
  256. {
  257. if (video.Studios.Length == 0 || replaceData)
  258. {
  259. video.SetStudios(data.Studios);
  260. }
  261. }
  262. if (!video.IsLocked && video is MusicVideo musicVideo)
  263. {
  264. if (string.IsNullOrEmpty(musicVideo.Album) || replaceData)
  265. {
  266. musicVideo.Album = data.Album;
  267. }
  268. if (musicVideo.Artists.Count == 0 || replaceData)
  269. {
  270. musicVideo.Artists = data.Artists;
  271. }
  272. }
  273. if (data.ProductionYear.HasValue)
  274. {
  275. if (!video.ProductionYear.HasValue || replaceData)
  276. {
  277. video.ProductionYear = data.ProductionYear;
  278. }
  279. }
  280. if (data.PremiereDate.HasValue)
  281. {
  282. if (!video.PremiereDate.HasValue || replaceData)
  283. {
  284. video.PremiereDate = data.PremiereDate;
  285. }
  286. }
  287. if (data.IndexNumber.HasValue)
  288. {
  289. if (!video.IndexNumber.HasValue || replaceData)
  290. {
  291. video.IndexNumber = data.IndexNumber;
  292. }
  293. }
  294. if (data.ParentIndexNumber.HasValue)
  295. {
  296. if (!video.ParentIndexNumber.HasValue || replaceData)
  297. {
  298. video.ParentIndexNumber = data.ParentIndexNumber;
  299. }
  300. }
  301. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Name))
  302. {
  303. if (!string.IsNullOrWhiteSpace(data.Name) && libraryOptions.EnableEmbeddedTitles)
  304. {
  305. // Separate option to use the embedded name for extras because it will often be the same name as the movie
  306. if (!video.ExtraType.HasValue || libraryOptions.EnableEmbeddedExtrasTitles)
  307. {
  308. video.Name = data.Name;
  309. }
  310. }
  311. if (!string.IsNullOrWhiteSpace(data.ForcedSortName))
  312. {
  313. video.ForcedSortName = data.ForcedSortName;
  314. }
  315. }
  316. // If we don't have a ProductionYear try and get it from PremiereDate
  317. if (video.PremiereDate.HasValue && !video.ProductionYear.HasValue)
  318. {
  319. video.ProductionYear = video.PremiereDate.Value.ToLocalTime().Year;
  320. }
  321. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Overview))
  322. {
  323. if (string.IsNullOrWhiteSpace(video.Overview) || replaceData)
  324. {
  325. video.Overview = data.Overview;
  326. }
  327. }
  328. }
  329. private void FetchPeople(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options)
  330. {
  331. var replaceData = options.ReplaceAllMetadata;
  332. if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Cast))
  333. {
  334. if (replaceData || _libraryManager.GetPeople(video).Count == 0)
  335. {
  336. var people = new List<PersonInfo>();
  337. foreach (var person in data.People)
  338. {
  339. PeopleHelper.AddPerson(people, new PersonInfo
  340. {
  341. Name = person.Name,
  342. Type = person.Type,
  343. Role = person.Role
  344. });
  345. }
  346. _libraryManager.UpdatePeople(video, people);
  347. }
  348. }
  349. }
  350. private SubtitleOptions GetOptions()
  351. {
  352. return _config.GetConfiguration<SubtitleOptions>("subtitles");
  353. }
  354. /// <summary>
  355. /// Adds the external subtitles.
  356. /// </summary>
  357. /// <param name="video">The video.</param>
  358. /// <param name="currentStreams">The current streams.</param>
  359. /// <param name="options">The refreshOptions.</param>
  360. /// <param name="cancellationToken">The cancellation token.</param>
  361. /// <returns>Task.</returns>
  362. private async Task AddExternalSubtitlesAsync(
  363. Video video,
  364. List<MediaStream> currentStreams,
  365. MetadataRefreshOptions options,
  366. CancellationToken cancellationToken)
  367. {
  368. var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
  369. var externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false);
  370. var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
  371. options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
  372. var subtitleOptions = GetOptions();
  373. var libraryOptions = _libraryManager.GetLibraryOptions(video);
  374. string[] subtitleDownloadLanguages;
  375. bool skipIfEmbeddedSubtitlesPresent;
  376. bool skipIfAudioTrackMatches;
  377. bool requirePerfectMatch;
  378. bool enabled;
  379. if (libraryOptions.SubtitleDownloadLanguages is null)
  380. {
  381. subtitleDownloadLanguages = subtitleOptions.DownloadLanguages;
  382. skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent;
  383. skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches;
  384. requirePerfectMatch = subtitleOptions.RequirePerfectMatch;
  385. enabled = (subtitleOptions.DownloadEpisodeSubtitles &&
  386. video is Episode) ||
  387. (subtitleOptions.DownloadMovieSubtitles &&
  388. video is Movie);
  389. }
  390. else
  391. {
  392. subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
  393. skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
  394. skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
  395. requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
  396. enabled = true;
  397. }
  398. if (enableSubtitleDownloading && enabled)
  399. {
  400. var downloadedLanguages = await new SubtitleDownloader(
  401. _logger,
  402. _subtitleManager).DownloadSubtitles(
  403. video,
  404. currentStreams.Concat(externalSubtitleStreams).ToList(),
  405. skipIfEmbeddedSubtitlesPresent,
  406. skipIfAudioTrackMatches,
  407. requirePerfectMatch,
  408. subtitleDownloadLanguages,
  409. libraryOptions.DisabledSubtitleFetchers,
  410. libraryOptions.SubtitleFetcherOrder,
  411. true,
  412. cancellationToken).ConfigureAwait(false);
  413. // Rescan
  414. if (downloadedLanguages.Count > 0)
  415. {
  416. externalSubtitleStreams = await _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken).ConfigureAwait(false);
  417. }
  418. }
  419. video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).Distinct().ToArray();
  420. currentStreams.AddRange(externalSubtitleStreams);
  421. }
  422. /// <summary>
  423. /// Adds the external audio.
  424. /// </summary>
  425. /// <param name="video">The video.</param>
  426. /// <param name="currentStreams">The current streams.</param>
  427. /// <param name="options">The refreshOptions.</param>
  428. /// <param name="cancellationToken">The cancellation token.</param>
  429. private async Task AddExternalAudioAsync(
  430. Video video,
  431. List<MediaStream> currentStreams,
  432. MetadataRefreshOptions options,
  433. CancellationToken cancellationToken)
  434. {
  435. var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
  436. var externalAudioStreams = await _audioResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken).ConfigureAwait(false);
  437. video.AudioFiles = externalAudioStreams.Select(i => i.Path).Distinct().ToArray();
  438. currentStreams.AddRange(externalAudioStreams);
  439. }
  440. /// <summary>
  441. /// Creates dummy chapters.
  442. /// </summary>
  443. /// <param name="video">The video.</param>
  444. /// <returns>An array of dummy chapters.</returns>
  445. private ChapterInfo[] CreateDummyChapters(Video video)
  446. {
  447. var runtime = video.RunTimeTicks ?? 0;
  448. long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks;
  449. if (runtime < 0)
  450. {
  451. throw new ArgumentException(
  452. string.Format(
  453. CultureInfo.InvariantCulture,
  454. "{0} has invalid runtime of {1}",
  455. video.Name,
  456. runtime));
  457. }
  458. if (runtime < dummyChapterDuration)
  459. {
  460. return Array.Empty<ChapterInfo>();
  461. }
  462. // Limit the chapters just in case there's some incorrect metadata here
  463. int chapterCount = (int)Math.Min(runtime / dummyChapterDuration, _config.Configuration.DummyChapterCount);
  464. var chapters = new ChapterInfo[chapterCount];
  465. long currentChapterTicks = 0;
  466. for (int i = 0; i < chapterCount; i++)
  467. {
  468. chapters[i] = new ChapterInfo
  469. {
  470. StartPositionTicks = currentChapterTicks
  471. };
  472. currentChapterTicks += dummyChapterDuration;
  473. }
  474. return chapters;
  475. }
  476. }
  477. }