mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-04 03:27:21 -05:00 
			
		
		
		
	#62 - File locking problem in cache
This commit is contained in:
		
							parent
							
								
									713afcf675
								
							
						
					
					
						commit
						fa884f3fea
					
				@ -15,7 +15,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 | 
			
		||||
    [Route("/Audio/{Id}/stream.flac", "GET")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.ogg", "GET")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.oga", "GET")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.webma", "GET")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.webm", "GET")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream", "GET")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.mp3", "HEAD")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.wma", "HEAD")]
 | 
			
		||||
@ -23,7 +23,7 @@ namespace MediaBrowser.Api.Playback.Progressive
 | 
			
		||||
    [Route("/Audio/{Id}/stream.flac", "HEAD")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.ogg", "HEAD")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.oga", "HEAD")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.webma", "HEAD")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream.webm", "HEAD")]
 | 
			
		||||
    [Route("/Audio/{Id}/stream", "HEAD")]
 | 
			
		||||
    [Api(Description = "Gets an audio stream")]
 | 
			
		||||
    public class GetAudioStream : StreamRequest
 | 
			
		||||
 | 
			
		||||
@ -167,11 +167,9 @@ namespace MediaBrowser.Api
 | 
			
		||||
        {
 | 
			
		||||
            var dtoBuilder = new DtoBuilder(Logger, _libraryManager, _userManager);
 | 
			
		||||
 | 
			
		||||
            var tasks = _userManager.Users.OrderBy(u => u.Name).Select(dtoBuilder.GetUserDto).ToArray();
 | 
			
		||||
            var users = _userManager.Users.OrderBy(u => u.Name).Select(dtoBuilder.GetUserDto).ToArray();
 | 
			
		||||
 | 
			
		||||
            var task = Task.WhenAll(tasks);
 | 
			
		||||
 | 
			
		||||
            return ToOptimizedResult(task.Result);
 | 
			
		||||
            return ToOptimizedResult(users);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -188,7 +186,7 @@ namespace MediaBrowser.Api
 | 
			
		||||
                throw new ResourceNotFoundException("User not found");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(user).Result;
 | 
			
		||||
            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(user);
 | 
			
		||||
 | 
			
		||||
            return ToOptimizedResult(result);
 | 
			
		||||
        }
 | 
			
		||||
