Luke Pulverenti пре 12 година
родитељ
комит
eb45e67c81

+ 393 - 12
MediaBrowser.Api/Images/ImageService.cs

@@ -1,14 +1,20 @@
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using ServiceStack.ServiceHost;
 using ServiceStack.Text.Controller;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Threading;
@@ -19,6 +25,18 @@ namespace MediaBrowser.Api.Images
     /// <summary>
     /// Class GetItemImage
     /// </summary>
+    [Route("/Items/{Id}/Images", "GET")]
+    [Api(Description = "Gets information about an item's images")]
+    public class GetItemImageInfos : IReturn<List<ImageInfo>>
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
     [Route("/Items/{Id}/Images/{Type}", "GET")]
     [Route("/Items/{Id}/Images/{Type}/{Index}", "GET")]
     [Api(Description = "Gets an item image")]
@@ -32,6 +50,58 @@ namespace MediaBrowser.Api.Images
         public string Id { get; set; }
     }
 
+    /// <summary>
+    /// Class UpdateItemImageIndex
+    /// </summary>
+    [Route("/Items/{Id}/Images/{Type}/{Index}/Index", "POST")]
+    [Api(Description = "Updates the index for an item image")]
+    public class UpdateItemImageIndex : IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public Guid Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the type of the image.
+        /// </summary>
+        /// <value>The type of the image.</value>
+        [ApiMember(Name = "Type", Description = "Image Type", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public ImageType Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index.
+        /// </summary>
+        /// <value>The index.</value>
+        [ApiMember(Name = "Index", Description = "Image Index", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
+        public int Index { get; set; }
+
+        /// <summary>
+        /// Gets or sets the new index.
+        /// </summary>
+        /// <value>The new index.</value>
+        [ApiMember(Name = "NewIndex", Description = "The new image index", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
+        public int NewIndex { get; set; }
+    }
+
+    /// <summary>
+    /// Class DeleteItemImage
+    /// </summary>
+    [Route("/Items/{Id}/Images/{Type}", "DELETE")]
+    [Route("/Items/{Id}/Images/{Type}/{Index}", "DELETE")]
+    [Api(Description = "Deletes an item image")]
+    public class DeleteItemImage : DeleteImageRequest, IReturnVoid
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public Guid Id { get; set; }
+    }
+
     /// <summary>
     /// Class GetPersonImage
     /// </summary>
@@ -48,6 +118,9 @@ namespace MediaBrowser.Api.Images
         public string Name { get; set; }
     }
 
+    /// <summary>
+    /// Class GetArtistImage
+    /// </summary>
     [Route("/Artists/{Name}/Images/{Type}", "GET")]
     [Route("/Artists/{Name}/Images/{Type}/{Index}", "GET")]
     [Api(Description = "Gets an artist image")]
@@ -141,6 +214,9 @@ namespace MediaBrowser.Api.Images
         public Guid Id { get; set; }
     }
 
+    /// <summary>
+    /// Class PostUserImage
+    /// </summary>
     [Route("/Users/{Id}/Images/{Type}", "POST")]
     [Route("/Users/{Id}/Images/{Type}/{Index}", "POST")]
     [Api(Description = "Posts a user image")]
@@ -160,6 +236,9 @@ namespace MediaBrowser.Api.Images
         public Stream RequestStream { get; set; }
     }
 
+    /// <summary>
+    /// Class PostItemImage
+    /// </summary>
     [Route("/Items/{Id}/Images/{Type}", "POST")]
     [Route("/Items/{Id}/Images/{Type}/{Index}", "POST")]
     [Api(Description = "Posts an item image")]
@@ -178,7 +257,7 @@ namespace MediaBrowser.Api.Images
         /// <value>The request stream.</value>
         public Stream RequestStream { get; set; }
     }
-    
+
     /// <summary>
     /// Class ImageService
     /// </summary>
@@ -194,17 +273,154 @@ namespace MediaBrowser.Api.Images
         /// </summary>
         private readonly ILibraryManager _libraryManager;
 
+        private readonly IApplicationPaths _appPaths;
+
+        private readonly IProviderManager _providerManager;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ImageService" /> class.
         /// </summary>
         /// <param name="userManager">The user manager.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public ImageService(IUserManager userManager, ILibraryManager libraryManager)
