FFProbeAudioInfo.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. using MediaBrowser.Common.Extensions;
  2. using MediaBrowser.Controller.Entities;
  3. using MediaBrowser.Controller.Entities.Audio;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Controller.MediaInfo;
  6. using MediaBrowser.Controller.Persistence;
  7. using MediaBrowser.Model.Entities;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Globalization;
  11. using System.Linq;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. namespace MediaBrowser.Providers.MediaInfo
  15. {
  16. class FFProbeAudioInfo
  17. {
  18. private readonly IMediaEncoder _mediaEncoder;
  19. private readonly IItemRepository _itemRepo;
  20. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  21. public FFProbeAudioInfo(IMediaEncoder mediaEncoder, IItemRepository itemRepo)
  22. {
  23. _mediaEncoder = mediaEncoder;
  24. _itemRepo = itemRepo;
  25. }
  26. public async Task<ItemUpdateType> Probe<T>(T item, CancellationToken cancellationToken)
  27. where T : Audio
  28. {
  29. var result = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
  30. cancellationToken.ThrowIfCancellationRequested();
  31. FFProbeHelpers.NormalizeFFProbeResult(result);
  32. cancellationToken.ThrowIfCancellationRequested();
  33. await Fetch(item, cancellationToken, result).ConfigureAwait(false);
  34. return ItemUpdateType.MetadataImport;
  35. }
  36. private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
  37. {
  38. cancellationToken.ThrowIfCancellationRequested();
  39. const InputType type = InputType.File;
  40. var inputPath = new[] { item.Path };
  41. return await _mediaEncoder.GetMediaInfo(inputPath, type, false, cancellationToken).ConfigureAwait(false);
  42. }
  43. /// <summary>
  44. /// Fetches the specified audio.
  45. /// </summary>
  46. /// <param name="audio">The audio.</param>
  47. /// <param name="cancellationToken">The cancellation token.</param>
  48. /// <param name="data">The data.</param>
  49. /// <returns>Task.</returns>
  50. protected Task Fetch(Audio audio, CancellationToken cancellationToken, InternalMediaInfoResult data)
  51. {
  52. var mediaStreams = MediaEncoderHelpers.GetMediaInfo(data).MediaStreams;
  53. audio.HasEmbeddedImage = mediaStreams.Any(i => i.Type == MediaStreamType.Video);
  54. if (data.streams != null)
  55. {
  56. // Get the first audio stream
  57. var stream = data.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
  58. if (stream != null)
  59. {
  60. // Get duration from stream properties
  61. var duration = stream.duration;
  62. // If it's not there go into format properties
  63. if (string.IsNullOrEmpty(duration))
  64. {
  65. duration = data.format.duration;
  66. }
  67. // If we got something, parse it
  68. if (!string.IsNullOrEmpty(duration))
  69. {
  70. audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
  71. }
  72. }
  73. }
  74. if (data.format.tags != null)
  75. {
  76. FetchDataFromTags(audio, data.format.tags);
  77. }
  78. return _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
  79. }
  80. /// <summary>
  81. /// Fetches data from the tags dictionary
  82. /// </summary>
  83. /// <param name="audio">The audio.</param>
  84. /// <param name="tags">The tags.</param>
  85. private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
  86. {
  87. var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
  88. // Only set Name if title was found in the dictionary
  89. if (!string.IsNullOrEmpty(title))
  90. {
  91. audio.Name = title;
  92. }
  93. if (!audio.LockedFields.Contains(MetadataFields.Cast))
  94. {
  95. audio.People.Clear();
  96. var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
  97. if (!string.IsNullOrWhiteSpace(composer))
  98. {
  99. foreach (var person in Split(composer))
  100. {
  101. audio.AddPerson(new PersonInfo { Name = person, Type = PersonType.Composer });
  102. }
  103. }
  104. }
  105. audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
  106. var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
  107. if (string.IsNullOrWhiteSpace(artist))
  108. {
  109. audio.Artists.Clear();
  110. }
  111. else
  112. {
  113. audio.Artists = SplitArtists(artist)
  114. .Distinct(StringComparer.OrdinalIgnoreCase)
  115. .ToList();
  116. }
  117. // Several different forms of albumartist
  118. audio.AlbumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album artist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
  119. // Track number
  120. audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
  121. // Disc number
  122. audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
  123. audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
  124. // Several different forms of retaildate
  125. audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date");
  126. // If we don't have a ProductionYear try and get it from PremiereDate
  127. if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
  128. {
  129. audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
  130. }
  131. if (!audio.LockedFields.Contains(MetadataFields.Genres))
  132. {
  133. FetchGenres(audio, tags);
  134. }
  135. if (!audio.LockedFields.Contains(MetadataFields.Studios))
  136. {
  137. audio.Studios.Clear();
  138. // There's several values in tags may or may not be present
  139. FetchStudios(audio, tags, "organization");
  140. FetchStudios(audio, tags, "ensemble");
  141. FetchStudios(audio, tags, "publisher");
  142. }
  143. }
  144. private readonly char[] _nameDelimiters = new[] { '/', '|', ';', '\\' };
  145. /// <summary>
  146. /// Splits the specified val.
  147. /// </summary>
  148. /// <param name="val">The val.</param>
  149. /// <returns>System.String[][].</returns>
  150. private IEnumerable<string> Split(string val)
  151. {
  152. // Only use the comma as a delimeter if there are no slashes or pipes.
  153. // We want to be careful not to split names that have commas in them
  154. var delimeter = _nameDelimiters.Any(i => val.IndexOf(i) != -1) ? _nameDelimiters : new[] { ',' };
  155. return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
  156. .Where(i => !string.IsNullOrWhiteSpace(i))
  157. .Select(i => i.Trim());
  158. }
  159. private const string ArtistReplaceValue = " | ";
  160. private IEnumerable<string> SplitArtists(string val)
  161. {
  162. val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
  163. .Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
  164. // Only use the comma as a delimeter if there are no slashes or pipes.
  165. // We want to be careful not to split names that have commas in them
  166. var delimeter = _nameDelimiters;
  167. return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
  168. .Where(i => !string.IsNullOrWhiteSpace(i))
  169. .Select(i => i.Trim());
  170. }
  171. /// <summary>
  172. /// Gets the studios from the tags collection
  173. /// </summary>
  174. /// <param name="audio">The audio.</param>
  175. /// <param name="tags">The tags.</param>
  176. /// <param name="tagName">Name of the tag.</param>
  177. private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
  178. {
  179. var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
  180. if (!string.IsNullOrEmpty(val))
  181. {
  182. // Sometimes the artist name is listed here, account for that
  183. var studios = Split(val).Where(i => !audio.HasArtist(i));
  184. foreach (var studio in studios)
  185. {
  186. audio.AddStudio(studio);
  187. }
  188. }
  189. }
  190. /// <summary>
  191. /// Gets the genres from the tags collection
  192. /// </summary>
  193. /// <param name="audio">The audio.</param>
  194. /// <param name="tags">The tags.</param>
  195. private void FetchGenres(Audio audio, Dictionary<string, string> tags)
  196. {
  197. var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
  198. if (!string.IsNullOrEmpty(val))
  199. {
  200. audio.Genres.Clear();
  201. foreach (var genre in Split(val))
  202. {
  203. audio.AddGenre(genre);
  204. }
  205. }
  206. }
  207. /// <summary>
  208. /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
  209. /// </summary>
  210. /// <param name="tags">The tags.</param>
  211. /// <param name="tagName">Name of the tag.</param>
  212. /// <returns>System.Nullable{System.Int32}.</returns>
  213. private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
  214. {
  215. var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
  216. if (!string.IsNullOrEmpty(disc))
  217. {
  218. disc = disc.Split('/')[0];
  219. int num;
  220. if (int.TryParse(disc, out num))
  221. {
  222. return num;
  223. }
  224. }
  225. return null;
  226. }
  227. }
  228. }