diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 5816b23f8c..b7508a641e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -542,6 +542,7 @@ namespace MediaBrowser.Controller.Entities var options = new ParallelOptions { + MaxDegreeOfParallelism = 50 }; Parallel.ForEach(nonCachedChildren, options, child => @@ -606,6 +607,12 @@ namespace MediaBrowser.Controller.Entities _children.Add(item); } + if (saveTasks.Count > 50) + { + await Task.WhenAll(saveTasks).ConfigureAwait(false); + saveTasks.Clear(); + } + saveTasks.Add(LibraryManager.SaveItem(item, CancellationToken.None)); } @@ -642,65 +649,77 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// if set to true [recursive]. /// Task. - private Task RefreshChildren(IEnumerable> children, IProgress progress, CancellationToken cancellationToken, bool? recursive) + private async Task RefreshChildren(IEnumerable> children, IProgress progress, CancellationToken cancellationToken, bool? recursive) { var list = children.ToList(); var percentages = new ConcurrentDictionary(list.Select(i => new KeyValuePair(i.Item1.Id, 0))); - var tasks = list.Select(tuple => Task.Run(async () => + var tasks = new List(); + + foreach (var tuple in list) { - cancellationToken.ThrowIfCancellationRequested(); - - var child = tuple.Item1; - - //refresh it - await child.RefreshMetadata(cancellationToken, resetResolveArgs: child.IsFolder).ConfigureAwait(false); - - // Refresh children if a folder and the item changed or recursive is set to true - var refreshChildren = child.IsFolder && (tuple.Item2 || (recursive.HasValue && recursive.Value)); - - if (refreshChildren) + if (tasks.Count > 50) { - // Don't refresh children if explicitly set to false - if (recursive.HasValue && recursive.Value == false) - { - refreshChildren = false; - } + await Task.WhenAll(tasks).ConfigureAwait(false); } - if (refreshChildren) + Tuple currentTuple = tuple; + + tasks.Add(Task.Run(async () => { cancellationToken.ThrowIfCancellationRequested(); - var innerProgress = new ActionableProgress(); + var child = currentTuple.Item1; - innerProgress.RegisterAction(p => + //refresh it + await child.RefreshMetadata(cancellationToken, resetResolveArgs: child.IsFolder).ConfigureAwait(false); + + // Refresh children if a folder and the item changed or recursive is set to true + var refreshChildren = child.IsFolder && (currentTuple.Item2 || (recursive.HasValue && recursive.Value)); + + if (refreshChildren) { - percentages.TryUpdate(child.Id, p / 100, percentages[child.Id]); + // Don't refresh children if explicitly set to false + if (recursive.HasValue && recursive.Value == false) + { + refreshChildren = false; + } + } + + if (refreshChildren) + { + cancellationToken.ThrowIfCancellationRequested(); + + var innerProgress = new ActionableProgress(); + + innerProgress.RegisterAction(p => + { + percentages.TryUpdate(child.Id, p / 100, percentages[child.Id]); + + var percent = percentages.Values.Sum(); + percent /= list.Count; + + progress.Report((90 * percent) + 10); + }); + + await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive).ConfigureAwait(false); + } + else + { + percentages.TryUpdate(child.Id, 1, percentages[child.Id]); var percent = percentages.Values.Sum(); percent /= list.Count; progress.Report((90 * percent) + 10); - }); - - await ((Folder) child).ValidateChildren(innerProgress, cancellationToken, recursive).ConfigureAwait(false); - } - else - { - percentages.TryUpdate(child.Id, 1, percentages[child.Id]); - - var percent = percentages.Values.Sum(); - percent /= list.Count; - - progress.Report((90 * percent) + 10); - } - })); + } + })); + } cancellationToken.ThrowIfCancellationRequested(); - return Task.WhenAll(tasks); + await Task.WhenAll(tasks).ConfigureAwait(false); } /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 90d3810d0f..9dd558308c 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -148,7 +148,7 @@ - + @@ -170,7 +170,6 @@ - diff --git a/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs b/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs index 4700f41f53..509d1e1879 100644 --- a/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs +++ b/MediaBrowser.Controller/MediaInfo/FFMpegManager.cs @@ -23,12 +23,6 @@ namespace MediaBrowser.Controller.MediaInfo /// The video image cache. internal FileSystemRepository VideoImageCache { get; set; } - /// - /// Gets or sets the image cache. - /// - /// The image cache. - internal FileSystemRepository AudioImageCache { get; set; } - /// /// Gets or sets the subtitle cache. /// @@ -54,7 +48,6 @@ namespace MediaBrowser.Controller.MediaInfo _libraryManager = libraryManager; VideoImageCache = new FileSystemRepository(VideoImagesDataPath); - AudioImageCache = new FileSystemRepository(AudioImagesDataPath); SubtitleCache = new FileSystemRepository(SubtitleCachePath); } diff --git a/MediaBrowser.Controller/Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/AudioImageProvider.cs new file mode 100644 index 0000000000..05e4ba1e3a --- /dev/null +++ b/MediaBrowser.Controller/Providers/MediaInfo/AudioImageProvider.cs @@ -0,0 +1,101 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Providers.MediaInfo +{ + /// + /// Uses ffmpeg to create video images + /// + public class AudioImageProvider : BaseMetadataProvider + { + /// + /// Initializes a new instance of the class. + /// + /// The log manager. + /// The configuration manager. + public AudioImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager) + : base(logManager, configurationManager) + { + } + + /// + /// The true task result + /// + protected static readonly Task TrueTaskResult = Task.FromResult(true); + + /// + /// Supportses the specified item. + /// + /// The item. + /// true if XXXX, false otherwise + public override bool Supports(BaseItem item) + { + return item.LocationType == LocationType.FileSystem && item is Audio; + } + + /// + /// 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; } + } + + /// + /// Needses the refresh internal. + /// + /// The item. + /// The provider info. + /// true if XXXX, false otherwise + protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo) + { + if (!string.IsNullOrEmpty(item.PrimaryImagePath)) + { + return false; + } + return base.NeedsRefreshInternal(item, providerInfo); + } + + /// + /// 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 Task FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken) + { + if (force || string.IsNullOrEmpty(item.PrimaryImagePath)) + { + var album = item.ResolveArgs.Parent as MusicAlbum; + + if (album != null) + { + // First try to use the parent's image + item.PrimaryImagePath = item.ResolveArgs.Parent.PrimaryImagePath; + } + } + + SetLastRefreshed(item, DateTime.UtcNow); + return TrueTaskResult; + } + } +} diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs deleted file mode 100644 index e1cbc6932c..0000000000 --- a/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs +++ /dev/null @@ -1,143 +0,0 @@ -using MediaBrowser.Common.MediaInfo; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Providers.MediaInfo -{ - /// - /// Uses ffmpeg to create video images - /// - public class FFMpegAudioImageProvider : BaseFFMpegProvider public interface IHasMediaStreams { + /// + /// Gets or sets the media streams. + /// + /// The media streams. List MediaStreams { get; set; } + /// + /// Gets or sets the path. + /// + /// The path. + string Path { get; set; } + /// + /// Gets or sets the primary image path. + /// + /// The primary image path. + string PrimaryImagePath { get; set; } } } diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 66ec69f08f..286710bc32 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -153,7 +153,9 @@ + + diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs index 32c18822f9..08e2eb7748 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs @@ -50,12 +50,12 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder /// /// The video image resource pool /// - private readonly SemaphoreSlim _videoImageResourcePool = new SemaphoreSlim(2, 2); + private readonly SemaphoreSlim _videoImageResourcePool = new SemaphoreSlim(1, 1); /// /// The audio image resource pool /// - private readonly SemaphoreSlim _audioImageResourcePool = new SemaphoreSlim(3, 3); + private readonly SemaphoreSlim _audioImageResourcePool = new SemaphoreSlim(2, 2); /// /// The _subtitle extraction resource pool @@ -65,7 +65,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder /// /// The FF probe resource pool /// - private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(3, 3); + private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(2, 2); /// /// Gets or sets the versioned directory path. @@ -370,7 +370,18 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder try { process.Start(); + } + catch (Exception ex) + { + _ffProbeResourcePool.Release(); + _logger.ErrorException("Error starting ffprobe", ex); + + throw; + } + + try + { Task standardErrorReadTask = null; // MUST read both stdout and stderr asynchronously or a deadlock may occurr diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/AudioImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/AudioImagesTask.cs new file mode 100644 index 0000000000..db809a47b7 --- /dev/null +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/AudioImagesTask.cs @@ -0,0 +1,178 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.MediaInfo; +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.ScheduledTasks +{ + /// + /// Class AudioImagesTask + /// + public class AudioImagesTask : 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 _locks + /// + private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The media encoder. + public AudioImagesTask(ILibraryManager libraryManager, IMediaEncoder mediaEncoder) + { + _libraryManager = libraryManager; + _mediaEncoder = mediaEncoder; + + ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.AudioImagesDataPath); + } + + /// + /// Gets the name of the task + /// + /// The name. + public string Name + { + get { return "Audio image extraction"; } + } + + /// + /// Gets the description. + /// + /// The description. + public string Description + { + get { return "Extracts images from audio 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 = _libraryManager.RootFolder.RecursiveChildren + .OfType private readonly ILogger _logger; + /// + /// The _library manager + /// private readonly ILibraryManager _libraryManager; /// @@ -99,7 +102,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks /// The name. public string Name { - get { return "Create video chapter thumbnails"; } + get { return "Chapter image extraction"; } } /// diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/VideoImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/VideoImagesTask.cs new file mode 100644 index 0000000000..a82c22fe9b --- /dev/null +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/VideoImagesTask.cs @@ -0,0 +1,265 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.MediaInfo; +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers.MediaInfo; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +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; + + /// + /// The _locks + /// + private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The media encoder. + /// The iso manager. + public VideoImagesTask(ILibraryManager libraryManager, IMediaEncoder mediaEncoder, IIsoManager isoManager) + { + _libraryManager = libraryManager; + _mediaEncoder = mediaEncoder; + _isoManager = isoManager; + + ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.VideoImagesDataPath); + } + + /// + /// 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 audio 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 = _libraryManager.RootFolder.RecursiveChildren + .OfType