+        /// <param name="appPaths">The app paths.</param>
+        /// <param name="providerManager">The provider manager.</param>
+        public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager)
         {
             _userManager = userManager;
             _libraryManager = libraryManager;
+            _appPaths = appPaths;
+            _providerManager = providerManager;
         }
 
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetItemImageInfos request)
+        {
+            var item = DtoBuilder.GetItemByClientId(request.Id, _userManager, _libraryManager);
+
+            var result = GetItemImageInfos(item).Result;
+
+            return ToOptimizedResult(result);
+        }
+
+        /// <summary>
+        /// Gets the item image infos.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>Task{List{ImageInfo}}.</returns>
+        public async Task<List<ImageInfo>> GetItemImageInfos(BaseItem item)
+        {
+            var list = new List<ImageInfo>();
+
+            foreach (var image in item.Images)
+            {
+                var path = image.Value;
+
+                var fileInfo = new FileInfo(path);
+
+                var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, path);
+
+                var size = await Kernel.Instance.ImageManager.GetImageSize(path, dateModified).ConfigureAwait(false);
+
+                list.Add(new ImageInfo
+                {
+                    Path = path,
+                    ImageType = image.Key,
+                    ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, image.Key, path),
+                    Size = fileInfo.Length,
+                    Width = Convert.ToInt32(size.Width),
+                    Height = Convert.ToInt32(size.Height)
+                });
+            }
+
+            var index = 0;
+
+            foreach (var image in item.BackdropImagePaths)
+            {
+                var fileInfo = new FileInfo(image);
+
+                var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, image);
+
+                var size = await Kernel.Instance.ImageManager.GetImageSize(image, dateModified).ConfigureAwait(false);
+
+                list.Add(new ImageInfo
+                {
+                    Path = image,
+                    ImageIndex = index,
+                    ImageType = ImageType.Backdrop,
+                    ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, image),
+                    Size = fileInfo.Length,
+                    Width = Convert.ToInt32(size.Width),
+                    Height = Convert.ToInt32(size.Height)
+                });
+
+                index++;
+            }
+            
+            index = 0;
+
+            foreach (var image in item.ScreenshotImagePaths)
+            {
+                var fileInfo = new FileInfo(image);
+
+                var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, image);
+
+                var size = await Kernel.Instance.ImageManager.GetImageSize(image, dateModified).ConfigureAwait(false);
+
+                list.Add(new ImageInfo
+                {
+                    Path = image,
+                    ImageIndex = index,
+                    ImageType = ImageType.Screenshot,
+                    ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Screenshot, image),
+                    Size = fileInfo.Length,
+                    Width = Convert.ToInt32(size.Width),
+                    Height = Convert.ToInt32(size.Height)
+                });
+
+                index++;
+            }
+
+            var video = item as Video;
+
+            if (video != null)
+            {
+                index = 0;
+
+                foreach (var chapter in video.Chapters)
+                {
+                    if (!string.IsNullOrEmpty(chapter.ImagePath))
+                    {
+                        var image = chapter.ImagePath;
+
+                        var fileInfo = new FileInfo(image);
+
+                        var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, image);
+
+                        var size = await Kernel.Instance.ImageManager.GetImageSize(image, dateModified).ConfigureAwait(false);
+
+                        list.Add(new ImageInfo
+                        {
+                            Path = image,
+                            ImageIndex = index,
+                            ImageType = ImageType.Chapter,
+                            ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Chapter, image),
+                            Size = fileInfo.Length,
+                            Width = Convert.ToInt32(size.Width),
+                            Height = Convert.ToInt32(size.Height)
+                        });
+                    }
+
+                    index++;
+                }
+            }
+
+            return list;
+        }
+        
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -276,7 +492,7 @@ namespace MediaBrowser.Api.Images
 
             return GetImage(request, item);
         }
-        
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>
@@ -333,18 +549,97 @@ namespace MediaBrowser.Api.Images
         {
             var item = _userManager.Users.First(i => i.Id == request.Id);
 
-            var task = item.DeleteImage(request.Type);
+            var task = item.DeleteImage(request.Type, request.Index);
 
             Task.WaitAll(task);
         }