@ -302,7 +300,7 @@ namespace MediaBrowser.Api
 | 
			
		||||
 | 
			
		||||
            newUser.UpdateConfiguration(dtoUser.Configuration, _xmlSerializer);
 | 
			
		||||
 | 
			
		||||
            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(newUser).Result;
 | 
			
		||||
            var result = new DtoBuilder(Logger, _libraryManager, _userManager).GetUserDto(newUser);
 | 
			
		||||
 | 
			
		||||
            return ToOptimizedResult(result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,97 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Common.Extensions
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Class NamedLock
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class NamedLock : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The _locks
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly Dictionary<string, SemaphoreSlim> _locks = new Dictionary<string, SemaphoreSlim>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Waits the async.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="name">The name.</param>
 | 
			
		||||
        /// <returns>Task.</returns>
 | 
			
		||||
        public Task WaitAsync(string name)
 | 
			
		||||
        {
 | 
			
		||||
            return GetLock(name).WaitAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Releases the specified name.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="name">The name.</param>
 | 
			
		||||
        public void Release(string name)
 | 
			
		||||
        {
 | 
			
		||||
            SemaphoreSlim semaphore;
 | 
			
		||||
 | 
			
		||||
            if (_locks.TryGetValue(name, out semaphore))
 | 
			
		||||
            {
 | 
			
		||||
                semaphore.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the lock.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="filename">The filename.</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        private SemaphoreSlim GetLock(string filename)
 | 
			
		||||
        {
 | 
			
		||||
            SemaphoreSlim fileLock;
 | 
			
		||||
            lock (_locks)
 | 
			
		||||
            {
 | 
			
		||||
                if (!_locks.TryGetValue(filename, out fileLock))
 | 
			
		||||
                {
 | 
			
		||||
                    fileLock = new SemaphoreSlim(1,1);
 | 
			
		||||
                    _locks[filename] = fileLock;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return fileLock;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            Dispose(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Releases unmanaged and - optionally - managed resources.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
 | 
			
		||||
        protected virtual void Dispose(bool dispose)
 | 
			
		||||
        {
 | 
			
		||||
            if (dispose)
 | 
			
		||||
            {
 | 
			
		||||
                DisposeLocks();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Disposes the locks.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private void DisposeLocks()
 | 
			
		||||
        {
 | 
			
		||||
            lock (_locks)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var semaphore in _locks.Values)
 | 
			
		||||
                {
 | 
			
		||||
                    semaphore.Dispose();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _locks.Clear();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,7 +2,6 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Common.IO
 | 
			
		||||
{
 | 
			
		||||
@ -18,11 +17,6 @@ namespace MediaBrowser.Common.IO
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly ConcurrentDictionary<string, string> _subFolderPaths = new ConcurrentDictionary<string, string>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The _file locks
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly NamedLock _fileLocks = new NamedLock();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the path.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@ -170,24 +164,6 @@ namespace MediaBrowser.Common.IO
 | 
			
		||||
            return File.Exists(path);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Waits for lock.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="resourcePath">The resource path.</param>
 | 
			
		||||
        public Task WaitForLockAsync(string resourcePath)
 | 
			
		||||
        {
 | 
			
		||||
            return _fileLocks.WaitAsync(resourcePath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Releases the lock.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="resourcePath">The resource path.</param>
 | 
			
		||||
        public void ReleaseLock(string resourcePath)
 | 
			
		||||
        {
 | 
			
		||||
            _fileLocks.Release(resourcePath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@ -204,7 +180,6 @@ namespace MediaBrowser.Common.IO
 | 
			
		||||
        {
 | 
			
		||||
            if (dispose)
 | 
			
		||||
            {
 | 
			
		||||
                _fileLocks.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,6 @@
 | 
			
		||||
    <Compile Include="Events\EventHelper.cs" />
 | 
			
		||||
    <Compile Include="Extensions\BaseExtensions.cs" />
 | 
			
		||||
    <Compile Include="Events\GenericEventArgs.cs" />
 | 
			
		||||
    <Compile Include="Extensions\NamedLock.cs" />
 | 
			
		||||
    <Compile Include="Extensions\ResourceNotFoundException.cs" />
 | 
			
		||||
    <Compile Include="IO\FileSystemRepository.cs" />
 | 
			
		||||
    <Compile Include="IO\IIsoManager.cs" />
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ using System.Drawing.Drawing2D;
 | 
			
		||||
using System.Drawing.Imaging;
 | 
			
		||||
using System.IO;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
@ -50,7 +51,7 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The cached imaged sizes
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly ConcurrentDictionary<string, Task<ImageSize>> _cachedImagedSizes = new ConcurrentDictionary<string, Task<ImageSize>>();
 | 
			
		||||
        private readonly ConcurrentDictionary<string, ImageSize> _cachedImagedSizes = new ConcurrentDictionary<string, ImageSize>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The _logger
 | 
			
		||||
@ -67,12 +68,18 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly Kernel _kernel;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// The _locks
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="ImageManager" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="kernel">The kernel.</param>
 | 
			
		||||
        /// <param name="protobufSerializer">The protobuf serializer.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="appPaths">The app paths.</param>
 | 
			
		||||
        public ImageManager(Kernel kernel, IProtobufSerializer protobufSerializer, ILogger logger, IServerApplicationPaths appPaths)
 | 
			
		||||
        {
 | 
			
		||||
            _protobufSerializer = protobufSerializer;
 | 
			
		||||
@ -116,17 +123,9 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
            var originalImagePath = GetImagePath(entity, imageType, imageIndex);
 | 
			
		||||
 | 
			
		||||
            if (cropWhitespace)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
            {
 | 
			
		||||
                originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
 | 
			
		||||
                    _logger.ErrorException("Error cropping image", ex);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@ -140,12 +139,12 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
                    originalImagePath = ehnancedImagePath;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                _logger.Error("Error enhancing image");
 | 
			
		||||
                _logger.Error("Error enhancing image", ex);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var originalImageSize = await GetImageSize(originalImagePath, dateModified).ConfigureAwait(false);
 | 
			
		||||
            var originalImageSize = GetImageSize(originalImagePath, dateModified);
 | 
			
		||||
 | 
			
		||||
            // Determine the output size based on incoming parameters
 | 
			
		||||
            var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight);
 | 
			
		||||
@ -158,22 +157,41 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
            var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified);
 | 
			
		||||
 | 
			
		||||
            // Grab the cache file if it already exists
 | 
			
		||||
            if (File.Exists(cacheFilePath))
 | 
			
		||||
            {
 | 
			
		||||
                using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
 | 
			
		||||
                {
 | 
			
		||||
                    await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var semaphore = GetLock(cacheFilePath);
 | 
			
		||||
 | 
			
		||||
            await semaphore.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            // Check again in case of lock contention
 | 
			
		||||
            if (File.Exists(cacheFilePath))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
 | 
			
		||||
                    {
 | 
			
		||||
                        await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
            catch (FileNotFoundException)
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                // Cache file doesn't exist. No biggie.
 | 
			
		||||
                    semaphore.Release();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using (var fileStream = File.OpenRead(originalImagePath))
 | 
			
		||||
                {
 | 
			
		||||
                using (var originalImage = Bitmap.FromStream(fileStream, true, false))
 | 
			
		||||
                    using (var originalImage = Image.FromStream(fileStream, true, false))
 | 
			
		||||
                    {
 | 
			
		||||
                        var newWidth = Convert.ToInt32(newSize.Width);
 | 
			
		||||
                        var newHeight = Convert.ToInt32(newSize.Height);
 | 
			
		||||
@ -203,14 +221,10 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
 | 
			
		||||
                            var bytes = memoryStream.ToArray();
 | 
			
		||||
 | 
			
		||||
                        var outputTask = Task.Run(async () => await toStream.WriteAsync(bytes, 0, bytes.Length));
 | 
			
		||||
                            var outputTask = toStream.WriteAsync(bytes, 0, bytes.Length);
 | 
			
		||||
 | 
			
		||||
                        // Save to the cache location
 | 
			
		||||
                        using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
 | 
			
		||||
                        {
 | 
			
		||||
                            // Save to the filestream
 | 
			
		||||
                            await cacheFileStream.WriteAsync(bytes, 0, bytes.Length);
 | 
			
		||||
                        }
 | 
			
		||||
                            // kick off a task to cache the result
 | 
			
		||||
                            Task.Run(() => CacheResizedImage(cacheFilePath, bytes));
 | 
			
		||||
 | 
			
		||||
                            await outputTask.ConfigureAwait(false);
 | 
			
		||||
                        }
 | 
			
		||||
@ -220,6 +234,26 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                semaphore.Release();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Caches the resized image.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="cacheFilePath">The cache file path.</param>
 | 
			
		||||
        /// <param name="bytes">The bytes.</param>
 | 
			
		||||
        private async void CacheResizedImage(string cacheFilePath, byte[] bytes)
 | 
			
		||||
        {
 | 
			
		||||
            // Save to the cache location
 | 
			
		||||
            using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
 | 
			
		||||
            {
 | 
			
		||||
                // Save to the filestream
 | 
			
		||||
                await cacheFileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the cache file path based on a set of parameters
 | 
			
		||||
@ -252,7 +286,7 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
        /// <param name="dateModified">The date modified.</param>
 | 
			
		||||
        /// <returns>Task{ImageSize}.</returns>
 | 
			
		||||
        /// <exception cref="System.ArgumentNullException">imagePath</exception>
 | 
			
		||||
        public Task<ImageSize> GetImageSize(string imagePath, DateTime dateModified)
 | 
			
		||||
        public ImageSize GetImageSize(string imagePath, DateTime dateModified)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrEmpty(imagePath))
 | 
			
		||||
            {
 | 
			
		||||
@ -261,18 +295,7 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
 | 
			
		||||
            var name = imagePath + "datemodified=" + dateModified.Ticks;
 | 
			
		||||
 | 
			
		||||
            return _cachedImagedSizes.GetOrAdd(name, keyName => GetImageSizeTask(keyName, imagePath));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets cached image dimensions, or results null if non-existant
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="keyName">Name of the key.</param>
 | 
			
		||||
        /// <param name="imagePath">The image path.</param>
 | 
			
		||||
        /// <returns>Task{ImageSize}.</returns>
 | 
			
		||||
        private Task<ImageSize> GetImageSizeTask(string keyName, string imagePath)
 | 
			
		||||
        {
 | 
			
		||||
            return Task.Run(() => GetImageSize(keyName, imagePath));
 | 
			
		||||
            return _cachedImagedSizes.GetOrAdd(name, keyName => GetImageSize(keyName, imagePath));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -297,27 +320,14 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
                // Cache file doesn't exist no biggie
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _logger.Debug("Getting image size for {0}", imagePath);
 | 
			
		||||
 | 
			
		||||
            var size = ImageHeader.GetDimensions(imagePath, _logger);
 | 
			
		||||
 | 
			
		||||
            var imageSize = new ImageSize { Width = size.Width, Height = size.Height };
 | 
			
		||||
 | 
			
		||||
            // Update the file system cache
 | 
			
		||||
            CacheImageSize(fullCachePath, size.Width, size.Height);
 | 
			
		||||
            Task.Run(() => _protobufSerializer.SerializeToFile(new[] { size.Width, size.Height }, fullCachePath));
 | 
			
		||||
 | 
			
		||||
            return imageSize;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Caches image dimensions
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="cachePath">The cache path.</param>
 | 
			
		||||
        /// <param name="width">The width.</param>
 | 
			
		||||
        /// <param name="height">The height.</param>
 | 
			
		||||
        private void CacheImageSize(string cachePath, int width, int height)
 | 
			
		||||
        {
 | 
			
		||||
            var output = new[] { width, height };
 | 
			
		||||
 | 
			
		||||
            _protobufSerializer.SerializeToFile(output, cachePath);
 | 
			
		||||
            return new ImageSize { Width = size.Width, Height = size.Height };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -440,40 +450,55 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
 | 
			
		||||
            var croppedImagePath = CroppedImageCache.GetResourcePath(name, Path.GetExtension(originalImagePath));
 | 
			
		||||
 | 
			
		||||
            if (!CroppedImageCache.ContainsFilePath(croppedImagePath))
 | 
			
		||||
            if (CroppedImageCache.ContainsFilePath(croppedImagePath))
 | 
			
		||||
            {
 | 
			
		||||
                return croppedImagePath;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var semaphore = GetLock(croppedImagePath);
 | 
			
		||||
 | 
			
		||||
            await semaphore.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            // Check again in case of contention
 | 
			
		||||
            if (CroppedImageCache.ContainsFilePath(croppedImagePath))
 | 
			
		||||
            {
 | 
			
		||||
                semaphore.Release();
 | 
			
		||||
                return croppedImagePath;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using (var fileStream = File.OpenRead(originalImagePath))
 | 
			
		||||
                {
 | 
			
		||||
                    using (var originalImage = (Bitmap)Bitmap.FromStream(fileStream, true, false))
 | 
			
		||||
                    using (var originalImage = (Bitmap)Image.FromStream(fileStream, true, false))
 | 
			
		||||
                    {
 | 
			
		||||
                        var outputFormat = originalImage.RawFormat;
 | 
			
		||||
 | 
			
		||||
                        using (var croppedImage = originalImage.CropWhitespace())
 | 
			
		||||
                        {
 | 
			
		||||
                            await SaveImageToFile(croppedImage, outputFormat, croppedImagePath).ConfigureAwait(false);
 | 
			
		||||
                            using (var outputStream = new FileStream(croppedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
 | 
			
		||||
                            {
 | 
			
		||||
                                croppedImage.Save(outputFormat, outputStream, 100);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                // We have to have a catch-all here because some of the .net image methods throw a plain old Exception
 | 
			
		||||
                _logger.ErrorException("Error cropping image {0}", ex, originalImagePath);
 | 
			
		||||
 | 
			
		||||
                return originalImagePath;
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                semaphore.Release();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return croppedImagePath;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task SaveImageToFile(Image image, ImageFormat outputFormat, string file)
 | 
			
		||||
        {
 | 
			
		||||
            using (var memoryStream = new MemoryStream())
 | 
			
		||||
            {
 | 
			
		||||
                image.Save(outputFormat, memoryStream, 100);
 | 
			
		||||
 | 
			
		||||
                memoryStream.Position = 0;
 | 
			
		||||
 | 
			
		||||
                using (var cacheFileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
 | 
			
		||||
                {
 | 
			
		||||
                    await memoryStream.CopyToAsync(cacheFileStream).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Runs an image through the image enhancers, caches the result, and returns the cached path
 | 
			
		||||
        /// </summary>
 | 
			
		||||
@ -509,7 +534,23 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
            // All enhanced images are saved as png to allow transparency
 | 
			
		||||
            var enhancedImagePath = EnhancedImageCache.GetResourcePath(cacheGuid + ".png");
 | 
			
		||||
 | 
			
		||||
            if (!EnhancedImageCache.ContainsFilePath(enhancedImagePath))
 | 
			
		||||
            if (EnhancedImageCache.ContainsFilePath(enhancedImagePath))
 | 
			
		||||
            {
 | 
			
		||||
                return enhancedImagePath;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var semaphore = GetLock(enhancedImagePath);
 | 
			
		||||
 | 
			
		||||
            await semaphore.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            // Check again in case of contention
 | 
			
		||||
            if (EnhancedImageCache.ContainsFilePath(enhancedImagePath))
 | 
			
		||||
            {
 | 
			
		||||
                semaphore.Release();
 | 
			
		||||
                return enhancedImagePath;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using (var fileStream = File.OpenRead(originalImagePath))
 | 
			
		||||
                {
 | 
			
		||||
@ -519,11 +560,18 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
                        using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false))
 | 
			
		||||
                        {
 | 
			
		||||
                            //And then save it in the cache
 | 
			
		||||
                            await SaveImageToFile(newImage, ImageFormat.Png, enhancedImagePath).ConfigureAwait(false);
 | 
			
		||||
                            using (var outputStream = new FileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read))
 | 
			
		||||
                            {
 | 
			
		||||
                                newImage.Save(ImageFormat.Png, outputStream, 100);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                semaphore.Release();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return enhancedImagePath;
 | 
			
		||||
        }
 | 
			
		||||
@ -624,6 +672,19 @@ namespace MediaBrowser.Controller.Drawing
 | 
			
		||||
            return result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the lock.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="filename">The filename.</param>
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        private SemaphoreSlim GetLock(string filename)
 | 
			
		||||
        {
 | 
			
		||||
            return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            Dispose(true);
 | 
			
		||||
 | 
			
		||||
@ -59,19 +59,6 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
 | 
			
		||||
            var tasks = new List<Task>();
 | 
			
		||||
 | 
			
		||||
            if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
 | 
			
		||||
                    _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (fields.Contains(ItemFields.Studios))
 | 
			
		||||
            {
 | 
			
		||||
                dto.Studios = item.Studios;
 | 
			
		||||
@ -82,6 +69,19 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
                tasks.Add(AttachPeople(dto, item));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    AttachPrimaryImageAspectRatio(dto, item);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
 | 
			
		||||
                    _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AttachBasicFields(dto, item, fields);
 | 
			
		||||
 | 
			
		||||
            // Make sure all the tasks we kicked off have completed.
 | 
			
		||||
@ -120,19 +120,6 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
 | 
			
		||||
            var tasks = new List<Task>();
 | 
			
		||||
 | 
			
		||||
            if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    tasks.Add(AttachPrimaryImageAspectRatio(dto, item));
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
 | 
			
		||||
                    _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (fields.Contains(ItemFields.Studios))
 | 
			
		||||
            {
 | 
			
		||||
                dto.Studios = item.Studios;
 | 
			
		||||
@ -145,6 +132,19 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
 | 
			
		||||
            tasks.Add(AttachUserSpecificInfo(dto, item, user, fields));
 | 
			
		||||
 | 
			
		||||
            if (fields.Contains(ItemFields.PrimaryImageAspectRatio))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    AttachPrimaryImageAspectRatio(dto, item);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
 | 
			
		||||
                    _logger.ErrorException("Error generating PrimaryImageAspectRatio for {0}", ex, item.Name);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AttachBasicFields(dto, item, fields);
 | 
			
		||||
 | 
			
		||||
            // Make sure all the tasks we kicked off have completed.
 | 
			
		||||
@ -199,7 +199,7 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
        /// <param name="dto">The dto.</param>
 | 
			
		||||
        /// <param name="item">The item.</param>
 | 
			
		||||
        /// <returns>Task.</returns>
 | 
			
		||||
        private async Task AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
 | 
			
		||||
        private void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
 | 
			
		||||
        {
 | 
			
		||||
            var path = item.PrimaryImagePath;
 | 
			
		||||
 | 
			
		||||
@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                size = await Kernel.Instance.ImageManager.GetImageSize(path, dateModified).ConfigureAwait(false);
 | 
			
		||||
                size = Kernel.Instance.ImageManager.GetImageSize(path, dateModified);
 | 
			
		||||
            }
 | 
			
		||||
            catch (FileNotFoundException)
 | 
			
		||||
            {
 | 
			
		||||
@ -771,7 +771,7 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
        /// <param name="user">The user.</param>
 | 
			
		||||
        /// <returns>DtoUser.</returns>
 | 
			
		||||
        /// <exception cref="System.ArgumentNullException">user</exception>
 | 
			
		||||
        public async Task<UserDto> GetUserDto(User user)
 | 
			
		||||
        public UserDto GetUserDto(User user)
 | 
			
		||||
        {
 | 
			
		||||
            if (user == null)
 | 
			
		||||
            {
 | 
			
		||||
@ -796,7 +796,7 @@ namespace MediaBrowser.Controller.Library
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await AttachPrimaryImageAspectRatio(dto, user).ConfigureAwait(false);
 | 
			
		||||
                    AttachPrimaryImageAspectRatio(dto, user);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
 | 
			
		||||
@ -300,10 +300,18 @@ namespace MediaBrowser.Server.Implementations.Library
 | 
			
		||||
 | 
			
		||||
            var activityDate = DateTime.UtcNow;
 | 
			
		||||
 | 
			
		||||
            var lastActivityDate = user.LastActivityDate;
 | 
			
		||||
 | 
			
		||||
            user.LastActivityDate = activityDate;
 | 
			
		||||
 | 
			
		||||
            LogConnection(user.Id, clientType, deviceId, deviceName, activityDate);
 | 
			
		||||
 | 
			
		||||
            // Don't log in the db anymore frequently than 10 seconds
 | 
			
		||||
            if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10)
 | 
			
		||||
            {
 | 
			
		||||
                return Task.FromResult(true);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Save this directly. No need to fire off all the events for this.
 | 
			
		||||
            return Kernel.UserRepository.SaveUser(user, CancellationToken.None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -48,10 +48,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appPaths">The app paths.</param>
 | 
			
		||||
        /// <param name="protobufSerializer">The protobuf serializer.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="logManager">The log manager.</param>
 | 
			
		||||
        /// <exception cref="System.ArgumentNullException">protobufSerializer</exception>
 | 
			
		||||
        public SQLiteDisplayPreferencesRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogger logger)
 | 
			
		||||
            : base(logger)
 | 
			
		||||
        public SQLiteDisplayPreferencesRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogManager logManager)
 | 
			
		||||
            : base(logManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (protobufSerializer == null)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -56,10 +56,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appPaths">The app paths.</param>
 | 
			
		||||
        /// <param name="jsonSerializer">The json serializer.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="logManager">The log manager.</param>
 | 
			
		||||
        /// <exception cref="System.ArgumentNullException">appPaths</exception>
 | 
			
		||||
        public SQLiteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogger logger)
 | 
			
		||||
            : base(logger)
 | 
			
		||||
        public SQLiteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
 | 
			
		||||
            : base(logManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (appPaths == null)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -46,16 +46,16 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="SqliteRepository" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="logManager">The log manager.</param>
 | 
			
		||||
        /// <exception cref="System.ArgumentNullException">logger</exception>
 | 
			
		||||
        protected SqliteRepository(ILogger logger)
 | 
			
		||||
        protected SqliteRepository(ILogManager logManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (logger == null)
 | 
			
		||||
            if (logManager == null)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentNullException("logger");
 | 
			
		||||
                throw new ArgumentNullException("logManager");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Logger = logger;
 | 
			
		||||
            Logger = logManager.GetLogger(GetType().Name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
 | 
			
		||||
@ -49,10 +49,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appPaths">The app paths.</param>
 | 
			
		||||
        /// <param name="protobufSerializer">The protobuf serializer.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="logManager">The log manager.</param>
 | 
			
		||||
        /// <exception cref="System.ArgumentNullException">protobufSerializer</exception>
 | 
			
		||||
        public SQLiteUserDataRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogger logger)
 | 
			
		||||
            : base(logger)
 | 
			
		||||
        public SQLiteUserDataRepository(IApplicationPaths appPaths, IProtobufSerializer protobufSerializer, ILogManager logManager)
 | 
			
		||||
            : base(logManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (protobufSerializer == null)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -50,10 +50,10 @@ namespace MediaBrowser.Server.Implementations.Sqlite
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appPaths">The app paths.</param>
 | 
			
		||||
        /// <param name="jsonSerializer">The json serializer.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="logManager">The log manager.</param>
 | 
			
		||||
        /// <exception cref="System.ArgumentNullException">appPaths</exception>
 | 
			
		||||
        public SQLiteUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogger logger)
 | 
			
		||||
            : base(logger)
 | 
			
		||||
        public SQLiteUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
 | 
			
		||||
            : base(logManager)
 | 
			
		||||
        {
 | 
			
		||||
            if (appPaths == null)
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
@ -172,9 +172,9 @@ namespace MediaBrowser.ServerApplication.EntryPoints
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="sender">The sender.</param>
 | 
			
		||||
        /// <param name="e">The e.</param>
 | 
			
		||||
        async void userManager_UserUpdated(object sender, GenericEventArgs<User> e)
 | 
			
		||||
        void userManager_UserUpdated(object sender, GenericEventArgs<User> e)
 | 
			
		||||
        {
 | 
			
		||||
            var dto = await new DtoBuilder(_logger, _libraryManager, _userManager).GetUserDto(e.Argument).ConfigureAwait(false);
 | 
			
		||||
            var dto = new DtoBuilder(_logger, _libraryManager, _userManager).GetUserDto(e.Argument);
 | 
			
		||||
 | 
			
		||||
            _serverManager.SendWebSocketMessage("UserUpdated", dto);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -40,9 +40,11 @@ namespace MediaBrowser.WebDashboard.Api
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="DashboardInfoWebSocketListener" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="appHost">The app host.</param>
 | 
			
		||||
        /// <param name="logger">The logger.</param>
 | 
			
		||||
        /// <param name="taskManager">The task manager.</param>
 | 
			
		||||
        /// <param name="userManager">The user manager.</param>
 | 
			
		||||
        /// <param name="libraryManager">The library manager.</param>
 | 
			
		||||
        public DashboardInfoWebSocketListener(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager)
 | 
			
		||||
            : base(logger)
 | 
			
		||||
        {
 | 
			
		||||
@ -59,7 +61,7 @@ namespace MediaBrowser.WebDashboard.Api
 | 
			
		||||
        /// <returns>Task{IEnumerable{TaskInfo}}.</returns>
 | 
			
		||||
        protected override Task<DashboardInfo> GetDataToSend(object state)
 | 
			
		||||
        {
 | 
			
		||||
            return DashboardService.GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager);
 | 
			
		||||
            return Task.FromResult(DashboardService.GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -147,7 +147,7 @@ namespace MediaBrowser.WebDashboard.Api
 | 
			
		||||
        /// <returns>System.Object.</returns>
 | 
			
		||||
        public object Get(GetDashboardInfo request)
 | 
			
		||||
        {
 | 
			
		||||
            return GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager).Result;
 | 
			
		||||
            return GetDashboardInfo(_appHost, Logger, _taskManager, _userManager, _libraryManager);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
@ -159,14 +159,13 @@ namespace MediaBrowser.WebDashboard.Api
 | 
			
		||||
        /// <param name="userManager">The user manager.</param>
 | 
			
		||||
        /// <param name="libraryManager">The library manager.</param>
 | 
			
		||||
        /// <returns>DashboardInfo.</returns>
 | 
			
		||||
        public static async Task<DashboardInfo> GetDashboardInfo(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager)
 | 
			
		||||
        public static DashboardInfo GetDashboardInfo(IServerApplicationHost appHost, ILogger logger, ITaskManager taskManager, IUserManager userManager, ILibraryManager libraryManager)
 | 
			
		||||
        {
 | 
			
		||||
            var connections = userManager.RecentConnections.ToArray();
 | 
			
		||||
 | 
			
		||||
            var dtoBuilder = new DtoBuilder(logger, libraryManager, userManager);
 | 
			
		||||
 | 
			
		||||
            var tasks = userManager.Users.Where(u => connections.Any(c => c.UserId == u.Id)).Select(dtoBuilder.GetUserDto);
 | 
			
		||||
            var users = await Task.WhenAll(tasks).ConfigureAwait(false);
 | 
			
		||||
            var users = userManager.Users.Where(u => connections.Any(c => c.UserId == u.Id)).Select(dtoBuilder.GetUserDto);
 | 
			
		||||
 | 
			
		||||
            return new DashboardInfo
 | 
			
		||||
            {
 | 
			
		||||
@ -180,7 +179,7 @@ namespace MediaBrowser.WebDashboard.Api
 | 
			
		||||
 | 
			
		||||
                ActiveConnections = connections,
 | 
			
		||||
 | 
			
		||||
                Users = users
 | 
			
		||||
                Users = users.ToArray()
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user