VideoImagesTask.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Common.MediaInfo;
  3. using MediaBrowser.Common.ScheduledTasks;
  4. using MediaBrowser.Controller;
  5. using MediaBrowser.Controller.Entities;
  6. using MediaBrowser.Controller.Entities.Movies;
  7. using MediaBrowser.Controller.Library;
  8. using MediaBrowser.Controller.Providers.MediaInfo;
  9. using MediaBrowser.Model.Entities;
  10. using System;
  11. using System.Collections.Concurrent;
  12. using System.Collections.Generic;
  13. using System.Linq;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using MediaBrowser.Model.Logging;
  17. using MoreLinq;
  18. namespace MediaBrowser.Server.Implementations.ScheduledTasks
  19. {
  20. /// <summary>
  21. /// Class VideoImagesTask
  22. /// </summary>
  23. public class VideoImagesTask : IScheduledTask
  24. {
  25. /// <summary>
  26. /// Gets or sets the image cache.
  27. /// </summary>
  28. /// <value>The image cache.</value>
  29. public FileSystemRepository ImageCache { get; set; }
  30. /// <summary>
  31. /// The _library manager
  32. /// </summary>
  33. private readonly ILibraryManager _libraryManager;
  34. /// <summary>
  35. /// The _media encoder
  36. /// </summary>
  37. private readonly IMediaEncoder _mediaEncoder;
  38. /// <summary>
  39. /// The _iso manager
  40. /// </summary>
  41. private readonly IIsoManager _isoManager;
  42. private readonly ILogger _logger;
  43. /// <summary>
  44. /// The _locks
  45. /// </summary>
  46. private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
  47. private readonly List<BaseItem> _newlyAddedItems = new List<BaseItem>();
  48. private const int NewItemDelay = 300000;
  49. /// <summary>
  50. /// The current new item timer
  51. /// </summary>
  52. /// <value>The new item timer.</value>
  53. private Timer NewItemTimer { get; set; }
  54. /// <summary>
  55. /// Initializes a new instance of the <see cref="AudioImagesTask" /> class.
  56. /// </summary>
  57. /// <param name="libraryManager">The library manager.</param>
  58. /// <param name="logManager">The log manager.</param>
  59. /// <param name="mediaEncoder">The media encoder.</param>
  60. /// <param name="isoManager">The iso manager.</param>
  61. public VideoImagesTask(ILibraryManager libraryManager, ILogManager logManager, IMediaEncoder mediaEncoder, IIsoManager isoManager)
  62. {
  63. _libraryManager = libraryManager;
  64. _mediaEncoder = mediaEncoder;
  65. _isoManager = isoManager;
  66. _logger = logManager.GetLogger(GetType().Name);
  67. ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.VideoImagesDataPath);
  68. libraryManager.ItemAdded += libraryManager_ItemAdded;
  69. libraryManager.ItemUpdated += libraryManager_ItemAdded;
  70. }
  71. /// <summary>
  72. /// Handles the ItemAdded event of the libraryManager control.
  73. /// </summary>
  74. /// <param name="sender">The source of the event.</param>
  75. /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
  76. void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
  77. {
  78. lock (_newlyAddedItems)
  79. {
  80. _newlyAddedItems.Add(e.Item);
  81. if (NewItemTimer == null)
  82. {
  83. NewItemTimer = new Timer(NewItemTimerCallback, null, NewItemDelay, Timeout.Infinite);
  84. }
  85. else
  86. {
  87. NewItemTimer.Change(NewItemDelay, Timeout.Infinite);
  88. }
  89. }
  90. }
  91. /// <summary>
  92. /// News the item timer callback.
  93. /// </summary>
  94. /// <param name="state">The state.</param>
  95. private async void NewItemTimerCallback(object state)
  96. {
  97. List<BaseItem> newItems;
  98. // Lock the list and release all resources
  99. lock (_newlyAddedItems)
  100. {
  101. newItems = _newlyAddedItems.DistinctBy(i => i.Id).ToList();
  102. _newlyAddedItems.Clear();
  103. NewItemTimer.Dispose();
  104. NewItemTimer = null;
  105. }
  106. foreach (var item in GetItemsForExtraction(newItems.Take(5)))
  107. {
  108. try
  109. {
  110. await ExtractImage(item, CancellationToken.None).ConfigureAwait(false);
  111. }
  112. catch (Exception ex)
  113. {
  114. _logger.ErrorException("Error creating image for {0}", ex, item.Name);
  115. }
  116. }
  117. }
  118. /// <summary>
  119. /// Gets the name of the task
  120. /// </summary>
  121. /// <value>The name.</value>
  122. public string Name
  123. {
  124. get { return "Video image extraction"; }
  125. }
  126. /// <summary>
  127. /// Gets the description.
  128. /// </summary>
  129. /// <value>The description.</value>
  130. public string Description
  131. {
  132. get { return "Extracts images from video files that do not have external images."; }
  133. }
  134. /// <summary>
  135. /// Gets the category.
  136. /// </summary>
  137. /// <value>The category.</value>
  138. public string Category
  139. {
  140. get { return "Library"; }
  141. }
  142. /// <summary>
  143. /// Executes the task
  144. /// </summary>
  145. /// <param name="cancellationToken">The cancellation token.</param>
  146. /// <param name="progress">The progress.</param>
  147. /// <returns>Task.</returns>
  148. public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
  149. {
  150. var items = GetItemsForExtraction(_libraryManager.RootFolder.RecursiveChildren).ToList();
  151. progress.Report(0);
  152. var numComplete = 0;
  153. foreach (var item in items)
  154. {
  155. try
  156. {
  157. await ExtractImage(item, cancellationToken).ConfigureAwait(false);
  158. }
  159. catch
  160. {
  161. // Already logged at lower levels.
  162. // Just don't let the task fail
  163. }
  164. numComplete++;
  165. double percent = numComplete;
  166. percent /= items.Count;
  167. progress.Report(100 * percent);
  168. }
  169. progress.Report(100);
  170. }
  171. /// <summary>
  172. /// Gets the items for extraction.
  173. /// </summary>
  174. /// <param name="sourceItems">The source items.</param>
  175. /// <returns>IEnumerable{BaseItem}.</returns>
  176. private IEnumerable<Video> GetItemsForExtraction(IEnumerable<BaseItem> sourceItems)
  177. {
  178. var allItems = sourceItems.ToList();
  179. var localTrailers = allItems.SelectMany(i => i.LocalTrailers);
  180. var themeVideos = allItems.SelectMany(i => i.ThemeVideos);
  181. var videos = allItems.OfType<Video>().ToList();
  182. var items = videos;
  183. items.AddRange(localTrailers);
  184. items.AddRange(themeVideos);
  185. items.AddRange(videos.OfType<Movie>().SelectMany(i => i.SpecialFeatures).ToList());
  186. return items.Where(i =>
  187. {
  188. if (!string.IsNullOrEmpty(i.PrimaryImagePath))
  189. {
  190. return false;
  191. }
  192. if (i.LocationType != LocationType.FileSystem)
  193. {
  194. return false;
  195. }
  196. if (i.VideoType == VideoType.HdDvd)
  197. {
  198. return false;
  199. }
  200. if (i.VideoType == VideoType.Iso && !i.IsoType.HasValue)
  201. {
  202. return false;
  203. }
  204. return i.MediaStreams != null && i.MediaStreams.Any(m => m.Type == MediaStreamType.Video);
  205. });
  206. }
  207. /// <summary>
  208. /// Extracts the image.
  209. /// </summary>
  210. /// <param name="item">The item.</param>
  211. /// <param name="cancellationToken">The cancellation token.</param>
  212. /// <returns>Task.</returns>
  213. private async Task ExtractImage(Video item, CancellationToken cancellationToken)
  214. {
  215. cancellationToken.ThrowIfCancellationRequested();
  216. var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
  217. var path = ImageCache.GetResourcePath(filename, ".jpg");
  218. if (!ImageCache.ContainsFilePath(path))
  219. {
  220. var semaphore = GetLock(path);
  221. // Acquire a lock
  222. await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
  223. // Check again
  224. if (!ImageCache.ContainsFilePath(path))
  225. {
  226. try
  227. {
  228. await ExtractImageInternal(item, path, cancellationToken).ConfigureAwait(false);
  229. }
  230. finally
  231. {
  232. semaphore.Release();
  233. }
  234. // Image is already in the cache
  235. item.PrimaryImagePath = path;
  236. await _libraryManager.UpdateItem(item, cancellationToken).ConfigureAwait(false);
  237. }
  238. else
  239. {
  240. semaphore.Release();
  241. }
  242. }
  243. }
  244. /// <summary>
  245. /// Extracts the image.
  246. /// </summary>
  247. /// <param name="video">The video.</param>
  248. /// <param name="path">The path.</param>
  249. /// <param name="cancellationToken">The cancellation token.</param>
  250. /// <returns>Task.</returns>
  251. private async Task ExtractImageInternal(Video video, string path, CancellationToken cancellationToken)
  252. {
  253. var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false);
  254. try
  255. {
  256. // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
  257. // Always use 10 seconds for dvd because our duration could be out of whack
  258. var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue &&
  259. video.RunTimeTicks.Value > 0
  260. ? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1))
  261. : TimeSpan.FromSeconds(10);
  262. InputType type;
  263. var inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
  264. await _mediaEncoder.ExtractImage(inputPath, type, imageOffset, path, cancellationToken).ConfigureAwait(false);
  265. video.PrimaryImagePath = path;
  266. }
  267. finally
  268. {
  269. if (isoMount != null)
  270. {
  271. isoMount.Dispose();
  272. }
  273. }
  274. }
  275. /// <summary>
  276. /// The null mount task result
  277. /// </summary>
  278. protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null);
  279. /// <summary>
  280. /// Mounts the iso if needed.
  281. /// </summary>
  282. /// <param name="item">The item.</param>
  283. /// <param name="cancellationToken">The cancellation token.</param>
  284. /// <returns>Task{IIsoMount}.</returns>
  285. protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
  286. {
  287. if (item.VideoType == VideoType.Iso)
  288. {
  289. return _isoManager.Mount(item.Path, cancellationToken);
  290. }
  291. return NullMountTaskResult;
  292. }
  293. /// <summary>
  294. /// Gets the default triggers.
  295. /// </summary>
  296. /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
  297. public IEnumerable<ITaskTrigger> GetDefaultTriggers()
  298. {
  299. return new ITaskTrigger[]
  300. {
  301. new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }
  302. };
  303. }
  304. /// <summary>
  305. /// Gets the lock.
  306. /// </summary>
  307. /// <param name="filename">The filename.</param>
  308. /// <returns>System.Object.</returns>
  309. private SemaphoreSlim GetLock(string filename)
  310. {
  311. return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
  312. }
  313. }
  314. }