-        
+
+        /// <summary>
+        /// Deletes the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Delete(DeleteItemImage request)
+        {
+            var item = _libraryManager.GetItemById(request.Id);
+
+            var task = item.DeleteImage(request.Type, request.Index);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Posts the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(UpdateItemImageIndex request)
+        {
+            var item = _libraryManager.GetItemById(request.Id);
+
+            var task = UpdateItemIndex(item, request.Type, request.Index, request.NewIndex);
+
+            Task.WaitAll(task);
+        }
+
+        /// <summary>
+        /// Updates the index of the item.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="type">The type.</param>
+        /// <param name="currentIndex">Index of the current.</param>
+        /// <param name="newIndex">The new index.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentException">The change index operation is only applicable to backdrops and screenshots</exception>
+        private Task UpdateItemIndex(BaseItem item, ImageType type, int currentIndex, int newIndex)
+        {
+            string file1;
+            string file2;
+
+            if (type == ImageType.Screenshot)
+            {
+                file1 = item.ScreenshotImagePaths[currentIndex];
+                file2 = item.ScreenshotImagePaths[newIndex];
+            }
+            else if (type == ImageType.Backdrop)
+            {
+                file1 = item.BackdropImagePaths[currentIndex];
+                file2 = item.BackdropImagePaths[newIndex];
+            }
+            else
+            {
+                throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots");
+            }
+
+            SwapFiles(file1, file2);
+
+            // Directory watchers should repeat this, but do a quick refresh first
+            return item.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false);
+        }
+
+        /// <summary>
+        /// Swaps the files.
+        /// </summary>
+        /// <param name="file1">The file1.</param>
+        /// <param name="file2">The file2.</param>
+        private void SwapFiles(string file1, string file2)
+        {
+            var temp1 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
+            var temp2 = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp");
+
+            File.Copy(file1, temp1);
+            File.Copy(file2, temp2);
+
+            File.Copy(temp1, file2, true);
+            File.Copy(temp2, file1, true);
+        }
+
         /// <summary>
         /// Gets the image.
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="item">The item.</param>
         /// <returns>System.Object.</returns>
-        /// <exception cref="ResourceNotFoundException"></exception>
+        /// <exception cref="ResourceNotFoundException">
+        /// </exception>
         private object GetImage(ImageRequest request, BaseItem item)
         {
             var kernel = Kernel.Instance;
@@ -430,7 +725,13 @@ namespace MediaBrowser.Api.Images
                         filename = "clearart";
                         break;
                     case ImageType.Primary:
-                        filename = "folder";
+                        filename = entity is Episode ? Path.GetFileNameWithoutExtension(entity.Path) : "folder";
+                        break;
+                    case ImageType.Backdrop:
+                        filename = GetBackdropFilenameToSave(entity);
+                        break;
+                    case ImageType.Screenshot:
+                        filename = GetScreenshotFilenameToSave(entity);
                         break;
                     default:
                         filename = imageType.ToString().ToLower();
@@ -440,9 +741,30 @@ namespace MediaBrowser.Api.Images
 
                 var extension = mimeType.Split(';').First().Split('/').Last();
 
-                var oldImagePath = entity.GetImage(imageType);
+                string oldImagePath;
+                switch (imageType)
+                {
+                    case ImageType.Backdrop:
+                    case ImageType.Screenshot:
+                        oldImagePath = null;
+                        break;
+                    default:
+                        oldImagePath = entity.GetImage(imageType);
+                        break;
+                }
+
+                // Don't save locally if there's no parent (special feature, trailer, etc)
+                var saveLocally = !(entity is Audio) && entity.Parent != null;
+
+                if (imageType != ImageType.Primary)
+                {
+                    if (entity is Episode)
+                    {
+                        saveLocally = false;
+                    }
+                }
 
-                var imagePath = Path.Combine(entity.MetaLocation, filename + "." + extension);
+                var imagePath = _providerManager.GetSavePath(entity, filename + "." + extension, saveLocally);
 
                 // Save to file system
                 using (var fs = new FileStream(imagePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true))
@@ -450,8 +772,19 @@ namespace MediaBrowser.Api.Images
                     await fs.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
                 }
 
