FFProbeAudioInfo.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Extensions;
  3. using MediaBrowser.Controller.Entities;
  4. using MediaBrowser.Controller.Entities.Audio;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.MediaInfo;
  7. using MediaBrowser.Controller.Persistence;
  8. using MediaBrowser.Model.Entities;
  9. using MediaBrowser.Model.Serialization;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Globalization;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. namespace MediaBrowser.Providers.MediaInfo
  18. {
  19. class FFProbeAudioInfo
  20. {
  21. private readonly IMediaEncoder _mediaEncoder;
  22. private readonly IItemRepository _itemRepo;
  23. private readonly IApplicationPaths _appPaths;
  24. private readonly IJsonSerializer _json;
  25. private readonly CultureInfo _usCulture = new CultureInfo("en-US");
  26. public FFProbeAudioInfo(IMediaEncoder mediaEncoder, IItemRepository itemRepo, IApplicationPaths appPaths, IJsonSerializer json)
  27. {
  28. _mediaEncoder = mediaEncoder;
  29. _itemRepo = itemRepo;
  30. _appPaths = appPaths;
  31. _json = json;
  32. }
  33. public async Task<ItemUpdateType> Probe<T>(T item, CancellationToken cancellationToken)
  34. where T : Audio
  35. {
  36. var result = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
  37. cancellationToken.ThrowIfCancellationRequested();
  38. FFProbeHelpers.NormalizeFFProbeResult(result);
  39. cancellationToken.ThrowIfCancellationRequested();
  40. await Fetch(item, cancellationToken, result).ConfigureAwait(false);
  41. return ItemUpdateType.MetadataImport;
  42. }
  43. private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, CancellationToken cancellationToken)
  44. {
  45. cancellationToken.ThrowIfCancellationRequested();
  46. var idString = item.Id.ToString("N");
  47. var cachePath = Path.Combine(_appPaths.CachePath, "ffprobe-audio", idString.Substring(0, 2), idString, "v" + _mediaEncoder.Version + item.DateModified.Ticks.ToString(_usCulture) + ".json");
  48. try
  49. {
  50. return _json.DeserializeFromFile<InternalMediaInfoResult>(cachePath);
  51. }
  52. catch (FileNotFoundException)
  53. {
  54. }
  55. catch (DirectoryNotFoundException)
  56. {
  57. }
  58. const InputType type = InputType.File;
  59. var inputPath = new[] { item.Path };
  60. var result = await _mediaEncoder.GetMediaInfo(inputPath, type, false, cancellationToken).ConfigureAwait(false);
  61. Directory.CreateDirectory(Path.GetDirectoryName(cachePath));
  62. _json.SerializeToFile(result, cachePath);
  63. return result;
  64. }
  65. /// <summary>
  66. /// Fetches the specified audio.
  67. /// </summary>
  68. /// <param name="audio">The audio.</param>
  69. /// <param name="cancellationToken">The cancellation token.</param>
  70. /// <param name="data">The data.</param>
  71. /// <returns>Task.</returns>
  72. protected Task Fetch(Audio audio, CancellationToken cancellationToken, InternalMediaInfoResult data)
  73. {
  74. var mediaStreams = MediaEncoderHelpers.GetMediaInfo(data).MediaStreams;
  75. audio.HasEmbeddedImage = mediaStreams.Any(i => i.Type == MediaStreamType.Video);
  76. if (data.streams != null)
  77. {
  78. // Get the first audio stream
  79. var stream = data.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
  80. if (stream != null)
  81. {
  82. // Get duration from stream properties
  83. var duration = stream.duration;
  84. // If it's not there go into format properties
  85. if (string.IsNullOrEmpty(duration))
  86. {
  87. duration = data.format.duration;
  88. }
  89. // If we got something, parse it
  90. if (!string.IsNullOrEmpty(duration))
  91. {
  92. audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
  93. }
  94. }
  95. }
  96. if (data.format.tags != null)
  97. {
  98. FetchDataFromTags(audio, data.format.tags);
  99. }
  100. return _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
  101. }
  102. /// <summary>
  103. /// Fetches data from the tags dictionary
  104. /// </summary>
  105. /// <param name="audio">The audio.</param>
  106. /// <param name="tags">The tags.</param>
  107. private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
  108. {
  109. var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
  110. // Only set Name if title was found in the dictionary
  111. if (!string.IsNullOrEmpty(title))
  112. {
  113. audio.Name = title;
  114. }
  115. if (!audio.LockedFields.Contains(MetadataFields.Cast))
  116. {
  117. audio.People.Clear();
  118. var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
  119. if (!string.IsNullOrWhiteSpace(composer))
  120. {
  121. foreach (var person in Split(composer))
  122. {
  123. audio.AddPerson(new PersonInfo { Name = person, Type = PersonType.Composer });
  124. }
  125. }
  126. }
  127. audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
  128. var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
  129. if (string.IsNullOrWhiteSpace(artist))
  130. {
  131. audio.Artists.Clear();
  132. }
  133. else
  134. {
  135. audio.Artists = SplitArtists(artist)
  136. .Distinct(StringComparer.OrdinalIgnoreCase)
  137. .ToList();
  138. }
  139. // Several different forms of albumartist
  140. audio.AlbumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album artist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
  141. // Track number
  142. audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
  143. // Disc number
  144. audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
  145. audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
  146. // Several different forms of retaildate
  147. audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date");
  148. // If we don't have a ProductionYear try and get it from PremiereDate
  149. if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
  150. {
  151. audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
  152. }
  153. if (!audio.LockedFields.Contains(MetadataFields.Genres))
  154. {
  155. FetchGenres(audio, tags);
  156. }
  157. if (!audio.LockedFields.Contains(MetadataFields.Studios))
  158. {
  159. audio.Studios.Clear();
  160. // There's several values in tags may or may not be present
  161. FetchStudios(audio, tags, "organization");
  162. FetchStudios(audio, tags, "ensemble");
  163. FetchStudios(audio, tags, "publisher");
  164. }
  165. audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id"));
  166. audio.SetProviderId(MetadataProviders.MusicBrainzArtist, FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id"));
  167. audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id"));
  168. audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id"));
  169. }
  170. private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
  171. /// <summary>
  172. /// Splits the specified val.
  173. /// </summary>
  174. /// <param name="val">The val.</param>
  175. /// <returns>System.String[][].</returns>
  176. private IEnumerable<string> Split(string val)
  177. {
  178. // Only use the comma as a delimeter if there are no slashes or pipes.
  179. // We want to be careful not to split names that have commas in them
  180. var delimeter = _nameDelimiters.Any(i => val.IndexOf(i) != -1) ? _nameDelimiters : new[] { ',' };
  181. return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
  182. .Where(i => !string.IsNullOrWhiteSpace(i))
  183. .Select(i => i.Trim());
  184. }
  185. private const string ArtistReplaceValue = " | ";
  186. private IEnumerable<string> SplitArtists(string val)
  187. {
  188. val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
  189. .Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
  190. var artistsFound = new List<string>();
  191. foreach (var whitelistArtist in GetSplitWhitelist())
  192. {
  193. var originalVal = val;
  194. val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
  195. if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase))
  196. {
  197. // TODO: Preserve casing from original value
  198. artistsFound.Add(whitelistArtist);
  199. }
  200. }
  201. // Only use the comma as a delimeter if there are no slashes or pipes.
  202. // We want to be careful not to split names that have commas in them
  203. var delimeter = _nameDelimiters;
  204. var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
  205. .Where(i => !string.IsNullOrWhiteSpace(i))
  206. .Select(i => i.Trim());
  207. artistsFound.AddRange(artists);
  208. return artistsFound;
  209. }
  210. private List<string> _splitWhiteList = null;
  211. private IEnumerable<string> GetSplitWhitelist()
  212. {
  213. if (_splitWhiteList == null)
  214. {
  215. var file = GetType().Namespace + ".whitelist.txt";
  216. using (var stream = GetType().Assembly.GetManifestResourceStream(file))
  217. {
  218. using (var reader = new StreamReader(stream))
  219. {
  220. var list = new List<string>();
  221. while (!reader.EndOfStream)
  222. {
  223. var val = reader.ReadLine();
  224. if (!string.IsNullOrWhiteSpace(val))
  225. {
  226. list.Add(val);
  227. }
  228. }
  229. _splitWhiteList = list;
  230. }
  231. }
  232. }
  233. return _splitWhiteList;
  234. }
  235. /// <summary>
  236. /// Gets the studios from the tags collection
  237. /// </summary>
  238. /// <param name="audio">The audio.</param>
  239. /// <param name="tags">The tags.</param>
  240. /// <param name="tagName">Name of the tag.</param>
  241. private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
  242. {
  243. var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
  244. if (!string.IsNullOrEmpty(val))
  245. {
  246. // Sometimes the artist name is listed here, account for that
  247. var studios = Split(val).Where(i => !audio.HasArtist(i));
  248. foreach (var studio in studios)
  249. {
  250. audio.AddStudio(studio);
  251. }
  252. }
  253. }
  254. /// <summary>
  255. /// Gets the genres from the tags collection
  256. /// </summary>
  257. /// <param name="audio">The audio.</param>
  258. /// <param name="tags">The tags.</param>
  259. private void FetchGenres(Audio audio, Dictionary<string, string> tags)
  260. {
  261. var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
  262. if (!string.IsNullOrEmpty(val))
  263. {
  264. audio.Genres.Clear();
  265. foreach (var genre in Split(val))
  266. {
  267. audio.AddGenre(genre);
  268. }
  269. }
  270. }
  271. /// <summary>
  272. /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
  273. /// </summary>
  274. /// <param name="tags">The tags.</param>
  275. /// <param name="tagName">Name of the tag.</param>
  276. /// <returns>System.Nullable{System.Int32}.</returns>
  277. private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
  278. {
  279. var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
  280. if (!string.IsNullOrEmpty(disc))
  281. {
  282. disc = disc.Split('/')[0];
  283. int num;
  284. if (int.TryParse(disc, out num))
  285. {
  286. return num;
  287. }
  288. }
  289. return null;
  290. }
  291. }
  292. }