From 17d22c013035fec3144e54b3b4fbcb4a988d7b77 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 14 Sep 2013 17:19:32 -0400 Subject: [PATCH] add video image extraction back to library scan --- .../Configuration/ServerConfiguration.cs | 3 + .../MediaBrowser.Providers.csproj | 1 + .../MediaInfo/VideoImageProvider.cs | 315 ++++++++++++++ .../TV/RemoteEpisodeProvider.cs | 11 +- ...MediaBrowser.Server.Implementations.csproj | 1 - .../ScheduledTasks/VideoImagesTask.cs | 387 ------------------ .../ApplicationHost.cs | 14 - 7 files changed, 324 insertions(+), 408 deletions(-) create mode 100644 MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/ScheduledTasks/VideoImagesTask.cs diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 865b4af5f..0d4f18f3a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -231,6 +231,8 @@ namespace MediaBrowser.Model.Configuration /// true if [enable tv db updates]; otherwise, false. public bool EnableTvDbUpdates { get; set; } + public bool EnableVideoImageExtraction { get; set; } + /// /// Initializes a new instance of the class. /// @@ -241,6 +243,7 @@ namespace MediaBrowser.Model.Configuration LegacyWebSocketPortNumber = 8945; EnableHttpLevelLogging = true; EnableDashboardResponseCaching = true; + EnableVideoImageExtraction = true; #if (DEBUG) EnableDeveloperTools = true; diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index d62c2f27f..139c622fc 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -57,6 +57,7 @@ + diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs new file mode 100644 index 000000000..b202e4866 --- /dev/null +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -0,0 +1,315 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.MediaInfo; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaInfo; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.MediaInfo +{ + class VideoImageProvider : BaseMetadataProvider + { + /// + /// Gets or sets the image cache. + /// + /// The image cache. + public FileSystemRepository ImageCache { get; set; } + + /// + /// The _locks + /// + private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); + + /// + /// The _media encoder + /// + private readonly IMediaEncoder _mediaEncoder; + private readonly IIsoManager _isoManager; + + public VideoImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder, IIsoManager isoManager) + : base(logManager, configurationManager) + { + _mediaEncoder = mediaEncoder; + _isoManager = isoManager; + + ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.AudioImagesDataPath); + } + + /// + /// Gets a value indicating whether [refresh on version change]. + /// + /// true if [refresh on version change]; otherwise, false. + protected override bool RefreshOnVersionChange + { + get + { + return true; + } + } + + /// + /// Gets the provider version. + /// + /// The provider version. + protected override string ProviderVersion + { + get + { + return "1"; + } + } + + /// + /// Supportses the specified item. + /// + /// The item. + /// true if XXXX, false otherwise + public override bool Supports(BaseItem item) + { + return item.LocationType == LocationType.FileSystem && item is Video; + } + + /// + /// Needses the refresh internal. + /// + /// The item. + /// The provider info. + /// true if XXXX, false otherwise + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + var video = (Video)item; + + if (!QualifiesForExtraction(video)) + { + return false; + } + + return base.NeedsRefreshInternal(item, providerInfo); + } + + /// + /// Qualifieses for extraction. + /// + /// The item. + /// true if XXXX, false otherwise + private bool QualifiesForExtraction(Video item) + { + if (!ConfigurationManager.Configuration.EnableVideoImageExtraction) + { + return false; + } + + if (!string.IsNullOrEmpty(item.PrimaryImagePath)) + { + return false; + } + + // No support for this + if (item.VideoType == VideoType.HdDvd) + { + return false; + } + + // Can't extract from iso's if we weren't unable to determine iso type + if (item.VideoType == VideoType.Iso && !item.IsoType.HasValue) + { + return false; + } + + // Can't extract if we didn't find a video stream in the file + if (item.MediaStreams.All(m => m.Type != MediaStreamType.Video)) + { + return false; + } + + return true; + } + + /// + /// Override this to return the date that should be compared to the last refresh date + /// to determine if this provider should be re-fetched. + /// + /// The item. + /// DateTime. + protected override DateTime CompareDate(BaseItem item) + { + return item.DateModified; + } + + /// + /// Gets the priority. + /// + /// The priority. + public override MetadataProviderPriority Priority + { + get { return MetadataProviderPriority.Last; } + } + + public override ItemUpdateType ItemUpdateType + { + get + { + return ItemUpdateType.ImageUpdate; + } + } + + /// + /// Fetches metadata and returns true or false indicating if any work that requires persistence was done + /// + /// The item. + /// if set to true [force]. + /// The cancellation token. + /// Task{System.Boolean}. + public override async Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) + { + var video = (Video)item; + + // Double check this here in case force was used + if (QualifiesForExtraction(video)) + { + try + { + await ExtractImage(video, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + // Swallow this so that we don't keep on trying over and over again + + Logger.ErrorException("Error extracting image for {0}", ex, item.Name); + } + } + + SetLastRefreshed(item, DateTime.UtcNow); + return true; + } + + /// + /// Extracts the image. + /// + /// The item. + /// The cancellation token. + /// Task. + private async Task ExtractImage(Video item, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var filename = item.Path + "_" + item.DateModified.Ticks + "_primary"; + + var path = ImageCache.GetResourcePath(filename, ".jpg"); + + if (!File.Exists(path)) + { + var semaphore = GetLock(path); + + // Acquire a lock + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + // Check again + if (!File.Exists(path)) + { + try + { + var parentPath = Path.GetDirectoryName(path); + + if (!Directory.Exists(parentPath)) + { + Directory.CreateDirectory(parentPath); + } + + await ExtractImageInternal(item, path, cancellationToken).ConfigureAwait(false); + } + finally + { + semaphore.Release(); + } + } + else + { + semaphore.Release(); + } + } + + // Image is already in the cache + item.PrimaryImagePath = path; + } + + /// + /// Extracts the image. + /// + /// The video. + /// The path. + /// The cancellation token. + /// Task. + private async Task ExtractImageInternal(Video video, string path, CancellationToken cancellationToken) + { + var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false); + + try + { + // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in. + // Always use 10 seconds for dvd because our duration could be out of whack + var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue && + video.RunTimeTicks.Value > 0 + ? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1)) + : TimeSpan.FromSeconds(10); + + InputType type; + + var inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type); + + await _mediaEncoder.ExtractImage(inputPath, type, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false); + + video.PrimaryImagePath = path; + } + finally + { + if (isoMount != null) + { + isoMount.Dispose(); + } + } + } + + /// + /// The null mount task result + /// + protected readonly Task NullMountTaskResult = Task.FromResult(null); + + /// + /// Mounts the iso if needed. + /// + /// The item. + /// The cancellation token. + /// Task{IIsoMount}. + protected Task MountIsoIfNeeded(Video item, CancellationToken cancellationToken) + { + if (item.VideoType == VideoType.Iso) + { + return _isoManager.Mount(item.Path, cancellationToken); + } + + return NullMountTaskResult; + } + + /// + /// Gets the lock. + /// + /// The filename. + /// SemaphoreSlim. + private SemaphoreSlim GetLock(string filename) + { + return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); + } + } +} diff --git a/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs b/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs index 5cf1651a9..88116eb54 100644 --- a/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/RemoteEpisodeProvider.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -11,13 +7,16 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; +using MediaBrowser.Providers.Extensions; using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml; -using MediaBrowser.Providers.Extensions; +using System.Xml.Linq; namespace MediaBrowser.Providers.TV { diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 4e1e6bb3f..22de1e898 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -171,7 +171,6 @@ - diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/VideoImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/VideoImagesTask.cs deleted file mode 100644 index 447bb4956..000000000 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/VideoImagesTask.cs +++ /dev/null @@ -1,387 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaInfo; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MoreLinq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.ScheduledTasks -{ - /// - /// Class VideoImagesTask - /// - public class VideoImagesTask : IScheduledTask - { - /// - /// Gets or sets the image cache. - /// - /// The image cache. - public FileSystemRepository ImageCache { get; set; } - - /// - /// The _library manager - /// - private readonly ILibraryManager _libraryManager; - /// - /// The _media encoder - /// - private readonly IMediaEncoder _mediaEncoder; - - /// - /// The _iso manager - /// - private readonly IIsoManager _isoManager; - - private readonly IItemRepository _itemRepo; - - private readonly ILogger _logger; - - /// - /// The _locks - /// - private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); - - private readonly List _newlyAddedItems = new List(); - - private const int NewItemDelay = 60000; - - /// - /// The current new item timer - /// - /// The new item timer. - private Timer NewItemTimer { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The log manager. - /// The media encoder. - /// The iso manager. - public VideoImagesTask(ILibraryManager libraryManager, ILogManager logManager, IMediaEncoder mediaEncoder, IIsoManager isoManager, IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _mediaEncoder = mediaEncoder; - _isoManager = isoManager; - _itemRepo = itemRepo; - _logger = logManager.GetLogger(GetType().Name); - - ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.VideoImagesDataPath); - - libraryManager.ItemAdded += libraryManager_ItemAdded; - libraryManager.ItemUpdated += libraryManager_ItemAdded; - } - - /// - /// Handles the ItemAdded event of the libraryManager control. - /// - /// The source of the event. - /// The instance containing the event data. - void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e) - { - lock (_newlyAddedItems) - { - _newlyAddedItems.Add(e.Item); - - if (NewItemTimer == null) - { - NewItemTimer = new Timer(NewItemTimerCallback, null, NewItemDelay, Timeout.Infinite); - } - else - { - NewItemTimer.Change(NewItemDelay, Timeout.Infinite); - } - } - } - - /// - /// News the item timer callback. - /// - /// The state. - private async void NewItemTimerCallback(object state) - { - List newItems; - - // Lock the list and release all resources - lock (_newlyAddedItems) - { - newItems = _newlyAddedItems.DistinctBy(i => i.Id).ToList(); - _newlyAddedItems.Clear(); - - NewItemTimer.Dispose(); - NewItemTimer = null; - } - - foreach (var item in GetItemsForExtraction(newItems.Take(5))) - { - try - { - await ExtractImage(item, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error creating image for {0}", ex, item.Name); - } - } - } - - /// - /// Gets the name of the task - /// - /// The name. - public string Name - { - get { return "Video image extraction"; } - } - - /// - /// Gets the description. - /// - /// The description. - public string Description - { - get { return "Extracts images from video files that do not have external images."; } - } - - /// - /// Gets the category. - /// - /// The category. - public string Category - { - get { return "Library"; } - } - - /// - /// Executes the task - /// - /// The cancellation token. - /// The progress. - /// Task. - public async Task Execute(CancellationToken cancellationToken, IProgress progress) - { - var items = GetItemsForExtraction(_libraryManager.RootFolder.RecursiveChildren).ToList(); - - progress.Report(0); - - var numComplete = 0; - - foreach (var item in items) - { - try - { - await ExtractImage(item, cancellationToken).ConfigureAwait(false); - } - catch - { - // Already logged at lower levels. - // Just don't let the task fail - } - - numComplete++; - double percent = numComplete; - percent /= items.Count; - - progress.Report(100 * percent); - } - - progress.Report(100); - } - - /// - /// Gets the items for extraction. - /// - /// The source items. - /// IEnumerable{BaseItem}. - private IEnumerable