-                // Set the image
-                entity.SetImage(imageType, imagePath);
+                if (imageType == ImageType.Screenshot)
+                {
+                    entity.ScreenshotImagePaths.Add(imagePath);
+                }
+                else if (imageType == ImageType.Backdrop)
+                {
+                    entity.BackdropImagePaths.Add(imagePath);
+                }
+                else
+                {
+                    // Set the image
+                    entity.SetImage(imageType, imagePath);
+                }
 
                 // If the new and old paths are different, delete the old one
                 if (!string.IsNullOrEmpty(oldImagePath) && !oldImagePath.Equals(imagePath, StringComparison.OrdinalIgnoreCase))
@@ -463,5 +796,53 @@ namespace MediaBrowser.Api.Images
                 await entity.RefreshMetadata(CancellationToken.None, forceSave: true, allowSlowProviders: false).ConfigureAwait(false);
             }
         }
+
+        /// <summary>
+        /// Gets the backdrop filename to save.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>System.String.</returns>
+        private string GetBackdropFilenameToSave(BaseItem item)
+        {
+            var paths = item.BackdropImagePaths.ToList();
+
+            if (!paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "backdrop", StringComparison.OrdinalIgnoreCase)))
+            {
+                return "screenshot";
+            }
+
+            var index = 1;
+
+            while (paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "backdrop" + index, StringComparison.OrdinalIgnoreCase)))
+            {
+                index++;
+            }
+
+            return "backdrop" + index;
+        }
+
+        /// <summary>
+        /// Gets the screenshot filename to save.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>System.String.</returns>
+        private string GetScreenshotFilenameToSave(BaseItem item)
+        {
+            var paths = item.ScreenshotImagePaths.ToList();
+
+            if (!paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "screenshot", StringComparison.OrdinalIgnoreCase)))
+            {
+                return "screenshot";
+            }
+
+            var index = 1;
+
+            while (paths.Any(i => string.Equals(Path.GetFileNameWithoutExtension(i), "screenshot" + index, StringComparison.OrdinalIgnoreCase)))
+            {
+                index++;
+            }
+
+            return "screenshot" + index;
+        }
     }
 }

+ 1 - 1
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -241,7 +241,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                 }
             }
 
-            return new ImageService(UserManager, LibraryManager)
+            return new ImageService(UserManager, LibraryManager, ApplicationPaths, null)
             {
                 Logger = Logger,
                 RequestContext = RequestContext,

+ 16 - 0
MediaBrowser.Controller/Dto/DtoBuilder.cs

@@ -287,6 +287,7 @@ namespace MediaBrowser.Controller.Dto
             dto.AspectRatio = item.AspectRatio;
 
             dto.BackdropImageTags = GetBackdropImageTags(item);
+            dto.ScreenshotImageTags = GetScreenshotImageTags(item);
 
             if (fields.Contains(ItemFields.Genres))
             {
@@ -981,5 +982,20 @@ namespace MediaBrowser.Controller.Dto
 
             return item.BackdropImagePaths.Select(p => Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, p)).ToList();
         }
+
+        /// <summary>
+        /// Gets the screenshot image tags.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>List{Guid}.</returns>
+        private List<Guid> GetScreenshotImageTags(BaseItem item)
+        {
+            if (item.ScreenshotImagePaths == null)
+            {
+                return new List<Guid>();
+            }
+
+            return item.ScreenshotImagePaths.Select(p => Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Screenshot, p)).ToList();
+        }
     }
 }

+ 35 - 8
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1475,22 +1475,49 @@ namespace MediaBrowser.Controller.Entities
         /// Deletes the image.
         /// </summary>
         /// <param name="type">The type.</param>
+        /// <param name="index">The index.</param>
         /// <returns>Task.</returns>
-        public async Task DeleteImage(ImageType type)
+        public Task DeleteImage(ImageType type, int? index)
         {
-            if (!HasImage(type))
+            if (type == ImageType.Backdrop)
             {
-                return;
+                if (!index.HasValue)
+                {
+                    throw new ArgumentException("Please specify a backdrop image index to delete.");
+                }
+
+                var file = BackdropImagePaths[index.Value];
+
+                BackdropImagePaths.Remove(file);
+
+                // Delete the source file
+                File.Delete(file);
             }
+            else if (type == ImageType.Screenshot)
+            {
+                if (!index.HasValue)
+                {
+                    throw new ArgumentException("Please specify a screenshot image index to delete.");
+                }
+
+                var file = ScreenshotImagePaths[index.Value];
 
-            // Delete the source file
-            File.Delete(GetImage(type));
+                ScreenshotImagePaths.Remove(file);
+
+                // Delete the source file
+                File.Delete(file);
+            }
+            else
+            {
+                // Delete the source file
+                File.Delete(GetImage(type));
 
-            // Remove it from the item
-            SetImage(type, null);
+                // Remove it from the item
+                SetImage(type, null);
+            }
 
             // Refresh metadata
-            await RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
+            return RefreshMetadata(CancellationToken.None);
         }
     }
 }

+ 1 - 1
MediaBrowser.Controller/IO/FileSystem.cs

@@ -55,7 +55,7 @@ namespace MediaBrowser.Controller.IO
 
             try
             {
-                return info.LastAccessTimeUtc;
+                return info.LastWriteTimeUtc;
             }
             catch (Exception ex)
             {

+ 9 - 0
MediaBrowser.Controller/Providers/IProviderManager.cs

@@ -47,5 +47,14 @@ namespace MediaBrowser.Controller.Providers
         /// </summary>
         /// <param name="providers">The providers.</param>
         void AddMetadataProviders(IEnumerable<BaseMetadataProvider> providers);
+
+        /// <summary>
+        /// Gets the save path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="targetFileName">Name of the target file.</param>
+        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
+        /// <returns>System.String.</returns>
+        string GetSavePath(BaseItem item, string targetFileName, bool saveLocally);
     }
 }

+ 16 - 0
MediaBrowser.Model/DTO/BaseItemDto.cs

@@ -338,6 +338,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The backdrop image tags.</value>
         public List<Guid> BackdropImageTags { get; set; }
 
+        /// <summary>
+        /// Gets or sets the screenshot image tags.
+        /// </summary>
+        /// <value>The screenshot image tags.</value>
+        public List<Guid> ScreenshotImageTags { get; set; }
+        
         /// <summary>
         /// Gets or sets the parent logo image tag.
         /// </summary>
@@ -440,6 +446,16 @@ namespace MediaBrowser.Model.Dto
             get { return BackdropImageTags == null ? 0 : BackdropImageTags.Count; }
         }
 
+        /// <summary>
+        /// Gets the screenshot count.
+        /// </summary>
+        /// <value>The screenshot count.</value>
+        [IgnoreDataMember]
+        public int ScreenshotCount
+        {
+            get { return ScreenshotImageTags == null ? 0 : ScreenshotImageTags.Count; }
+        }
+        
         /// <summary>
         /// Gets a value indicating whether this instance has banner.
         /// </summary>

+ 52 - 0
MediaBrowser.Model/Dto/ImageInfo.cs

@@ -0,0 +1,52 @@
+using MediaBrowser.Model.Entities;
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+    /// <summary>
+    /// Class ImageInfo
+    /// </summary>
+    public class ImageInfo
+    {
+        /// <summary>
+        /// Gets or sets the type of the image.
+        /// </summary>
+        /// <value>The type of the image.</value>
+        public ImageType ImageType { get; set; }
+
+        /// <summary>
+        /// Gets or sets the index of the image.
+        /// </summary>
+        /// <value>The index of the image.</value>
+        public int? ImageIndex { get; set; }
+
+        /// <summary>
+        /// The image tag
+        /// </summary>
+        public Guid ImageTag;
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public string Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets the height.
+        /// </summary>
+        /// <value>The height.</value>
+        public int Height { get; set; }
+
+        /// <summary>
+        /// Gets or sets the width.
+        /// </summary>
+        /// <value>The width.</value>
+        public int Width { get; set; }
+
+        /// <summary>
+        /// Gets or sets the size.
+        /// </summary>
+        /// <value>The size.</value>
+        public long Size { get; set; }
+    }
+}

+ 1 - 0
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -45,6 +45,7 @@
     <Compile Include="Dto\BaseItemPerson.cs" />
     <Compile Include="Dto\ChapterInfoDto.cs" />
     <Compile Include="Dto\IItemDto.cs" />
+    <Compile Include="Dto\ImageInfo.cs" />
     <Compile Include="Dto\ItemByNameCounts.cs" />
     <Compile Include="Dto\StudioDto.cs" />
     <Compile Include="Entities\IByReferenceItem.cs" />

+ 13 - 0
MediaBrowser.Server.Implementations/Providers/ProviderManager.cs

@@ -397,6 +397,19 @@ namespace MediaBrowser.Server.Implementations.Providers
             return localPath;
         }
 
+        /// <summary>
+        /// Gets the save path.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="targetFileName">Name of the target file.</param>
+        /// <param name="saveLocally">if set to <c>true</c> [save locally].</param>
+        /// <returns>System.String.</returns>
+        public string GetSavePath(BaseItem item, string targetFileName, bool saveLocally)
+        {
+            return (saveLocally && item.MetaLocation != null) ?
+                Path.Combine(item.MetaLocation, targetFileName) :
+                _remoteImageCache.GetResourcePath(item.GetType().FullName + item.Path.ToLower(), targetFileName);
+        }
 
         /// <summary>
         /// Saves to library filesystem.

+ 61 - 2
MediaBrowser.WebDashboard/ApiClient.js

@@ -758,7 +758,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
          * @param {String} userId
          * @param {String} imageType The type of image to delete, based on the server-side ImageType enum.
          */
-        self.deleteUserImage = function (userId, imageType) {
+        self.deleteUserImage = function (userId, imageType, imageIndex) {
 
             if (!userId) {
                 throw new Error("null userId");
@@ -770,12 +770,71 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
 
             var url = self.getUrl("Users/" + userId + "/Images/" + imageType);
 
+            if (imageIndex != null) {
+                url += "/" + imageIndex;
+            }
+
             return self.ajax({
                 type: "DELETE",
                 url: url
             });
         };
 
+        self.deleteItemImage = function (itemId, imageType, imageIndex) {
+
+            if (!itemId) {
+                throw new Error("null itemId");
+            }
+
+            if (!imageType) {
+                throw new Error("null imageType");
+            }
+
+            var url = self.getUrl("Items/" + itemId + "/Images/" + imageType);
+
+            if (imageIndex != null) {
+                url += "/" + imageIndex;
+            }
+
+            return self.ajax({
+                type: "DELETE",
+                url: url
+            });
+        };
+
+        self.updateItemImageIndex = function (itemId, imageType, imageIndex, newIndex) {
+
+            if (!itemId) {
+                throw new Error("null itemId");
+            }
+
+            if (!imageType) {
+                throw new Error("null imageType");
+            }
+
+            var url = self.getUrl("Items/" + itemId + "/Images/" + imageType + "/" + imageIndex + "/Index", { newIndex: newIndex });
+
+            return self.ajax({
+                type: "POST",
+                url: url
+            });
+        };
+
+        self.getItemImageInfos = function (itemId) {
+
+            if (!itemId) {
+                throw new Error("null itemId");
+            }
+
+            var url = self.getUrl("Items/" + itemId + "/Images");
+
+            return self.ajax({
+                type: "GET",
+                url: url,
+                dataType: "json"
+            });
+        };
+
         /**
          * Uploads a user image
          * @param {String} userId
@@ -839,7 +898,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
             return deferred.promise();
         };
 
-        self.uploadImage = function (itemId, imageType, file) {
+        self.uploadItemImage = function (itemId, imageType, file) {
 
             if (!itemId) {
                 throw new Error("null itemId");

+ 1 - 1
MediaBrowser.WebDashboard/packages.config

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
-  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.94" targetFramework="net45" />
+  <package id="MediaBrowser.ApiClient.Javascript" version="3.0.99" targetFramework="net45" />
   <package id="ServiceStack.Common" version="3.9.43" targetFramework="net45" />
   <package id="ServiceStack.Text" version="3.9.43" targetFramework="net45" />
 </packages>